Add MASQUE support

This commit is contained in:
世界 committed 2026-11-09 15:17:20 +00:00
1 parent 256ef16489
commit 7a949fa40f
53 files changed
+4664 -1365

No files matched your search

+6
View File
@@ -29,6 +29,8 @@ const (
TypeOpenConnect = "openconnect"
TypeOpenVPNClient = "openvpn-client"
TypeOpenVPNServer = "openvpn-server"
TypeMASQUEClient = "masque-client"
TypeMASQUEServer = "masque-server"
TypeTailscale = "tailscale"
TypeTailcat = "tailcat"
TypeCloudflared = "cloudflared"
@@ -109,6 +111,10 @@ func ProxyDisplayName(proxyType string) string {
return "OpenVPN Client"
case TypeOpenVPNServer:
return "OpenVPN Server"
case TypeMASQUEClient:
return "MASQUE Client"
case TypeMASQUEServer:
return "MASQUE Server"
case TypeTailscale:
return "Tailscale"
case TypeTailcat:
+2
View File
@@ -26,6 +26,8 @@ An endpoint is a protocol with inbound and outbound behavior.
| `openconnect` | [OpenConnect Client](./openconnect/) |
| `openvpn-client` | [OpenVPN Client](./openvpn-client/) |
| `openvpn-server` | [OpenVPN Server](./openvpn-server/) |
| `masque-client` | [MASQUE Client](./masque-client/) |
| `masque-server` | [MASQUE Server](./masque-server/) |
#### tag
+2
View File
@@ -26,6 +26,8 @@
| `openconnect` | [OpenConnect 客户端](./openconnect/) |
| `openvpn-client` | [OpenVPN 客户端](./openvpn-client/) |
| `openvpn-server` | [OpenVPN 服务器](./openvpn-server/) |
| `masque-client` | [MASQUE 客户端](./masque-client/) |
| `masque-server` | [MASQUE 服务器](./masque-server/) |
#### tag
@@ -0,0 +1,145 @@
# MASQUE Client
!!! question "Since sing-box 1.15.0"
`masque-client` endpoint is an IP proxying over HTTP ([RFC 9484](https://datatracker.ietf.org/doc/html/rfc9484), CONNECT-IP) client.
## Structure
```json
{
"type": "masque-client",
"tag": "masque-client",
"server": "127.0.0.1",
"server_port": 443,
"username": "",
"password": "",
"path": "",
"headers": {},
"version": 0,
"disable_version_fallback": false,
"tls": {},
"advertise_routes": [],
"system": false,
"name": "",
"mtu": 1280,
"on_demand": false,
... // HTTP2 Fields / QUIC Fields
... // UDP NAT Fields
... // Dial Fields
}
```
!!! note ""
You can ignore the JSON Array [] tag when the content is only one item
## Fields
### server
==Required==
The server address.
### server_port
==Required==
The server port.
### username
Basic authorization username.
### password
Basic authorization password.
### path
URI template path of the IP proxying resource, may contain the `target` and `ipproto` variables.
`/.well-known/masque/ip/{target}/{ipproto}/` is used by default.
### headers
Extra headers of HTTP request.
### version
HTTP version.
Available values: `1`, `2`, `3`.
`3` is used by default.
When `2`, [QUIC Fields](#quic-fields) are replaced by [HTTP2 Fields](#http2-fields).
### disable_version_fallback
Disable automatic fallback to lower HTTP version.
### tls
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
Required for HTTP/3.
### advertise_routes
List of IP prefixes to advertise to the server.
The server will route traffic for these prefixes into this endpoint, where it is handled as inbound traffic.
### system
Use system interface.
Requires privilege and cannot conflict with existing system interfaces.
If disabled, sing-box uses the internal network stack.
### name
Custom interface name for system interface.
An automatically generated `masque` interface name is used by default.
### mtu
Tunnel MTU.
`1280` will be used by default.
### on_demand
Allow the endpoint to be disconnected when necessary.
## HTTP2 Fields
When `version` is `2`.
See [HTTP2 Fields](/configuration/shared/http2/) for details.
`keep_alive_period` is `10s` by default.
## QUIC Fields
When `version` is `3` (default).
See [QUIC Fields](/configuration/shared/quic/) for details.
`keep_alive_period` is `10s` by default.
`initial_packet_size` is `mtu + 51` by default, so that IP packets up to the tunnel MTU fit into a QUIC datagram. QUIC packets cannot exceed 1452 bytes; with a larger `mtu`, IP packets that do not fit are answered with ICMP Packet Too Big. If the path cannot carry packets of that size, the QUIC handshake fails and the client falls back to a lower HTTP version.
## UDP NAT Fields
See [UDP NAT Fields](/configuration/shared/udp-nat/) for details.
## Dial Fields
See [Dial Fields](/configuration/shared/dial/) for details.
@@ -0,0 +1,145 @@
# MASQUE 客户端
!!! question "自 sing-box 1.15.0 起"
`masque-client` endpoint 是一个基于 HTTP 的 IP 代理([RFC 9484](https://datatracker.ietf.org/doc/html/rfc9484),CONNECT-IP)客户端。
## 结构
```json
{
"type": "masque-client",
"tag": "masque-client",
"server": "127.0.0.1",
"server_port": 443,
"username": "",
"password": "",
"path": "",
"headers": {},
"version": 0,
"disable_version_fallback": false,
"tls": {},
"advertise_routes": [],
"system": false,
"name": "",
"mtu": 1280,
"on_demand": false,
... // HTTP2 字段 / QUIC 字段
... // UDP NAT 字段
... // 拨号字段
}
```
!!! note ""
当内容只有一项时,可以忽略 JSON 数组 [] 标签
## 字段
### server
==必填==
服务器地址。
### server_port
==必填==
服务器端口。
### username
Basic 认证用户名。
### password
Basic 认证密码。
### path
IP 代理资源的 URI 模板路径,可以包含 `target` 和 `ipproto` 变量。
默认使用 `/.well-known/masque/ip/{target}/{ipproto}/`。
### headers
HTTP 请求的额外标头。
### version
HTTP 版本。
可用值:`1`、`2`、`3`。
默认使用 `3`。
当为 `2` 时,[QUIC 字段](#quic-字段) 替换为 [HTTP2 字段](#http2-字段)。
### disable_version_fallback
禁用自动回退到更低的 HTTP 版本。
### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
HTTP/3 需要 TLS。
### advertise_routes
向服务器声明的 IP 前缀列表。
服务器会把发往这些前缀的流量路由到此 endpoint,并在此作为入站流量处理。
### system
使用系统接口。
需要特权且不能与已有系统接口冲突。
如果禁用,sing-box 将使用内部网络栈。
### name
系统接口的自定义接口名称。
默认使用自动生成的 `masque` 接口名称。
### mtu
隧道 MTU。
默认使用 `1280`。
### on_demand
允许该 endpoint 在需要时断开连接。
## HTTP2 字段
当 `version` 为 `2` 时。
参阅 [HTTP2 字段](/zh/configuration/shared/http2/)。
`keep_alive_period` 默认为 `10s`。
## QUIC 字段
当 `version` 为 `3`(默认)时。
参阅 [QUIC 字段](/zh/configuration/shared/quic/)。
`keep_alive_period` 默认为 `10s`。
`initial_packet_size` 默认为 `mtu + 51`,使不超过隧道 MTU 的 IP 数据包能放入一个 QUIC 数据报。QUIC 数据包最大为 1452 字节,`mtu` 更大时,放不下的 IP 数据包会收到 ICMP Packet Too Big 回复。如果路径无法传输这个大小的数据包,QUIC 握手会失败,客户端将回退到更低的 HTTP 版本。
## UDP NAT 字段
参阅 [UDP NAT 字段](/zh/configuration/shared/udp-nat/)。
## 拨号字段
参阅 [拨号字段](/zh/configuration/shared/dial/)。
@@ -0,0 +1,126 @@
# MASQUE Server
!!! question "Since sing-box 1.15.0"
`masque-server` endpoint is an IP proxying over HTTP ([RFC 9484](https://datatracker.ietf.org/doc/html/rfc9484), CONNECT-IP) server.
## Structure
```json
{
"type": "masque-server",
"tag": "masque-server",
... // Listen Fields
"version": [],
"users": [
{
"username": "",
"password": ""
}
],
"tls": {},
"path": "",
"address": [],
"advertise_routes": [],
"system": false,
"name": "",
"mtu": 1280,
... // HTTP2 Fields / QUIC Fields
... // UDP NAT Fields
}
```
!!! note ""
You can ignore the JSON Array [] tag when the content is only one item
## Listen Fields
See [Listen Fields](/configuration/shared/listen/) for details. `udp_timeout` is part of the [UDP NAT Fields](#udp-nat-fields) below.
## Fields
### version
List of HTTP versions to serve.
Available values: `1`, `2`, `3`.
All versions are used by default.
TLS is required for `3`.
### users
HTTP users, verified by the `Authorization` header.
No authentication required if empty.
### tls
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
IP proxying must be operated over TLS or QUIC. Leave it disabled only when the server is placed behind an HTTP intermediary that terminates TLS.
### path
URI template path of the IP proxying resource, may contain the `target` and `ipproto` variables.
`/.well-known/masque/ip/{target}/{ipproto}/` is used by default.
### address
==Required==
List of IP prefixes of the tunnel network, at most one for each IP version.
The address of the prefix is used by the server itself, other addresses in the prefix are assigned to clients.
### advertise_routes
List of IP prefixes to advertise to clients, in addition to the tunnel network.
Traffic from clients to other destinations is rejected.
All addresses are advertised by default.
### system
Use system interface.
Requires privilege and cannot conflict with existing system interfaces.
If disabled, sing-box uses the internal network stack.
### name
Custom interface name for system interface.
An automatically generated `masque` interface name is used by default.
### mtu
Tunnel MTU.
`1280` will be used by default.
## HTTP2 Fields
When `version` contains `2`.
See [HTTP2 Fields](/configuration/shared/http2/) for details.
## QUIC Fields
When `version` contains `3` (default), [HTTP2 Fields](#http2-fields) are replaced by QUIC Fields.
See [QUIC Fields](/configuration/shared/quic/) for details.
`initial_packet_size` is `mtu + 51` by default, so that IP packets up to the tunnel MTU fit into a QUIC datagram. QUIC packets cannot exceed 1452 bytes; with a larger `mtu`, IP packets that do not fit are answered with ICMP Packet Too Big.
## UDP NAT Fields
See [UDP NAT Fields](/configuration/shared/udp-nat/) for details.
@@ -0,0 +1,126 @@
# MASQUE 服务器
!!! question "自 sing-box 1.15.0 起"
`masque-server` endpoint 是一个基于 HTTP 的 IP 代理([RFC 9484](https://datatracker.ietf.org/doc/html/rfc9484),CONNECT-IP)服务器。
## 结构
```json
{
"type": "masque-server",
"tag": "masque-server",
... // 监听字段
"version": [],
"users": [
{
"username": "",
"password": ""
}
],
"tls": {},
"path": "",
"address": [],
"advertise_routes": [],
"system": false,
"name": "",
"mtu": 1280,
... // HTTP2 字段 / QUIC 字段
... // UDP NAT 字段
}
```
!!! note ""
当内容只有一项时,可以忽略 JSON 数组 [] 标签
## 监听字段
参阅 [监听字段](/zh/configuration/shared/listen/)。`udp_timeout` 属于下方的 [UDP NAT 字段](#udp-nat-字段)。
## 字段
### version
提供的 HTTP 版本列表。
可用值:`1`、`2`、`3`。
默认提供全部版本。
`3` 需要 TLS。
### users
HTTP 用户,通过 `Authorization` 标头验证。
如果为空则不需要验证。
### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
IP 代理必须运行在 TLS 或 QUIC 之上。仅当服务器位于终止 TLS 的 HTTP 中间层之后时才可以不启用。
### path
IP 代理资源的 URI 模板路径,可以包含 `target` 和 `ipproto` 变量。
默认使用 `/.well-known/masque/ip/{target}/{ipproto}/`。
### address
==必填==
隧道网络的 IP 前缀列表,每个 IP 版本最多一个。
前缀中的地址由服务器自己使用,前缀内的其他地址分配给客户端。
### advertise_routes
除隧道网络外,向客户端声明的 IP 前缀列表。
客户端发往其他目的地址的流量会被拒绝。
默认声明全部地址。
### system
使用系统接口。
需要特权且不能与已有系统接口冲突。
如果禁用,sing-box 将使用内部网络栈。
### name
系统接口的自定义接口名称。
默认使用自动生成的 `masque` 接口名称。
### mtu
隧道 MTU。
默认使用 `1280`。
## HTTP2 字段
当 `version` 包含 `2` 时。
参阅 [HTTP2 字段](/zh/configuration/shared/http2/)。
## QUIC 字段
当 `version` 包含 `3`(默认)时,[HTTP2 字段](#http2-字段) 替换为 QUIC 字段。
参阅 [QUIC 字段](/zh/configuration/shared/quic/)。
`initial_packet_size` 默认为 `mtu + 51`,使不超过隧道 MTU 的 IP 数据包能放入一个 QUIC 数据报。QUIC 数据包最大为 1452 字节,`mtu` 更大时,放不下的 IP 数据包会收到 ICMP Packet Too Big 回复。
## UDP NAT 字段
参阅 [UDP NAT 字段](/zh/configuration/shared/udp-nat/)。
+427
View File
@@ -3405,6 +3405,433 @@
},
"Endpoint": {
"oneOf": [
{
"type": "object",
"properties": {
"type": {
"const": "masque-client"
},
"tag": {
"type": "string"
},
"detour": {
"type": "string",
"x-tag-reference": "outbound"
},
"bind_interface": {
"type": "string"
},
"inet4_bind_address": {
"$ref": "#/$defs/IPAddress"
},
"inet6_bind_address": {
"$ref": "#/$defs/IPAddress"
},
"bind_address_no_port": {
"type": "boolean"
},
"protect_path": {
"type": "string"
},
"routing_mark": {
"anyOf": [
{
"type": "integer",
"minimum": 0,
"maximum": 4294967295
},
{
"type": "string"
}
]
},
"reuse_addr": {
"type": "boolean"
},
"netns": {
"type": "string",
"x-tag-reference": "network_namespace"
},
"connect_timeout": {
"$ref": "#/$defs/Duration"
},
"tcp_fast_open": {
"type": "boolean"
},
"tcp_multi_path": {
"type": "boolean"
},
"disable_tcp_keep_alive": {
"type": "boolean"
},
"tcp_keep_alive": {
"$ref": "#/$defs/Duration"
},
"tcp_keep_alive_interval": {
"$ref": "#/$defs/Duration"
},
"udp_fragment": {
"type": "boolean"
},
"domain_resolver": {
"$ref": "#/$defs/DomainResolver"
},
"network_strategy": {
"type": "string",
"enum": [
"default",
"fallback",
"hybrid"
]
},
"network_type": {
"anyOf": [
{
"$ref": "#/$defs/InterfaceType"
},
{
"type": "array",
"items": {
"$ref": "#/$defs/InterfaceType"
}
}
]
},
"fallback_network_type": {
"anyOf": [
{
"$ref": "#/$defs/InterfaceType"
},
{
"type": "array",
"items": {
"$ref": "#/$defs/InterfaceType"
}
}
]
},
"fallback_delay": {
"$ref": "#/$defs/Duration"
},
"server": {
"type": "string"
},
"server_port": {
"type": "integer",
"minimum": 0,
"maximum": 65535
},
"system": {
"type": "boolean"
},
"name": {
"type": "string"
},
"mtu": {
"type": "integer",
"minimum": 0,
"maximum": 4294967295
},
"udp_mapping": {
"type": "string",
"enum": [
"",
"endpoint_independent",
"address_dependent",
"address_and_port_dependent"
]
},
"udp_filtering": {
"type": "string",
"enum": [
"",
"endpoint_independent",
"address_dependent",
"address_and_port_dependent"
]
},
"udp_nat_max": {
"type": "integer",
"minimum": 0,
"maximum": 4294967295
},
"username": {
"type": "string"
},
"password": {
"type": "string"
},
"tls": {
"$ref": "#/$defs/OutboundTLSOptions"
},
"path": {
"type": "string"
},
"headers": {
"$ref": "#/$defs/HTTPHeader"
},
"version": {
"type": "integer",
"enum": [
0,
1,
2,
3
]
},
"disable_version_fallback": {
"type": "boolean"
},
"advertise_routes": {
"type": "array",
"items": {
"$ref": "#/$defs/IPPrefix"
}
},
"udp_timeout": {
"$ref": "#/$defs/Duration"
},
"on_demand": {
"type": "boolean"
},
"idle_timeout": {
"$ref": "#/$defs/Duration"
},
"keep_alive_period": {
"$ref": "#/$defs/Duration"
},
"stream_receive_window": {
"anyOf": [
{
"type": "integer",
"minimum": 0
},
{
"type": "string"
}
]
},
"connection_receive_window": {
"anyOf": [
{
"type": "integer",
"minimum": 0
},
{
"type": "string"
}
]
},
"max_concurrent_streams": {
"type": "integer"
},
"initial_packet_size": {
"type": "integer"
},
"disable_path_mtu_discovery": {
"type": "boolean"
}
},
"required": [
"type"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"const": "masque-server"
},
"tag": {
"type": "string"
},
"listen": {
"$ref": "#/$defs/IPAddress"
},
"listen_port": {
"type": "integer",
"minimum": 0,
"maximum": 65535
},
"bind_interface": {
"type": "string"
},
"routing_mark": {
"anyOf": [
{
"type": "integer",
"minimum": 0,
"maximum": 4294967295
},
{
"type": "string"
}
]
},
"reuse_addr": {
"type": "boolean"
},
"netns": {
"type": "string",
"x-tag-reference": "network_namespace"
},
"disable_tcp_keep_alive": {
"type": "boolean"
},
"tcp_keep_alive": {
"$ref": "#/$defs/Duration"
},
"tcp_keep_alive_interval": {
"$ref": "#/$defs/Duration"
},
"tcp_fast_open": {
"type": "boolean"
},
"tcp_multi_path": {
"type": "boolean"
},
"udp_fragment": {
"type": "boolean"
},
"udp_timeout": {
"$ref": "#/$defs/UDPTimeout"
},
"detour": {
"type": "string",
"x-tag-reference": "inbound"
},
"system": {
"type": "boolean"
},
"name": {
"type": "string"
},
"mtu": {
"type": "integer",
"minimum": 0,
"maximum": 4294967295
},
"udp_mapping": {
"type": "string",
"enum": [
"",
"endpoint_independent",
"address_dependent",
"address_and_port_dependent"
]
},
"udp_filtering": {
"type": "string",
"enum": [
"",
"endpoint_independent",
"address_dependent",
"address_and_port_dependent"
]
},
"udp_nat_max": {
"type": "integer",
"minimum": 0,
"maximum": 4294967295
},
"users": {
"type": "array",
"items": {
"$ref": "#/$defs/User"
}
},
"version": {
"anyOf": [
{
"type": "integer",
"enum": [
1,
2,
3
]
},
{
"type": "array",
"items": {
"type": "integer",
"enum": [
1,
2,
3
]
}
}
]
},
"tls": {
"$ref": "#/$defs/InboundTLSOptions"
},
"path": {
"type": "string"
},
"address": {
"anyOf": [
{
"$ref": "#/$defs/IPPrefix"
},
{
"type": "array",
"items": {
"$ref": "#/$defs/IPPrefix"
}
}
]
},
"advertise_routes": {
"type": "array",
"items": {
"$ref": "#/$defs/IPPrefix"
}
},
"idle_timeout": {
"$ref": "#/$defs/Duration"
},
"keep_alive_period": {
"$ref": "#/$defs/Duration"
},
"stream_receive_window": {
"anyOf": [
{
"type": "integer",
"minimum": 0
},
{
"type": "string"
}
]
},
"connection_receive_window": {
"anyOf": [
{
"type": "integer",
"minimum": 0
},
{
"type": "string"
}
]
},
"max_concurrent_streams": {
"type": "integer"
},
"initial_packet_size": {
"type": "integer"
},
"disable_path_mtu_discovery": {
"type": "boolean"
}
},
"required": [
"type"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
-1
View File
@@ -8,7 +8,6 @@ import (
"github.com/sagernet/sing-box/adapter/service"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/dns/transport/quic"
_ "github.com/sagernet/sing-box/protocol/http/quic"
"github.com/sagernet/sing-box/protocol/hysteria"
"github.com/sagernet/sing-box/protocol/hysteria2"
_ "github.com/sagernet/sing-box/protocol/naive/quic"
+2
View File
@@ -25,6 +25,7 @@ import (
"github.com/sagernet/sing-box/protocol/direct"
"github.com/sagernet/sing-box/protocol/group"
"github.com/sagernet/sing-box/protocol/http"
"github.com/sagernet/sing-box/protocol/masque"
"github.com/sagernet/sing-box/protocol/mixed"
"github.com/sagernet/sing-box/protocol/naive"
"github.com/sagernet/sing-box/protocol/redirect"
@@ -115,6 +116,7 @@ func EndpointRegistry() *endpoint.Registry {
registerWireGuardEndpoint(registry)
registerOpenConnectEndpoint(registry)
registerOpenVPNEndpoints(registry)
masque.RegisterEndpoint(registry)
registerTailscaleEndpoint(registry)
return registry
+2
View File
@@ -158,6 +158,8 @@ nav:
- OpenConnect Client: configuration/endpoint/openconnect.md
- OpenVPN Client: configuration/endpoint/openvpn-client.md
- OpenVPN Server: configuration/endpoint/openvpn-server.md
- MASQUE Client: configuration/endpoint/masque-client.md
- MASQUE Server: configuration/endpoint/masque-server.md
- Inbound:
- configuration/inbound/index.md
- Direct: configuration/inbound/direct.md
+121
View File
@@ -0,0 +1,121 @@
package option
import (
"context"
"net/netip"
"reflect"
"github.com/sagernet/sing-box/schema"
"github.com/sagernet/sing/common/auth"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
"github.com/sagernet/sing/common/json/badoption"
)
type MASQUEEndpointOptions struct {
System bool `json:"system,omitempty"`
Name string `json:"name,omitempty"`
MTU uint32 `json:"mtu,omitempty"`
UDPMapping UDPNATBehavior `json:"udp_mapping,omitempty"`
UDPFiltering UDPNATBehavior `json:"udp_filtering,omitempty"`
UDPNATMax uint32 `json:"udp_nat_max,omitempty"`
}
type _MASQUEClientEndpointOptions struct {
DialerOptions
ServerOptions
MASQUEEndpointOptions
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
OutboundTLSOptionsContainer
Path string `json:"path,omitempty"`
Headers badoption.HTTPHeader `json:"headers,omitempty"`
Version int `json:"version,omitempty" enum:"0,1,2,3"`
DisableVersionFallback bool `json:"disable_version_fallback,omitempty"`
AdvertiseRoutes []netip.Prefix `json:"advertise_routes,omitempty"`
UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"`
OnDemand bool `json:"on_demand,omitempty"`
HTTP2Options HTTP2Options `json:"-"`
HTTP3Options QUICOptions `json:"-"`
}
type MASQUEClientEndpointOptions _MASQUEClientEndpointOptions
func (o MASQUEClientEndpointOptions) ResolvedVersion() int {
if o.Version == 0 {
return 3
}
return o.Version
}
func (o MASQUEClientEndpointOptions) MarshalJSON() ([]byte, error) {
return badjson.MarshallObjects(_MASQUEClientEndpointOptions(o), httpVersionVariant(o.ResolvedVersion(), o.HTTP2Options, o.HTTP3Options))
}
func (o *MASQUEClientEndpointOptions) UnmarshalJSONContext(ctx context.Context, content []byte) error {
err := json.UnmarshalContext(ctx, content, (*_MASQUEClientEndpointOptions)(o))
if err != nil {
return err
}
return unmarshalHTTPVersionOptions(ctx, content, (*_MASQUEClientEndpointOptions)(o), o.ResolvedVersion(), &o.HTTP2Options, &o.HTTP3Options)
}
func (o MASQUEClientEndpointOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
node := schema.StrictObject()
err := builder.FlattenStruct(node, reflect.TypeFor[MASQUEClientEndpointOptions]())
if err != nil {
return nil, err
}
err = builder.FlattenStruct(node, reflect.TypeFor[QUICOptions]())
if err != nil {
return nil, err
}
return node, nil
}
type _MASQUEServerEndpointOptions struct {
ListenOptions
MASQUEEndpointOptions
Users []auth.User `json:"users,omitempty"`
Version badoption.Listable[int] `json:"version,omitempty" enum:"1,2,3"`
InboundTLSOptionsContainer
Path string `json:"path,omitempty"`
Address badoption.Listable[netip.Prefix] `json:"address"`
AdvertiseRoutes []netip.Prefix `json:"advertise_routes,omitempty"`
HTTP2Options HTTP2Options `json:"-"`
HTTP3Options QUICOptions `json:"-"`
}
type MASQUEServerEndpointOptions _MASQUEServerEndpointOptions
func (o MASQUEServerEndpointOptions) Versions() []int {
if len(o.Version) > 0 {
return o.Version
}
return []int{1, 2, 3}
}
func (o MASQUEServerEndpointOptions) MarshalJSON() ([]byte, error) {
return badjson.MarshallObjects(_MASQUEServerEndpointOptions(o), httpVersionsVariant(o.Versions(), o.HTTP2Options, o.HTTP3Options))
}
func (o *MASQUEServerEndpointOptions) UnmarshalJSONContext(ctx context.Context, content []byte) error {
err := json.UnmarshalContext(ctx, content, (*_MASQUEServerEndpointOptions)(o))
if err != nil {
return err
}
return unmarshalHTTPVersionsOptions(ctx, content, (*_MASQUEServerEndpointOptions)(o), o.Versions(), &o.HTTP2Options, &o.HTTP3Options)
}
func (o MASQUEServerEndpointOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
node := schema.StrictObject()
err := builder.FlattenStruct(node, reflect.TypeFor[MASQUEServerEndpointOptions]())
if err != nil {
return nil, err
}
err = builder.FlattenStruct(node, reflect.TypeFor[QUICOptions]())
if err != nil {
return nil, err
}
return node, nil
}
+3 -23
View File
@@ -2,9 +2,7 @@ package http
import (
"context"
"io"
"net"
std_http "net/http"
"slices"
"github.com/sagernet/sing-box/adapter"
@@ -19,14 +17,9 @@ import (
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/auth"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
N "github.com/sagernet/sing/common/network"
"golang.org/x/net/http2"
)
var ConfigureHTTP3ListenerFunc func(ctx context.Context, logger logger.Logger, listener *listener.Listener, handler std_http.Handler, tlsConfig tls.ServerConfig, options option.QUICOptions) (io.Closer, error)
func RegisterInbound(registry *inbound.Registry) {
inbound.Register[option.HTTPInboundOptions](registry, C.TypeHTTP, NewInbound)
}
@@ -82,15 +75,8 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
if err != nil {
return nil, err
}
if tlsConfig != nil && len(tlsConfig.NextProtos()) == 0 {
var nextProtos []string
if serveHTTP2 {
nextProtos = append(nextProtos, http2.NextProtoTLS)
}
if serveHTTP1 {
nextProtos = append(nextProtos, "http/1.1")
}
tlsConfig.SetNextProtos(nextProtos)
if tlsConfig != nil {
inbound.server.ConfigureTLS(tlsConfig)
}
inbound.tlsConfig = tlsConfig
}
@@ -133,16 +119,10 @@ func (h *Inbound) Start(stage adapter.StartStage, scope *adapter.Scope) error {
}
func (h *Inbound) startHTTP3(scope *adapter.Scope) error {
if ConfigureHTTP3ListenerFunc == nil {
return C.ErrQUICNotIncluded
}
if !slices.Contains(h.tlsConfig.NextProtos(), "h3") {
h.tlsConfig.SetNextProtos(append([]string{"h3"}, h.tlsConfig.NextProtos()...))
}
var metadata adapter.InboundContext
//nolint:staticcheck
metadata.InboundDetour = h.listener.ListenOptions().Detour
http3Server, err := ConfigureHTTP3ListenerFunc(h.ctx, h.logger, h.listener, h.server.HTTP3Handler(adapter.NewUpstreamHandler(metadata, h.newUserConnection, h.streamUserPacketConnection)), h.tlsConfig, h.quicOptions)
http3Server, err := h.server.ListenHTTP3(h.ctx, h.logger, h.listener, adapter.NewUpstreamHandler(metadata, h.newUserConnection, h.streamUserPacketConnection), h.tlsConfig, h.quicOptions)
if err != nil {
return err
}
+2 -48
View File
@@ -3,12 +3,10 @@ package http
import (
"context"
"net"
"slices"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/outbound"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
@@ -19,8 +17,6 @@ import (
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"golang.org/x/net/http2"
)
func RegisterOutbound(registry *outbound.Registry) {
@@ -41,54 +37,12 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
return nil, err
}
headers := options.Headers.Build()
version := http.ResolveVersion(options.Version, options.Path, headers.Get("Host"))
tlsOptions := common.PtrValueOrDefault(options.TLS)
if version == 3 && !tlsOptions.Enabled {
return nil, C.ErrTLSRequired
}
alpnIsDefault := tlsOptions.Enabled && len(tlsOptions.ALPN) == 0
if alpnIsDefault {
switch version {
case 1:
tlsOptions.ALPN = []string{"http/1.1"}
default:
tlsOptions.ALPN = []string{http2.NextProtoTLS, "http/1.1"}
}
}
detour, err := tls.NewDialerFromOptions(ctx, logger, outboundDialer, options.Server, tlsOptions)
if err != nil {
return nil, err
}
var http1Detour N.Dialer
if version >= 2 && (alpnIsDefault || slices.Contains(tlsOptions.ALPN, "http/1.1")) {
http1TLSOptions := tlsOptions
http1TLSOptions.ALPN = []string{"http/1.1"}
http1Detour, err = tls.NewDialerFromOptions(ctx, logger, outboundDialer, options.Server, http1TLSOptions)
if err != nil {
return nil, err
}
}
var quicTLSConfig tls.Config
if version == 3 {
if alpnIsDefault {
tlsOptions.ALPN = []string{"h3"}
}
quicTLSConfig, err = tls.NewClient(ctx, logger, options.Server, tlsOptions)
if err != nil {
return nil, err
}
}
client, err := http.NewClient(http.ClientOptions{
Dialer: detour,
HTTP1Dialer: http1Detour,
RawDialer: outboundDialer,
TLSConfig: quicTLSConfig,
Server: options.ServerOptions.Build(),
client, err := http.NewClientWithTLS(ctx, logger, outboundDialer, options.ServerOptions, common.PtrValueOrDefault(options.TLS), http.ClientOptions{
Username: options.Username,
Password: options.Password,
Path: options.Path,
Headers: headers,
Version: version,
Version: http.ResolveVersion(options.Version, options.Path, headers.Get("Host")),
DisableVersionFallback: options.DisableVersionFallback,
HTTP2Options: options.HTTP2Options,
HTTP3Options: options.HTTP3Options,
+374
View File
@@ -0,0 +1,374 @@
package masque
import (
"context"
"math"
"net"
"net/netip"
"slices"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/iponly"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/service/oomkiller"
"github.com/sagernet/sing-box/transport/device"
"github.com/sagernet/sing-box/transport/http"
"github.com/sagernet/sing-box/transport/masque"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing-tun/gtcpip/header"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json/badoption"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
)
const defaultKeepAlivePeriod = 10 * time.Second
var (
_ adapter.OutboundWithPreferredRoutes = (*ClientEndpoint)(nil)
_ adapter.FlowOutbound = (*ClientEndpoint)(nil)
_ adapter.InterfaceUpdateListener = (*ClientEndpoint)(nil)
_ adapter.OnDemandEndpoint = (*ClientEndpoint)(nil)
_ dialer.PacketDialerWithDestination = (*ClientEndpoint)(nil)
_ masque.ClientHandler = (*ClientEndpoint)(nil)
)
type ClientEndpoint struct {
endpointBase
ctx context.Context
dnsRouter adapter.DNSRouter
client *masque.Client
deviceOptions *device.Options
device device.Device
mtu uint32
onDemand bool
stateAccess sync.Mutex
deviceStarted bool
state atomic.Pointer[clientState]
}
type clientState struct {
configured bool
localAddresses []netip.Prefix
routes []masque.AddressRange
}
func NewClientEndpoint(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.MASQUEClientEndpointOptions) (adapter.Endpoint, error) {
if options.MTU == 0 {
options.MTU = masque.DefaultMTU
}
version := options.ResolvedVersion()
http2Options := options.HTTP2Options
if version == 3 {
http2Options = options.HTTP3Options.HTTP2Options
if options.Version == 0 && http.NewHTTP3Client == nil {
version = 2
}
}
if http2Options.KeepAlivePeriod == 0 {
http2Options.KeepAlivePeriod = badoption.Duration(defaultKeepAlivePeriod)
}
options.HTTP3Options.HTTP2Options = http2Options
if options.HTTP3Options.InitialPacketSize == 0 {
options.HTTP3Options.InitialPacketSize = min(int(options.MTU)+masque.QUICPacketOverhead, math.MaxUint16)
}
outboundDialer, err := dialer.NewWithOptions(dialer.Options{
Context: ctx,
Options: options.DialerOptions,
RemoteIsDomain: options.ServerIsDomain(),
ResolverOnDetour: true,
NewDialer: true,
})
if err != nil {
return nil, err
}
headers := options.Headers.Build()
authority := headers.Get("Host")
headers.Del("Host")
if authority == "" {
server := options.ServerOptions.Build()
authority = server.String()
if server.Port == 443 {
authority = server.AddrString()
if server.IsIPv6() {
authority = "[" + authority + "]"
}
}
}
httpClient, err := http.NewClientWithTLS(ctx, logger, outboundDialer, options.ServerOptions, common.PtrValueOrDefault(options.TLS), http.ClientOptions{
Authority: authority,
Username: options.Username,
Password: options.Password,
Headers: headers,
Version: version,
DisableVersionFallback: options.DisableVersionFallback,
HTTP2Options: http2Options,
HTTP3Options: options.HTTP3Options,
})
if err != nil {
return nil, err
}
clientEndpoint := &ClientEndpoint{
endpointBase: endpointBase{
Adapter: endpoint.NewAdapterWithDialerOptions(C.TypeMASQUEClient, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, options.DialerOptions),
router: router,
logger: logger,
},
ctx: ctx,
dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
mtu: options.MTU,
onDemand: options.OnDemand,
}
clientEndpoint.state.Store(&clientState{})
clientEndpoint.deviceOptions = newDeviceOptions(ctx, logger, clientEndpoint, options.MASQUEEndpointOptions, time.Duration(options.UDPTimeout), nil)
clientEndpoint.client, err = masque.NewClient(masque.ClientOptions{
Context: ctx,
Logger: logger,
HTTPClient: httpClient,
Path: options.Path,
AdvertiseRoutes: options.AdvertiseRoutes,
Handler: clientEndpoint,
})
if err != nil {
httpClient.Close()
return nil, err
}
return clientEndpoint, nil
}
func (c *ClientEndpoint) Start(stage adapter.StartStage, scope *adapter.Scope) error {
switch stage {
case adapter.StartStateInitialize:
c.deviceOptions.MemoryPressure = oomkiller.MemoryPressure(c.ctx)
tunnelDevice, err := device.New(*c.deviceOptions)
if err != nil {
return err
}
scope.Add(tunnelDevice.Close)
tunnelDevice.SetPacketWriter(c.writePacketBuffers)
c.device = tunnelDevice
c.deviceOptions = nil
scope.Add(c.client.Close)
case adapter.StartStatePostStart:
c.client.Start()
}
return nil
}
func (c *ClientEndpoint) UpdateConfiguration(configuration masque.Configuration) error {
c.stateAccess.Lock()
defer c.stateAccess.Unlock()
err := c.device.UpdateConfiguration(device.Configuration{
MTU: c.mtu,
Address: configuration.Address,
})
if err != nil {
return E.Cause(err, "update device configuration")
}
if !c.deviceStarted {
err = c.device.Start()
if err != nil {
return E.Cause(err, "start device")
}
c.deviceStarted = true
}
if !slices.Equal(c.state.Load().localAddresses, configuration.Address) {
c.logger.Info("assigned ", strings.Join(common.Map(configuration.Address, netip.Prefix.String), " "))
}
c.state.Store(&clientState{
configured: true,
localAddresses: configuration.Address,
routes: configuration.Routes,
})
return nil
}
func (c *ClientEndpoint) WriteInboundBuffers(packetBuffers []*buf.Buffer) error {
if !c.state.Load().configured {
buf.ReleaseMulti(packetBuffers)
return nil
}
err := c.device.WriteInboundBuffers(packetBuffers)
buf.ReleaseMulti(packetBuffers)
return err
}
func (c *ClientEndpoint) InterfaceUpdated(ctx context.Context) {
c.client.RestartSession()
}
func (c *ClientEndpoint) OnDemand() bool {
return c.onDemand
}
func (c *ClientEndpoint) SetKeepIdleConnections(keep bool) {
if !keep {
c.client.Suspend()
}
}
func (c *ClientEndpoint) waitReady(ctx context.Context) error {
if !c.onDemand {
if !c.client.Ready() {
return E.New("endpoint is not ready yet")
}
return nil
}
c.client.Resume()
waitCtx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
defer cancel()
return c.client.WaitReady(waitCtx)
}
func (c *ClientEndpoint) PreMatchFlow(network string, destination netip.Addr) adapter.PreMatchAction {
return adapter.PreMatchFlow
}
func (c *ClientEndpoint) PortAddresses() (netip.Addr, netip.Addr) {
return c.device.PortAddresses()
}
func (c *ClientEndpoint) PortMTU() uint32 {
return c.device.PortMTU()
}
func (c *ClientEndpoint) AttachReturn(returnPath tun.Return) error {
return c.device.AttachReturn(returnPath)
}
func (c *ClientEndpoint) DetachReturn(returnPath tun.Return) error {
return c.device.DetachReturn(returnPath)
}
func (c *ClientEndpoint) JudgeFlow(network uint8, source netip.AddrPort, destination netip.AddrPort, firstPacket []byte) tun.FlowVerdict {
return c.judgeFlow(c, c.state.Load().localAddresses, network, source, destination, firstPacket)
}
func (c *ClientEndpoint) NewDNSPacket(payload []byte, source M.Socksaddr, destination M.Socksaddr, writer N.PacketWriter) {
c.newDNSPacket(log.ContextWithNewID(c.ctx), c, payload, source, destination, writer)
}
func (c *ClientEndpoint) WritePackets(packets [][]byte) error {
if c.onDemand {
c.client.Resume()
}
if !c.client.Ready() {
return E.New("endpoint is not ready yet")
}
return c.client.WritePacketBuffers(common.Map(packets, func(packet []byte) *buf.Buffer {
packetBuffer := buf.NewSize(masque.PacketHeadroom + len(packet))
packetBuffer.Resize(masque.PacketHeadroom, 0)
packetBuffer.Write(packet)
return packetBuffer
}), true)
}
func (c *ClientEndpoint) writePacketBuffers(packetBuffers []*buf.Buffer) error {
if c.onDemand {
c.client.Resume()
}
return c.client.WritePacketBuffers(packetBuffers, false)
}
func (c *ClientEndpoint) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
c.newConnection(ctx, c, c.state.Load().localAddresses, conn, source, destination, onClose)
}
func (c *ClientEndpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
c.newPacketConnection(ctx, c, c.state.Load().localAddresses, conn, source, destination, onClose)
}
func (c *ClientEndpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
switch network {
case N.NetworkTCP:
c.logger.InfoContext(ctx, "outbound connection to ", destination)
case N.NetworkUDP:
c.logger.InfoContext(ctx, "outbound packet connection to ", destination)
}
err := c.waitReady(ctx)
if err != nil {
return nil, err
}
if destination.IsDomain() {
destinationAddresses, lookupErr := c.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{})
if lookupErr != nil {
return nil, lookupErr
}
return N.DialSerial(ctx, c.device, network, destination, destinationAddresses)
}
if !destination.Addr.IsValid() {
return nil, E.New("invalid destination: ", destination)
}
return c.device.DialContext(ctx, network, destination)
}
func (c *ClientEndpoint) ListenPacketWithDestination(ctx context.Context, destination M.Socksaddr) (net.PacketConn, netip.Addr, error) {
c.logger.InfoContext(ctx, "outbound packet connection to ", destination)
err := c.waitReady(ctx)
if err != nil {
return nil, netip.Addr{}, err
}
if destination.IsDomain() {
destinationAddresses, lookupErr := c.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{})
if lookupErr != nil {
return nil, netip.Addr{}, lookupErr
}
packetConn, destinationAddress, listenErr := N.ListenSerial(ctx, c.device, destination, destinationAddresses)
if listenErr != nil {
return nil, netip.Addr{}, listenErr
}
return iponly.NewPacketConn(c.logger, packetConn), destinationAddress, nil
}
packetConn, err := c.device.ListenPacket(ctx, destination)
if err != nil {
return nil, netip.Addr{}, err
}
if destination.IsIP() {
return iponly.NewPacketConn(c.logger, packetConn), destination.Addr, nil
}
return iponly.NewPacketConn(c.logger, packetConn), netip.Addr{}, nil
}
func (c *ClientEndpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
packetConn, destinationAddress, err := c.ListenPacketWithDestination(ctx, destination)
if err != nil {
return nil, err
}
if destinationAddress.IsValid() && destination != M.SocksaddrFrom(destinationAddress, destination.Port) {
return bufio.NewNATPacketConn(bufio.NewPacketConn(packetConn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil
}
return packetConn, nil
}
func (c *ClientEndpoint) PreferredDomain(metadata *adapter.InboundContext, domain string) bool {
return false
}
func (c *ClientEndpoint) PreferredAddress(metadata *adapter.InboundContext, address netip.Addr) bool {
state := c.state.Load()
if !state.configured || !c.client.Ready() {
return false
}
var protocol uint8
switch metadata.Network {
case N.NetworkTCP:
protocol = uint8(header.TCPProtocolNumber)
case N.NetworkUDP:
protocol = uint8(header.UDPProtocolNumber)
case N.NetworkICMP:
protocol = uint8(header.ICMPv4ProtocolNumber)
}
return masque.RoutesContain(state.routes, address, protocol)
}
+122
View File
@@ -0,0 +1,122 @@
package masque
import (
"context"
"net"
"net/netip"
"slices"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/endpoint"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/transport/device"
"github.com/sagernet/sing-box/transport/masque"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
)
func RegisterEndpoint(registry *endpoint.Registry) {
endpoint.Register[option.MASQUEClientEndpointOptions](registry, C.TypeMASQUEClient, NewClientEndpoint)
endpoint.Register[option.MASQUEServerEndpointOptions](registry, C.TypeMASQUEServer, NewServerEndpoint)
}
type endpointBase struct {
endpoint.Adapter
router adapter.Router
logger log.ContextLogger
}
func newDeviceOptions(ctx context.Context, logger log.ContextLogger, handler tun.Handler, options option.MASQUEEndpointOptions, udpTimeout time.Duration, address []netip.Prefix) *device.Options {
if udpTimeout == 0 {
udpTimeout = C.UDPTimeout
}
return &device.Options{
Context: ctx,
Logger: logger,
System: options.System,
Handler: handler,
UDPTimeout: udpTimeout,
ICMPTimeout: C.ICMPTimeout,
UDPMapping: tun.NATMapping(options.UDPMapping),
UDPFiltering: tun.NATFiltering(options.UDPFiltering),
UDPNATMax: options.UDPNATMax,
InterfaceFinder: service.FromContext[adapter.NetworkManager](ctx).InterfaceFinder(),
Name: options.Name,
NamePrefix: "masque",
MTU: options.MTU,
PacketHeadroom: masque.PacketHeadroom,
Configuration: device.Configuration{
MTU: options.MTU,
Address: address,
},
}
}
func (e *endpointBase) newConnection(ctx context.Context, endpoint adapter.Endpoint, localAddresses []netip.Prefix, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
var metadata adapter.InboundContext
metadata.Inbound = endpoint.Tag()
metadata.InboundType = endpoint.Type()
metadata.Source = source
if isLocalAddress(localAddresses, destination.Addr) {
metadata.OriginDestination = destination
destination.Addr = loopbackAddressFor(destination.Addr)
}
metadata.Destination = destination
e.logger.InfoContext(ctx, "inbound connection from ", source)
e.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
e.router.RouteConnectionEx(ctx, conn, metadata, onClose)
}
func (e *endpointBase) newPacketConnection(ctx context.Context, endpoint adapter.Endpoint, localAddresses []netip.Prefix, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
var metadata adapter.InboundContext
metadata.Inbound = endpoint.Tag()
metadata.InboundType = endpoint.Type()
metadata.Source = source
if isLocalAddress(localAddresses, destination.Addr) {
metadata.OriginDestination = destination
destination.Addr = loopbackAddressFor(destination.Addr)
conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, destination)
}
metadata.Destination = destination
e.logger.InfoContext(ctx, "inbound packet connection from ", source)
e.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)
e.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
}
func (e *endpointBase) newDNSPacket(ctx context.Context, endpoint adapter.Endpoint, payload []byte, source M.Socksaddr, destination M.Socksaddr, writer N.PacketWriter) {
var metadata adapter.InboundContext
metadata.Inbound = endpoint.Tag()
metadata.InboundType = endpoint.Type()
metadata.Network = N.NetworkUDP
metadata.Source = source
metadata.Destination = destination
metadata.Protocol = C.ProtocolDNS
e.logger.InfoContext(ctx, "inbound DNS packet from ", source)
e.router.HijackDNSPacket(ctx, payload, writer, metadata)
}
func (e *endpointBase) judgeFlow(endpoint adapter.Endpoint, localAddresses []netip.Prefix, network uint8, source netip.AddrPort, destination netip.AddrPort, firstPacket []byte) tun.FlowVerdict {
if isLocalAddress(localAddresses, destination.Addr()) {
return tun.FlowVerdict{Action: tun.ActionAccept}
}
return adapter.JudgeFlow(e.router, adapter.InboundContext{Inbound: endpoint.Tag(), InboundType: endpoint.Type()}, network, source, destination, firstPacket)
}
func isLocalAddress(localAddresses []netip.Prefix, address netip.Addr) bool {
return slices.ContainsFunc(localAddresses, func(localPrefix netip.Prefix) bool {
return address == localPrefix.Addr()
})
}
func loopbackAddressFor(address netip.Addr) netip.Addr {
if address.Is4() {
return netip.AddrFrom4([4]uint8{127, 0, 0, 1})
}
return netip.IPv6Loopback()
}
+325
View File
@@ -0,0 +1,325 @@
package masque
import (
"context"
"math"
"net"
"net/netip"
"slices"
"sync/atomic"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/iponly"
"github.com/sagernet/sing-box/common/listener"
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/service/oomkiller"
"github.com/sagernet/sing-box/transport/device"
"github.com/sagernet/sing-box/transport/http"
"github.com/sagernet/sing-box/transport/masque"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/auth"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
)
var (
_ adapter.OutboundWithPreferredRoutes = (*ServerEndpoint)(nil)
_ adapter.FlowOutbound = (*ServerEndpoint)(nil)
_ adapter.ConnectionHandler = (*serverConnectionHandler)(nil)
_ dialer.PacketDialerWithDestination = (*ServerEndpoint)(nil)
_ masque.ServerHandler = (*ServerEndpoint)(nil)
)
type ServerEndpoint struct {
endpointBase
ctx context.Context
dnsRouter adapter.DNSRouter
listener *listener.Listener
httpServer *http.Server
tlsConfig tls.ServerConfig
http3 bool
quicOptions option.QUICOptions
server *masque.Server
deviceOptions *device.Options
device device.Device
localAddresses []netip.Prefix
started atomic.Bool
}
func NewServerEndpoint(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.MASQUEServerEndpointOptions) (adapter.Endpoint, error) {
if options.MTU == 0 {
options.MTU = masque.DefaultMTU
}
versions := options.Versions()
if len(options.Version) == 0 && http.ConfigureHTTP3ListenerFunc == nil {
logger.Warn("QUIC is not included in this build, HTTP/3 is disabled")
versions = []int{1, 2}
}
serveHTTP1 := slices.Contains(versions, 1)
serveHTTP2 := slices.Contains(versions, 2)
serveHTTP3 := slices.Contains(versions, 3)
if serveHTTP3 && (options.TLS == nil || !options.TLS.Enabled) {
return nil, E.New("TLS is required for HTTP/3")
}
if options.HTTP3Options.InitialPacketSize == 0 {
options.HTTP3Options.InitialPacketSize = min(int(options.MTU)+masque.QUICPacketOverhead, math.MaxUint16)
}
serverEndpoint := &ServerEndpoint{
endpointBase: endpointBase{
Adapter: endpoint.NewAdapter(C.TypeMASQUEServer, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, nil),
router: router,
logger: logger,
},
ctx: ctx,
dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
http3: serveHTTP3,
quicOptions: options.HTTP3Options,
localAddresses: options.Address,
}
server, err := masque.NewServer(masque.ServerOptions{
Context: ctx,
Logger: logger,
Path: options.Path,
Address: options.Address,
AdvertiseRoutes: options.AdvertiseRoutes,
Resolve: serverEndpoint.resolve,
Handler: serverEndpoint,
})
if err != nil {
return nil, err
}
serverEndpoint.server = server
serverEndpoint.httpServer = http.NewServer(http.ServerOptions{
Authenticator: auth.NewAuthenticator(options.Users),
Logger: logger,
HTTP1: serveHTTP1,
HTTP2: serveHTTP2,
HTTP2Options: options.HTTP2Options,
Tunnels: map[string]http.TunnelHandler{"connect-ip": server},
})
if options.TLS != nil {
tlsConfig, tlsErr := tls.NewServerWithOptions(tls.ServerOptions{
Context: ctx,
Logger: logger,
Options: common.PtrValueOrDefault(options.TLS),
KTLSCompatible: true,
})
if tlsErr != nil {
return nil, tlsErr
}
if tlsConfig != nil {
serverEndpoint.httpServer.ConfigureTLS(tlsConfig)
}
serverEndpoint.tlsConfig = tlsConfig
}
var network []string
if serveHTTP1 || serveHTTP2 {
network = []string{N.NetworkTCP}
}
serverEndpoint.listener = listener.New(listener.Options{
Context: ctx,
Logger: logger,
Network: network,
Listen: options.ListenOptions,
ConnectionHandler: (*serverConnectionHandler)(serverEndpoint),
})
serverEndpoint.deviceOptions = newDeviceOptions(ctx, logger, serverEndpoint, options.MASQUEEndpointOptions, time.Duration(options.UDPTimeout), options.Address)
return serverEndpoint, nil
}
func (s *ServerEndpoint) resolve(ctx context.Context, domain string) ([]netip.Addr, error) {
return s.dnsRouter.Lookup(ctx, domain, adapter.DNSQueryOptions{})
}
func (s *ServerEndpoint) Start(stage adapter.StartStage, scope *adapter.Scope) error {
switch stage {
case adapter.StartStateInitialize:
s.deviceOptions.MemoryPressure = oomkiller.MemoryPressure(s.ctx)
tunnelDevice, err := device.New(*s.deviceOptions)
if err != nil {
return err
}
scope.Add(tunnelDevice.Close)
tunnelDevice.SetPacketWriter(s.writePacketBuffers)
s.device = tunnelDevice
s.deviceOptions = nil
scope.Add(s.server.Close)
case adapter.StartStateStart:
if s.tlsConfig != nil {
err := s.tlsConfig.Start()
if err != nil {
return E.Cause(err, "create TLS config")
}
scope.Add(s.tlsConfig.Close)
}
err := s.device.Start()
if err != nil {
return E.Cause(err, "start device")
}
err = s.listener.Start()
if err != nil {
return err
}
scope.Add(s.listener.Close)
if s.http3 {
http3Server, listenErr := s.httpServer.ListenHTTP3(s.ctx, s.logger, s.listener, nil, s.tlsConfig, s.quicOptions)
if listenErr != nil {
return listenErr
}
scope.Add(http3Server.Close)
}
s.started.Store(true)
scope.Add(func() error {
s.started.Store(false)
return nil
})
}
return nil
}
type serverConnectionHandler ServerEndpoint
func (h *serverConnectionHandler) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
if h.tlsConfig != nil {
tlsConn, err := tls.ServerHandshake(ctx, conn, h.tlsConfig)
if err != nil {
N.CloseOnHandshakeFailure(conn, onClose, err)
h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source, ": TLS handshake"))
return
}
conn = tlsConn
}
h.httpServer.ServeConnection(ctx, conn, http.NewReader(conn), nil, metadata.Source, onClose)
}
func (s *ServerEndpoint) WriteInboundBuffers(packetBuffers []*buf.Buffer) error {
err := s.device.WriteInboundBuffers(packetBuffers)
buf.ReleaseMulti(packetBuffers)
return err
}
func (s *ServerEndpoint) PreMatchFlow(network string, destination netip.Addr) adapter.PreMatchAction {
return adapter.PreMatchFlow
}
func (s *ServerEndpoint) PortAddresses() (netip.Addr, netip.Addr) {
return s.device.PortAddresses()
}
func (s *ServerEndpoint) PortMTU() uint32 {
return s.device.PortMTU()
}
func (s *ServerEndpoint) AttachReturn(returnPath tun.Return) error {
return s.device.AttachReturn(returnPath)
}
func (s *ServerEndpoint) DetachReturn(returnPath tun.Return) error {
return s.device.DetachReturn(returnPath)
}
func (s *ServerEndpoint) JudgeFlow(network uint8, source netip.AddrPort, destination netip.AddrPort, firstPacket []byte) tun.FlowVerdict {
return s.judgeFlow(s, s.localAddresses, network, source, destination, firstPacket)
}
func (s *ServerEndpoint) NewDNSPacket(payload []byte, source M.Socksaddr, destination M.Socksaddr, writer N.PacketWriter) {
s.newDNSPacket(log.ContextWithNewID(s.ctx), s, payload, source, destination, writer)
}
func (s *ServerEndpoint) WritePackets(packets [][]byte) error {
if !s.started.Load() {
return E.New("endpoint is not ready yet")
}
return s.server.WritePacketBuffers(common.Map(packets, buf.As), true)
}
func (s *ServerEndpoint) writePacketBuffers(packetBuffers []*buf.Buffer) error {
return s.server.WritePacketBuffers(packetBuffers, false)
}
func (s *ServerEndpoint) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
s.newConnection(ctx, s, s.localAddresses, conn, source, destination, onClose)
}
func (s *ServerEndpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
s.newPacketConnection(ctx, s, s.localAddresses, conn, source, destination, onClose)
}
func (s *ServerEndpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
switch network {
case N.NetworkTCP:
s.logger.InfoContext(ctx, "outbound connection to ", destination)
case N.NetworkUDP:
s.logger.InfoContext(ctx, "outbound packet connection to ", destination)
}
if !s.started.Load() {
return nil, E.New("endpoint is not ready yet")
}
if destination.IsDomain() {
destinationAddresses, err := s.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{})
if err != nil {
return nil, err
}
return N.DialSerial(ctx, s.device, network, destination, destinationAddresses)
}
if !destination.Addr.IsValid() {
return nil, E.New("invalid destination: ", destination)
}
return s.device.DialContext(ctx, network, destination)
}
func (s *ServerEndpoint) ListenPacketWithDestination(ctx context.Context, destination M.Socksaddr) (net.PacketConn, netip.Addr, error) {
s.logger.InfoContext(ctx, "outbound packet connection to ", destination)
if !s.started.Load() {
return nil, netip.Addr{}, E.New("endpoint is not ready yet")
}
if destination.IsDomain() {
destinationAddresses, err := s.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{})
if err != nil {
return nil, netip.Addr{}, err
}
packetConn, destinationAddress, err := N.ListenSerial(ctx, s.device, destination, destinationAddresses)
if err != nil {
return nil, netip.Addr{}, err
}
return iponly.NewPacketConn(s.logger, packetConn), destinationAddress, nil
}
packetConn, err := s.device.ListenPacket(ctx, destination)
if err != nil {
return nil, netip.Addr{}, err
}
if destination.IsIP() {
return iponly.NewPacketConn(s.logger, packetConn), destination.Addr, nil
}
return iponly.NewPacketConn(s.logger, packetConn), netip.Addr{}, nil
}
func (s *ServerEndpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
packetConn, destinationAddress, err := s.ListenPacketWithDestination(ctx, destination)
if err != nil {
return nil, err
}
if destinationAddress.IsValid() && destination != M.SocksaddrFrom(destinationAddress, destination.Port) {
return bufio.NewNATPacketConn(bufio.NewPacketConn(packetConn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil
}
return packetConn, nil
}
func (s *ServerEndpoint) PreferredDomain(metadata *adapter.InboundContext, domain string) bool {
return false
}
func (s *ServerEndpoint) PreferredAddress(metadata *adapter.InboundContext, address netip.Addr) bool {
return s.started.Load() && s.server.Contains(address)
}
+14 -11
View File
@@ -20,6 +20,7 @@ import (
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/service/oomkiller"
"github.com/sagernet/sing-box/transport/device"
openconnecttransport "github.com/sagernet/sing-box/transport/openconnect"
"github.com/sagernet/sing-openconnect"
"github.com/sagernet/sing-tun"
@@ -49,8 +50,8 @@ type Endpoint struct {
cancelLoop context.CancelFunc
dnsRouter adapter.DNSRouter
client *openconnect.Client
deviceOptions *openconnecttransport.DeviceOptions
device openconnecttransport.Device
deviceOptions *device.Options
device device.Device
onDemand bool
server string
flavor string
@@ -159,7 +160,7 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
udpTimeout = time.Duration(options.UDPTimeout)
}
networkManager := service.FromContext[adapter.NetworkManager](ctx)
openConnectEndpoint.deviceOptions = &openconnecttransport.DeviceOptions{
openConnectEndpoint.deviceOptions = &device.Options{
Context: ctx,
Logger: logger,
System: options.System,
@@ -171,8 +172,10 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
UDPNATMax: options.UDPNATMax,
InterfaceFinder: networkManager.InterfaceFinder(),
Name: options.Name,
NamePrefix: "oc",
MTU: openconnecttransport.DefaultMTU,
Configuration: openconnecttransport.Configuration{
PacketHeadroom: openconnecttransport.PacketHeadroom,
Configuration: device.Configuration{
MTU: openconnecttransport.DefaultMTU,
},
}
@@ -350,9 +353,9 @@ func (e *Endpoint) handleTunnelConfiguration(event openconnect.TunnelConfigurati
if err != nil {
return E.Cause(err, "build route set")
}
err = e.device.UpdateConfiguration(openconnecttransport.Configuration{
MTU: configuration.MTU,
Addresses: configuration.Addresses,
err = e.device.UpdateConfiguration(device.Configuration{
MTU: configuration.MTU,
Address: configuration.Addresses,
})
if err != nil {
return E.Cause(err, "update device configuration")
@@ -424,13 +427,13 @@ func (e *Endpoint) Start(stage adapter.StartStage, scope *adapter.Scope) error {
return nil
})
e.deviceOptions.MemoryPressure = oomkiller.MemoryPressure(e.loopContext)
device, err := openconnecttransport.NewDevice(*e.deviceOptions)
tunnelDevice, err := device.New(*e.deviceOptions)
if err != nil {
return err
}
scope.Add(device.Close)
device.SetPacketWriter(e.writePacketBuffers)
e.device = device
scope.Add(tunnelDevice.Close)
tunnelDevice.SetPacketWriter(e.writePacketBuffers)
e.device = tunnelDevice
e.deviceOptions = nil
case adapter.StartStatePostStart:
var loopGroup sync.WaitGroup
+18 -11
View File
@@ -18,6 +18,7 @@ import (
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/service/oomkiller"
"github.com/sagernet/sing-box/transport/device"
ovpntransport "github.com/sagernet/sing-box/transport/openvpn"
ovpn "github.com/sagernet/sing-openvpn"
"github.com/sagernet/sing-tun"
@@ -51,8 +52,8 @@ type ClientEndpoint struct {
outboundDialer N.Dialer
queryOptions adapter.DNSQueryOptions
client *ovpn.Client
deviceOptions *ovpntransport.DeviceOptions
device ovpntransport.Device
deviceOptions *device.Options
device device.Device
onDemand bool
stateAccess sync.Mutex
state atomic.Pointer[clientState]
@@ -122,7 +123,11 @@ func NewClientEndpoint(ctx context.Context, router adapter.Router, logger log.Co
if options.UDPTimeout != 0 {
udpTimeout = time.Duration(options.UDPTimeout)
}
clientEndpoint.deviceOptions = &ovpntransport.DeviceOptions{
deviceMTU := options.MTU
if deviceMTU == 0 {
deviceMTU = ovpntransport.DefaultMTU
}
clientEndpoint.deviceOptions = &device.Options{
Context: ctx,
Logger: logger,
System: options.System,
@@ -134,9 +139,11 @@ func NewClientEndpoint(ctx context.Context, router adapter.Router, logger log.Co
UDPNATMax: options.UDPNATMax,
InterfaceFinder: service.FromContext[adapter.NetworkManager](ctx).InterfaceFinder(),
Name: options.Name,
MTU: options.MTU,
Configuration: ovpntransport.Configuration{
MTU: options.MTU,
NamePrefix: "ovpn",
MTU: deviceMTU,
PacketHeadroom: ovpntransport.PacketHeadroom,
Configuration: device.Configuration{
MTU: deviceMTU,
Address: clientOptions.Tunnel.LocalAddress,
},
}
@@ -507,7 +514,7 @@ func (c *ClientEndpoint) handleTunnelConfiguration(event ovpn.TunnelConfiguratio
c.updateState(func(state *clientState) {
state.tunnelConfigured = false
})
deviceConfiguration := ovpntransport.Configuration{
deviceConfiguration := device.Configuration{
MTU: configuration.MTU,
Address: configuration.Address,
BlockIPv6: configuration.BlockIPv6,
@@ -603,13 +610,13 @@ func (c *ClientEndpoint) Start(stage adapter.StartStage, scope *adapter.Scope) e
return nil
})
c.deviceOptions.MemoryPressure = oomkiller.MemoryPressure(c.ctx)
device, err := ovpntransport.NewDevice(*c.deviceOptions)
tunnelDevice, err := device.New(*c.deviceOptions)
if err != nil {
return err
}
scope.Add(device.Close)
device.SetPacketWriter(c.writePacketBuffers)
c.device = device
scope.Add(tunnelDevice.Close)
tunnelDevice.SetPacketWriter(c.writePacketBuffers)
c.device = tunnelDevice
c.deviceOptions = nil
case adapter.StartStatePostStart:
var loopGroup sync.WaitGroup
+14 -12
View File
@@ -19,6 +19,7 @@ import (
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/service/oomkiller"
"github.com/sagernet/sing-box/transport/device"
ovpntransport "github.com/sagernet/sing-box/transport/openvpn"
ovpn "github.com/sagernet/sing-openvpn"
"github.com/sagernet/sing-tun"
@@ -43,8 +44,8 @@ type ServerEndpoint struct {
dnsRouter adapter.DNSRouter
listener *listener.Listener
server *ovpn.Server
deviceOptions *ovpntransport.DeviceOptions
device ovpntransport.Device
deviceOptions *device.Options
device device.Device
localAddresses []netip.Prefix
started atomic.Bool
}
@@ -102,7 +103,7 @@ func NewServerEndpoint(ctx context.Context, router adapter.Router, logger log.Co
if options.UDPTimeout != 0 {
udpTimeout = time.Duration(options.UDPTimeout)
}
serverEndpoint.deviceOptions = &ovpntransport.DeviceOptions{
serverEndpoint.deviceOptions = &device.Options{
Context: ctx,
Logger: logger,
System: options.System,
@@ -114,11 +115,12 @@ func NewServerEndpoint(ctx context.Context, router adapter.Router, logger log.Co
UDPNATMax: options.UDPNATMax,
InterfaceFinder: service.FromContext[adapter.NetworkManager](ctx).InterfaceFinder(),
Name: options.Name,
NamePrefix: "ovpn",
MTU: options.MTU,
Configuration: ovpntransport.Configuration{
MTU: options.MTU,
Address: options.Address,
Topology: options.Topology,
PacketHeadroom: ovpntransport.PacketHeadroom,
Configuration: device.Configuration{
MTU: options.MTU,
Address: options.Address,
},
}
return serverEndpoint, nil
@@ -162,13 +164,13 @@ func (s *ServerEndpoint) Start(stage adapter.StartStage, scope *adapter.Scope) e
return nil
})
s.deviceOptions.MemoryPressure = oomkiller.MemoryPressure(s.ctx)
device, err := ovpntransport.NewDevice(*s.deviceOptions)
tunnelDevice, err := device.New(*s.deviceOptions)
if err != nil {
return err
}
scope.Add(device.Close)
device.SetPacketWriter(s.writePacketBuffersByDestination)
s.device = device
scope.Add(tunnelDevice.Close)
tunnelDevice.SetPacketWriter(s.writePacketBuffersByDestination)
s.device = tunnelDevice
s.deviceOptions = nil
return nil
}
@@ -696,7 +698,7 @@ func (s *ServerEndpoint) writeRouteMisses(routeMisses []*ovpn.RouteMissError) {
replies := make([][]byte, 0, len(routeMisses))
for _, routeMiss := range routeMisses {
sourceAddress := packetSourceAddress(routeMiss.Packet, inet4Address, inet6Address)
reply, built := tun.BuildUnreachable(routeMiss.Packet, sourceAddress, headroom)
reply, built := tun.BuildICMPError(routeMiss.Packet, tun.ICMPErrorNoRoute, sourceAddress, 0, headroom)
if built {
replies = append(replies, reply)
}
+1 -1
View File
@@ -133,7 +133,7 @@ func (t *Endpoint) WritePackets(packets [][]byte) error {
if header.IPVersion(packet) == header.IPv6Version {
source = inet6Address
}
reply, replyOk := tun.BuildUnreachable(packet, source, headroom)
reply, replyOk := tun.BuildICMPError(packet, tun.ICMPErrorNoRoute, source, 0, headroom)
if replyOk {
replies = append(replies, reply)
}
+24 -80
View File
@@ -8,8 +8,7 @@ import (
"testing"
"time"
openconnecttransport "github.com/sagernet/sing-box/transport/openconnect"
openvpntransport "github.com/sagernet/sing-box/transport/openvpn"
"github.com/sagernet/sing-box/transport/device"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing-tun/gtcpip/header"
"github.com/sagernet/sing/common/buf"
@@ -20,13 +19,6 @@ import (
"github.com/stretchr/testify/require"
)
type endpointUDPNATDevice struct {
start func() error
writeInboundBuffers func([]*buf.Buffer) error
setPacketWriter func(func([]*buf.Buffer) error)
close func() error
}
type endpointUDPNATPacket struct {
session *endpointUDPNATSession
destination M.Socksaddr
@@ -87,78 +79,30 @@ func (h *endpointUDPNATHandler) NewPacketConnectionEx(_ context.Context, conn N.
}()
}
func TestOpenVPNEndpointUDPNATDataPlane(t *testing.T) {
testEndpointUDPNATDataPlane(t, func(ctx context.Context, handler tun.Handler) (endpointUDPNATDevice, error) {
device, err := openvpntransport.NewDevice(openvpntransport.DeviceOptions{
Context: ctx,
Logger: logger.NOP(),
Handler: handler,
UDPTimeout: time.Minute,
UDPMapping: tun.NATMappingAddressAndPortDependent,
UDPFiltering: tun.NATFilteringAddressAndPortDependent,
UDPNATMax: 1,
MTU: 1500,
Configuration: openvpntransport.Configuration{
MTU: 1500,
Address: []netip.Prefix{netip.MustParsePrefix("10.8.0.1/24")},
},
})
if err != nil {
return endpointUDPNATDevice{}, err
}
return endpointUDPNATDevice{
start: device.Start,
writeInboundBuffers: device.WriteInboundBuffers,
setPacketWriter: func(writer func([]*buf.Buffer) error) {
device.SetPacketWriter(openvpntransport.PacketWriter(writer))
},
close: device.Close,
}, nil
})
}
func TestOpenConnectEndpointUDPNATDataPlane(t *testing.T) {
testEndpointUDPNATDataPlane(t, func(ctx context.Context, handler tun.Handler) (endpointUDPNATDevice, error) {
device, err := openconnecttransport.NewDevice(openconnecttransport.DeviceOptions{
Context: ctx,
Logger: logger.NOP(),
Handler: handler,
UDPTimeout: time.Minute,
UDPMapping: tun.NATMappingAddressAndPortDependent,
UDPFiltering: tun.NATFilteringAddressAndPortDependent,
UDPNATMax: 1,
MTU: 1500,
Configuration: openconnecttransport.Configuration{
MTU: 1500,
Addresses: []netip.Prefix{netip.MustParsePrefix("10.8.0.1/24")},
},
})
if err != nil {
return endpointUDPNATDevice{}, err
}
return endpointUDPNATDevice{
start: device.Start,
writeInboundBuffers: device.WriteInboundBuffers,
setPacketWriter: func(writer func([]*buf.Buffer) error) {
device.SetPacketWriter(openconnecttransport.PacketWriter(writer))
},
close: device.Close,
}, nil
})
}
func testEndpointUDPNATDataPlane(t *testing.T, newDevice func(context.Context, tun.Handler) (endpointUDPNATDevice, error)) {
t.Helper()
func TestEndpointDeviceUDPNATDataPlane(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
handler := &endpointUDPNATHandler{packets: make(chan endpointUDPNATPacket, 4)}
device, err := newDevice(ctx, handler)
tunnelDevice, err := device.New(device.Options{
Context: ctx,
Logger: logger.NOP(),
Handler: handler,
UDPTimeout: time.Minute,
UDPMapping: tun.NATMappingAddressAndPortDependent,
UDPFiltering: tun.NATFilteringAddressAndPortDependent,
UDPNATMax: 1,
MTU: 1500,
Configuration: device.Configuration{
MTU: 1500,
Address: []netip.Prefix{netip.MustParsePrefix("10.8.0.1/24")},
},
})
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, device.close())
require.NoError(t, tunnelDevice.Close())
})
outboundPackets := make(chan []byte, 4)
device.setPacketWriter(func(packetBuffers []*buf.Buffer) error {
tunnelDevice.SetPacketWriter(func(packetBuffers []*buf.Buffer) error {
for _, packetBuffer := range packetBuffers {
payload, isUDP := endpointUDPPayload(packetBuffer.Bytes())
packetBuffer.Release()
@@ -172,9 +116,9 @@ func testEndpointUDPNATDataPlane(t *testing.T, newDevice func(context.Context, t
source := netip.MustParseAddrPort("10.8.0.2:40000")
firstDestination := netip.MustParseAddrPort("192.0.2.1:5001")
secondDestination := netip.MustParseAddrPort("192.0.2.1:5002")
writeEndpointUDPPacket(t, device, source, firstDestination, []byte("before-start"))
require.NoError(t, device.start())
writeEndpointUDPPacket(t, device, source, firstDestination, []byte("request-one"))
writeEndpointUDPPacket(t, tunnelDevice, source, firstDestination, []byte("before-start"))
require.NoError(t, tunnelDevice.Start())
writeEndpointUDPPacket(t, tunnelDevice, source, firstDestination, []byte("request-one"))
firstPacket := waitEndpointUDPNATPacket(t, handler.packets)
require.Equal(t, M.SocksaddrFromNetIP(firstDestination), firstPacket.destination)
require.Equal(t, []byte("request-one"), firstPacket.payload)
@@ -183,7 +127,7 @@ func testEndpointUDPNATDataPlane(t *testing.T, newDevice func(context.Context, t
require.NoError(t, firstPacket.session.conn.WritePacket(endpointUDPPacketBuffer(firstPacket.session.conn, "allowed-one"), M.SocksaddrFromNetIP(firstDestination)))
require.Equal(t, []byte("allowed-one"), waitEndpointUDPResponse(t, outboundPackets))
writeEndpointUDPPacket(t, device, source, secondDestination, []byte("request-two"))
writeEndpointUDPPacket(t, tunnelDevice, source, secondDestination, []byte("request-two"))
secondPacket := waitEndpointUDPNATPacket(t, handler.packets)
require.Equal(t, M.SocksaddrFromNetIP(secondDestination), secondPacket.destination)
require.Equal(t, []byte("request-two"), secondPacket.payload)
@@ -198,7 +142,7 @@ func testEndpointUDPNATDataPlane(t *testing.T, newDevice func(context.Context, t
require.Equal(t, []byte("allowed-two"), waitEndpointUDPResponse(t, outboundPackets))
}
func writeEndpointUDPPacket(t *testing.T, device endpointUDPNATDevice, source netip.AddrPort, destination netip.AddrPort, payload []byte) {
func writeEndpointUDPPacket(t *testing.T, tunnelDevice device.Device, source netip.AddrPort, destination netip.AddrPort, payload []byte) {
t.Helper()
packet := make([]byte, header.IPv4MinimumSize+header.UDPMinimumSize+len(payload))
ipHeader := header.IPv4(packet)
@@ -218,7 +162,7 @@ func writeEndpointUDPPacket(t *testing.T, device endpointUDPNATDevice, source ne
})
copy(udpHeader.Payload(), payload)
packetBuffer := buf.As(packet)
require.NoError(t, device.writeInboundBuffers([]*buf.Buffer{packetBuffer}))
require.NoError(t, tunnelDevice.WriteInboundBuffers([]*buf.Buffer{packetBuffer}))
packetBuffer.Release()
}
+287
View File
@@ -0,0 +1,287 @@
package main
import (
std_bufio "bufio"
"context"
"crypto/tls"
"encoding/base64"
"io"
"net"
"net/http"
"net/netip"
"strconv"
"sync/atomic"
"testing"
"time"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/auth"
"github.com/sagernet/sing/common/json/badoption"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/protocol/socks"
"github.com/stretchr/testify/require"
)
const (
masqueServerAddress = "10.8.0.1"
masqueSiteAddress = "10.9.0.5"
)
type masqueEnvironment struct {
serverPort uint16
serverProxyPort uint16
clientProxyPort uint16
}
func startMASQUE(t *testing.T, serverVersions []int, clientVersion int, disableVersionFallback bool, mtu uint32, serverRoutes []netip.Prefix, clientRoutes []netip.Prefix) masqueEnvironment {
t.Helper()
environment := masqueEnvironment{
serverProxyPort: reserveOpenVPNTCPPort(t),
clientProxyPort: reserveOpenVPNTCPPort(t),
}
masquePort := reserveOpenVPNEchoPort(t)
environment.serverPort = masquePort
_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
users := []auth.User{{Username: "sekai", Password: "password"}}
startInstance(t, masqueInstanceOptions(C.TypeMASQUEServer, &option.MASQUEServerEndpointOptions{
ListenOptions: option.ListenOptions{
Listen: common.Ptr(badoption.Addr(netip.MustParseAddr("127.0.0.1"))),
ListenPort: masquePort,
},
MASQUEEndpointOptions: option.MASQUEEndpointOptions{MTU: mtu},
Users: users,
Version: serverVersions,
InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{
TLS: &option.InboundTLSOptions{
Enabled: true,
ServerName: "example.org",
CertificatePath: certPem,
KeyPath: keyPem,
},
},
Address: []netip.Prefix{netip.MustParsePrefix(masqueServerAddress + "/24")},
AdvertiseRoutes: serverRoutes,
}, environment.serverProxyPort, ""))
startInstance(t, masqueInstanceOptions(C.TypeMASQUEClient, &option.MASQUEClientEndpointOptions{
ServerOptions: option.ServerOptions{
Server: "127.0.0.1",
ServerPort: masquePort,
},
MASQUEEndpointOptions: option.MASQUEEndpointOptions{MTU: mtu},
Username: users[0].Username,
Password: users[0].Password,
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
TLS: &option.OutboundTLSOptions{
Enabled: true,
ServerName: "example.org",
CertificatePath: certPem,
},
},
Version: clientVersion,
DisableVersionFallback: disableVersionFallback,
AdvertiseRoutes: clientRoutes,
}, environment.clientProxyPort, "127.0.0.1"))
waitForOpenVPNClientReady(t, environment.clientProxyPort, reserveOpenVPNEchoPort(t), masqueServerAddress)
return environment
}
func masqueInstanceOptions(endpointType string, endpointOptions any, proxyPort uint16, overrideAddress string) option.Options {
return option.Options{
Endpoints: []option.Endpoint{
{
Type: endpointType,
Tag: "masque",
Options: endpointOptions,
},
},
Inbounds: []option.Inbound{
{
Type: C.TypeSOCKS,
Tag: "socks-in",
Options: &option.SocksInboundOptions{
ListenOptions: option.ListenOptions{
Listen: common.Ptr(badoption.Addr(netip.MustParseAddr("127.0.0.1"))),
ListenPort: proxyPort,
},
},
},
},
Outbounds: []option.Outbound{
{
Type: C.TypeDirect,
Tag: "direct",
},
},
Route: &option.RouteOptions{
Rules: []option.Rule{
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultRule{
RawDefaultRule: option.RawDefaultRule{
Inbound: []string{"socks-in"},
},
RuleAction: option.RuleAction{
Action: C.RuleActionTypeRoute,
RouteOptions: option.RouteActionOptions{
Outbound: "masque",
},
},
},
},
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultRule{
RawDefaultRule: option.RawDefaultRule{
Inbound: []string{"masque"},
},
RuleAction: option.RuleAction{
Action: C.RuleActionTypeRoute,
RouteOptions: option.RouteActionOptions{
Outbound: "direct",
RawRouteOptionsActionOptions: option.RawRouteOptionsActionOptions{
OverrideAddress: overrideAddress,
},
},
},
},
},
},
},
}
}
func TestMASQUESelfToSelf(t *testing.T) {
for _, testCase := range []struct {
name string
version int
}{
{"HTTP3", 3},
{"HTTP2", 2},
{"HTTP1", 1},
} {
t.Run(testCase.name, func(t *testing.T) {
environment := startMASQUE(t, nil, testCase.version, true, 0, nil, nil)
testSuitOpenVPN(t, environment.clientProxyPort, reserveOpenVPNEchoPort(t), masqueServerAddress)
})
}
}
func TestMASQUEVersionFallback(t *testing.T) {
environment := startMASQUE(t, []int{1, 2}, 0, false, 0, nil, nil)
testSuitOpenVPN(t, environment.clientProxyPort, reserveOpenVPNEchoPort(t), masqueServerAddress)
}
func TestMASQUEAdvertiseRoutes(t *testing.T) {
environment := startMASQUE(t, nil, 0, false, 0, nil, []netip.Prefix{netip.MustParsePrefix("10.9.0.0/24")})
readinessPort := reserveOpenVPNEchoPort(t)
closeEcho := startOpenVPNReadinessEcho(t, readinessPort)
waitForOpenVPNRemoteReady(t, environment.serverProxyPort, masqueSiteAddress, readinessPort, 30*time.Second)
closeEcho()
testSuitOpenVPN(t, environment.serverProxyPort, reserveOpenVPNEchoPort(t), masqueSiteAddress)
}
func TestMASQUENoRoute(t *testing.T) {
environment := startMASQUE(t, nil, 0, false, 0, []netip.Prefix{netip.MustParsePrefix("10.9.0.0/24")}, nil)
for _, proxyPort := range []uint16{environment.clientProxyPort, environment.serverProxyPort} {
dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", proxyPort), socks.Version5, "", "")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
conn, err := dialer.DialContext(ctx, N.NetworkTCP, M.ParseSocksaddrHostPort("10.10.0.1", 80))
if err == nil {
conn.Close()
}
require.Error(t, err)
require.NoError(t, ctx.Err())
cancel()
}
}
func TestMASQUEPacketTooBig(t *testing.T) {
environment := startMASQUE(t, nil, 3, true, 1500, nil, nil)
testSuitOpenVPN(t, environment.clientProxyPort, reserveOpenVPNEchoPort(t), masqueServerAddress)
}
func TestMASQUEReconnectBackoff(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
var accepted atomic.Int32
go func() {
for {
conn, acceptErr := listener.Accept()
if acceptErr != nil {
return
}
accepted.Add(1)
conn.Close()
}
}()
t.Cleanup(func() {
listener.Close()
})
startInstance(t, masqueInstanceOptions(C.TypeMASQUEClient, &option.MASQUEClientEndpointOptions{
ServerOptions: option.ServerOptions{
Server: "127.0.0.1",
ServerPort: uint16(listener.Addr().(*net.TCPAddr).Port),
},
Version: 1,
}, reserveOpenVPNTCPPort(t), ""))
time.Sleep(2500 * time.Millisecond)
require.Positive(t, accepted.Load())
require.LessOrEqual(t, accepted.Load(), int32(3))
}
func TestMASQUEStuckClient(t *testing.T) {
environment := startMASQUE(t, nil, 0, false, 0, nil, nil)
stuckConn, err := tls.Dial("tcp", "127.0.0.1:"+strconv.Itoa(int(environment.serverPort)), &tls.Config{
ServerName: "example.org",
InsecureSkipVerify: true,
NextProtos: []string{"http/1.1"},
})
require.NoError(t, err)
t.Cleanup(func() {
stuckConn.Close()
})
_, err = stuckConn.Write([]byte("GET /.well-known/masque/ip/*/*/ HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: Upgrade\r\nUpgrade: connect-ip\r\nCapsule-Protocol: ?1\r\nAuthorization: Basic " + base64.StdEncoding.EncodeToString([]byte("sekai:password")) + "\r\n\r\n"))
require.NoError(t, err)
reader := std_bufio.NewReader(stuckConn)
response, err := http.ReadResponse(reader, nil)
require.NoError(t, err)
require.Equal(t, http.StatusSwitchingProtocols, response.StatusCode)
require.Equal(t, uint64(1), readVarint(t, reader))
capsuleLength := readVarint(t, reader)
capsule := make([]byte, capsuleLength)
_, err = io.ReadFull(reader, capsule)
require.NoError(t, err)
require.Equal(t, byte(4), capsule[1])
stuckAddress := netip.AddrFrom4([4]byte(capsule[2:6]))
dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", environment.serverProxyPort), socks.Version5, "", "")
floodConn, err := dialer.DialContext(context.Background(), N.NetworkUDP, M.SocksaddrFrom(stuckAddress, 9))
require.NoError(t, err)
t.Cleanup(func() {
floodConn.Close()
})
floodDone := make(chan struct{})
go func() {
defer close(floodDone)
payload := make([]byte, 1200)
for range 4096 {
_, writeErr := floodConn.Write(payload)
if writeErr != nil {
return
}
}
}()
select {
case <-floodDone:
case <-time.After(10 * time.Second):
}
echoPort := reserveOpenVPNEchoPort(t)
closeEcho := startOpenVPNReadinessEcho(t, echoPort)
defer closeEcho()
require.NoError(t, probeOpenVPNTCPWithTimeout(environment.clientProxyPort, masqueServerAddress, echoPort, 5*time.Second))
}
@@ -1,4 +1,4 @@
package openvpn
package device
import (
"context"
@@ -14,11 +14,6 @@ import (
N "github.com/sagernet/sing/common/network"
)
const (
DefaultMTU = 1500
PacketHeadroom = 4096
)
type PacketWriter func(packetBuffers []*buf.Buffer) error
type Device interface {
@@ -35,7 +30,7 @@ type Device interface {
Close() error
}
type DeviceOptions struct {
type Options struct {
Context context.Context
Logger logger.ContextLogger
System bool
@@ -48,47 +43,26 @@ type DeviceOptions struct {
InterfaceFinder control.InterfaceFinder
MemoryPressure func() tun.MemoryPressure
Name string
NamePrefix string
MTU uint32
PacketHeadroom int
Configuration Configuration
}
type Configuration struct {
MTU uint32
Address []netip.Prefix
Routes []Route
ExcludedRoutes []Route
DNS []netip.Addr
DNSServers []DNSServer
SearchDomains []string
DNSRoutes []string
Topology string
Interface string
BlockIPv6 bool
MTU uint32
Address []netip.Prefix
BlockIPv6 bool
}
type Route struct {
Prefix netip.Prefix
Gateway netip.Addr
Metric int
}
type DNSServer struct {
Priority int
Addresses []netip.AddrPort
ResolveDomains []string
DNSSEC string
Transport string
SNI string
}
func NewDevice(options DeviceOptions) (Device, error) {
func New(options Options) (Device, error) {
if !options.System {
return newStackDevice(options)
}
return newSystemStackDevice(options)
}
func newStack(options DeviceOptions, memoryTun *tun.MemoryTun) (*tun.Go, error) {
func newStack(options Options, memoryTun *tun.MemoryTun) (*tun.Go, error) {
return tun.NewGo(tun.StackOptions{
Context: options.Context,
Tun: memoryTun,
@@ -106,8 +80,9 @@ func newStack(options DeviceOptions, memoryTun *tun.MemoryTun) (*tun.Go, error)
}
type baseDevice struct {
packetWriter PacketWriter
returnState atomic.Pointer[returnPathState]
packetHeadroom int
packetWriter PacketWriter
returnState atomic.Pointer[returnPathState]
}
func (d *baseDevice) SetPacketWriter(writer PacketWriter) {
@@ -150,8 +125,8 @@ func (d *baseDevice) processInboundBuffers(packetBuffers []*buf.Buffer, writeBuf
func (d *baseDevice) AttachReturn(returnPath tun.Return) error {
headroom := returnPath.ReturnHeadroom()
if headroom > PacketHeadroom {
return E.New("return path headroom ", headroom, " exceeds available ", PacketHeadroom)
if headroom > d.packetHeadroom {
return E.New("return path headroom ", headroom, " exceeds available ", d.packetHeadroom)
}
newState := &returnPathState{
returnPath: returnPath,
@@ -217,12 +192,3 @@ func splitPrefixes(prefixes []netip.Prefix) ([]netip.Prefix, []netip.Prefix) {
}
return inet4Prefixes, inet6Prefixes
}
func hasRouteOptions(routes []Route) bool {
for _, route := range routes {
if route.Gateway.IsValid() || route.Metric != 0 {
return true
}
}
return false
}
@@ -1,4 +1,4 @@
package openconnect
package device
import (
"context"
@@ -19,7 +19,7 @@ var _ Device = (*stackDevice)(nil)
type stackDevice struct {
baseDevice
stateAccess sync.RWMutex
options DeviceOptions
options Options
stack *tun.Go
memoryTun *tun.MemoryTun
inet4Address netip.Addr
@@ -27,17 +27,15 @@ type stackDevice struct {
closeOnce sync.Once
}
func newStackDevice(options DeviceOptions) (*stackDevice, error) {
if options.MTU == 0 {
options.MTU = DefaultMTU
}
func newStackDevice(options Options) (*stackDevice, error) {
device := &stackDevice{
options: options,
baseDevice: baseDevice{packetHeadroom: options.PacketHeadroom},
options: options,
}
device.inet4Address, device.inet6Address = firstAddresses(options.Configuration.Addresses)
device.inet4Address, device.inet6Address = firstAddresses(options.Configuration.Address)
device.memoryTun = tun.NewMemoryTun(tun.MemoryTunOptions{
MTU: int(options.MTU),
Headroom: PacketHeadroom,
Headroom: options.PacketHeadroom,
RearSpace: systemDevicePacketRearSpace,
Outbound: device.writeOutbound,
})
@@ -61,7 +59,7 @@ func (d *stackDevice) UpdateConfiguration(configuration Configuration) error {
d.memoryTun.UpdateMTU(int(configuration.MTU))
}
d.options.Configuration = configuration
d.inet4Address, d.inet6Address = firstAddresses(configuration.Addresses)
d.inet4Address, d.inet6Address = firstAddresses(configuration.Address)
return nil
}
@@ -120,6 +118,9 @@ func (d *stackDevice) bindAddress(destination M.Socksaddr) (netip.Addr, error) {
}
return d.inet4Address, nil
}
if d.options.Configuration.BlockIPv6 {
return netip.Addr{}, E.New("IPv6 is blocked")
}
if !d.inet6Address.IsValid() {
return netip.Addr{}, E.New("missing IPv6 local address")
}
@@ -1,4 +1,4 @@
package openvpn
package device
import (
"context"
@@ -30,7 +30,7 @@ const (
type systemDevice struct {
baseDevice
stateAccess sync.RWMutex
options DeviceOptions
options Options
dialer N.Dialer
device tun.Tun
inet4Address netip.Addr
@@ -38,12 +38,9 @@ type systemDevice struct {
closed bool
}
func newSystemDevice(options DeviceOptions) (*systemDevice, error) {
func newSystemDevice(options Options) (*systemDevice, error) {
if options.Name == "" {
options.Name = tun.CalculateInterfaceName("ovpn")
}
if options.MTU == 0 {
options.MTU = DefaultMTU
options.Name = tun.CalculateInterfaceName(options.NamePrefix)
}
interfaceDialer, err := dialer.NewDefault(options.Context, option.DialerOptions{
AbstractDialerOptions: option.AbstractDialerOptions{
@@ -55,6 +52,7 @@ func newSystemDevice(options DeviceOptions) (*systemDevice, error) {
}
inet4Address, inet6Address := firstAddresses(options.Configuration.Address)
return &systemDevice{
baseDevice: baseDevice{packetHeadroom: options.PacketHeadroom},
options: options,
dialer: interfaceDialer,
inet4Address: inet4Address,
@@ -128,11 +126,11 @@ func (d *systemDevice) readLoop(tunInterface tun.Tun, mtu int) {
d.readLoopDarwin(darwinTUN)
return
}
packetBuffer := buf.NewSize(PacketHeadroom + systemDeviceReadBufferSize + systemDevicePacketRearSpace)
packetBuffer := buf.NewSize(d.packetHeadroom + systemDeviceReadBufferSize + systemDevicePacketRearSpace)
defer packetBuffer.Release()
for {
packetBuffer.Reset()
packetBuffer.Resize(PacketHeadroom, 0)
packetBuffer.Resize(d.packetHeadroom, 0)
readN, err := tunInterface.Read(packetBuffer.FreeBytes()[:systemDeviceReadBufferSize])
if err != nil {
if E.IsClosed(err) {
@@ -164,13 +162,13 @@ func (d *systemDevice) readLoopLinux(tunInterface tun.LinuxTUN, batchSize int, m
packetSizes := make([]int, batchSize)
outboundBuffers := make([]*buf.Buffer, 0, batchSize)
for i := range packetBuffers {
packetBuffers[i] = buf.NewSize(PacketHeadroom + mtu + systemDevicePacketRearSpace)
packetBuffers[i] = buf.NewSize(d.packetHeadroom + mtu + systemDevicePacketRearSpace)
}
defer buf.ReleaseMulti(packetBuffers)
for {
for i, packetBuffer := range packetBuffers {
packetBuffer.Reset()
packetBuffer.Resize(PacketHeadroom, 0)
packetBuffer.Resize(d.packetHeadroom, 0)
readBuffers[i] = packetBuffer.FreeBytes()[:mtu]
}
packetCount, readErr := tunInterface.BatchRead(readBuffers, 0, packetSizes)
@@ -1,4 +1,4 @@
package openconnect
package device
import (
"net/netip"
@@ -15,7 +15,7 @@ type systemStackDevice struct {
stackDevice *stackDevice
}
func newSystemStackDevice(options DeviceOptions) (*systemStackDevice, error) {
func newSystemStackDevice(options Options) (*systemStackDevice, error) {
system, err := newSystemDevice(options)
if err != nil {
return nil, err
+39 -26
View File
@@ -14,17 +14,18 @@ import (
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
const (
capsuleTypeDatagram = 0
capsuleHeadroom = 1 + 4 + 1
maxCapsuleLength = 1 << 20
CapsuleTypeDatagram = 0
CapsuleHeadroom = 1 + 4 + 1
MaxCapsuleLength = 1 << 20
)
func readVarint(reader *std_bufio.Reader) (uint64, int, error) {
func ReadVarint(reader *std_bufio.Reader) (uint64, int, error) {
first, err := reader.ReadByte()
if err != nil {
return 0, 0, err
@@ -42,7 +43,7 @@ func readVarint(reader *std_bufio.Reader) (uint64, int, error) {
return value, length, nil
}
func decodeVarint(b []byte) (uint64, int, bool) {
func DecodeVarint(b []byte) (uint64, int, bool) {
if len(b) == 0 {
return 0, 0, false
}
@@ -57,7 +58,7 @@ func decodeVarint(b []byte) (uint64, int, bool) {
return value, length, true
}
func varintLen(value uint64) int {
func VarintLen(value uint64) int {
switch {
case value < 1<<6:
return 1
@@ -70,8 +71,8 @@ func varintLen(value uint64) int {
}
}
func putVarint(b []byte, value uint64) int {
switch varintLen(value) {
func PutVarint(b []byte, value uint64) int {
switch VarintLen(value) {
case 1:
b[0] = byte(value)
return 1
@@ -89,25 +90,25 @@ func putVarint(b []byte, value uint64) int {
func readDatagramCapsule(reader *std_bufio.Reader, buffer *buf.Buffer) error {
for {
capsuleType, _, err := readVarint(reader)
capsuleType, _, err := ReadVarint(reader)
if err != nil {
return err
}
length, _, err := readVarint(reader)
length, _, err := ReadVarint(reader)
if err != nil {
return err
}
if length > maxCapsuleLength {
if length > MaxCapsuleLength {
return E.New("capsule too large: ", length)
}
if capsuleType != capsuleTypeDatagram {
if capsuleType != CapsuleTypeDatagram {
_, err = reader.Discard(int(length))
if err != nil {
return err
}
continue
}
contextID, contextLength, err := readVarint(reader)
contextID, contextLength, err := ReadVarint(reader)
if err != nil {
return err
}
@@ -127,7 +128,7 @@ func readDatagramCapsule(reader *std_bufio.Reader, buffer *buf.Buffer) error {
}
}
func prependContextID(buffer *buf.Buffer) *buf.Buffer {
func PrependContextID(buffer *buf.Buffer) *buf.Buffer {
if buffer.Start() >= 1 {
buffer.ExtendHeader(1)[0] = 0
return buffer
@@ -139,20 +140,20 @@ func prependContextID(buffer *buf.Buffer) *buf.Buffer {
return datagram
}
func writeDatagramCapsule(writer io.Writer, datagram *buf.Buffer) error {
func WriteDatagramCapsule(writer io.Writer, datagram *buf.Buffer) error {
length := uint64(datagram.Len())
headerLength := 1 + varintLen(length)
headerLength := 1 + VarintLen(length)
var capsule *buf.Buffer
if datagram.Start() >= headerLength {
header := datagram.ExtendHeader(headerLength)
header[0] = capsuleTypeDatagram
putVarint(header[1:], length)
header[0] = CapsuleTypeDatagram
PutVarint(header[1:], length)
capsule = datagram
} else {
capsule = buf.NewSize(headerLength + datagram.Len())
header := capsule.Extend(headerLength)
header[0] = capsuleTypeDatagram
putVarint(header[1:], length)
header[0] = CapsuleTypeDatagram
PutVarint(header[1:], length)
capsule.Write(datagram.Bytes())
datagram.Release()
}
@@ -189,7 +190,7 @@ func (c *capsuleConn) ReadPacket(buffer *buf.Buffer) (M.Socksaddr, error) {
func (c *capsuleConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
c.writeAccess.Lock()
defer c.writeAccess.Unlock()
return writeDatagramCapsule(c.writer, prependContextID(buffer))
return WriteDatagramCapsule(c.writer, PrependContextID(buffer))
}
func (c *capsuleConn) Close() error {
@@ -217,7 +218,7 @@ func (c *capsuleConn) NeedAdditionalReadDeadline() bool {
}
func (c *capsuleConn) FrontHeadroom() int {
return capsuleHeadroom
return CapsuleHeadroom
}
func (c *capsuleConn) Upstream() any {
@@ -237,6 +238,18 @@ var (
ErrDatagramUnsupported = E.New("datagram unsupported")
)
type DatagramTooLargeError struct {
MaxPayloadSize int
}
func (e *DatagramTooLargeError) Error() string {
return F.ToString("datagram too large, maximum payload size is ", e.MaxPayloadSize)
}
func (e *DatagramTooLargeError) Unwrap() error {
return ErrDatagramUnsupported
}
type http3PacketConn struct {
stream DatagramStream
reader *std_bufio.Reader
@@ -276,7 +289,7 @@ func (c *http3PacketConn) loopDatagram() {
c.closeWithError(err)
return
}
contextID, contextLength, valid := decodeVarint(datagram)
contextID, contextLength, valid := DecodeVarint(datagram)
if !valid || contextID != 0 {
continue
}
@@ -328,7 +341,7 @@ func (c *http3PacketConn) ReadPacket(buffer *buf.Buffer) (M.Socksaddr, error) {
}
func (c *http3PacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
datagram := prependContextID(buffer)
datagram := PrependContextID(buffer)
err := c.stream.SendDatagram(datagram.Bytes())
if err == nil {
datagram.Release()
@@ -340,7 +353,7 @@ func (c *http3PacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksadd
}
c.writeAccess.Lock()
defer c.writeAccess.Unlock()
return writeDatagramCapsule(c.stream, datagram)
return WriteDatagramCapsule(c.stream, datagram)
}
func (c *http3PacketConn) closeWithError(err error) {
@@ -396,7 +409,7 @@ func (c *http3PacketConn) NeedAdditionalReadDeadline() bool {
}
func (c *http3PacketConn) FrontHeadroom() int {
return capsuleHeadroom
return CapsuleHeadroom
}
var _ N.PacketConn = (*http3PacketConn)(nil)
+92 -72
View File
@@ -10,18 +10,21 @@ import (
"net/http"
"net/url"
"os"
"slices"
"sync"
"sync/atomic"
"time"
"github.com/sagernet/sing-box/common/badhttp"
"github.com/sagernet/sing-box/common/httpclient"
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
"github.com/sagernet/sing/common/bufio/deadline"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
aTLS "github.com/sagernet/sing/common/tls"
@@ -39,6 +42,7 @@ type ClientOptions struct {
RawDialer N.Dialer
TLSConfig aTLS.Config
Server M.Socksaddr
Authority string
Username string
Password string
Path string
@@ -51,7 +55,7 @@ type ClientOptions struct {
type http3Client interface {
DialContext(ctx context.Context, destination M.Socksaddr) (net.Conn, error)
ListenPacket(ctx context.Context, destination M.Socksaddr) (N.PacketConn, error)
OpenTunnel(ctx context.Context, request tunnelRequest) (DatagramStream, error)
ResetConnection()
Close() error
}
@@ -59,24 +63,64 @@ type http3Client interface {
var NewHTTP3Client func(options ClientOptions, authorization string) (http3Client, error)
type Client struct {
dialer N.Dialer
http1Dialer N.Dialer
tlsDialer tlsDialer
server M.Socksaddr
authorization string
host string
path string
headers http.Header
version int
disableVersionFallback bool
http2Transport *http2.Transport
http2Access sync.Mutex
http2Conns []*http2ClientConn
http2Unsupported atomic.Bool
http2ConnectUDPUnsupported atomic.Bool
http3 http3Client
http3Broken atomic.Int64
http3Backoff atomic.Int64
dialer N.Dialer
http1Dialer N.Dialer
tlsDialer tlsDialer
server M.Socksaddr
authorityOverride string
authorization string
host string
path string
headers http.Header
version int
disableVersionFallback bool
http2Transport *http2.Transport
http2Access sync.Mutex
http2Conns []*http2ClientConn
http2Unsupported atomic.Bool
http2ExtendedConnectUnsupported atomic.Bool
http3 http3Client
http3Broken atomic.Int64
http3Backoff atomic.Int64
}
func NewClientWithTLS(ctx context.Context, logger logger.ContextLogger, outboundDialer N.Dialer, serverOptions option.ServerOptions, tlsOptions option.OutboundTLSOptions, options ClientOptions) (*Client, error) {
if options.Version == 3 && !tlsOptions.Enabled {
return nil, C.ErrTLSRequired
}
alpnIsDefault := tlsOptions.Enabled && len(tlsOptions.ALPN) == 0
if alpnIsDefault {
if options.Version == 1 {
tlsOptions.ALPN = []string{"http/1.1"}
} else {
tlsOptions.ALPN = []string{http2.NextProtoTLS, "http/1.1"}
}
}
var err error
options.Dialer, err = tls.NewDialerFromOptions(ctx, logger, outboundDialer, serverOptions.Server, tlsOptions)
if err != nil {
return nil, err
}
if options.Version >= 2 && (alpnIsDefault || slices.Contains(tlsOptions.ALPN, "http/1.1")) {
http1TLSOptions := tlsOptions
http1TLSOptions.ALPN = []string{"http/1.1"}
options.HTTP1Dialer, err = tls.NewDialerFromOptions(ctx, logger, outboundDialer, serverOptions.Server, http1TLSOptions)
if err != nil {
return nil, err
}
}
if options.Version == 3 {
if alpnIsDefault {
tlsOptions.ALPN = []string{"h3"}
}
options.TLSConfig, err = tls.NewClient(ctx, logger, serverOptions.Server, tlsOptions)
if err != nil {
return nil, err
}
}
options.RawDialer = outboundDialer
options.Server = serverOptions.Build()
return NewClient(options)
}
func NewClient(options ClientOptions) (*Client, error) {
@@ -84,6 +128,7 @@ func NewClient(options ClientOptions) (*Client, error) {
dialer: options.Dialer,
http1Dialer: options.HTTP1Dialer,
server: options.Server,
authorityOverride: options.Authority,
path: options.Path,
headers: options.Headers.Clone(),
version: options.Version,
@@ -223,7 +268,7 @@ func (c *Client) ResetConnections() {
c.closeHTTP2Locked()
c.http2Access.Unlock()
c.http2Unsupported.Store(false)
c.http2ConnectUDPUnsupported.Store(false)
c.http2ExtendedConnectUnsupported.Store(false)
if c.http3 != nil {
c.http3.ResetConnection()
c.clearHTTP3Broken()
@@ -253,13 +298,7 @@ func (c *Client) connect(ctx context.Context, conn net.Conn, destination M.Socks
return nil, err
}
}
maps.Copy(request.Header, c.headers)
if _, loaded := request.Header["User-Agent"]; !loaded {
request.Header["User-Agent"] = nil
}
if c.authorization != "" {
request.Header.Set("Proxy-Authorization", c.authorization)
}
maps.Copy(request.Header, buildRequestHeader(c.headers, c.authorization, false))
err := request.Write(conn)
if err != nil {
return nil, E.Cause(err, "write request")
@@ -287,6 +326,24 @@ func (c *Client) connect(ctx context.Context, conn net.Conn, destination M.Socks
return conn, nil
}
func buildRequestHeader(headers http.Header, authorization string, originAuthorization bool) http.Header {
header := headers.Clone()
if header == nil {
header = make(http.Header)
}
if _, loaded := header["User-Agent"]; !loaded {
header["User-Agent"] = nil
}
if authorization != "" {
if originAuthorization {
header.Set("Authorization", authorization)
} else {
header.Set("Proxy-Authorization", authorization)
}
}
return header
}
func (c *Client) Close() error {
c.http2Access.Lock()
defer c.http2Access.Unlock()
@@ -328,55 +385,18 @@ func (c *Client) ListenPacket(ctx context.Context, destination M.Socksaddr) (net
}
func (c *Client) listenPacket(ctx context.Context, destination M.Socksaddr) (N.PacketConn, error) {
if c.http3Available() {
packetConn, err := c.http3.ListenPacket(ctx, destination)
if err == nil {
c.clearHTTP3Broken()
return packetConn, nil
}
if c.disableVersionFallback || !errors.Is(err, ErrHTTP3Unavailable) {
return nil, err
}
c.markHTTP3Broken()
}
if c.tlsDialer != nil && !c.http2Unsupported.Load() && !c.http2ConnectUDPUnsupported.Load() {
clientConn, conn, err := c.acquireHTTP2(ctx)
if err != nil {
return nil, err
}
if clientConn != nil {
packetConn, connectErr := c.connectUDPHTTP2(ctx, clientConn, destination)
if connectErr == nil {
return packetConn, nil
}
if !errors.Is(connectErr, errExtendedConnectUnsupported) || c.disableVersionFallback {
return nil, connectErr
}
c.http2ConnectUDPUnsupported.Store(true)
} else if c.disableVersionFallback {
conn.Close()
return nil, ErrHTTP2Unsupported
} else {
return c.connectUDPHTTP1AndClose(ctx, conn, destination)
}
}
conn, err := c.http1Dialer.DialContext(ctx, N.NetworkTCP, c.server)
conn, stream, err := c.openTunnel(ctx, tunnelRequest{
protocol: connectUDPProtocol,
url: connectUDPURL(destination),
destination: destination,
})
if err != nil {
return nil, err
}
return c.connectUDPHTTP1AndClose(ctx, conn, destination)
}
func (c *Client) connectUDPHTTP1AndClose(ctx context.Context, conn net.Conn, destination M.Socksaddr) (N.PacketConn, error) {
packetConn, err := c.connectUDPHTTP1(ctx, conn, destination)
if err != nil {
conn.Close()
if ctx.Err() != nil {
return nil, ctx.Err()
}
return nil, err
if stream != nil {
return newHTTP3PacketConn(stream, destination, M.Socksaddr{}), nil
}
return packetConn, nil
return newCapsuleConn(std_bufio.NewReader(conn), conn, destination), nil
}
var _ N.Dialer = (*Client)(nil)
+1 -10
View File
@@ -55,7 +55,7 @@ func (c *Client) connectHTTP2(ctx context.Context, clientConn *http2ClientConn,
Method: http.MethodConnect,
URL: &url.URL{Host: destination.String()},
Host: destination.String(),
Header: c.headers.Clone(),
Header: buildRequestHeader(c.headers, c.authorization, false),
}
streamConn, err := c.roundTripHTTP2(ctx, clientConn, request, destination)
if err != nil {
@@ -69,15 +69,6 @@ func (c *Client) roundTripHTTP2(ctx context.Context, clientConn *http2ClientConn
streamCtx, cancel := context.WithCancel(context.Background())
request.Body = pipeReader
request = request.WithContext(streamCtx)
if request.Header == nil {
request.Header = make(http.Header)
}
if _, loaded := request.Header["User-Agent"]; !loaded {
request.Header["User-Agent"] = nil
}
if c.authorization != "" {
request.Header.Set("Proxy-Authorization", c.authorization)
}
stop := context.AfterFunc(ctx, cancel)
response, err := clientConn.RoundTrip(request)
stopped := stop()
+13 -19
View File
@@ -53,6 +53,9 @@ func newHTTP3Client(options ClientOptions, authorization string) (http3Client, e
quicConfig.EnableDatagrams = true
headers := options.Headers.Clone()
authority := options.Server.String()
if options.Authority != "" {
authority = options.Authority
}
if headers != nil {
if host := headers.Get("Host"); host != "" {
authority = host
@@ -113,15 +116,6 @@ func (c *http3ClientImpl) openStream(ctx context.Context, request *http.Request)
}
return nil, nil, E.Cause1(ErrHTTP3Unavailable, err)
}
if request.Header == nil {
request.Header = make(http.Header)
}
if _, loaded := request.Header["User-Agent"]; !loaded {
request.Header["User-Agent"] = nil
}
if c.authorization != "" {
request.Header.Set("Proxy-Authorization", c.authorization)
}
stop := context.AfterFunc(ctx, func() {
stream.CancelRead(0)
stream.CancelWrite(0)
@@ -164,7 +158,7 @@ func (c *http3ClientImpl) DialContext(ctx context.Context, destination M.Socksad
Method: http.MethodConnect,
URL: &url.URL{Host: destination.String()},
Host: destination.String(),
Header: c.headers.Clone(),
Header: buildRequestHeader(c.headers, c.authorization, false),
})
if err != nil {
return nil, err
@@ -172,23 +166,23 @@ func (c *http3ClientImpl) DialContext(ctx context.Context, destination M.Socksad
return &http3StreamConn{stream: stream, remoteAddr: destination}, nil
}
func (c *http3ClientImpl) ListenPacket(ctx context.Context, destination M.Socksaddr) (N.PacketConn, error) {
header := c.headers.Clone()
if header == nil {
header = make(http.Header)
}
func (c *http3ClientImpl) OpenTunnel(ctx context.Context, request tunnelRequest) (DatagramStream, error) {
requestURL := *request.url
requestURL.Scheme = "https"
requestURL.Host = c.authority
header := buildRequestHeader(c.headers, c.authorization, request.originAuthorization)
header.Set("Capsule-Protocol", "?1")
stream, clientConn, err := c.openStream(ctx, &http.Request{
Method: http.MethodConnect,
Proto: connectUDPProtocol,
URL: connectUDPURL(c.authority, destination),
Proto: request.protocol,
URL: &requestURL,
Host: c.authority,
Header: header,
})
if err != nil {
return nil, err
}
return newHTTP3PacketConn(&http3RequestDatagramStream{stream: stream, datagramsEnabled: clientConn.Settings().EnableDatagrams}, destination, M.Socksaddr{}), nil
return &http3RequestDatagramStream{stream: stream, datagramsEnabled: clientConn.Settings().EnableDatagrams}, nil
}
func (c *http3ClientImpl) ResetConnection() {
@@ -315,7 +309,7 @@ func (s *http3RequestDatagramStream) SendDatagram(payload []byte) error {
}
var tooLarge *quic.DatagramTooLargeError
if errors.As(err, &tooLarge) {
return ErrDatagramUnsupported
return &DatagramTooLargeError{MaxPayloadSize: int(tooLarge.MaxDatagramPayloadSize) - VarintLen(uint64(s.stream.StreamID()/4))}
}
return err
}
+3 -78
View File
@@ -44,18 +44,13 @@ func parseConnectUDPTarget(path string) (M.Socksaddr, bool) {
return destination, true
}
func connectUDPURL(authority string, destination M.Socksaddr) *url.URL {
func connectUDPURL(destination M.Socksaddr) *url.URL {
host := destination.AddrString()
port := "/" + strconv.Itoa(int(destination.Port)) + "/"
requestURL := &url.URL{
return &url.URL{
Path: connectUDPPathPrefix + host + port,
RawPath: connectUDPPathPrefix + strings.ReplaceAll(url.PathEscape(host), ":", "%3A") + port,
}
if authority != "" {
requestURL.Scheme = "https"
requestURL.Host = authority
}
return requestURL
}
func requestIsConnectUDP(request *http.Request) bool {
@@ -65,7 +60,7 @@ func requestIsConnectUDP(request *http.Request) bool {
func (c *serverConn) serveConnectUDP(ctx context.Context, request *http.Request, source M.Socksaddr) (requestResult, error) {
destination, valid := parseConnectUDPTarget(request.URL.EscapedPath())
if !valid || !request.ProtoAtLeast(1, 1) {
return c.reject(request, requestKeepAlive(request), http.StatusBadRequest, E.New("invalid connect-udp request: ", request.URL.Path))
return c.reject(request, requestKeepAlive(request), http.StatusBadRequest, nil, E.New("invalid connect-udp request: ", request.URL.Path))
}
_, err := c.conn.Write([]byte("HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: connect-udp\r\nCapsule-Protocol: ?1\r\n\r\n"))
if err != nil {
@@ -106,73 +101,3 @@ func (h *httpHandler) serveConnectUDP(ctx context.Context, writer http.ResponseW
<-done
conn.CloseWrapper()
}
func (c *Client) connectUDPHTTP1(ctx context.Context, conn net.Conn, destination M.Socksaddr) (N.PacketConn, error) {
stop := context.AfterFunc(ctx, func() {
conn.Close()
})
defer stop()
request := &http.Request{
Method: http.MethodGet,
URL: connectUDPURL("", destination),
Host: c.authority(),
Header: c.headers.Clone(),
}
if request.Header == nil {
request.Header = make(http.Header)
}
request.Header.Set("Connection", "Upgrade")
request.Header.Set("Upgrade", connectUDPProtocol)
request.Header.Set("Capsule-Protocol", "?1")
if _, loaded := request.Header["User-Agent"]; !loaded {
request.Header["User-Agent"] = nil
}
if c.authorization != "" {
request.Header.Set("Proxy-Authorization", c.authorization)
}
err := request.Write(conn)
if err != nil {
return nil, E.Cause(err, "write request")
}
reader := std_bufio.NewReader(conn)
response, err := http.ReadResponse(reader, request)
if err != nil {
return nil, E.Cause(err, "read response")
}
if response.StatusCode != http.StatusSwitchingProtocols {
return nil, statusError(response)
}
if !strings.EqualFold(response.Header.Get("Upgrade"), connectUDPProtocol) {
return nil, E.New("unexpected upgrade protocol: ", response.Header.Get("Upgrade"))
}
if !stop() {
return nil, ctx.Err()
}
return newCapsuleConn(reader, conn, destination), nil
}
func (c *Client) connectUDPHTTP2(ctx context.Context, clientConn *http2ClientConn, destination M.Socksaddr) (N.PacketConn, error) {
request := &http.Request{
Method: http.MethodConnect,
URL: connectUDPURL(c.authority(), destination),
Host: c.authority(),
Header: c.headers.Clone(),
}
if request.Header == nil {
request.Header = make(http.Header)
}
request.Header.Set(":protocol", connectUDPProtocol)
request.Header.Set("Capsule-Protocol", "?1")
stream, err := c.roundTripHTTP2(ctx, clientConn, request, destination)
if err != nil {
return nil, err
}
return newCapsuleConn(std_bufio.NewReader(stream), stream, destination), nil
}
func (c *Client) authority() string {
if c.host != "" {
return c.host
}
return c.server.String()
}
+2 -2
View File
@@ -69,7 +69,7 @@ func (c *serverConn) openUpstream(ctx context.Context, source M.Socksaddr, desti
func (c *serverConn) serveForward(ctx context.Context, request *http.Request, source M.Socksaddr, upgrade bool) (requestResult, error) {
destination, valid := forwardDestination(request)
if !valid {
return c.reject(request, requestKeepAlive(request), http.StatusBadRequest, E.New("invalid forward target: ", request.URL.String()))
return c.reject(request, requestKeepAlive(request), http.StatusBadRequest, nil, E.New("invalid forward target: ", request.URL.String()))
}
keepAlive := requestKeepAlive(request)
upgradeProtocol := request.Header.Get("Upgrade")
@@ -110,7 +110,7 @@ func (c *serverConn) serveForward(ctx context.Context, request *http.Request, so
return c.rejectAndClose(request, http.StatusBadGateway, err)
}
request.Body = http.NoBody
return c.reject(request, true, http.StatusBadGateway, err)
return c.reject(request, true, http.StatusBadGateway, nil, err)
}
if response.StatusCode >= 200 || response.StatusCode == http.StatusSwitchingProtocols {
break
+33 -3
View File
@@ -2,11 +2,16 @@ package http
import (
"context"
"io"
"math"
"net"
"net/http"
"slices"
"time"
"github.com/sagernet/sing-box/common/listener"
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/auth"
E "github.com/sagernet/sing/common/exceptions"
@@ -25,6 +30,8 @@ const (
realm = "sing-box"
)
var ConfigureHTTP3ListenerFunc func(ctx context.Context, logger logger.Logger, listener *listener.Listener, handler http.Handler, tlsConfig tls.ServerConfig, options option.QUICOptions) (io.Closer, error)
type Handler interface {
N.TCPConnectionHandlerEx
N.UDPConnectionHandlerEx
@@ -37,6 +44,7 @@ type ServerOptions struct {
HTTP2 bool
HTTP2Options option.HTTP2Options
UDP bool
Tunnels map[string]TunnelHandler
}
type Server struct {
@@ -45,6 +53,7 @@ type Server struct {
http1 bool
http2Server *http2.Server
udp bool
tunnels map[string]TunnelHandler
}
func NewServer(options ServerOptions) *Server {
@@ -53,6 +62,7 @@ func NewServer(options ServerOptions) *Server {
logger: options.Logger,
http1: options.HTTP1,
udp: options.UDP,
tunnels: options.Tunnels,
}
if options.HTTP2 {
server.http2Server = &http2.Server{
@@ -101,11 +111,31 @@ func (s *Server) ServeConnection(ctx context.Context, conn net.Conn, reader *Rea
connection.serve()
}
func (s *Server) HTTP3Handler(handler Handler) http.Handler {
return &httpHandler{
func (s *Server) ConfigureTLS(tlsConfig tls.ServerConfig) {
if len(tlsConfig.NextProtos()) > 0 {
return
}
var nextProtos []string
if s.http2Server != nil {
nextProtos = append(nextProtos, http2.NextProtoTLS)
}
if s.http1 {
nextProtos = append(nextProtos, "http/1.1")
}
tlsConfig.SetNextProtos(nextProtos)
}
func (s *Server) ListenHTTP3(ctx context.Context, logger logger.Logger, listener *listener.Listener, handler Handler, tlsConfig tls.ServerConfig, options option.QUICOptions) (io.Closer, error) {
if ConfigureHTTP3ListenerFunc == nil {
return nil, C.ErrQUICNotIncluded
}
if !slices.Contains(tlsConfig.NextProtos(), "h3") {
tlsConfig.SetNextProtos(append([]string{"h3"}, tlsConfig.NextProtos()...))
}
return ConfigureHTTP3ListenerFunc(ctx, logger, listener, &httpHandler{
server: s,
handler: handler,
}
}, tlsConfig, options)
}
func (s *Server) finishConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, onClose N.CloseHandlerFunc, err error) {
+28 -21
View File
@@ -9,7 +9,6 @@ import (
"time"
"github.com/sagernet/sing-box/common/badhttp"
"github.com/sagernet/sing/common/auth"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
M "github.com/sagernet/sing/common/metadata"
@@ -68,20 +67,22 @@ func (c *serverConn) serveRequest() (requestResult, error) {
return c.rejectAndClose(nil, http.StatusBadRequest, E.Cause(err, "read request"))
}
}
ctx := c.ctx
if c.server.authenticator != nil {
username, password, valid := badhttp.ParseBasicAuth(request.Header.Get("Proxy-Authorization"))
if !valid || !c.server.authenticator.Verify(username, password) {
var authErr error
if !valid {
authErr = E.New("authentication failed: missing or malformed Proxy-Authorization")
} else {
authErr = E.New("authentication failed: username=", username)
}
c.server.logger.ErrorContext(ctx, E.Cause(authErr, "process connection from ", c.source))
return c.reject(request, requestKeepAlive(request), http.StatusProxyAuthRequired, authErr)
tunnelProtocol, tunnelHandler := c.server.upgradeTunnelHandler(request)
if tunnelHandler != nil {
tunnelCtx, tunnelAuthErr := c.server.authenticate(c.ctx, request, "Authorization")
if tunnelAuthErr != nil {
c.server.logger.ErrorContext(c.ctx, E.Cause(tunnelAuthErr, "process connection from ", c.source))
return c.reject(request, requestKeepAlive(request), http.StatusUnauthorized, nil, tunnelAuthErr)
}
ctx = auth.ContextWithUser(ctx, username)
return c.serveTunnel(tunnelCtx, request, badhttp.ForwardedSource(request, c.source), tunnelProtocol, tunnelHandler)
}
if c.handler == nil {
return c.reject(request, requestKeepAlive(request), http.StatusNotFound, nil, E.New("unexpected request: ", request.Method, " ", request.URL))
}
ctx, authErr := c.server.authenticate(c.ctx, request, "Proxy-Authorization")
if authErr != nil {
c.server.logger.ErrorContext(c.ctx, E.Cause(authErr, "process connection from ", c.source))
return c.reject(request, requestKeepAlive(request), http.StatusProxyAuthRequired, nil, authErr)
}
source := badhttp.ForwardedSource(request, c.source)
switch {
@@ -99,7 +100,7 @@ func (c *serverConn) serveRequest() (requestResult, error) {
func (c *serverConn) serveConnect(ctx context.Context, request *http.Request, source M.Socksaddr) (requestResult, error) {
destination := connectDestination(request)
if !destination.IsValid() {
return c.reject(request, requestKeepAlive(request), http.StatusBadRequest, E.New("invalid CONNECT target: ", request.URL.Host))
return c.reject(request, requestKeepAlive(request), http.StatusBadRequest, nil, E.New("invalid CONNECT target: ", request.URL.Host))
}
_, err := c.conn.Write([]byte(F.ToString("HTTP/", request.ProtoMajor, ".", request.ProtoMinor, " 200 Connection established\r\n\r\n")))
if err != nil {
@@ -109,11 +110,11 @@ func (c *serverConn) serveConnect(ctx context.Context, request *http.Request, so
return requestHandedOff, nil
}
func (c *serverConn) reject(request *http.Request, keepAlive bool, statusCode int, cause error) (requestResult, error) {
func (c *serverConn) reject(request *http.Request, keepAlive bool, statusCode int, header http.Header, cause error) (requestResult, error) {
if keepAlive && request.Body != nil && request.Body != http.NoBody {
keepAlive = c.discardBody(request.Body)
}
err := c.writeReject(request, statusCode, keepAlive)
err := c.writeReject(request, statusCode, header, keepAlive)
if err != nil {
return requestClose, E.Errors(cause, err)
}
@@ -124,28 +125,34 @@ func (c *serverConn) reject(request *http.Request, keepAlive bool, statusCode in
}
func (c *serverConn) rejectAndClose(request *http.Request, statusCode int, cause error) (requestResult, error) {
err := c.writeReject(request, statusCode, false)
err := c.writeReject(request, statusCode, nil, false)
if err != nil {
return requestClose, E.Errors(cause, err)
}
return requestClose, cause
}
func (c *serverConn) writeReject(request *http.Request, statusCode int, keepAlive bool) error {
func (c *serverConn) writeReject(request *http.Request, statusCode int, header http.Header, keepAlive bool) error {
response := &http.Response{
StatusCode: statusCode,
ProtoMajor: 1,
ProtoMinor: 1,
Header: make(http.Header),
Header: header.Clone(),
Request: request,
Close: !keepAlive,
}
if response.Header == nil {
response.Header = make(http.Header)
}
if request != nil {
response.ProtoMajor = request.ProtoMajor
response.ProtoMinor = request.ProtoMinor
}
if statusCode == http.StatusProxyAuthRequired {
switch statusCode {
case http.StatusProxyAuthRequired:
response.Header.Set("Proxy-Authenticate", `Basic realm="`+realm+`", charset="UTF-8"`)
case http.StatusUnauthorized:
response.Header.Set("WWW-Authenticate", `Basic realm="`+realm+`", charset="UTF-8"`)
}
if keepAlive && request.ProtoMajor == 1 && request.ProtoMinor == 0 {
response.Header.Set("Connection", "keep-alive")
+30 -19
View File
@@ -12,7 +12,6 @@ import (
"github.com/sagernet/sing-box/common/badhttp"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/transport/v2rayhttp"
"github.com/sagernet/sing/common/auth"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
@@ -88,28 +87,40 @@ func (h *httpHandler) ServeHTTP(writer http.ResponseWriter, request *http.Reques
if !connectionSource.IsValid() {
connectionSource = M.ParseSocksaddr(request.RemoteAddr).Unwrap()
}
if h.server.authenticator != nil {
username, password, valid := badhttp.ParseBasicAuth(request.Header.Get("Proxy-Authorization"))
if !valid || !h.server.authenticator.Verify(username, password) {
var authErr error
if !valid {
authErr = E.New("authentication failed: missing or malformed Proxy-Authorization")
} else {
authErr = E.New("authentication failed: username=", username)
}
h.server.logger.ErrorContext(ctx, E.Cause(authErr, "process connection from ", connectionSource))
writer.Header().Set("Proxy-Authenticate", `Basic realm="`+realm+`", charset="UTF-8"`)
writer.WriteHeader(http.StatusProxyAuthRequired)
return
}
ctx = auth.ContextWithUser(ctx, username)
}
source := badhttp.ForwardedSource(request, connectionSource)
var protocol string
if request.Method == http.MethodConnect {
protocol := request.Header.Get(":protocol")
protocol = request.Header.Get(":protocol")
if protocol == "" && request.ProtoMajor == 3 && !strings.HasPrefix(request.Proto, "HTTP/") {
protocol = request.Proto
}
}
tunnelHandler := h.server.tunnels[protocol]
if tunnelHandler != nil {
tunnelCtx, authErr := h.server.authenticate(ctx, request, "Authorization")
if authErr != nil {
h.server.logger.ErrorContext(ctx, E.Cause(authErr, "process connection from ", connectionSource))
writer.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`", charset="UTF-8"`)
writer.WriteHeader(http.StatusUnauthorized)
return
}
h.serveTunnel(tunnelCtx, writer, request, badhttp.ForwardedSource(request, connectionSource), tunnelHandler)
return
}
if h.handler == nil {
h.server.logger.ErrorContext(ctx, "process connection from ", connectionSource, ": unexpected request: ", request.Method, " ", request.URL)
writer.WriteHeader(http.StatusNotFound)
return
}
proxyCtx, authErr := h.server.authenticate(ctx, request, "Proxy-Authorization")
if authErr != nil {
h.server.logger.ErrorContext(ctx, E.Cause(authErr, "process connection from ", connectionSource))
writer.Header().Set("Proxy-Authenticate", `Basic realm="`+realm+`", charset="UTF-8"`)
writer.WriteHeader(http.StatusProxyAuthRequired)
return
}
ctx = proxyCtx
source := badhttp.ForwardedSource(request, connectionSource)
if request.Method == http.MethodConnect {
switch {
case protocol == "":
h.serveConnect(ctx, writer, request, source)
@@ -1,6 +1,6 @@
//go:build with_quic
package quic
package http
import (
"context"
@@ -16,8 +16,6 @@ import (
"github.com/sagernet/sing-box/common/tls"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
boxHTTP "github.com/sagernet/sing-box/protocol/http"
transportHTTP "github.com/sagernet/sing-box/transport/http"
"github.com/sagernet/sing-quic"
congestion_meta2 "github.com/sagernet/sing-quic/congestion_meta2"
E "github.com/sagernet/sing/common/exceptions"
@@ -25,7 +23,7 @@ import (
)
func init() {
boxHTTP.ConfigureHTTP3ListenerFunc = func(ctx context.Context, logger logger.Logger, listener *listener.Listener, handler http.Handler, tlsConfig tls.ServerConfig, options option.QUICOptions) (io.Closer, error) {
ConfigureHTTP3ListenerFunc = func(ctx context.Context, logger logger.Logger, listener *listener.Listener, handler http.Handler, tlsConfig tls.ServerConfig, options option.QUICOptions) (io.Closer, error) {
err := qtls.ConfigureHTTP3(tlsConfig)
if err != nil {
return nil, err
@@ -63,7 +61,7 @@ func init() {
}()
return quicListener, nil
}
transportHTTP.HTTP3StreamFunc = func(ctx context.Context, writer http.ResponseWriter) (transportHTTP.DatagramStream, bool) {
HTTP3StreamFunc = func(ctx context.Context, writer http.ResponseWriter) (DatagramStream, bool) {
streamer, isStreamer := writer.(http3.HTTPStreamer)
if !isStreamer {
return nil, false
@@ -91,7 +89,7 @@ type datagramStream struct {
func (s *datagramStream) SendDatagram(payload []byte) error {
if !s.datagramsEnabled {
return transportHTTP.ErrDatagramUnsupported
return ErrDatagramUnsupported
}
err := s.Stream.SendDatagram(payload)
if err == nil {
@@ -99,7 +97,7 @@ func (s *datagramStream) SendDatagram(payload []byte) error {
}
var tooLarge *quic.DatagramTooLargeError
if errors.As(err, &tooLarge) {
return transportHTTP.ErrDatagramUnsupported
return &DatagramTooLargeError{MaxPayloadSize: int(tooLarge.MaxDatagramPayloadSize) - VarintLen(uint64(s.Stream.StreamID()/4))}
}
return err
}
+170
View File
@@ -0,0 +1,170 @@
package http
import (
std_bufio "bufio"
"context"
"errors"
"io"
"net"
"net/http"
"net/url"
"strings"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type tunnelRequest struct {
protocol string
url *url.URL
destination M.Socksaddr
originAuthorization bool
}
func (c *Client) OpenTunnel(ctx context.Context, protocol string, path string) (io.ReadWriteCloser, error) {
requestURL, err := url.ParseRequestURI(path)
if err != nil {
return nil, E.Cause(err, "parse tunnel path")
}
conn, stream, err := c.openTunnel(ctx, tunnelRequest{
protocol: protocol,
url: &url.URL{Path: requestURL.Path, RawPath: requestURL.RawPath, RawQuery: requestURL.RawQuery},
destination: c.server,
originAuthorization: true,
})
if err != nil {
return nil, err
}
if stream != nil {
return stream, nil
}
return conn, nil
}
func (c *Client) openTunnel(ctx context.Context, request tunnelRequest) (net.Conn, DatagramStream, error) {
if c.http3Available() {
stream, err := c.http3.OpenTunnel(ctx, request)
if err == nil {
c.clearHTTP3Broken()
return nil, stream, nil
}
if c.disableVersionFallback || !errors.Is(err, ErrHTTP3Unavailable) {
return nil, nil, err
}
c.markHTTP3Broken()
}
if c.tlsDialer != nil && !c.http2Unsupported.Load() && !c.http2ExtendedConnectUnsupported.Load() {
clientConn, conn, err := c.acquireHTTP2(ctx)
if err != nil {
return nil, nil, err
}
if clientConn != nil {
streamConn, connectErr := c.openTunnelHTTP2(ctx, clientConn, request)
if connectErr == nil {
return streamConn, nil, nil
}
if !errors.Is(connectErr, errExtendedConnectUnsupported) || c.disableVersionFallback {
return nil, nil, connectErr
}
c.http2ExtendedConnectUnsupported.Store(true)
} else if c.disableVersionFallback {
conn.Close()
return nil, nil, ErrHTTP2Unsupported
} else {
return c.openTunnelHTTP1AndClose(ctx, conn, request)
}
}
conn, err := c.http1Dialer.DialContext(ctx, N.NetworkTCP, c.server)
if err != nil {
return nil, nil, err
}
return c.openTunnelHTTP1AndClose(ctx, conn, request)
}
func (c *Client) openTunnelHTTP1AndClose(ctx context.Context, conn net.Conn, request tunnelRequest) (net.Conn, DatagramStream, error) {
tunnelConn, err := c.openTunnelHTTP1(ctx, conn, request)
if err != nil {
conn.Close()
if ctx.Err() != nil {
return nil, nil, ctx.Err()
}
return nil, nil, err
}
return tunnelConn, nil, nil
}
func (c *Client) openTunnelHTTP1(ctx context.Context, conn net.Conn, request tunnelRequest) (net.Conn, error) {
stop := context.AfterFunc(ctx, func() {
conn.Close()
})
defer stop()
httpRequest := &http.Request{
Method: http.MethodGet,
URL: request.url,
Host: c.authority(),
Header: buildRequestHeader(c.headers, c.authorization, request.originAuthorization),
}
httpRequest.Header.Set("Connection", "Upgrade")
httpRequest.Header.Set("Upgrade", request.protocol)
httpRequest.Header.Set("Capsule-Protocol", "?1")
err := httpRequest.Write(conn)
if err != nil {
return nil, E.Cause(err, "write request")
}
reader := std_bufio.NewReader(conn)
response, err := http.ReadResponse(reader, httpRequest)
if err != nil {
return nil, E.Cause(err, "read response")
}
if response.StatusCode != http.StatusSwitchingProtocols {
return nil, statusError(response)
}
if !strings.EqualFold(response.Header.Get("Upgrade"), request.protocol) {
return nil, E.New("unexpected upgrade protocol: ", response.Header.Get("Upgrade"))
}
if !stop() {
return nil, ctx.Err()
}
if reader.Buffered() > 0 {
buffer := buf.NewSize(reader.Buffered())
_, err = buffer.ReadFullFrom(reader, buffer.FreeLen())
if err != nil {
buffer.Release()
return nil, err
}
return bufio.NewCachedConn(conn, buffer), nil
}
return conn, nil
}
func (c *Client) openTunnelHTTP2(ctx context.Context, clientConn *http2ClientConn, request tunnelRequest) (net.Conn, error) {
requestURL := *request.url
requestURL.Scheme = "https"
requestURL.Host = c.authority()
httpRequest := &http.Request{
Method: http.MethodConnect,
URL: &requestURL,
Host: c.authority(),
Header: buildRequestHeader(c.headers, c.authorization, request.originAuthorization),
}
httpRequest.Header.Set(":protocol", request.protocol)
httpRequest.Header.Set("Capsule-Protocol", "?1")
streamConn, err := c.roundTripHTTP2(ctx, clientConn, httpRequest, request.destination)
if err != nil {
return nil, err
}
return streamConn, nil
}
func (c *Client) authority() string {
if c.host != "" {
return c.host
}
if c.authorityOverride != "" {
return c.authorityOverride
}
return c.server.String()
}
+160
View File
@@ -0,0 +1,160 @@
package http
import (
"context"
"io"
"maps"
"net/http"
"strings"
"github.com/sagernet/sing-box/common/badhttp"
"github.com/sagernet/sing-box/transport/v2rayhttp"
"github.com/sagernet/sing/common/auth"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
)
type TunnelHandler interface {
NewTunnelRequest(ctx context.Context, request TunnelRequest)
}
type TunnelRequest interface {
Request() *http.Request
Source() M.Socksaddr
Accept() (io.ReadWriteCloser, error)
Reject(statusCode int, header http.Header)
}
func (s *Server) authenticate(ctx context.Context, request *http.Request, headerName string) (context.Context, error) {
if s.authenticator == nil {
return ctx, nil
}
username, password, valid := badhttp.ParseBasicAuth(request.Header.Get(headerName))
if !valid {
return nil, E.New("authentication failed: missing or malformed ", headerName)
}
if !s.authenticator.Verify(username, password) {
return nil, E.New("authentication failed: username=", username)
}
return auth.ContextWithUser(ctx, username), nil
}
func (s *Server) upgradeTunnelHandler(request *http.Request) (string, TunnelHandler) {
if len(s.tunnels) == 0 || request.Method != http.MethodGet || !requestIsUpgrade(request) {
return "", nil
}
protocol := strings.ToLower(request.Header.Get("Upgrade"))
return protocol, s.tunnels[protocol]
}
type http1TunnelRequest struct {
conn *serverConn
request *http.Request
source M.Socksaddr
protocol string
responded bool
accepted bool
result requestResult
err error
}
func (r *http1TunnelRequest) Request() *http.Request {
return r.request
}
func (r *http1TunnelRequest) Source() M.Socksaddr {
return r.source
}
func (r *http1TunnelRequest) Accept() (io.ReadWriteCloser, error) {
r.responded = true
_, err := r.conn.conn.Write([]byte("HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: " + r.protocol + "\r\nCapsule-Protocol: ?1\r\n\r\n"))
if err != nil {
r.result = requestClose
r.err = E.Cause(err, "write response")
return nil, r.err
}
r.accepted = true
return r.conn.reader.cachedConn(r.conn.conn), nil
}
func (r *http1TunnelRequest) Reject(statusCode int, header http.Header) {
r.responded = true
r.result, r.err = r.conn.reject(r.request, requestKeepAlive(r.request), statusCode, header, nil)
}
func (c *serverConn) serveTunnel(ctx context.Context, request *http.Request, source M.Socksaddr, protocol string, handler TunnelHandler) (requestResult, error) {
if !request.ProtoAtLeast(1, 1) {
return c.reject(request, requestKeepAlive(request), http.StatusBadRequest, nil, E.New("invalid ", protocol, " request"))
}
current := &http1TunnelRequest{
conn: c,
request: request,
source: source,
protocol: protocol,
}
handler.NewTunnelRequest(ctx, current)
if !current.responded {
return c.reject(request, requestKeepAlive(request), http.StatusInternalServerError, nil, E.New(protocol, " request left unanswered"))
}
if current.accepted {
return requestClose, nil
}
return current.result, current.err
}
type httpTunnelRequest struct {
writer http.ResponseWriter
request *http.Request
source M.Socksaddr
responded bool
wrapper *v2rayhttp.HTTP2ConnWrapper
}
func (r *httpTunnelRequest) Request() *http.Request {
return r.request
}
func (r *httpTunnelRequest) Source() M.Socksaddr {
return r.source
}
func (r *httpTunnelRequest) Accept() (io.ReadWriteCloser, error) {
r.responded = true
r.writer.Header().Set("Capsule-Protocol", "?1")
r.writer.WriteHeader(http.StatusOK)
r.writer.(http.Flusher).Flush()
if r.request.ProtoMajor == 3 && HTTP3StreamFunc != nil {
stream, isDatagramStream := HTTP3StreamFunc(r.request.Context(), r.writer)
if isDatagramStream {
return stream, nil
}
}
r.wrapper = v2rayhttp.NewHTTP2Wrapper(&v2rayhttp.ServerHTTPConn{
HTTP2Conn: v2rayhttp.NewHTTPConn(r.request.Body, r.writer),
Flusher: r.writer.(http.Flusher),
})
return r.wrapper, nil
}
func (r *httpTunnelRequest) Reject(statusCode int, header http.Header) {
r.responded = true
maps.Copy(r.writer.Header(), header)
r.writer.WriteHeader(statusCode)
}
func (h *httpHandler) serveTunnel(ctx context.Context, writer http.ResponseWriter, request *http.Request, source M.Socksaddr, handler TunnelHandler) {
current := &httpTunnelRequest{
writer: writer,
request: request,
source: source,
}
handler.NewTunnelRequest(ctx, current)
if !current.responded {
writer.WriteHeader(http.StatusInternalServerError)
return
}
if current.wrapper != nil {
current.wrapper.CloseWrapper()
}
}
+166
View File
@@ -0,0 +1,166 @@
package masque
import (
"net/netip"
transportHTTP "github.com/sagernet/sing-box/transport/http"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
"go4.org/netipx"
)
const (
capsuleTypeAddressAssign = 0x01
capsuleTypeAddressRequest = 0x02
capsuleTypeRouteAdvertisement = 0x03
)
type AssignedAddress struct {
RequestID uint64
Prefix netip.Prefix
}
type AddressRange struct {
Start netip.Addr
End netip.Addr
Protocol uint8
}
func (r AddressRange) Contains(address netip.Addr) bool {
return address.BitLen() == r.Start.BitLen() && r.Start.Compare(address) <= 0 && address.Compare(r.End) <= 0
}
func parseAddresses(payload []byte) ([]AssignedAddress, error) {
var addresses []AssignedAddress
for len(payload) > 0 {
requestID, requestIDLength, valid := transportHTTP.DecodeVarint(payload)
if !valid {
return nil, E.New("truncated request ID")
}
payload = payload[requestIDLength:]
addressLength, err := parseVersion(payload)
if err != nil {
return nil, err
}
if len(payload) < 1+addressLength+1 {
return nil, E.New("truncated address")
}
address, _ := netip.AddrFromSlice(payload[1 : 1+addressLength])
prefixLength := int(payload[1+addressLength])
payload = payload[1+addressLength+1:]
if prefixLength > address.BitLen() {
return nil, E.New("invalid prefix length: ", prefixLength)
}
prefix := netip.PrefixFrom(address, prefixLength)
if prefix.Masked() != prefix {
return nil, E.New("prefix has host bits set: ", prefix)
}
addresses = append(addresses, AssignedAddress{RequestID: requestID, Prefix: prefix})
}
return addresses, nil
}
func parseVersion(payload []byte) (int, error) {
if len(payload) < 1 {
return 0, E.New("truncated IP version")
}
switch payload[0] {
case 4:
return 4, nil
case 6:
return 16, nil
default:
return 0, E.New("invalid IP version: ", payload[0])
}
}
func parseRoutes(payload []byte) ([]AddressRange, error) {
var routes []AddressRange
for len(payload) > 0 {
addressLength, err := parseVersion(payload)
if err != nil {
return nil, err
}
if len(payload) < 1+addressLength*2+1 {
return nil, E.New("truncated address range")
}
start, _ := netip.AddrFromSlice(payload[1 : 1+addressLength])
end, _ := netip.AddrFromSlice(payload[1+addressLength : 1+addressLength*2])
route := AddressRange{Start: start, End: end, Protocol: payload[1+addressLength*2]}
payload = payload[1+addressLength*2+1:]
if start.Compare(end) > 0 {
return nil, E.New("invalid address range: ", start, "-", end)
}
if len(routes) > 0 {
previous := routes[len(routes)-1]
switch {
case previous.Start.BitLen() > start.BitLen():
return nil, E.New("address ranges are not ordered by IP version")
case previous.Start.BitLen() < start.BitLen():
case previous.Protocol > route.Protocol:
return nil, E.New("address ranges are not ordered by IP protocol")
case previous.Protocol < route.Protocol:
case previous.End.Compare(start) >= 0:
return nil, E.New("address ranges overlap: ", previous.End, " and ", start)
}
}
routes = append(routes, route)
}
return routes, nil
}
func appendAddress(payload []byte, address netip.Addr) []byte {
if address.Is4() {
payload = append(payload, 4)
} else {
payload = append(payload, 6)
}
return append(payload, address.AsSlice()...)
}
func newCapsule(capsuleType uint64, payload []byte) *buf.Buffer {
capsule := buf.NewSize(transportHTTP.VarintLen(capsuleType) + transportHTTP.VarintLen(uint64(len(payload))) + len(payload))
transportHTTP.PutVarint(capsule.Extend(transportHTTP.VarintLen(capsuleType)), capsuleType)
transportHTTP.PutVarint(capsule.Extend(transportHTTP.VarintLen(uint64(len(payload)))), uint64(len(payload)))
capsule.Write(payload)
return capsule
}
func newAddressCapsule(capsuleType uint64, addresses []AssignedAddress) *buf.Buffer {
var payload []byte
for _, address := range addresses {
var requestID [8]byte
payload = append(payload, requestID[:transportHTTP.PutVarint(requestID[:], address.RequestID)]...)
payload = appendAddress(payload, address.Prefix.Addr())
payload = append(payload, byte(address.Prefix.Bits()))
}
return newCapsule(capsuleType, payload)
}
func newRouteCapsule(routes []AddressRange) *buf.Buffer {
var payload []byte
for _, route := range routes {
payload = appendAddress(payload, route.Start)
payload = append(payload, route.End.AsSlice()...)
payload = append(payload, route.Protocol)
}
return newCapsule(capsuleTypeRouteAdvertisement, payload)
}
func RangesFromPrefixes(prefixes []netip.Prefix, protocol uint8) ([]AddressRange, error) {
var builder netipx.IPSetBuilder
for _, prefix := range prefixes {
builder.AddPrefix(prefix)
}
ipSet, err := builder.IPSet()
if err != nil {
return nil, err
}
ipRanges := ipSet.Ranges()
routes := make([]AddressRange, 0, len(ipRanges))
for _, ipRange := range ipRanges {
routes = append(routes, AddressRange{Start: ipRange.From(), End: ipRange.To(), Protocol: protocol})
}
return routes, nil
}
+478
View File
@@ -0,0 +1,478 @@
package masque
import (
"context"
"net/netip"
"slices"
"sync"
"time"
C "github.com/sagernet/sing-box/constant"
transportHTTP "github.com/sagernet/sing-box/transport/http"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
)
const (
reconnectBackoffInitial = time.Second
reconnectBackoffMax = time.Minute
)
type Configuration struct {
Address []netip.Prefix
Routes []AddressRange
RoutesAdvertised bool
}
type ClientHandler interface {
UpdateConfiguration(configuration Configuration) error
WriteInboundBuffers(packetBuffers []*buf.Buffer) error
}
type ClientOptions struct {
Context context.Context
Logger logger.ContextLogger
HTTPClient *transportHTTP.Client
Path string
AdvertiseRoutes []netip.Prefix
Handler ClientHandler
}
type Client struct {
ctx context.Context
cancel context.CancelFunc
logger logger.ContextLogger
httpClient *transportHTTP.Client
template *Template
advertiseRoutes []AddressRange
handler ClientHandler
access sync.Mutex
current *clientSession
suspended bool
restarting bool
lastError error
stateUpdated chan struct{}
loopDone chan struct{}
}
type clientSession struct {
*session
client *Client
access sync.Mutex
configuration Configuration
ready bool
}
func NewClient(options ClientOptions) (*Client, error) {
template, err := ParseTemplate(options.Path)
if err != nil {
return nil, err
}
advertiseRoutes, err := RangesFromPrefixes(options.AdvertiseRoutes, 0)
if err != nil {
return nil, E.Cause(err, "build advertised routes")
}
ctx, cancel := context.WithCancel(options.Context)
return &Client{
ctx: ctx,
cancel: cancel,
logger: options.Logger,
httpClient: options.HTTPClient,
template: template,
advertiseRoutes: advertiseRoutes,
handler: options.Handler,
stateUpdated: make(chan struct{}),
}, nil
}
func (c *Client) Start() {
c.loopDone = make(chan struct{})
go c.loop()
}
func (c *Client) Close() error {
c.cancel()
if c.loopDone != nil {
<-c.loopDone
}
return c.httpClient.Close()
}
func (c *Client) notifyStateLocked() {
close(c.stateUpdated)
c.stateUpdated = make(chan struct{})
}
func (c *Client) loop() {
defer close(c.loopDone)
backoff := reconnectBackoffInitial
for {
c.access.Lock()
suspended := c.suspended
stateUpdated := c.stateUpdated
c.restarting = false
if !suspended {
c.lastError = nil
}
c.access.Unlock()
if suspended {
select {
case <-stateUpdated:
continue
case <-c.ctx.Done():
return
}
}
established, err := c.connect()
if c.ctx.Err() != nil {
return
}
c.access.Lock()
interrupted := c.suspended || c.restarting
if !interrupted {
c.lastError = err
}
c.notifyStateLocked()
stateUpdated = c.stateUpdated
c.access.Unlock()
if interrupted {
backoff = reconnectBackoffInitial
continue
}
if err != nil {
c.logger.Error(E.Cause(err, "connection closed"))
}
if established {
backoff = reconnectBackoffInitial
}
timer := time.NewTimer(backoff)
select {
case <-timer.C:
case <-stateUpdated:
timer.Stop()
case <-c.ctx.Done():
timer.Stop()
return
}
backoff = min(backoff*2, reconnectBackoffMax)
}
}
func (c *Client) connect() (bool, error) {
dialCtx, cancelDial := context.WithTimeout(c.ctx, C.TCPTimeout)
stream, err := c.httpClient.OpenTunnel(dialCtx, upgradeToken, c.template.Expand())
cancelDial()
if err != nil {
return false, err
}
current := &clientSession{client: c}
c.access.Lock()
if c.suspended || c.restarting {
suspended := c.suspended
c.access.Unlock()
stream.Close()
if suspended {
c.httpClient.ResetConnections()
}
return false, nil
}
current.session = newSession(c.ctx, stream, current, false)
c.current = current
c.access.Unlock()
err = current.writeCapsule(newAddressCapsule(capsuleTypeAddressRequest, []AssignedAddress{
{RequestID: 1, Prefix: netip.PrefixFrom(netip.IPv4Unspecified(), 32)},
{RequestID: 2, Prefix: netip.PrefixFrom(netip.IPv6Unspecified(), 128)},
}))
if err == nil && len(c.advertiseRoutes) > 0 {
err = current.writeCapsule(newRouteCapsule(c.advertiseRoutes))
}
if err != nil {
current.cancel(err)
}
err = current.run()
c.access.Lock()
c.current = nil
c.access.Unlock()
current.access.Lock()
established := current.ready
current.ready = false
current.access.Unlock()
return established, err
}
func (c *Client) activeSession() *clientSession {
c.access.Lock()
defer c.access.Unlock()
return c.current
}
func (c *Client) Ready() bool {
current := c.activeSession()
if current == nil {
return false
}
current.access.Lock()
defer current.access.Unlock()
return current.ready
}
func (c *Client) WaitReady(ctx context.Context) error {
for {
c.access.Lock()
current := c.current
lastError := c.lastError
stateUpdated := c.stateUpdated
c.access.Unlock()
if current != nil {
current.access.Lock()
ready := current.ready
current.access.Unlock()
if ready {
return nil
}
} else if lastError != nil {
return lastError
}
select {
case <-stateUpdated:
case <-ctx.Done():
return ctx.Err()
case <-c.ctx.Done():
return c.ctx.Err()
}
}
}
func (c *Client) Suspend() {
c.access.Lock()
if c.suspended {
c.access.Unlock()
return
}
c.suspended = true
if c.current != nil {
c.current.cancel(E.New("suspended"))
}
c.notifyStateLocked()
c.access.Unlock()
c.httpClient.ResetConnections()
}
func (c *Client) Resume() {
c.access.Lock()
defer c.access.Unlock()
if !c.suspended {
return
}
c.suspended = false
c.lastError = nil
c.notifyStateLocked()
}
func (c *Client) RestartSession() {
c.access.Lock()
c.restarting = true
if c.current != nil {
c.current.cancel(E.New("network changed"))
}
c.lastError = nil
c.notifyStateLocked()
c.access.Unlock()
c.httpClient.ResetConnections()
}
func (c *Client) WritePacketBuffers(packetBuffers []*buf.Buffer, forwarded bool) error {
current := c.activeSession()
if current == nil {
buf.ReleaseMulti(packetBuffers)
return nil
}
current.access.Lock()
configuration := current.configuration
ready := current.ready
current.access.Unlock()
if !ready {
buf.ReleaseMulti(packetBuffers)
return nil
}
inet4Address, inet6Address := firstAddresses(configuration.Address)
var replies []*buf.Buffer
for i, packetBuffer := range packetBuffers {
_, destination, protocol, valid := packetAddresses(packetBuffer.Bytes())
if !valid {
packetBuffer.Release()
continue
}
errorType := tun.ICMPErrorNoRoute
routed := !configuration.RoutesAdvertised || RoutesContain(configuration.Routes, destination, protocol)
if routed && forwarded && !decrementHopLimit(packetBuffer.Bytes()) {
routed = false
errorType = tun.ICMPErrorHopLimitExceeded
}
if !routed {
reply, built := buildICMPError(packetBuffer.Bytes(), errorType, inet4Address, inet6Address, 0, PacketHeadroom)
if built {
replies = append(replies, reply)
}
packetBuffer.Release()
continue
}
err := current.writePacket(packetBuffer)
if err != nil {
current.cancel(err)
buf.ReleaseMulti(packetBuffers[i+1:])
break
}
}
if len(replies) > 0 {
return c.handler.WriteInboundBuffers(replies)
}
return nil
}
func (s *clientSession) handleAddressAssign(addresses []AssignedAddress) error {
var assigned []netip.Prefix
for _, address := range addresses {
if address.RequestID != 0 && address.Prefix.Addr().IsUnspecified() && address.Prefix.IsSingleIP() {
continue
}
localAddress := address.Prefix.Addr()
if !address.Prefix.IsSingleIP() {
localAddress = localAddress.Next()
}
assigned = append(assigned, netip.PrefixFrom(localAddress, address.Prefix.Bits()))
}
s.access.Lock()
if slices.Equal(s.configuration.Address, assigned) {
s.access.Unlock()
return nil
}
s.configuration.Address = assigned
configuration := s.configuration
s.access.Unlock()
return s.updateConfiguration(configuration)
}
func (s *clientSession) handleRouteAdvertisement(routes []AddressRange) error {
s.access.Lock()
if s.configuration.RoutesAdvertised && slices.Equal(s.configuration.Routes, routes) {
s.access.Unlock()
return nil
}
s.configuration.Routes = routes
s.configuration.RoutesAdvertised = true
configuration := s.configuration
s.access.Unlock()
if len(configuration.Address) == 0 {
return nil
}
return s.updateConfiguration(configuration)
}
func (s *clientSession) updateConfiguration(configuration Configuration) error {
if len(configuration.Address) == 0 {
s.access.Lock()
s.ready = false
s.access.Unlock()
s.client.access.Lock()
s.client.notifyStateLocked()
s.client.access.Unlock()
return nil
}
err := s.client.handler.UpdateConfiguration(configuration)
if err != nil {
return E.Cause(err, "update configuration")
}
s.access.Lock()
s.ready = true
s.access.Unlock()
s.client.access.Lock()
s.client.lastError = nil
s.client.notifyStateLocked()
s.client.access.Unlock()
return nil
}
func (s *clientSession) handleAddressRequest(addresses []AssignedAddress) error {
rejections := make([]AssignedAddress, 0, len(addresses))
for _, address := range addresses {
unspecified := netip.IPv4Unspecified()
if address.Prefix.Addr().Is6() {
unspecified = netip.IPv6Unspecified()
}
rejections = append(rejections, AssignedAddress{RequestID: address.RequestID, Prefix: netip.PrefixFrom(unspecified, unspecified.BitLen())})
}
return s.writeCapsule(newAddressCapsule(capsuleTypeAddressAssign, rejections))
}
func (s *clientSession) handlePacket(buffer *buf.Buffer) {
_, destination, _, valid := packetAddresses(buffer.Bytes())
if !valid {
buffer.Release()
return
}
s.access.Lock()
configuration := s.configuration
s.access.Unlock()
if !prefixesContain(configuration.Address, destination) && !rangesContain(s.client.advertiseRoutes, destination) {
inet4Address, inet6Address := firstAddresses(configuration.Address)
reply, built := buildICMPError(buffer.Bytes(), tun.ICMPErrorNoRoute, inet4Address, inet6Address, 0, transportHTTP.CapsuleHeadroom)
buffer.Release()
if built {
_ = s.writePacket(reply)
}
return
}
err := s.client.handler.WriteInboundBuffers([]*buf.Buffer{buffer})
if err != nil {
s.client.logger.Debug(E.Cause(err, "write packet to device"))
}
}
func (s *clientSession) handlePacketTooBig(buffer *buf.Buffer, mtu int) {
s.access.Lock()
inet4Address, inet6Address := firstAddresses(s.configuration.Address)
s.access.Unlock()
reply, built := buildICMPError(buffer.Bytes(), tun.ICMPErrorPacketTooBig, inet4Address, inet6Address, mtu, PacketHeadroom)
buffer.Release()
if !built {
return
}
err := s.client.handler.WriteInboundBuffers([]*buf.Buffer{reply})
if err != nil {
s.client.logger.Debug(E.Cause(err, "write packet to device"))
}
}
func firstAddresses(addresses []netip.Prefix) (netip.Addr, netip.Addr) {
var inet4Address netip.Addr
var inet6Address netip.Addr
for _, prefix := range addresses {
if prefix.Addr().Is4() && !inet4Address.IsValid() {
inet4Address = prefix.Addr()
} else if prefix.Addr().Is6() && !inet6Address.IsValid() {
inet6Address = prefix.Addr()
}
}
return inet4Address, inet6Address
}
func prefixesContain(prefixes []netip.Prefix, address netip.Addr) bool {
return slices.ContainsFunc(prefixes, func(prefix netip.Prefix) bool {
return prefix.Contains(address)
})
}
func RoutesContain(routes []AddressRange, address netip.Addr, protocol uint8) bool {
return slices.ContainsFunc(routes, func(route AddressRange) bool {
return route.Contains(address) && (route.Protocol == 0 || route.Protocol == protocol || isControlProtocol(protocol))
})
}
func rangesContain(routes []AddressRange, address netip.Addr) bool {
return slices.ContainsFunc(routes, func(route AddressRange) bool {
return route.Contains(address)
})
}
+59
View File
@@ -0,0 +1,59 @@
package masque
import (
"net/netip"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing-tun/gtcpip/header"
"github.com/sagernet/sing/common/buf"
)
func packetAddresses(packet []byte) (netip.Addr, netip.Addr, uint8, bool) {
protocol, valid := tun.IPTransportProtocol(packet)
if !valid {
return netip.Addr{}, netip.Addr{}, 0, false
}
if header.IPVersion(packet) == header.IPv4Version {
ipHeader := header.IPv4(packet)
return ipHeader.SourceAddr(), ipHeader.DestinationAddr(), protocol, true
}
ipHeader := header.IPv6(packet)
return ipHeader.SourceAddr(), ipHeader.DestinationAddr(), protocol, true
}
func isControlProtocol(protocol uint8) bool {
return protocol == uint8(header.ICMPv4ProtocolNumber) || protocol == uint8(header.ICMPv6ProtocolNumber)
}
func decrementHopLimit(packet []byte) bool {
if header.IPVersion(packet) == header.IPv4Version {
ipHeader := header.IPv4(packet)
if ipHeader.TTL() <= 1 {
return false
}
ipHeader.SetTTL(ipHeader.TTL() - 1)
ipHeader.SetChecksum(0)
ipHeader.SetChecksum(^ipHeader.CalculateChecksum())
return true
}
ipHeader := header.IPv6(packet)
if ipHeader.HopLimit() <= 1 {
return false
}
ipHeader.SetHopLimit(ipHeader.HopLimit() - 1)
return true
}
func buildICMPError(packet []byte, errorType tun.ICMPError, inet4Source netip.Addr, inet6Source netip.Addr, mtu int, headroom int) (*buf.Buffer, bool) {
source := inet4Source
if header.IPVersion(packet) == header.IPv6Version {
source = inet6Source
}
reply, built := tun.BuildICMPError(packet, errorType, source, uint32(mtu), headroom)
if !built {
return nil, false
}
buffer := buf.As(reply)
buffer.Advance(headroom)
return buffer, true
}
+63
View File
@@ -0,0 +1,63 @@
package masque
import (
"net/netip"
"sync"
"go4.org/netipx"
)
type addressPool struct {
access sync.Mutex
prefix netip.Prefix
reserved netip.Addr
last netip.Addr
next netip.Addr
allocated map[netip.Addr]struct{}
}
func newAddressPool(address netip.Prefix) *addressPool {
prefix := address.Masked()
return &addressPool{
prefix: prefix,
reserved: address.Addr(),
last: netipx.PrefixLastIP(prefix),
next: prefix.Addr(),
allocated: make(map[netip.Addr]struct{}),
}
}
func (p *addressPool) usable(address netip.Addr) bool {
if address == p.reserved || address == p.prefix.Addr() {
return false
}
return !address.Is4() || address != p.last
}
func (p *addressPool) allocate() (netip.Addr, bool) {
p.access.Lock()
defer p.access.Unlock()
start := p.next
for {
candidate := p.next
if candidate == p.last {
p.next = p.prefix.Addr()
} else {
p.next = candidate.Next()
}
_, inUse := p.allocated[candidate]
if !inUse && p.usable(candidate) {
p.allocated[candidate] = struct{}{}
return candidate, true
}
if p.next == start {
return netip.Addr{}, false
}
}
}
func (p *addressPool) release(address netip.Addr) {
p.access.Lock()
defer p.access.Unlock()
delete(p.allocated, address)
}
+432
View File
@@ -0,0 +1,432 @@
package masque
import (
"context"
"net"
"net/http"
"net/netip"
"slices"
"strings"
"sync"
transportHTTP "github.com/sagernet/sing-box/transport/http"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/auth"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"go4.org/netipx"
)
type ServerHandler interface {
WriteInboundBuffers(packetBuffers []*buf.Buffer) error
}
type ServerOptions struct {
Context context.Context
Logger logger.ContextLogger
Path string
Address []netip.Prefix
AdvertiseRoutes []netip.Prefix
Resolve func(ctx context.Context, domain string) ([]netip.Addr, error)
Handler ServerHandler
}
type Server struct {
ctx context.Context
logger logger.ContextLogger
template *Template
pools []*addressPool
inet4Address netip.Addr
inet6Address netip.Addr
advertiseRoutes *netipx.IPSet
resolve func(ctx context.Context, domain string) ([]netip.Addr, error)
handler ServerHandler
access sync.RWMutex
addresses map[netip.Addr]*serverSession
advertisements []*serverSession
}
type serverSession struct {
*session
server *Server
ctx context.Context
user string
addresses []netip.Addr
advertisedRoutes []AddressRange
peerRoutes []AddressRange
}
func NewServer(options ServerOptions) (*Server, error) {
template, err := ParseTemplate(options.Path)
if err != nil {
return nil, err
}
if len(options.Address) == 0 {
return nil, E.New("missing address")
}
server := &Server{
ctx: options.Context,
logger: options.Logger,
template: template,
resolve: options.Resolve,
handler: options.Handler,
addresses: make(map[netip.Addr]*serverSession),
}
var builder netipx.IPSetBuilder
for _, address := range options.Address {
builder.AddPrefix(address.Masked())
if address.Addr().Is4() {
if server.inet4Address.IsValid() {
return nil, E.New("multiple IPv4 addresses are not supported")
}
server.inet4Address = address.Addr()
} else {
if server.inet6Address.IsValid() {
return nil, E.New("multiple IPv6 addresses are not supported")
}
server.inet6Address = address.Addr()
}
pool := newAddressPool(address)
probe, allocated := pool.allocate()
if !allocated {
return nil, E.New("address ", address, " leaves no addresses to assign")
}
pool.release(probe)
pool.next = pool.prefix.Addr()
server.pools = append(server.pools, pool)
}
if len(options.AdvertiseRoutes) == 0 {
builder.AddPrefix(netip.PrefixFrom(netip.IPv4Unspecified(), 0))
builder.AddPrefix(netip.PrefixFrom(netip.IPv6Unspecified(), 0))
}
for _, route := range options.AdvertiseRoutes {
builder.AddPrefix(route)
}
server.advertiseRoutes, err = builder.IPSet()
if err != nil {
return nil, E.Cause(err, "build advertised routes")
}
return server, nil
}
func (s *Server) NewTunnelRequest(ctx context.Context, request transportHTTP.TunnelRequest) {
scope, matched, err := s.template.Match(request.Request().URL)
if !matched {
s.logger.ErrorContext(ctx, "process connection from ", request.Source(), ": unexpected path: ", request.Request().URL.Path)
request.Reject(http.StatusNotFound, nil)
return
}
if err != nil {
s.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", request.Source()))
request.Reject(http.StatusBadRequest, nil)
return
}
var builder netipx.IPSetBuilder
switch {
case scope.Domain != "":
addresses, resolveErr := s.resolve(ctx, scope.Domain)
if resolveErr != nil {
s.logger.ErrorContext(ctx, E.Cause(resolveErr, "process connection from ", request.Source(), ": resolve ", scope.Domain))
request.Reject(http.StatusBadGateway, http.Header{"Proxy-Status": []string{"sing-box; error=dns_error"}})
return
}
for _, address := range addresses {
builder.Add(address)
}
builder.Intersect(s.advertiseRoutes)
case scope.Prefix.IsValid():
builder.AddPrefix(scope.Prefix)
builder.Intersect(s.advertiseRoutes)
default:
builder.AddSet(s.advertiseRoutes)
}
current := &serverSession{
server: s,
ctx: ctx,
}
current.user, _ = auth.UserFromContext[string](ctx)
for _, pool := range s.pools {
address, allocated := pool.allocate()
if !allocated {
continue
}
current.addresses = append(current.addresses, address)
}
defer s.releaseSession(current)
if len(current.addresses) == 0 {
s.logger.ErrorContext(ctx, "process connection from ", request.Source(), ": address pool exhausted")
request.Reject(http.StatusServiceUnavailable, nil)
return
}
var familyBuilder netipx.IPSetBuilder
for _, address := range current.addresses {
if address.Is4() {
familyBuilder.AddPrefix(netip.PrefixFrom(netip.IPv4Unspecified(), 0))
} else {
familyBuilder.AddPrefix(netip.PrefixFrom(netip.IPv6Unspecified(), 0))
}
}
families, _ := familyBuilder.IPSet()
builder.Intersect(families)
routeSet, err := builder.IPSet()
if err != nil {
s.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", request.Source(), ": build routes"))
request.Reject(http.StatusInternalServerError, nil)
return
}
current.advertisedRoutes = common.Map(routeSet.Ranges(), func(it netipx.IPRange) AddressRange {
return AddressRange{Start: it.From(), End: it.To(), Protocol: scope.Protocol}
})
if len(current.advertisedRoutes) == 0 {
s.logger.ErrorContext(ctx, "process connection from ", request.Source(), ": requested target is not routable")
request.Reject(http.StatusForbidden, nil)
return
}
stream, err := request.Accept()
if err != nil {
s.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", request.Source()))
return
}
current.session = newSession(s.ctx, stream, current, true)
s.access.Lock()
for _, address := range current.addresses {
s.addresses[address] = current
}
s.access.Unlock()
assignedAddresses := strings.Join(common.Map(current.addresses, netip.Addr.String), " ")
if current.user != "" {
s.logger.InfoContext(ctx, "[", current.user, "] inbound tunnel from ", request.Source(), " assigned ", assignedAddresses)
} else {
s.logger.InfoContext(ctx, "inbound tunnel from ", request.Source(), " assigned ", assignedAddresses)
}
err = current.writeCapsule(newAddressCapsule(capsuleTypeAddressAssign, current.assignedAddresses(nil)))
if err == nil {
err = current.writeCapsule(newRouteCapsule(current.advertisedRoutes))
}
if err != nil {
current.cancel(err)
}
err = current.run()
if err != nil && !E.IsClosedOrCanceled(err) {
s.logger.ErrorContext(ctx, E.Cause(err, "tunnel from ", request.Source(), " closed"))
} else {
s.logger.DebugContext(ctx, "tunnel from ", request.Source(), " closed")
}
}
func (s *Server) releaseSession(current *serverSession) {
s.access.Lock()
for _, address := range current.addresses {
if s.addresses[address] == current {
delete(s.addresses, address)
}
}
s.advertisements = slices.DeleteFunc(s.advertisements, func(it *serverSession) bool {
return it == current
})
s.access.Unlock()
for _, address := range current.addresses {
for _, pool := range s.pools {
if pool.prefix.Contains(address) {
pool.release(address)
}
}
}
}
func (s *Server) lookup(destination netip.Addr, protocol uint8) *serverSession {
s.access.RLock()
defer s.access.RUnlock()
current, loaded := s.addresses[destination]
if loaded {
return current
}
if destination == s.inet4Address || destination == s.inet6Address {
return nil
}
for _, advertisement := range slices.Backward(s.advertisements) {
if RoutesContain(advertisement.peerRoutes, destination, protocol) {
return advertisement
}
}
return nil
}
func (s *Server) Contains(address netip.Addr) bool {
s.access.RLock()
defer s.access.RUnlock()
_, loaded := s.addresses[address]
return loaded || slices.ContainsFunc(s.advertisements, func(it *serverSession) bool {
return rangesContain(it.peerRoutes, address)
})
}
func (s *Server) WritePacketBuffers(packetBuffers []*buf.Buffer, forwarded bool) error {
var replies []*buf.Buffer
for _, packetBuffer := range packetBuffers {
source, destination, protocol, valid := packetAddresses(packetBuffer.Bytes())
if !valid {
packetBuffer.Release()
continue
}
errorType := tun.ICMPErrorNoRoute
target := s.lookup(destination, protocol)
routed := target != nil && target.accepts(source, protocol)
if routed && forwarded && !decrementHopLimit(packetBuffer.Bytes()) {
routed = false
errorType = tun.ICMPErrorHopLimitExceeded
}
if routed {
buffer := buf.NewSize(transportHTTP.CapsuleHeadroom + packetBuffer.Len())
buffer.Resize(transportHTTP.CapsuleHeadroom, 0)
buffer.Write(packetBuffer.Bytes())
target.queuePacket(buffer)
} else {
reply, built := buildICMPError(packetBuffer.Bytes(), errorType, s.inet4Address, s.inet6Address, 0, PacketHeadroom)
if built {
replies = append(replies, reply)
}
}
packetBuffer.Release()
}
if len(replies) > 0 {
return s.handler.WriteInboundBuffers(replies)
}
return nil
}
func (s *Server) Close() error {
s.access.RLock()
sessions := make([]*serverSession, 0, len(s.addresses))
for _, current := range s.addresses {
sessions = append(sessions, current)
}
s.access.RUnlock()
for _, current := range sessions {
current.cancel(net.ErrClosed)
}
return nil
}
func (s *serverSession) accepts(source netip.Addr, protocol uint8) bool {
return isControlProtocol(protocol) || RoutesContain(s.advertisedRoutes, source, protocol)
}
func (s *serverSession) assignedAddresses(requests []AssignedAddress) []AssignedAddress {
assigned := make([]AssignedAddress, 0, len(s.addresses)+len(requests))
answered := make(map[netip.Addr]bool)
for _, request := range requests {
index := slices.IndexFunc(s.addresses, func(address netip.Addr) bool {
return address.Is4() == request.Prefix.Addr().Is4() && !answered[address]
})
if index == -1 {
unspecified := netip.IPv4Unspecified()
if request.Prefix.Addr().Is6() {
unspecified = netip.IPv6Unspecified()
}
assigned = append(assigned, AssignedAddress{RequestID: request.RequestID, Prefix: netip.PrefixFrom(unspecified, unspecified.BitLen())})
continue
}
address := s.addresses[index]
answered[address] = true
assigned = append(assigned, AssignedAddress{RequestID: request.RequestID, Prefix: netip.PrefixFrom(address, address.BitLen())})
}
for _, address := range s.addresses {
if !answered[address] {
assigned = append(assigned, AssignedAddress{Prefix: netip.PrefixFrom(address, address.BitLen())})
}
}
return assigned
}
func (s *serverSession) handleAddressAssign(addresses []AssignedAddress) error {
return nil
}
func (s *serverSession) handleAddressRequest(addresses []AssignedAddress) error {
return s.writeCapsule(newAddressCapsule(capsuleTypeAddressAssign, s.assignedAddresses(addresses)))
}
func (s *serverSession) handleRouteAdvertisement(routes []AddressRange) error {
s.server.access.Lock()
s.peerRoutes = routes
s.server.advertisements = slices.DeleteFunc(s.server.advertisements, func(it *serverSession) bool {
return it == s
})
if len(routes) > 0 {
s.server.advertisements = append(s.server.advertisements, s)
}
s.server.access.Unlock()
s.server.logger.InfoContext(s.ctx, "peer advertised ", len(routes), " routes")
return nil
}
func (s *serverSession) handlePacket(buffer *buf.Buffer) {
source, destination, protocol, valid := packetAddresses(buffer.Bytes())
if !valid {
buffer.Release()
return
}
s.server.access.RLock()
peerRoutes := s.peerRoutes
s.server.access.RUnlock()
errorType := tun.ICMPErrorNoRoute
switch {
case !slices.Contains(s.addresses, source) && !rangesContain(peerRoutes, source):
errorType = tun.ICMPErrorSourcePolicy
case destination.IsLinkLocalUnicast() || destination.IsLinkLocalMulticast() || destination.IsInterfaceLocalMulticast():
buffer.Release()
return
case !RoutesContain(s.advertisedRoutes, destination, protocol):
default:
target := s.server.lookup(destination, protocol)
if target == nil && destination != s.server.inet4Address && destination != s.server.inet6Address && slices.ContainsFunc(s.server.pools, func(pool *addressPool) bool {
return pool.prefix.Contains(destination)
}) {
errorType = tun.ICMPErrorAddressUnreachable
break
}
if target == nil {
err := s.server.handler.WriteInboundBuffers([]*buf.Buffer{buffer})
if err != nil {
s.server.logger.DebugContext(s.ctx, E.Cause(err, "write packet to device"))
}
return
}
if !target.accepts(source, protocol) {
break
}
if !decrementHopLimit(buffer.Bytes()) {
errorType = tun.ICMPErrorHopLimitExceeded
break
}
target.queuePacket(buffer)
return
}
reply, built := buildICMPError(buffer.Bytes(), errorType, s.server.inet4Address, s.server.inet6Address, 0, transportHTTP.CapsuleHeadroom)
buffer.Release()
if built {
s.queuePacket(reply)
}
}
func (s *serverSession) handlePacketTooBig(buffer *buf.Buffer, mtu int) {
reply, built := buildICMPError(buffer.Bytes(), tun.ICMPErrorPacketTooBig, s.server.inet4Address, s.server.inet6Address, mtu, PacketHeadroom)
buffer.Release()
if !built {
return
}
_, destination, protocol, _ := packetAddresses(reply.Bytes())
target := s.server.lookup(destination, protocol)
if target != nil {
target.queuePacket(reply)
return
}
err := s.server.handler.WriteInboundBuffers([]*buf.Buffer{reply})
if err != nil {
s.server.logger.DebugContext(s.ctx, E.Cause(err, "write packet to device"))
}
}
+249
View File
@@ -0,0 +1,249 @@
package masque
import (
std_bufio "bufio"
"context"
"errors"
"io"
"sync"
transportHTTP "github.com/sagernet/sing-box/transport/http"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
)
const (
upgradeToken = "connect-ip"
DefaultMTU = 1280
PacketHeadroom = 64
QUICPacketOverhead = 51
minimumLinkMTU = 1280
maxPacketSize = 65535
sendQueueSize = 256
)
type sessionHandler interface {
handleAddressAssign(addresses []AssignedAddress) error
handleAddressRequest(addresses []AssignedAddress) error
handleRouteAdvertisement(routes []AddressRange) error
handlePacket(buffer *buf.Buffer)
handlePacketTooBig(buffer *buf.Buffer, mtu int)
}
type session struct {
ctx context.Context
cancel context.CancelCauseFunc
stream io.ReadWriteCloser
datagrams transportHTTP.DatagramStream
reader *std_bufio.Reader
handler sessionHandler
sendQueue chan *buf.Buffer
writeAccess sync.Mutex
}
func newSession(ctx context.Context, stream io.ReadWriteCloser, handler sessionHandler, queued bool) *session {
sessionCtx, cancel := context.WithCancelCause(ctx)
datagrams, _ := stream.(transportHTTP.DatagramStream)
current := &session{
ctx: sessionCtx,
cancel: cancel,
stream: stream,
datagrams: datagrams,
reader: std_bufio.NewReader(stream),
handler: handler,
}
if queued {
current.sendQueue = make(chan *buf.Buffer, sendQueueSize)
}
return current
}
func (s *session) run() error {
stop := context.AfterFunc(s.ctx, func() {
s.stream.Close()
})
defer stop()
var loops sync.WaitGroup
if s.datagrams != nil {
loops.Go(s.loopDatagram)
}
if s.sendQueue != nil {
loops.Go(s.loopSend)
}
err := s.loopCapsule()
s.cancel(err)
s.stream.Close()
loops.Wait()
return context.Cause(s.ctx)
}
func (s *session) loopDatagram() {
for {
datagram, err := s.datagrams.ReceiveDatagram(s.ctx)
if err != nil {
s.cancel(err)
return
}
contextID, contextLength, valid := transportHTTP.DecodeVarint(datagram)
if !valid || contextID != 0 || len(datagram) == contextLength {
continue
}
buffer := buf.NewSize(PacketHeadroom + len(datagram) - contextLength)
buffer.Resize(PacketHeadroom, 0)
buffer.Write(datagram[contextLength:])
s.handler.handlePacket(buffer)
}
}
func (s *session) loopSend() {
for {
select {
case buffer := <-s.sendQueue:
err := s.writePacket(buffer)
if err != nil {
s.cancel(err)
}
case <-s.ctx.Done():
for {
select {
case buffer := <-s.sendQueue:
buffer.Release()
default:
return
}
}
}
}
}
func (s *session) loopCapsule() error {
for {
capsuleType, _, err := transportHTTP.ReadVarint(s.reader)
if err != nil {
return err
}
length, _, err := transportHTTP.ReadVarint(s.reader)
if err != nil {
return err
}
if length > transportHTTP.MaxCapsuleLength {
return E.New("capsule too large: ", length)
}
switch capsuleType {
case transportHTTP.CapsuleTypeDatagram:
err = s.readDatagramCapsule(int(length))
case capsuleTypeAddressAssign, capsuleTypeAddressRequest, capsuleTypeRouteAdvertisement:
err = s.readControlCapsule(capsuleType, int(length))
default:
_, err = s.reader.Discard(int(length))
}
if err != nil {
return err
}
}
}
func (s *session) readDatagramCapsule(length int) error {
contextID, contextLength, err := transportHTTP.ReadVarint(s.reader)
if err != nil {
return err
}
if contextLength > length {
return E.New("malformed datagram capsule")
}
payloadLength := length - contextLength
if contextID != 0 || payloadLength == 0 || payloadLength > maxPacketSize {
_, err = s.reader.Discard(payloadLength)
return err
}
buffer := buf.NewSize(PacketHeadroom + payloadLength)
buffer.Resize(PacketHeadroom, 0)
_, err = buffer.ReadFullFrom(s.reader, payloadLength)
if err != nil {
buffer.Release()
return err
}
s.handler.handlePacket(buffer)
return nil
}
func (s *session) readControlCapsule(capsuleType uint64, length int) error {
payload := make([]byte, length)
_, err := io.ReadFull(s.reader, payload)
if err != nil {
return err
}
switch capsuleType {
case capsuleTypeAddressAssign:
addresses, parseErr := parseAddresses(payload)
if parseErr != nil {
return E.Cause(parseErr, "parse ADDRESS_ASSIGN capsule")
}
return s.handler.handleAddressAssign(addresses)
case capsuleTypeAddressRequest:
addresses, parseErr := parseAddresses(payload)
if parseErr != nil {
return E.Cause(parseErr, "parse ADDRESS_REQUEST capsule")
}
if len(addresses) == 0 {
return E.New("empty ADDRESS_REQUEST capsule")
}
for _, address := range addresses {
if address.RequestID == 0 {
return E.New("ADDRESS_REQUEST capsule with zero request ID")
}
}
return s.handler.handleAddressRequest(addresses)
default:
routes, parseErr := parseRoutes(payload)
if parseErr != nil {
return E.Cause(parseErr, "parse ROUTE_ADVERTISEMENT capsule")
}
return s.handler.handleRouteAdvertisement(routes)
}
}
func (s *session) writeCapsule(capsule *buf.Buffer) error {
defer capsule.Release()
s.writeAccess.Lock()
defer s.writeAccess.Unlock()
_, err := s.stream.Write(capsule.Bytes())
return err
}
func (s *session) queuePacket(buffer *buf.Buffer) {
select {
case s.sendQueue <- buffer:
default:
buffer.Release()
}
}
func (s *session) writePacket(buffer *buf.Buffer) error {
datagram := transportHTTP.PrependContextID(buffer)
if s.datagrams != nil {
err := s.datagrams.SendDatagram(datagram.Bytes())
var tooLarge *transportHTTP.DatagramTooLargeError
switch {
case err == nil:
datagram.Release()
return nil
case errors.As(err, &tooLarge):
mtu := tooLarge.MaxPayloadSize - 1
if mtu < minimumLinkMTU {
datagram.Release()
return E.New("QUIC connection is unable to carry ", minimumLinkMTU, " bytes packets")
}
datagram.Advance(1)
s.handler.handlePacketTooBig(datagram, mtu)
return nil
case errors.Is(err, transportHTTP.ErrDatagramUnsupported):
default:
datagram.Release()
return err
}
}
s.writeAccess.Lock()
defer s.writeAccess.Unlock()
return transportHTTP.WriteDatagramCapsule(s.stream, datagram)
}
+237
View File
@@ -0,0 +1,237 @@
package masque
import (
"net/netip"
"net/url"
"regexp"
"strconv"
"strings"
E "github.com/sagernet/sing/common/exceptions"
)
const DefaultPath = "/.well-known/masque/ip/{target}/{ipproto}/"
type expression struct {
operator byte
variables []string
}
type Template struct {
literals []string
expressions []expression
pathExpression *regexp.Regexp
pathVariables []string
queryVariables map[string]string
}
type Scope struct {
Domain string
Prefix netip.Prefix
Protocol uint8
}
func ParseTemplate(path string) (*Template, error) {
if path == "" {
path = DefaultPath
}
if !strings.HasPrefix(path, "/") {
return nil, E.New("path must start with a slash: ", path)
}
for _, character := range []byte(path) {
if character < 0x21 || character > 0x7E {
return nil, E.New("path contains invalid characters: ", path)
}
}
template := &Template{queryVariables: make(map[string]string)}
var pathPattern strings.Builder
pathPattern.WriteString("^")
inQuery := false
remaining := path
for {
open := strings.IndexByte(remaining, '{')
if open == -1 {
break
}
closing := strings.IndexByte(remaining[open:], '}')
if closing == -1 {
return nil, E.New("unterminated expression in path: ", path)
}
literal := remaining[:open]
body := remaining[open+1 : open+closing]
remaining = remaining[open+closing+1:]
if body == "" {
return nil, E.New("empty expression in path: ", path)
}
var item expression
switch body[0] {
case '?', '&':
item.operator = body[0]
body = body[1:]
case '+', '#', '.', '/', ';':
return nil, E.New("unsupported expression operator in path: ", path)
}
item.variables = strings.Split(body, ",")
for _, variable := range item.variables {
if variable == "" || strings.ContainsAny(variable, ":*") {
return nil, E.New("unsupported expression in path: ", path)
}
}
if !inQuery {
literalPath, _, hasQuery := strings.Cut(literal, "?")
if hasQuery {
inQuery = true
pathPattern.WriteString(regexp.QuoteMeta(literalPath))
} else if item.operator != 0 {
inQuery = true
pathPattern.WriteString(regexp.QuoteMeta(literal))
} else {
if len(item.variables) != 1 {
return nil, E.New("unsupported expression in path: ", path)
}
pathPattern.WriteString(regexp.QuoteMeta(literal))
pathPattern.WriteString("([^/]*)")
template.pathVariables = append(template.pathVariables, item.variables[0])
}
}
if inQuery && item.operator == 0 {
queryKey := literal[strings.LastIndexAny(literal, "?&")+1:]
if len(item.variables) != 1 || len(queryKey) < 2 || !strings.HasSuffix(queryKey, "=") {
return nil, E.New("unsupported expression in path: ", path)
}
template.queryVariables[strings.TrimSuffix(queryKey, "=")] = item.variables[0]
}
template.literals = append(template.literals, literal)
template.expressions = append(template.expressions, item)
}
if !inQuery {
remainingPath, _, _ := strings.Cut(remaining, "?")
pathPattern.WriteString(regexp.QuoteMeta(remainingPath))
}
pathPattern.WriteString("$")
template.literals = append(template.literals, remaining)
pathExpression, err := regexp.Compile(pathPattern.String())
if err != nil {
return nil, E.Cause(err, "compile path: ", path)
}
template.pathExpression = pathExpression
return template, nil
}
func (t *Template) Expand() string {
var result strings.Builder
for i, item := range t.expressions {
result.WriteString(t.literals[i])
separator := item.operator
for _, variable := range item.variables {
if variable != "target" && variable != "ipproto" {
continue
}
if separator != 0 {
result.WriteByte(separator)
result.WriteString(variable)
result.WriteByte('=')
separator = '&'
}
result.WriteByte('*')
}
}
result.WriteString(t.literals[len(t.literals)-1])
return result.String()
}
func (t *Template) Match(requestURL *url.URL) (Scope, bool, error) {
match := t.pathExpression.FindStringSubmatch(requestURL.EscapedPath())
if match == nil {
return Scope{}, false, nil
}
target := "*"
protocol := "*"
query := requestURL.Query()
for _, item := range t.expressions {
if item.operator == 0 {
continue
}
for _, variable := range item.variables {
if !query.Has(variable) {
continue
}
switch variable {
case "target":
target = query.Get(variable)
case "ipproto":
protocol = query.Get(variable)
}
}
}
for queryKey, variable := range t.queryVariables {
if !query.Has(queryKey) {
continue
}
switch variable {
case "target":
target = query.Get(queryKey)
case "ipproto":
protocol = query.Get(queryKey)
}
}
for i, variable := range t.pathVariables {
value, err := url.PathUnescape(match[i+1])
if err != nil {
return Scope{}, true, E.Cause(err, "decode ", variable)
}
switch variable {
case "target":
target = value
case "ipproto":
protocol = value
}
}
var scope Scope
switch target {
case "":
return Scope{}, true, E.New("empty target")
case "*":
default:
prefix, err := parseTarget(target)
if err == nil {
scope.Prefix = prefix
} else if strings.ContainsAny(target, ":/") {
return Scope{}, true, E.Cause(err, "parse target")
} else {
scope.Domain = target
}
}
if protocol != "*" {
protocolNumber, err := strconv.ParseUint(protocol, 10, 8)
if err != nil {
return Scope{}, true, E.New("invalid ipproto: ", protocol)
}
scope.Protocol = uint8(protocolNumber)
}
return scope, true, nil
}
func parseTarget(target string) (netip.Prefix, error) {
var prefix netip.Prefix
if strings.Contains(target, "/") {
var err error
prefix, err = netip.ParsePrefix(target)
if err != nil {
return netip.Prefix{}, err
}
} else {
address, err := netip.ParseAddr(target)
if err != nil {
return netip.Prefix{}, err
}
prefix = netip.PrefixFrom(address, address.BitLen())
}
if prefix.Addr().Zone() != "" {
return netip.Prefix{}, E.New("zone identifiers are not supported: ", target)
}
if prefix.Masked() != prefix {
return netip.Prefix{}, E.New("prefix has host bits set: ", target)
}
return prefix, nil
}
+42
View File
@@ -0,0 +1,42 @@
package openconnect
import (
"net/netip"
"time"
"github.com/sagernet/sing-openconnect"
)
const (
DefaultMTU = 1500
PacketHeadroom = openconnect.PacketHeadroom
)
type Configuration struct {
MTU uint32
Addresses []netip.Prefix
Routes []Route
ExcludedRoutes []Route
DNS []netip.Addr
NBNS []netip.Addr
SearchDomains []string
SplitDNS []string
SplitDNSRules []SplitDNSRule
ProxyAutoConfigURL string
Banner string
TunnelAllDNS bool
ClientBypassProtocol bool
IdleTimeout time.Duration
AuthenticationExpiration time.Time
}
type Route struct {
Prefix netip.Prefix
Gateway netip.Addr
Metric int
}
type SplitDNSRule struct {
Domains []string
Servers []netip.Addr
}
-220
View File
@@ -1,220 +0,0 @@
package openconnect
import (
"context"
"net/netip"
"sync/atomic"
"time"
"github.com/sagernet/sing-openconnect"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
N "github.com/sagernet/sing/common/network"
)
const (
DefaultMTU = 1500
PacketHeadroom = openconnect.PacketHeadroom
)
type PacketWriter func(packetBuffers []*buf.Buffer) error
type Device interface {
N.Dialer
Start() error
UpdateConfiguration(configuration Configuration) error
WriteInboundBuffers(packetBuffers []*buf.Buffer) error
SetPacketWriter(writer PacketWriter)
PortAddresses() (netip.Addr, netip.Addr)
PortMTU() uint32
AttachReturn(returnPath tun.Return) error
DetachReturn(returnPath tun.Return) error
ReturnPath() (tun.Return, int)
Close() error
}
type DeviceOptions struct {
Context context.Context
Logger logger.ContextLogger
System bool
Handler tun.Handler
UDPTimeout time.Duration
ICMPTimeout time.Duration
UDPMapping tun.NATMapping
UDPFiltering tun.NATFiltering
UDPNATMax uint32
InterfaceFinder control.InterfaceFinder
MemoryPressure func() tun.MemoryPressure
Name string
MTU uint32
Configuration Configuration
}
type Configuration struct {
MTU uint32
Addresses []netip.Prefix
Routes []Route
ExcludedRoutes []Route
DNS []netip.Addr
NBNS []netip.Addr
SearchDomains []string
SplitDNS []string
SplitDNSRules []SplitDNSRule
ProxyAutoConfigURL string
Banner string
TunnelAllDNS bool
ClientBypassProtocol bool
IdleTimeout time.Duration
AuthenticationExpiration time.Time
}
type Route struct {
Prefix netip.Prefix
Gateway netip.Addr
Metric int
}
type SplitDNSRule struct {
Domains []string
Servers []netip.Addr
}
func NewDevice(options DeviceOptions) (Device, error) {
if !options.System {
return newStackDevice(options)
}
return newSystemStackDevice(options)
}
func newStack(options DeviceOptions, memoryTun *tun.MemoryTun) (*tun.Go, error) {
return tun.NewGo(tun.StackOptions{
Context: options.Context,
Tun: memoryTun,
TunOptions: tun.Options{MTU: options.MTU},
UDPTimeout: options.UDPTimeout,
ICMPTimeout: options.ICMPTimeout,
UDPMapping: options.UDPMapping,
UDPFiltering: options.UDPFiltering,
UDPNATMax: options.UDPNATMax,
Handler: options.Handler,
Logger: options.Logger,
InterfaceFinder: options.InterfaceFinder,
MemoryPressure: options.MemoryPressure,
})
}
type baseDevice struct {
packetWriter PacketWriter
returnState atomic.Pointer[returnPathState]
}
func (d *baseDevice) SetPacketWriter(writer PacketWriter) {
d.packetWriter = writer
}
func (d *baseDevice) writeOutbound(packetBuffers []*buf.Buffer) error {
if d.packetWriter == nil {
buf.ReleaseMulti(packetBuffers)
return E.New("missing packet writer")
}
return d.packetWriter(packetBuffers)
}
func (d *baseDevice) processInboundBuffers(packetBuffers []*buf.Buffer, writeBuffers func(packetBuffers []*buf.Buffer) error) error {
if len(packetBuffers) == 0 {
return nil
}
state := d.returnState.Load()
if state == nil {
return writeBuffers(packetBuffers)
}
packets := make([][]byte, len(packetBuffers))
for i, packetBuffer := range packetBuffers {
packetBuffer.ExtendHeader(state.headroom)
packets[i] = packetBuffer.Bytes()
}
unconsumed := state.returnPath.ReturnPackets(packets)
if len(unconsumed) == 0 {
return nil
}
unconsumedBuffers := make([]*buf.Buffer, len(unconsumed))
for i, packet := range unconsumed {
packetBuffer := buf.As(packet)
packetBuffer.Advance(state.headroom)
unconsumedBuffers[i] = packetBuffer
}
return writeBuffers(unconsumedBuffers)
}
func (d *baseDevice) AttachReturn(returnPath tun.Return) error {
headroom := returnPath.ReturnHeadroom()
if headroom > PacketHeadroom {
return E.New("return path headroom ", headroom, " exceeds available ", PacketHeadroom)
}
newState := &returnPathState{
returnPath: returnPath,
headroom: headroom,
}
for {
currentState := d.returnState.Load()
if currentState != nil {
if currentState.returnPath == returnPath {
return nil
}
return E.New("return path already attached")
}
if d.returnState.CompareAndSwap(nil, newState) {
return nil
}
}
}
func (d *baseDevice) DetachReturn(returnPath tun.Return) error {
currentState := d.returnState.Load()
if currentState != nil && currentState.returnPath == returnPath {
d.returnState.CompareAndSwap(currentState, nil)
}
return nil
}
func (d *baseDevice) ReturnPath() (tun.Return, int) {
state := d.returnState.Load()
if state == nil {
return nil, 0
}
return state.returnPath, state.headroom
}
type returnPathState struct {
returnPath tun.Return
headroom int
}
func firstAddresses(addresses []netip.Prefix) (netip.Addr, netip.Addr) {
var inet4Address netip.Addr
var inet6Address netip.Addr
for _, prefix := range addresses {
if prefix.Addr().Is4() && !inet4Address.IsValid() {
inet4Address = prefix.Addr()
} else if prefix.Addr().Is6() && !inet6Address.IsValid() {
inet6Address = prefix.Addr()
}
}
return inet4Address, inet6Address
}
func splitPrefixes(prefixes []netip.Prefix) ([]netip.Prefix, []netip.Prefix) {
var inet4Prefixes []netip.Prefix
var inet6Prefixes []netip.Prefix
for _, prefix := range prefixes {
if prefix.Addr().Is4() {
inet4Prefixes = append(inet4Prefixes, prefix)
} else {
inet6Prefixes = append(inet6Prefixes, prefix)
}
}
return inet4Prefixes, inet6Prefixes
}
-356
View File
@@ -1,356 +0,0 @@
package openconnect
import (
"context"
"net"
"net/netip"
"slices"
"sync"
"syscall"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing-tun/gtcpip/header"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
)
var _ Device = (*systemDevice)(nil)
const (
systemDeviceReadBufferSize = 65535 + tun.PacketOffset
systemDevicePacketRearSpace = 64
)
type systemDevice struct {
baseDevice
stateAccess sync.RWMutex
options DeviceOptions
dialer N.Dialer
device tun.Tun
inet4Address netip.Addr
inet6Address netip.Addr
closed bool
}
func newSystemDevice(options DeviceOptions) (*systemDevice, error) {
if options.Name == "" {
options.Name = tun.CalculateInterfaceName("oc")
}
if options.MTU == 0 {
options.MTU = DefaultMTU
}
interfaceDialer, err := dialer.NewDefault(options.Context, option.DialerOptions{
AbstractDialerOptions: option.AbstractDialerOptions{
BindInterface: options.Name,
},
})
if err != nil {
return nil, err
}
inet4Address, inet6Address := firstAddresses(options.Configuration.Addresses)
return &systemDevice{
options: options,
dialer: interfaceDialer,
inet4Address: inet4Address,
inet6Address: inet6Address,
}, nil
}
func (d *systemDevice) Start() error {
d.stateAccess.Lock()
defer d.stateAccess.Unlock()
return d.startLocked()
}
func (d *systemDevice) startLocked() error {
if d.closed {
return net.ErrClosed
}
if d.device != nil {
return nil
}
tunOptions := d.buildTunOptions()
tunInterface, err := tun.New(tunOptions)
if err != nil {
return err
}
err = tunInterface.Start()
if err != nil {
tunInterface.Close()
return err
}
d.device = tunInterface
d.options.Logger.Info("started at ", d.options.Name)
go d.readLoop(tunInterface, int(d.options.MTU))
return nil
}
func (d *systemDevice) buildTunOptions() tun.Options {
inet4Address, inet6Address := firstAddresses(d.options.Configuration.Addresses)
d.inet4Address = inet4Address
d.inet6Address = inet6Address
inet4Addresses, inet6Addresses := splitPrefixes(d.options.Configuration.Addresses)
networkManager := service.FromContext[adapter.NetworkManager](d.options.Context)
tunOptions := tun.Options{
Name: d.options.Name,
Inet4Address: inet4Addresses,
Inet6Address: inet6Addresses,
MTU: d.options.MTU,
GSO: true,
InterfaceScope: true,
DNSMode: tun.DNSModeDisabled,
InterfaceMonitor: nil,
InterfaceFinder: nil,
Logger: d.options.Logger,
EXP_DisableDNSHijack: true,
}
if networkManager != nil {
tunOptions.InterfaceMonitor = networkManager.InterfaceMonitor()
tunOptions.InterfaceFinder = networkManager.InterfaceFinder()
}
return tunOptions
}
func (d *systemDevice) readLoop(tunInterface tun.Tun, mtu int) {
linuxTUN, isLinuxTUN := tunInterface.(tun.LinuxTUN)
if isLinuxTUN && linuxTUN.BatchSize() > 1 {
d.readLoopLinux(linuxTUN, linuxTUN.BatchSize(), mtu)
return
}
darwinTUN, isDarwinTUN := tunInterface.(tun.DarwinTUN)
if isDarwinTUN {
d.readLoopDarwin(darwinTUN)
return
}
packetBuffer := buf.NewSize(PacketHeadroom + systemDeviceReadBufferSize + systemDevicePacketRearSpace)
defer packetBuffer.Release()
for {
packetBuffer.Reset()
packetBuffer.Resize(PacketHeadroom, 0)
readN, err := tunInterface.Read(packetBuffer.FreeBytes()[:systemDeviceReadBufferSize])
if err != nil {
if E.IsClosed(err) {
return
}
d.options.Logger.Error(E.Cause(err, "read packet"))
return
}
if readN <= tun.PacketOffset {
continue
}
packetBuffer.Truncate(readN)
packetBuffer.Advance(tun.PacketOffset)
packetBuffer.IncRef()
err = d.writeOutbound([]*buf.Buffer{packetBuffer})
packetBuffer.DecRef()
if err != nil {
d.options.Logger.Debug(E.Cause(err, "write packet"))
}
}
}
func (d *systemDevice) readLoopLinux(tunInterface tun.LinuxTUN, batchSize int, mtu int) {
packetBuffers := make([]*buf.Buffer, batchSize)
readBuffers := make([][]byte, batchSize)
packetSizes := make([]int, batchSize)
for i := range packetBuffers {
packetBuffers[i] = buf.NewSize(PacketHeadroom + mtu + systemDevicePacketRearSpace)
}
defer buf.ReleaseMulti(packetBuffers)
for {
for i, packetBuffer := range packetBuffers {
packetBuffer.Reset()
packetBuffer.Resize(PacketHeadroom, 0)
readBuffers[i] = packetBuffer.FreeBytes()[:mtu]
}
packetCount, readErr := tunInterface.BatchRead(readBuffers, 0, packetSizes)
for i := range packetCount {
packetBuffers[i].Truncate(packetSizes[i])
packetBuffers[i].IncRef()
}
if packetCount > 0 {
writeErr := d.writeOutbound(packetBuffers[:packetCount])
for i := range packetCount {
packetBuffers[i].DecRef()
}
if writeErr != nil {
d.options.Logger.Debug(E.Cause(writeErr, "write packet batch"))
}
}
if readErr != nil {
if E.IsClosed(readErr) {
return
}
d.options.Logger.Error(E.Cause(readErr, "batch read packet"))
return
}
}
}
func (d *systemDevice) readLoopDarwin(tunInterface tun.DarwinTUN) {
for {
packetBuffers, readErr := tunInterface.BatchRead()
outboundBuffers := packetBuffers[:0]
for _, packetBuffer := range packetBuffers {
if packetBuffer.IsEmpty() {
packetBuffer.Release()
continue
}
outboundBuffers = append(outboundBuffers, packetBuffer)
}
if len(outboundBuffers) > 0 {
writeErr := d.writeOutbound(outboundBuffers)
if writeErr != nil {
d.options.Logger.Debug(E.Cause(writeErr, "write packet batch"))
}
}
if readErr != nil {
if E.IsClosed(readErr) || E.IsMulti(readErr, syscall.EBADF) {
return
}
d.options.Logger.Error(E.Cause(readErr, "batch read packet"))
return
}
}
}
func (d *systemDevice) UpdateConfiguration(configuration Configuration) error {
d.stateAccess.Lock()
defer d.stateAccess.Unlock()
previousConfiguration := d.options.Configuration
previousMTU := d.options.MTU
updatedMTU := d.options.MTU
if configuration.MTU != 0 {
updatedMTU = configuration.MTU
}
d.options.MTU = updatedMTU
d.options.Configuration = configuration
if d.device == nil {
inet4Address, inet6Address := firstAddresses(configuration.Addresses)
d.inet4Address = inet4Address
d.inet6Address = inet6Address
return nil
}
if !slices.Equal(previousConfiguration.Addresses, configuration.Addresses) ||
previousMTU != updatedMTU {
d.device.Close()
d.device = nil
return d.startLocked()
}
return nil
}
func (d *systemDevice) WriteInboundBuffers(packetBuffers []*buf.Buffer) error {
return d.processInboundBuffers(packetBuffers, d.writeBuffers)
}
func (d *systemDevice) writeBuffers(packetBuffers []*buf.Buffer) error {
d.stateAccess.RLock()
tunInterface := d.device
d.stateAccess.RUnlock()
if tunInterface == nil {
return E.New("system device is not ready")
}
linuxTUN, isLinuxTUN := tunInterface.(tun.LinuxTUN)
if isLinuxTUN {
headroom := linuxTUN.FrontHeadroom()
packets := make([][]byte, len(packetBuffers))
var temporaryBuffers []*buf.Buffer
for i, packetBuffer := range packetBuffers {
if packetBuffer.Start() >= headroom {
packetBuffer.ExtendHeader(headroom)
packets[i] = packetBuffer.Bytes()
packetBuffer.Advance(headroom)
continue
}
temporaryBuffer := buf.NewSize(headroom + packetBuffer.Len())
temporaryBuffer.Resize(headroom, 0)
_, _ = temporaryBuffer.Write(packetBuffer.Bytes())
temporaryBuffer.ExtendHeader(headroom)
packets[i] = temporaryBuffer.Bytes()
temporaryBuffers = append(temporaryBuffers, temporaryBuffer)
}
_, err := linuxTUN.BatchWrite(packets, headroom)
buf.ReleaseMulti(temporaryBuffers)
return err
}
darwinTUN, isDarwinTUN := tunInterface.(tun.DarwinTUN)
if isDarwinTUN {
return darwinTUN.BatchWrite(packetBuffers)
}
for _, packetBuffer := range packetBuffers {
err := d.writePacket(packetBuffer.Bytes())
if err != nil {
return err
}
}
return nil
}
func (d *systemDevice) writePacket(packet []byte) error {
d.stateAccess.RLock()
tunInterface := d.device
d.stateAccess.RUnlock()
if tunInterface == nil {
return E.New("system device is not ready")
}
if tun.PacketOffset == 0 {
_, err := tunInterface.Write(packet)
return err
}
writeBuffer := make([]byte, tun.PacketOffset+len(packet))
tun.PacketFillHeader(writeBuffer[:tun.PacketOffset], header.IPVersion(packet))
copy(writeBuffer[tun.PacketOffset:], packet)
_, err := tunInterface.Write(writeBuffer)
return err
}
func (d *systemDevice) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
if !destination.Addr.IsValid() {
return nil, E.New("invalid non-IP destination")
}
return d.dialer.DialContext(ctx, network, destination)
}
func (d *systemDevice) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
if !destination.Addr.IsValid() {
return nil, E.New("invalid non-IP destination")
}
return d.dialer.ListenPacket(ctx, destination)
}
func (d *systemDevice) PortAddresses() (netip.Addr, netip.Addr) {
d.stateAccess.RLock()
defer d.stateAccess.RUnlock()
return d.inet4Address, d.inet6Address
}
func (d *systemDevice) PortMTU() uint32 {
d.stateAccess.RLock()
defer d.stateAccess.RUnlock()
return d.options.MTU
}
func (d *systemDevice) Close() error {
d.stateAccess.Lock()
defer d.stateAccess.Unlock()
d.closed = true
if d.device == nil {
return nil
}
err := d.device.Close()
d.device = nil
return err
}
func (d *systemDevice) configurationAddresses() []netip.Prefix {
d.stateAccess.RLock()
defer d.stateAccess.RUnlock()
return slices.Clone(d.options.Configuration.Addresses)
}
+37
View File
@@ -0,0 +1,37 @@
package openvpn
import "net/netip"
const (
DefaultMTU = 1500
PacketHeadroom = 4096
)
type Configuration struct {
MTU uint32
Address []netip.Prefix
Routes []Route
ExcludedRoutes []Route
DNS []netip.Addr
DNSServers []DNSServer
SearchDomains []string
DNSRoutes []string
Topology string
Interface string
BlockIPv6 bool
}
type Route struct {
Prefix netip.Prefix
Gateway netip.Addr
Metric int
}
type DNSServer struct {
Priority int
Addresses []netip.AddrPort
ResolveDomains []string
DNSSEC string
Transport string
SNI string
}
-156
View File
@@ -1,156 +0,0 @@
package openvpn
import (
"context"
"net"
"net/netip"
"sync"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
var _ Device = (*stackDevice)(nil)
type stackDevice struct {
baseDevice
stateAccess sync.RWMutex
options DeviceOptions
stack *tun.Go
memoryTun *tun.MemoryTun
inet4Address netip.Addr
inet6Address netip.Addr
logRouteOptions bool
closeOnce sync.Once
}
func newStackDevice(options DeviceOptions) (*stackDevice, error) {
if options.MTU == 0 {
options.MTU = DefaultMTU
}
device := &stackDevice{
options: options,
logRouteOptions: true,
}
device.inet4Address, device.inet6Address = firstAddresses(options.Configuration.Address)
device.memoryTun = tun.NewMemoryTun(tun.MemoryTunOptions{
MTU: int(options.MTU),
Headroom: PacketHeadroom,
RearSpace: systemDevicePacketRearSpace,
Outbound: device.writeOutbound,
})
var err error
device.stack, err = newStack(options, device.memoryTun)
if err != nil {
return nil, err
}
return device, nil
}
func (d *stackDevice) Start() error {
return d.stack.Start()
}
func (d *stackDevice) UpdateConfiguration(configuration Configuration) error {
d.stateAccess.Lock()
defer d.stateAccess.Unlock()
if d.logRouteOptions && hasRouteOptions(configuration.Routes) {
d.options.Logger.Debug("route gateway and metric options are not representable by the stack device; routes are installed by prefix")
d.logRouteOptions = false
}
if configuration.MTU != 0 {
d.options.MTU = configuration.MTU
d.memoryTun.UpdateMTU(int(configuration.MTU))
}
d.options.Configuration = configuration
d.inet4Address, d.inet6Address = firstAddresses(configuration.Address)
return nil
}
func (d *stackDevice) WriteInboundBuffers(packetBuffers []*buf.Buffer) error {
return d.processInboundBuffers(packetBuffers, d.writeBuffers)
}
func (d *stackDevice) writeBuffers(packetBuffers []*buf.Buffer) error {
_, err := d.memoryTun.WritePackets(common.Map(packetBuffers, (*buf.Buffer).Bytes))
return err
}
func (d *stackDevice) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
bind, err := d.bindAddress(destination)
if err != nil {
return nil, err
}
switch N.NetworkName(network) {
case N.NetworkTCP:
var tcpConn *tun.GoConn
tcpConn, err = d.stack.DialTCP(ctx, bind, destination.AddrPort())
if err != nil {
return nil, err
}
return tcpConn, nil
case N.NetworkUDP:
var udpConn *tun.GoUDPConn
udpConn, err = d.stack.DialUDP(netip.AddrPortFrom(bind, 0), destination.AddrPort())
if err != nil {
return nil, err
}
return udpConn, nil
default:
return nil, E.Extend(N.ErrUnknownNetwork, network)
}
}
func (d *stackDevice) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
bind, err := d.bindAddress(destination)
if err != nil {
return nil, err
}
udpConn, err := d.stack.ListenUDP(netip.AddrPortFrom(bind, 0))
if err != nil {
return nil, err
}
return udpConn, nil
}
func (d *stackDevice) bindAddress(destination M.Socksaddr) (netip.Addr, error) {
d.stateAccess.RLock()
defer d.stateAccess.RUnlock()
if destination.IsIPv4() {
if !d.inet4Address.IsValid() {
return netip.Addr{}, E.New("missing IPv4 local address")
}
return d.inet4Address, nil
}
if d.options.Configuration.BlockIPv6 {
return netip.Addr{}, E.New("IPv6 blocked by pushed block-ipv6")
}
if !d.inet6Address.IsValid() {
return netip.Addr{}, E.New("missing IPv6 local address")
}
return d.inet6Address, nil
}
func (d *stackDevice) PortAddresses() (netip.Addr, netip.Addr) {
d.stateAccess.RLock()
defer d.stateAccess.RUnlock()
return d.inet4Address, d.inet6Address
}
func (d *stackDevice) PortMTU() uint32 {
d.stateAccess.RLock()
defer d.stateAccess.RUnlock()
return d.options.MTU
}
func (d *stackDevice) Close() error {
var err error
d.closeOnce.Do(func() {
err = E.Errors(d.stack.Close(), d.memoryTun.Close())
})
return err
}
-117
View File
@@ -1,117 +0,0 @@
package openvpn
import (
"net/netip"
"github.com/sagernet/sing-tun/gtcpip/header"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
)
var _ Device = (*systemStackDevice)(nil)
type systemStackDevice struct {
*systemDevice
stackDevice *stackDevice
}
func newSystemStackDevice(options DeviceOptions) (*systemStackDevice, error) {
system, err := newSystemDevice(options)
if err != nil {
return nil, err
}
stackDevice, err := newStackDevice(options)
if err != nil {
system.Close()
return nil, err
}
stackDevice.logRouteOptions = false
return &systemStackDevice{
systemDevice: system,
stackDevice: stackDevice,
}, nil
}
func (d *systemStackDevice) SetPacketWriter(writer PacketWriter) {
d.systemDevice.SetPacketWriter(writer)
d.stackDevice.SetPacketWriter(writer)
}
func (d *systemStackDevice) Start() error {
err := d.stackDevice.Start()
if err != nil {
return err
}
err = d.systemDevice.Start()
return err
}
func (d *systemStackDevice) UpdateConfiguration(configuration Configuration) error {
err := d.systemDevice.UpdateConfiguration(configuration)
if err != nil {
return err
}
return d.stackDevice.UpdateConfiguration(configuration)
}
func (d *systemStackDevice) WriteInboundBuffers(packetBuffers []*buf.Buffer) error {
return d.systemDevice.processInboundBuffers(packetBuffers, d.writeBuffers)
}
func (d *systemStackDevice) writeBuffers(packetBuffers []*buf.Buffer) error {
addresses := d.systemDevice.configurationAddresses()
runStart := 0
runUsesSystemDevice := false
var writeErr error
for i, packetBuffer := range packetBuffers {
destination := packetDestination(packetBuffer.Bytes())
useSystemDevice := false
for _, prefix := range addresses {
if prefix.Contains(destination) {
useSystemDevice = true
break
}
}
if i > runStart && useSystemDevice != runUsesSystemDevice {
var err error
if runUsesSystemDevice {
err = d.systemDevice.writeBuffers(packetBuffers[runStart:i])
} else {
err = d.stackDevice.writeBuffers(packetBuffers[runStart:i])
}
writeErr = E.Errors(writeErr, err)
runStart = i
}
if i == runStart {
runUsesSystemDevice = useSystemDevice
}
}
if runStart == len(packetBuffers) {
return writeErr
}
if runUsesSystemDevice {
return E.Errors(writeErr, d.systemDevice.writeBuffers(packetBuffers[runStart:]))
}
return E.Errors(writeErr, d.stackDevice.writeBuffers(packetBuffers[runStart:]))
}
func packetDestination(packet []byte) netip.Addr {
switch header.IPVersion(packet) {
case header.IPv4Version:
if len(packet) < header.IPv4MinimumSize {
return netip.Addr{}
}
return header.IPv4(packet).DestinationAddr()
case header.IPv6Version:
if len(packet) < header.IPv6MinimumSize {
return netip.Addr{}
}
return header.IPv6(packet).DestinationAddr()
default:
return netip.Addr{}
}
}
func (d *systemStackDevice) Close() error {
return E.Errors(d.stackDevice.Close(), d.systemDevice.Close())
}
+1 -1
View File
@@ -73,7 +73,7 @@ func (e *Endpoint) WritePackets(packets [][]byte) error {
} else {
source = e.tunDevice.Inet6Address()
}
reply, replyOk := tun.BuildUnreachable(packet, source, state.headroom)
reply, replyOk := tun.BuildICMPError(packet, tun.ICMPErrorNoRoute, source, 0, state.headroom)
if replyOk {
replies = append(replies, reply)
}