mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-09-15 21:00:27 +00:00
Refactor OpenVPN and OpenConnect endpoints
This commit is contained in:
@@ -13,6 +13,9 @@
|
||||
|
||||
"system": false,
|
||||
"name": "",
|
||||
|
||||
... // UDP NAT Fields
|
||||
|
||||
"server": "vpn.example.com",
|
||||
"flavor": "anyconnect",
|
||||
"username": "",
|
||||
@@ -378,6 +381,10 @@ Ask for the matching authentication field interactively instead of supplying an
|
||||
|
||||
Conflict with `form_entries.value`.
|
||||
|
||||
## UDP NAT Fields
|
||||
|
||||
See [UDP NAT Fields](/configuration/shared/udp-nat/) for details.
|
||||
|
||||
## Dial Fields
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
|
||||
"system": false,
|
||||
"name": "",
|
||||
|
||||
... // UDP NAT 字段
|
||||
|
||||
"server": "vpn.example.com",
|
||||
"flavor": "anyconnect",
|
||||
"username": "",
|
||||
@@ -378,6 +381,10 @@ MCA 证书和私钥必须同时设置或同时为空。
|
||||
|
||||
与 `form_entries.value` 冲突。
|
||||
|
||||
## UDP NAT 字段
|
||||
|
||||
参阅 [UDP NAT 字段](/zh/configuration/shared/udp-nat/)。
|
||||
|
||||
## 拨号字段
|
||||
|
||||
参阅[拨号字段](/zh/configuration/shared/dial/)了解详情。
|
||||
|
||||
@@ -69,14 +69,15 @@
|
||||
"route_metric": 0,
|
||||
"redirect_gateway": false,
|
||||
"redirect_gateway_flags": [],
|
||||
"keepalive_interval": "",
|
||||
"keepalive_timeout": "",
|
||||
"ping_interval": "",
|
||||
"ping_restart": "",
|
||||
"renegotiate_interval": "",
|
||||
"explicit_exit_notify": 0,
|
||||
"system": false,
|
||||
"name": "",
|
||||
"mtu": 1500,
|
||||
"udp_timeout": "",
|
||||
|
||||
... // UDP NAT Fields
|
||||
|
||||
... // Dial Fields
|
||||
}
|
||||
@@ -342,7 +343,8 @@ Maximum OpenVPN UDP packet size used to clamp the MSS of TCP connections sent th
|
||||
|
||||
This prevents TCP packets from exceeding the path MTU after OpenVPN encapsulation.
|
||||
|
||||
Disabled when `0`.
|
||||
When empty, the upstream OpenVPN default is used: `fragment` when configured,
|
||||
otherwise `1492` for the default tunnel MTU or the configured tunnel MTU.
|
||||
|
||||
### fragment
|
||||
|
||||
@@ -378,9 +380,10 @@ Conflict with non-stub compression enabled by `compression` or `compression_lzo`
|
||||
|
||||
### route_no_pull
|
||||
|
||||
Ignore routes, route gateways, and redirect-gateway options pushed by the server.
|
||||
Ignore routes, DNS and DHCP settings, route metrics, `redirect-gateway`,
|
||||
`redirect-private`, `block-ipv6`, and `block-outside-dns` pushed by the server.
|
||||
|
||||
Other pushed options are still accepted, and locally configured `routes` are still used.
|
||||
Interface configuration, topology, tunnel MTU, `route-gateway`, and locally configured routes are still used.
|
||||
|
||||
Disabled by default.
|
||||
|
||||
@@ -388,7 +391,7 @@ Disabled by default.
|
||||
|
||||
Ordered filters for options pushed by the server.
|
||||
|
||||
The first filter whose `text` is a case-insensitive prefix of the complete pushed option is applied. Options that match no filter are accepted.
|
||||
The first filter whose `text` is a case-sensitive prefix of the complete pushed option is applied. Options that match no filter are accepted.
|
||||
|
||||
### pull_filters.action
|
||||
|
||||
@@ -402,7 +405,7 @@ Filter action, one of `accept`, `ignore`, or `reject`.
|
||||
|
||||
==Required==
|
||||
|
||||
Case-insensitive prefix to match against the pushed option name and value.
|
||||
Case-sensitive prefix to match against the pushed option name and value.
|
||||
|
||||
For example, `route ` matches pushed IPv4 route options without matching `route-gateway`.
|
||||
|
||||
@@ -438,33 +441,38 @@ OpenVPN `redirect-gateway` flags.
|
||||
|
||||
Empty by default.
|
||||
|
||||
### keepalive_interval
|
||||
### ping_interval
|
||||
|
||||
Interval for sending OpenVPN keepalive ping packets.
|
||||
Interval after which the client sends a data-channel ping when no packet has been sent to the server.
|
||||
|
||||
Locally configured values take precedence over server-pushed keepalive values.
|
||||
A server-pushed OpenVPN `ping` value overrides this value.
|
||||
|
||||
The value must use whole seconds.
|
||||
|
||||
Disabled by default.
|
||||
|
||||
### keepalive_timeout
|
||||
### ping_restart
|
||||
|
||||
Time without receiving OpenVPN traffic before the connection is restarted.
|
||||
Time without receiving a packet after which the client reconnects to the server.
|
||||
|
||||
Locally configured values take precedence over server-pushed keepalive values.
|
||||
A server-pushed OpenVPN `ping-restart` value overrides this value.
|
||||
|
||||
Disabled by default.
|
||||
The value must use whole seconds.
|
||||
|
||||
When empty, `120s` is used for UDP connections with pull enabled until the
|
||||
server pushes another value. No default receive timeout is used for TCP.
|
||||
|
||||
### renegotiate_interval
|
||||
|
||||
OpenVPN TLS renegotiation interval.
|
||||
|
||||
If empty or set to `0s`, the OpenVPN default `1h` is used.
|
||||
When empty, the OpenVPN default `1h` is used.
|
||||
|
||||
### explicit_exit_notify
|
||||
|
||||
Number of OpenVPN exit notifications sent when closing a UDP connection.
|
||||
|
||||
Disabled when `0`. At most `10` notifications are sent.
|
||||
Notifications are sent one second apart. Disabled when `0`.
|
||||
|
||||
### system
|
||||
|
||||
@@ -486,11 +494,9 @@ OpenVPN interface MTU.
|
||||
|
||||
When empty, `1500` is used until a server-pushed MTU is received.
|
||||
|
||||
### udp_timeout
|
||||
## UDP NAT Fields
|
||||
|
||||
UDP NAT expiration time.
|
||||
|
||||
`5m` is used by default.
|
||||
See [UDP NAT Fields](/configuration/shared/udp-nat/) for details.
|
||||
|
||||
## Dial Fields
|
||||
|
||||
|
||||
@@ -69,14 +69,15 @@
|
||||
"route_metric": 0,
|
||||
"redirect_gateway": false,
|
||||
"redirect_gateway_flags": [],
|
||||
"keepalive_interval": "",
|
||||
"keepalive_timeout": "",
|
||||
"ping_interval": "",
|
||||
"ping_restart": "",
|
||||
"renegotiate_interval": "",
|
||||
"explicit_exit_notify": 0,
|
||||
"system": false,
|
||||
"name": "",
|
||||
"mtu": 1500,
|
||||
"udp_timeout": "",
|
||||
|
||||
... // UDP NAT 字段
|
||||
|
||||
... // 拨号字段
|
||||
}
|
||||
@@ -342,7 +343,7 @@ OpenVPN UDP packet 的最大大小,用于限制通过隧道发送的 TCP 连
|
||||
|
||||
这可以避免 TCP packet 在 OpenVPN 封装后超过 path MTU。
|
||||
|
||||
设为 `0` 时禁用。
|
||||
为空时使用上游 OpenVPN 默认值:配置了 `fragment` 时使用其值;否则默认 tunnel MTU 使用 `1492`,自定义 tunnel MTU 使用该 MTU。
|
||||
|
||||
### fragment
|
||||
|
||||
@@ -378,9 +379,10 @@ Compression 可能削弱流量机密性。仅在服务器要求时启用。
|
||||
|
||||
### route_no_pull
|
||||
|
||||
忽略服务器推送的 route、route gateway 和 redirect-gateway 选项。
|
||||
忽略服务器推送的 route、DNS 和 DHCP 设置、route metric、`redirect-gateway`、
|
||||
`redirect-private`、`block-ipv6` 和 `block-outside-dns`。
|
||||
|
||||
仍会接受其他推送选项,并继续使用本地配置的 `routes`。
|
||||
仍会使用接口配置、topology、tunnel MTU、`route-gateway` 和本地配置的 route。
|
||||
|
||||
默认禁用。
|
||||
|
||||
@@ -388,7 +390,7 @@ Compression 可能削弱流量机密性。仅在服务器要求时启用。
|
||||
|
||||
服务器推送选项的有序 pull filter 列表。
|
||||
|
||||
应用第一个 `text` 为完整推送选项大小写不敏感前缀的 filter。未匹配任何 filter 的选项会被接受。
|
||||
应用第一个 `text` 为完整推送选项大小写敏感前缀的 filter。未匹配任何 filter 的选项会被接受。
|
||||
|
||||
### pull_filters.action
|
||||
|
||||
@@ -402,7 +404,7 @@ Filter action,可选值为 `accept`、`ignore` 或 `reject`。
|
||||
|
||||
==必填==
|
||||
|
||||
用于匹配推送选项名称和值的大小写不敏感前缀。
|
||||
用于匹配推送选项名称和值的大小写敏感前缀。
|
||||
|
||||
例如,`route ` 会匹配推送的 IPv4 route 选项,但不会匹配 `route-gateway`。
|
||||
|
||||
@@ -438,33 +440,37 @@ OpenVPN `redirect-gateway` flag。
|
||||
|
||||
默认为空。
|
||||
|
||||
### keepalive_interval
|
||||
### ping_interval
|
||||
|
||||
发送 OpenVPN keepalive ping packet 的间隔。
|
||||
客户端未向服务器发送任何 packet 时,发送 data channel ping 的间隔。
|
||||
|
||||
本地配置值优先于服务器推送的 keepalive 值。
|
||||
服务器推送的 OpenVPN `ping` 值优先于该值。
|
||||
|
||||
该值必须使用整秒。
|
||||
|
||||
默认禁用。
|
||||
|
||||
### keepalive_timeout
|
||||
### ping_restart
|
||||
|
||||
未接收 OpenVPN 流量后重新启动连接的时间。
|
||||
客户端未收到任何 packet 后重新连接服务器的时间。
|
||||
|
||||
本地配置值优先于服务器推送的 keepalive 值。
|
||||
服务器推送的 OpenVPN `ping-restart` 值优先于该值。
|
||||
|
||||
默认禁用。
|
||||
该值必须使用整秒。
|
||||
|
||||
为空时,启用了 pull 的 UDP 连接会使用 `120s`,直到服务器推送其他值。TCP 不使用默认接收超时。
|
||||
|
||||
### renegotiate_interval
|
||||
|
||||
OpenVPN TLS 重新协商间隔。
|
||||
|
||||
如果为空或设为 `0s`,使用 OpenVPN 默认值 `1h`。
|
||||
为空时使用 OpenVPN 默认值 `1h`。
|
||||
|
||||
### explicit_exit_notify
|
||||
|
||||
关闭 UDP 连接时发送的 OpenVPN exit notification 数量。
|
||||
|
||||
设为 `0` 时禁用。最多发送 `10` 个 notification。
|
||||
Notification 之间间隔一秒。设为 `0` 时禁用。
|
||||
|
||||
### system
|
||||
|
||||
@@ -486,11 +492,9 @@ OpenVPN 接口 MTU。
|
||||
|
||||
为空时使用服务器推送的 MTU;收到服务器配置前使用 `1500`。
|
||||
|
||||
### udp_timeout
|
||||
## UDP NAT 字段
|
||||
|
||||
UDP NAT 过期时间。
|
||||
|
||||
默认使用 `5m`。
|
||||
参阅 [UDP NAT 字段](/zh/configuration/shared/udp-nat/)。
|
||||
|
||||
## 拨号字段
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"max_clients": 1024,
|
||||
"address": [],
|
||||
"topology": "subnet",
|
||||
"duplicate_cn": false,
|
||||
"users": [
|
||||
{
|
||||
"username": "",
|
||||
@@ -36,7 +37,8 @@
|
||||
"type": "tls_crypt",
|
||||
"key": [],
|
||||
"key_path": "",
|
||||
"direction": ""
|
||||
"direction": "",
|
||||
"force_cookie": false
|
||||
}
|
||||
},
|
||||
"data_ciphers": [],
|
||||
@@ -47,12 +49,16 @@
|
||||
"dns": [],
|
||||
"redirect_gateway": false,
|
||||
"redirect_gateway_flags": [],
|
||||
"block_outside_dns": false
|
||||
"block_outside_dns": false,
|
||||
"ping_interval": "",
|
||||
"ping_restart": ""
|
||||
},
|
||||
"keepalive_interval": "",
|
||||
"keepalive_timeout": "",
|
||||
"ping_interval": "",
|
||||
"ping_restart": "",
|
||||
"renegotiate_interval": "",
|
||||
"udp_timeout": ""
|
||||
"handshake_window": "1m",
|
||||
|
||||
... // UDP NAT Fields
|
||||
}
|
||||
```
|
||||
|
||||
@@ -62,7 +68,7 @@
|
||||
|
||||
## Listen Fields
|
||||
|
||||
See [Listen Fields](/configuration/shared/listen/) for details.
|
||||
See [Listen Fields](/configuration/shared/listen/) for details. `udp_timeout` is part of the [UDP NAT Fields](#udp-nat-fields) below.
|
||||
|
||||
## Fields
|
||||
|
||||
@@ -120,6 +126,14 @@ OpenVPN topology pushed to clients, one of `subnet`, `p2p` or `net30`.
|
||||
|
||||
`subnet` will be used by default.
|
||||
|
||||
### duplicate_cn
|
||||
|
||||
Allow multiple active clients with the same authenticated certificate common name or username.
|
||||
|
||||
When disabled, a newly authenticated session replaces the existing session with the same identity and reuses its tunnel address when available.
|
||||
|
||||
Disabled by default.
|
||||
|
||||
### users
|
||||
|
||||
List of OpenVPN username/password users.
|
||||
@@ -242,6 +256,15 @@ Only available when `tls.control_wrap.type` is `tls_auth`.
|
||||
|
||||
If empty, the key is used bidirectionally, matching an omitted `key-direction` on both peers.
|
||||
|
||||
### tls.control_wrap.force_cookie
|
||||
|
||||
Require `tls-crypt-v2` clients over UDP to support stateless session cookies.
|
||||
|
||||
Only available when `tls.control_wrap.type` is `tls_crypt_v2`. When disabled,
|
||||
clients without cookie support are accepted using the upstream `allow-noncookie` behavior.
|
||||
|
||||
Disabled by default.
|
||||
|
||||
### data_ciphers
|
||||
|
||||
Allowed OpenVPN data channel ciphers.
|
||||
@@ -294,15 +317,45 @@ Only available when `push.redirect_gateway` is enabled.
|
||||
|
||||
Push `block-outside-dns` to clients, which blocks DNS queries outside the VPN on Windows clients.
|
||||
|
||||
### keepalive_interval
|
||||
### push.ping_interval
|
||||
|
||||
OpenVPN keepalive ping interval to push to clients.
|
||||
OpenVPN `ping` interval pushed to clients.
|
||||
|
||||
After the interval passes without sending a packet, the client sends a data-channel ping to the server.
|
||||
|
||||
The value must use whole seconds.
|
||||
|
||||
Disabled by default.
|
||||
|
||||
### keepalive_timeout
|
||||
### push.ping_restart
|
||||
|
||||
OpenVPN keepalive ping timeout to push to clients.
|
||||
OpenVPN `ping-restart` timeout pushed to clients.
|
||||
|
||||
After the timeout passes without receiving a packet, the client reconnects to the server.
|
||||
|
||||
The value must use whole seconds.
|
||||
|
||||
Disabled by default.
|
||||
|
||||
### ping_interval
|
||||
|
||||
Interval after which the server sends a data-channel ping when no packet has been sent to a client.
|
||||
|
||||
This value applies to the server. Use `push.ping_interval` to configure clients.
|
||||
|
||||
The value must use whole seconds.
|
||||
|
||||
Disabled by default.
|
||||
|
||||
### ping_restart
|
||||
|
||||
Time without receiving a packet after which the server closes the client session.
|
||||
|
||||
This value applies to the server. Use `push.ping_restart` to configure clients.
|
||||
|
||||
The server timeout should be longer than the client timeout so the client can reconnect before the server discards its session.
|
||||
|
||||
The value must use whole seconds.
|
||||
|
||||
Disabled by default.
|
||||
|
||||
@@ -310,10 +363,16 @@ Disabled by default.
|
||||
|
||||
OpenVPN TLS renegotiation interval.
|
||||
|
||||
If empty or set to `0s`, the OpenVPN default `1h` is used.
|
||||
When empty, the OpenVPN default `1h` is used.
|
||||
|
||||
### udp_timeout
|
||||
### handshake_window
|
||||
|
||||
UDP NAT expiration time for traffic through the OpenVPN interface.
|
||||
Maximum time allowed for the initial TLS handshake and each TLS renegotiation.
|
||||
|
||||
`5m` will be used by default.
|
||||
`1m` is used by default.
|
||||
|
||||
## UDP NAT Fields
|
||||
|
||||
These fields configure UDP sessions for traffic through the OpenVPN interface.
|
||||
|
||||
See [UDP NAT Fields](/configuration/shared/udp-nat/) for details.
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"max_clients": 1024,
|
||||
"address": [],
|
||||
"topology": "subnet",
|
||||
"duplicate_cn": false,
|
||||
"users": [
|
||||
{
|
||||
"username": "",
|
||||
@@ -36,7 +37,8 @@
|
||||
"type": "tls_crypt",
|
||||
"key": [],
|
||||
"key_path": "",
|
||||
"direction": ""
|
||||
"direction": "",
|
||||
"force_cookie": false
|
||||
}
|
||||
},
|
||||
"data_ciphers": [],
|
||||
@@ -47,12 +49,16 @@
|
||||
"dns": [],
|
||||
"redirect_gateway": false,
|
||||
"redirect_gateway_flags": [],
|
||||
"block_outside_dns": false
|
||||
"block_outside_dns": false,
|
||||
"ping_interval": "",
|
||||
"ping_restart": ""
|
||||
},
|
||||
"keepalive_interval": "",
|
||||
"keepalive_timeout": "",
|
||||
"ping_interval": "",
|
||||
"ping_restart": "",
|
||||
"renegotiate_interval": "",
|
||||
"udp_timeout": ""
|
||||
"handshake_window": "1m",
|
||||
|
||||
... // UDP NAT 字段
|
||||
}
|
||||
```
|
||||
|
||||
@@ -62,7 +68,7 @@
|
||||
|
||||
## 监听字段
|
||||
|
||||
参阅 [监听字段](/zh/configuration/shared/listen/)。
|
||||
参阅 [监听字段](/zh/configuration/shared/listen/)。`udp_timeout` 属于下方的 [UDP NAT 字段](#udp-nat-字段)。
|
||||
|
||||
## 字段
|
||||
|
||||
@@ -120,6 +126,14 @@ OpenVPN 服务器地址前缀列表。
|
||||
|
||||
默认使用 `subnet`。
|
||||
|
||||
### duplicate_cn
|
||||
|
||||
允许具有相同认证证书 common name 或用户名的多个客户端同时在线。
|
||||
|
||||
禁用时,新认证的会话会替换具有相同身份的现有会话,并在可用时复用其 tunnel 地址。
|
||||
|
||||
默认禁用。
|
||||
|
||||
### users
|
||||
|
||||
OpenVPN 用户名/密码用户列表。
|
||||
@@ -242,6 +256,14 @@ OpenVPN `tls-auth` 密钥方向,`server` 或 `client` 之一。
|
||||
|
||||
如果为空,密钥被双向使用,与两端均省略 `key-direction` 的行为一致。
|
||||
|
||||
### tls.control_wrap.force_cookie
|
||||
|
||||
要求 UDP 上的 `tls-crypt-v2` 客户端支持无状态 session cookie。
|
||||
|
||||
仅当 `tls.control_wrap.type` 为 `tls_crypt_v2` 时可用。禁用时,不支持 cookie 的客户端会按照上游 `allow-noncookie` 行为被接受。
|
||||
|
||||
默认禁用。
|
||||
|
||||
### data_ciphers
|
||||
|
||||
允许的 OpenVPN 数据信道加密方式。
|
||||
@@ -294,15 +316,45 @@ IPv4 和 IPv6 前缀可以混用。
|
||||
|
||||
向客户端推送 `block-outside-dns`,在 Windows 客户端上阻止 VPN 之外的 DNS 查询。
|
||||
|
||||
### keepalive_interval
|
||||
### push.ping_interval
|
||||
|
||||
推送给客户端的 OpenVPN keepalive ping 间隔。
|
||||
推送给客户端的 OpenVPN `ping` 间隔。
|
||||
|
||||
在该间隔内未发送任何 packet 后,客户端会向服务器发送一个 data channel ping。
|
||||
|
||||
该值必须使用整秒。
|
||||
|
||||
默认禁用。
|
||||
|
||||
### keepalive_timeout
|
||||
### push.ping_restart
|
||||
|
||||
推送给客户端的 OpenVPN keepalive ping 超时。
|
||||
推送给客户端的 OpenVPN `ping-restart` 超时。
|
||||
|
||||
在该超时时间内未收到任何 packet 后,客户端会重新连接服务器。
|
||||
|
||||
该值必须使用整秒。
|
||||
|
||||
默认禁用。
|
||||
|
||||
### ping_interval
|
||||
|
||||
服务器未向客户端发送任何 packet 时,发送 data channel ping 的间隔。
|
||||
|
||||
该值应用于服务器。使用 `push.ping_interval` 配置客户端。
|
||||
|
||||
该值必须使用整秒。
|
||||
|
||||
默认禁用。
|
||||
|
||||
### ping_restart
|
||||
|
||||
服务器未收到任何 packet 后关闭客户端会话的时间。
|
||||
|
||||
该值应用于服务器。使用 `push.ping_restart` 配置客户端。
|
||||
|
||||
服务器超时应长于客户端超时,以便客户端在服务器丢弃其会话前重新连接。
|
||||
|
||||
该值必须使用整秒。
|
||||
|
||||
默认禁用。
|
||||
|
||||
@@ -310,10 +362,16 @@ IPv4 和 IPv6 前缀可以混用。
|
||||
|
||||
OpenVPN TLS 重协商间隔。
|
||||
|
||||
如果为空或设为 `0s`,使用 OpenVPN 默认值 `1h`。
|
||||
为空时使用 OpenVPN 默认值 `1h`。
|
||||
|
||||
### udp_timeout
|
||||
### handshake_window
|
||||
|
||||
通过 OpenVPN 接口的流量的 UDP NAT 过期时间。
|
||||
初始 TLS 握手和每次 TLS 重协商允许使用的最长时间。
|
||||
|
||||
默认使用 `5m`。
|
||||
默认使用 `1m`。
|
||||
|
||||
## UDP NAT 字段
|
||||
|
||||
这些字段配置通过 OpenVPN 接口的流量的 UDP 会话。
|
||||
|
||||
参阅 [UDP NAT 字段](/zh/configuration/shared/udp-nat/)。
|
||||
|
||||
@@ -48,7 +48,7 @@ require (
|
||||
github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3
|
||||
github.com/sagernet/sing-mux v0.3.5
|
||||
github.com/sagernet/sing-openconnect v0.0.0-20260717081856-cf2c71a71aba
|
||||
github.com/sagernet/sing-openvpn v0.0.0-20260717092601-0db6ebb53109
|
||||
github.com/sagernet/sing-openvpn v0.0.0-20260718013246-3cd8a7b83247
|
||||
github.com/sagernet/sing-quic v0.6.4-0.20260803041931-6c84c468bea2
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.1
|
||||
|
||||
@@ -287,8 +287,8 @@ github.com/sagernet/sing-mux v0.3.5 h1:RHnhVEc+SFqkrK4xMygYjDwwLhzp2Bj3lztSukONf
|
||||
github.com/sagernet/sing-mux v0.3.5/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
|
||||
github.com/sagernet/sing-openconnect v0.0.0-20260717081856-cf2c71a71aba h1:S87Ej/jFssn0qhPF1ExF0YIV0USfGZF2If6kSjGLPt8=
|
||||
github.com/sagernet/sing-openconnect v0.0.0-20260717081856-cf2c71a71aba/go.mod h1:EIzh5HtImfQJxPKXFwS9lyMnmMy4aCQCx7ntQ4u41Gs=
|
||||
github.com/sagernet/sing-openvpn v0.0.0-20260717092601-0db6ebb53109 h1:j1cyRquNhaFXHkkbQ5oyldS75jhv0WrBToDXUYPVSOw=
|
||||
github.com/sagernet/sing-openvpn v0.0.0-20260717092601-0db6ebb53109/go.mod h1:CmTGnS5ijVSqFQV1dTq4WvFLUoz7bk9xasBPsX8NcYo=
|
||||
github.com/sagernet/sing-openvpn v0.0.0-20260718013246-3cd8a7b83247 h1:IfZqHohaWz13eqc6SAUHkmP9xhMpMBDIShl7OtgRd5Y=
|
||||
github.com/sagernet/sing-openvpn v0.0.0-20260718013246-3cd8a7b83247/go.mod h1:CmTGnS5ijVSqFQV1dTq4WvFLUoz7bk9xasBPsX8NcYo=
|
||||
github.com/sagernet/sing-quic v0.6.4-0.20260803041931-6c84c468bea2 h1:XhJro6+Ou+WOPjfs22m14lY7Sh6sWPm/f0QiyFUgM60=
|
||||
github.com/sagernet/sing-quic v0.6.4-0.20260803041931-6c84c468bea2/go.mod h1:9k+dzGsWMttUGldBzq3dU792YHXzW6NgfbOGltnXq+0=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
|
||||
|
||||
@@ -6,6 +6,10 @@ type OpenConnectEndpointOptions struct {
|
||||
DialerOptions
|
||||
System bool `json:"system,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"`
|
||||
UDPMapping UDPNATBehavior `json:"udp_mapping,omitempty"`
|
||||
UDPFiltering UDPNATBehavior `json:"udp_filtering,omitempty"`
|
||||
UDPNATMax uint32 `json:"udp_nat_max,omitempty"`
|
||||
Server string `json:"server"`
|
||||
Flavor string `json:"flavor,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
|
||||
+20
-5
@@ -11,6 +11,9 @@ type OpenVPNEndpointOptions 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 OpenVPNClientEndpointOptions struct {
|
||||
@@ -41,8 +44,8 @@ type OpenVPNClientEndpointOptions struct {
|
||||
RouteMetric int `json:"route_metric,omitempty"`
|
||||
RedirectGateway bool `json:"redirect_gateway,omitempty"`
|
||||
RedirectGatewayFlags badoption.Listable[string] `json:"redirect_gateway_flags,omitempty"`
|
||||
KeepaliveInterval badoption.Duration `json:"keepalive_interval,omitempty"`
|
||||
KeepaliveTimeout badoption.Duration `json:"keepalive_timeout,omitempty"`
|
||||
PingInterval badoption.Duration `json:"ping_interval,omitempty"`
|
||||
PingRestart badoption.Duration `json:"ping_restart,omitempty"`
|
||||
RenegotiateInterval badoption.Duration `json:"renegotiate_interval,omitempty"`
|
||||
ExplicitExitNotify uint32 `json:"explicit_exit_notify,omitempty"`
|
||||
UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"`
|
||||
@@ -55,15 +58,17 @@ type OpenVPNServerEndpointOptions struct {
|
||||
MaxClients int `json:"max_clients,omitempty"`
|
||||
Address badoption.Listable[netip.Prefix] `json:"address"`
|
||||
Topology string `json:"topology,omitempty"`
|
||||
DuplicateCN bool `json:"duplicate_cn,omitempty"`
|
||||
Users []auth.User `json:"users,omitempty"`
|
||||
TLS *OpenVPNInboundTLSOptions `json:"tls,omitempty"`
|
||||
DataCiphers badoption.Listable[string] `json:"data_ciphers,omitempty"`
|
||||
DataCiphersFallback string `json:"data_ciphers_fallback,omitempty"`
|
||||
Auth string `json:"auth,omitempty"`
|
||||
Push *OpenVPNPushOptions `json:"push,omitempty"`
|
||||
KeepaliveInterval badoption.Duration `json:"keepalive_interval,omitempty"`
|
||||
KeepaliveTimeout badoption.Duration `json:"keepalive_timeout,omitempty"`
|
||||
PingInterval badoption.Duration `json:"ping_interval,omitempty"`
|
||||
PingRestart badoption.Duration `json:"ping_restart,omitempty"`
|
||||
RenegotiateInterval badoption.Duration `json:"renegotiate_interval,omitempty"`
|
||||
HandshakeWindow badoption.Duration `json:"handshake_window,omitempty"`
|
||||
}
|
||||
|
||||
type OpenVPNRemoteOptions struct {
|
||||
@@ -104,7 +109,7 @@ type OpenVPNInboundTLSOptions struct {
|
||||
ClientCertificate badoption.Listable[string] `json:"client_certificate,omitempty"`
|
||||
ClientCertificatePath string `json:"client_certificate_path,omitempty"`
|
||||
VerifyClientCertificate string `json:"verify_client_certificate,omitempty"`
|
||||
ControlWrap *OpenVPNControlWrapOptions `json:"control_wrap,omitempty"`
|
||||
ControlWrap *OpenVPNInboundControlWrapOptions `json:"control_wrap,omitempty"`
|
||||
}
|
||||
|
||||
type OpenVPNControlWrapOptions struct {
|
||||
@@ -114,10 +119,20 @@ type OpenVPNControlWrapOptions struct {
|
||||
Direction string `json:"direction,omitempty"`
|
||||
}
|
||||
|
||||
type OpenVPNInboundControlWrapOptions struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Key badoption.Listable[string] `json:"key,omitempty"`
|
||||
KeyPath string `json:"key_path,omitempty"`
|
||||
Direction string `json:"direction,omitempty"`
|
||||
ForceCookie bool `json:"force_cookie,omitempty"`
|
||||
}
|
||||
|
||||
type OpenVPNPushOptions struct {
|
||||
Routes badoption.Listable[netip.Prefix] `json:"routes,omitempty"`
|
||||
DNS badoption.Listable[netip.Addr] `json:"dns,omitempty"`
|
||||
RedirectGateway bool `json:"redirect_gateway,omitempty"`
|
||||
RedirectGatewayFlags badoption.Listable[string] `json:"redirect_gateway_flags,omitempty"`
|
||||
BlockOutsideDNS bool `json:"block_outside_dns,omitempty"`
|
||||
PingInterval badoption.Duration `json:"ping_interval,omitempty"`
|
||||
PingRestart badoption.Duration `json:"ping_restart,omitempty"`
|
||||
}
|
||||
|
||||
@@ -120,13 +120,22 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
udpTimeout := C.UDPTimeout
|
||||
if options.UDPTimeout != 0 {
|
||||
udpTimeout = time.Duration(options.UDPTimeout)
|
||||
}
|
||||
networkManager := service.FromContext[adapter.NetworkManager](ctx)
|
||||
device, err := openconnecttransport.NewDevice(openconnecttransport.DeviceOptions{
|
||||
Context: ctx,
|
||||
Logger: logger,
|
||||
System: options.System,
|
||||
Handler: openConnectEndpoint,
|
||||
UDPTimeout: C.UDPTimeout,
|
||||
UDPTimeout: udpTimeout,
|
||||
ICMPTimeout: C.ICMPTimeout,
|
||||
UDPMapping: tun.NATMapping(options.UDPMapping),
|
||||
UDPFiltering: tun.NATFiltering(options.UDPFiltering),
|
||||
UDPNATMax: options.UDPNATMax,
|
||||
InterfaceFinder: networkManager.InterfaceFinder(),
|
||||
Name: options.Name,
|
||||
MTU: openconnecttransport.DefaultMTU,
|
||||
Configuration: openconnecttransport.Configuration{
|
||||
|
||||
+15
-11
@@ -122,6 +122,10 @@ func NewClientEndpoint(ctx context.Context, router adapter.Router, logger log.Co
|
||||
Handler: clientEndpoint,
|
||||
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,
|
||||
MTU: options.MTU,
|
||||
Configuration: ovpntransport.Configuration{
|
||||
@@ -264,7 +268,7 @@ func (c *ClientEndpoint) buildClientOptions(options option.OpenVPNClientEndpoint
|
||||
RemoteRandom: options.RemoteRandom,
|
||||
Protocol: protocol,
|
||||
ExplicitExitNotify: options.ExplicitExitNotify,
|
||||
DialContext: c.transportDialContext,
|
||||
DialContextWithAddressIndex: c.transportDialContextWithAddressIndex,
|
||||
},
|
||||
DataChannel: ovpn.ClientDataChannelOptions{
|
||||
MTU: options.MTU,
|
||||
@@ -301,8 +305,8 @@ func (c *ClientEndpoint) buildClientOptions(options option.OpenVPNClientEndpoint
|
||||
},
|
||||
Timing: ovpn.ClientTimingOptions{
|
||||
RenegotiationInterval: time.Duration(options.RenegotiateInterval),
|
||||
PingInterval: time.Duration(options.KeepaliveInterval),
|
||||
PingRestart: time.Duration(options.KeepaliveTimeout),
|
||||
PingInterval: time.Duration(options.PingInterval),
|
||||
PingRestart: time.Duration(options.PingRestart),
|
||||
},
|
||||
KeyDirection: keyDirection,
|
||||
OnTunnelConfiguration: c.handleTunnelConfiguration,
|
||||
@@ -310,21 +314,21 @@ func (c *ClientEndpoint) buildClientOptions(options option.OpenVPNClientEndpoint
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *ClientEndpoint) transportDialContext(ctx context.Context, network string, address string) (net.Conn, error) {
|
||||
func (c *ClientEndpoint) transportDialContextWithAddressIndex(ctx context.Context, network string, address string, addressIndex int) (net.Conn, error) {
|
||||
destination := M.ParseSocksaddr(address)
|
||||
var (
|
||||
connection net.Conn
|
||||
err error
|
||||
)
|
||||
if destination.IsDomain() {
|
||||
destinationAddresses, lookupErr := c.dnsRouter.Lookup(ctx, destination.Fqdn, c.queryOptions)
|
||||
if lookupErr != nil {
|
||||
return nil, lookupErr
|
||||
}
|
||||
connection, err = N.DialSerial(ctx, c.outboundDialer, network, destination, destinationAddresses)
|
||||
} else {
|
||||
connection, err = c.outboundDialer.DialContext(ctx, network, destination)
|
||||
if addressIndex < 0 || addressIndex >= len(destinationAddresses) {
|
||||
return nil, ovpn.ErrRemoteAddressExhausted
|
||||
}
|
||||
destination = M.SocksaddrFrom(destinationAddresses[addressIndex], destination.Port)
|
||||
} else if addressIndex != 0 {
|
||||
return nil, ovpn.ErrRemoteAddressExhausted
|
||||
}
|
||||
connection, err := c.outboundDialer.DialContext(ctx, network, destination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ func NewServerEndpoint(ctx context.Context, router adapter.Router, logger log.Co
|
||||
}
|
||||
serverOptions.Context = loopContext
|
||||
serverOptions.Authentication.Authenticator = authenticatorFromUsers(options.Users)
|
||||
serverOptions.Authentication.DuplicateCN = options.DuplicateCN
|
||||
serverOptions.Logger = logger
|
||||
serverEndpoint.serverOptions = serverOptions
|
||||
udpTimeout := C.UDPTimeout
|
||||
@@ -108,6 +109,10 @@ func NewServerEndpoint(ctx context.Context, router adapter.Router, logger log.Co
|
||||
Handler: serverEndpoint,
|
||||
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,
|
||||
MTU: options.MTU,
|
||||
Configuration: ovpntransport.Configuration{
|
||||
@@ -299,6 +304,9 @@ func buildServerOptions(options option.OpenVPNServerEndpointOptions) (ovpn.Serve
|
||||
TLS: tlsOptions,
|
||||
Timing: ovpn.ServerTimingOptions{
|
||||
RenegotiationInterval: time.Duration(options.RenegotiateInterval),
|
||||
HandWindow: time.Duration(options.HandshakeWindow),
|
||||
PingInterval: time.Duration(options.PingInterval),
|
||||
PingRestart: time.Duration(options.PingRestart),
|
||||
},
|
||||
}
|
||||
applyServerPushOptions(&serverOptions, options)
|
||||
@@ -331,13 +339,16 @@ func buildServerTLSOptions(options option.OpenVPNInboundTLSOptions) (ovpn.Server
|
||||
}
|
||||
keyDirection := -1
|
||||
controlWrap := options.ControlWrap
|
||||
if controlWrap != nil && (controlWrap.Type != "" || len(controlWrap.Key) > 0 || controlWrap.KeyPath != "" || controlWrap.Direction != "") {
|
||||
if controlWrap != nil && (controlWrap.Type != "" || len(controlWrap.Key) > 0 || controlWrap.KeyPath != "" || controlWrap.Direction != "" || controlWrap.ForceCookie) {
|
||||
wrapKey, wrapErr := requiredMaterialSource("tls.control_wrap.key", controlWrap.Key, controlWrap.KeyPath)
|
||||
if wrapErr != nil {
|
||||
return ovpn.ServerTLSOptions{}, 0, wrapErr
|
||||
}
|
||||
switch controlWrap.Type {
|
||||
case "tls_auth":
|
||||
if controlWrap.ForceCookie {
|
||||
return ovpn.ServerTLSOptions{}, 0, E.New("`tls.control_wrap.force_cookie` is only supported by `tls_crypt_v2`")
|
||||
}
|
||||
keyDirection, err = keyDirectionValue(controlWrap.Direction)
|
||||
if err != nil {
|
||||
return ovpn.ServerTLSOptions{}, 0, err
|
||||
@@ -348,9 +359,13 @@ func buildServerTLSOptions(options option.OpenVPNInboundTLSOptions) (ovpn.Server
|
||||
return ovpn.ServerTLSOptions{}, 0, E.New("`tls.control_wrap.direction` is only supported by `tls_auth`")
|
||||
}
|
||||
if controlWrap.Type == "tls_crypt" {
|
||||
if controlWrap.ForceCookie {
|
||||
return ovpn.ServerTLSOptions{}, 0, E.New("`tls.control_wrap.force_cookie` is only supported by `tls_crypt_v2`")
|
||||
}
|
||||
tlsOptions.Crypt = wrapKey
|
||||
} else {
|
||||
tlsOptions.CryptV2 = wrapKey
|
||||
tlsOptions.CryptV2ForceCookie = controlWrap.ForceCookie
|
||||
}
|
||||
case "":
|
||||
return ovpn.ServerTLSOptions{}, 0, E.New("missing OpenVPN control wrap type")
|
||||
@@ -382,16 +397,14 @@ func applyServerPushOptions(serverOptions *ovpn.ServerOptions, options option.Op
|
||||
Topology: topology,
|
||||
LocalAddress: localAddresses,
|
||||
}
|
||||
serverOptions.Push = ovpn.ServerPushOptions{
|
||||
PingInterval: time.Duration(options.KeepaliveInterval),
|
||||
PingRestart: time.Duration(options.KeepaliveTimeout),
|
||||
}
|
||||
if options.Push == nil {
|
||||
return
|
||||
}
|
||||
serverOptions.Push.Routes = slices.Clone(options.Push.Routes)
|
||||
serverOptions.Push.DNS = slices.Clone(options.Push.DNS)
|
||||
serverOptions.Push.BlockOutsideDNS = options.Push.BlockOutsideDNS
|
||||
serverOptions.Push.PingInterval = time.Duration(options.Push.PingInterval)
|
||||
serverOptions.Push.PingRestart = time.Duration(options.Push.PingRestart)
|
||||
if options.Push.RedirectGateway {
|
||||
serverOptions.Push.RedirectGateway = true
|
||||
if len(options.Push.RedirectGatewayFlags) > 0 {
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
openconnecttransport "github.com/sagernet/sing-box/transport/openconnect"
|
||||
openvpntransport "github.com/sagernet/sing-box/transport/openvpn"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing-tun/gtcpip/header"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"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
|
||||
payload []byte
|
||||
}
|
||||
|
||||
type endpointUDPNATSession struct {
|
||||
id uint64
|
||||
conn N.PacketConn
|
||||
closed chan struct{}
|
||||
}
|
||||
|
||||
type endpointUDPNATHandler struct {
|
||||
nextSessionID atomic.Uint64
|
||||
packets chan endpointUDPNATPacket
|
||||
}
|
||||
|
||||
func (h *endpointUDPNATHandler) JudgeFlow(uint8, netip.AddrPort, netip.AddrPort, []byte) tun.FlowVerdict {
|
||||
return tun.FlowVerdict{Action: tun.ActionAccept}
|
||||
}
|
||||
|
||||
func (h *endpointUDPNATHandler) NewConnectionEx(_ context.Context, conn net.Conn, _ M.Socksaddr, _ M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
err := conn.Close()
|
||||
if onClose != nil {
|
||||
onClose(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *endpointUDPNATHandler) NewPacketConnectionEx(_ context.Context, conn N.PacketConn, _ M.Socksaddr, _ M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
session := &endpointUDPNATSession{
|
||||
id: h.nextSessionID.Add(1),
|
||||
conn: conn,
|
||||
closed: make(chan struct{}),
|
||||
}
|
||||
go func() {
|
||||
defer close(session.closed)
|
||||
for {
|
||||
packetBuffer := buf.NewPacket()
|
||||
destination, err := conn.ReadPacket(packetBuffer)
|
||||
if err != nil {
|
||||
packetBuffer.Release()
|
||||
if onClose != nil {
|
||||
onClose(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
payload := append([]byte(nil), packetBuffer.Bytes()...)
|
||||
packetBuffer.Release()
|
||||
h.packets <- endpointUDPNATPacket{
|
||||
session: session,
|
||||
destination: destination,
|
||||
payload: payload,
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
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()
|
||||
if !tun.WithGVisor {
|
||||
t.Skip("requires gVisor")
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
handler := &endpointUDPNATHandler{packets: make(chan endpointUDPNATPacket, 4)}
|
||||
device, err := newDevice(ctx, handler)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, device.close())
|
||||
})
|
||||
outboundPackets := make(chan []byte, 4)
|
||||
device.setPacketWriter(func(packetBuffers []*buf.Buffer) error {
|
||||
for _, packetBuffer := range packetBuffers {
|
||||
payload, isUDP := endpointUDPPayload(packetBuffer.Bytes())
|
||||
packetBuffer.Release()
|
||||
if isUDP {
|
||||
outboundPackets <- payload
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
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"))
|
||||
firstPacket := waitEndpointUDPNATPacket(t, handler.packets)
|
||||
require.Equal(t, M.SocksaddrFromNetIP(firstDestination), firstPacket.destination)
|
||||
require.Equal(t, []byte("request-one"), firstPacket.payload)
|
||||
|
||||
require.NoError(t, firstPacket.session.conn.WritePacket(buf.As([]byte("blocked")), M.SocksaddrFromNetIP(secondDestination)))
|
||||
require.NoError(t, firstPacket.session.conn.WritePacket(buf.As([]byte("allowed-one")), M.SocksaddrFromNetIP(firstDestination)))
|
||||
require.Equal(t, []byte("allowed-one"), waitEndpointUDPResponse(t, outboundPackets))
|
||||
|
||||
writeEndpointUDPPacket(t, device, 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)
|
||||
require.NotEqual(t, firstPacket.session.id, secondPacket.session.id)
|
||||
select {
|
||||
case <-firstPacket.session.closed:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("first UDP NAT session was not evicted at max size")
|
||||
}
|
||||
|
||||
require.NoError(t, secondPacket.session.conn.WritePacket(buf.As([]byte("allowed-two")), M.SocksaddrFromNetIP(secondDestination)))
|
||||
require.Equal(t, []byte("allowed-two"), waitEndpointUDPResponse(t, outboundPackets))
|
||||
}
|
||||
|
||||
func writeEndpointUDPPacket(t *testing.T, device endpointUDPNATDevice, source netip.AddrPort, destination netip.AddrPort, payload []byte) {
|
||||
t.Helper()
|
||||
packet := make([]byte, header.IPv4MinimumSize+header.UDPMinimumSize+len(payload))
|
||||
ipHeader := header.IPv4(packet)
|
||||
ipHeader.Encode(&header.IPv4Fields{
|
||||
TotalLength: uint16(len(packet)),
|
||||
TTL: 64,
|
||||
Protocol: uint8(header.UDPProtocolNumber),
|
||||
SrcAddr: source.Addr(),
|
||||
DstAddr: destination.Addr(),
|
||||
})
|
||||
ipHeader.SetChecksum(^ipHeader.CalculateChecksum())
|
||||
udpHeader := header.UDP(packet[header.IPv4MinimumSize:])
|
||||
udpHeader.Encode(&header.UDPFields{
|
||||
SrcPort: source.Port(),
|
||||
DstPort: destination.Port(),
|
||||
Length: uint16(header.UDPMinimumSize + len(payload)),
|
||||
})
|
||||
copy(udpHeader.Payload(), payload)
|
||||
packetBuffer := buf.As(packet)
|
||||
require.NoError(t, device.writeInboundBuffers([]*buf.Buffer{packetBuffer}))
|
||||
packetBuffer.Release()
|
||||
}
|
||||
|
||||
func endpointUDPPayload(packet []byte) ([]byte, bool) {
|
||||
if len(packet) < header.IPv4MinimumSize {
|
||||
return nil, false
|
||||
}
|
||||
ipHeader := header.IPv4(packet)
|
||||
if !ipHeader.IsValid(len(packet)) || ipHeader.Protocol() != uint8(header.UDPProtocolNumber) {
|
||||
return nil, false
|
||||
}
|
||||
udpPayload := ipHeader.Payload()
|
||||
if len(udpPayload) < header.UDPMinimumSize {
|
||||
return nil, false
|
||||
}
|
||||
udpHeader := header.UDP(udpPayload)
|
||||
return append([]byte(nil), udpHeader.Payload()...), true
|
||||
}
|
||||
|
||||
func waitEndpointUDPNATPacket(t *testing.T, packets <-chan endpointUDPNATPacket) endpointUDPNATPacket {
|
||||
t.Helper()
|
||||
select {
|
||||
case packet := <-packets:
|
||||
return packet
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("timed out waiting for UDP NAT packet")
|
||||
return endpointUDPNATPacket{}
|
||||
}
|
||||
}
|
||||
|
||||
func waitEndpointUDPResponse(t *testing.T, packets <-chan []byte) []byte {
|
||||
t.Helper()
|
||||
select {
|
||||
case packet := <-packets:
|
||||
return packet
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("timed out waiting for UDP response")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -12,10 +12,11 @@ require (
|
||||
github.com/gofrs/uuid/v5 v5.4.0
|
||||
github.com/opencontainers/image-spec v1.1.0
|
||||
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4
|
||||
github.com/sagernet/sing v0.8.12-0.20260717023913-84ab32b56cb8
|
||||
github.com/sagernet/sing v0.8.12-0.20260717153536-4f1ed45a99a5
|
||||
github.com/sagernet/sing-quic v0.6.4-0.20260709034545-e23afe1172dc
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.1
|
||||
github.com/sagernet/sing-tun v0.8.12-0.20260717024008-39eed1f6361d
|
||||
github.com/spyzhov/ajson v0.9.4
|
||||
github.com/stretchr/testify v1.11.1
|
||||
go.uber.org/goleak v1.3.0
|
||||
@@ -152,15 +153,14 @@ require (
|
||||
github.com/sagernet/nftables v0.3.0-mod.4 // indirect
|
||||
github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 // indirect
|
||||
github.com/sagernet/sing-mux v0.3.5 // indirect
|
||||
github.com/sagernet/sing-openconnect v0.0.0-20260717061548-458a8732933e // indirect
|
||||
github.com/sagernet/sing-openvpn v0.0.0-20260717055507-7e569eca5e4d // indirect
|
||||
github.com/sagernet/sing-openconnect v0.0.0-20260717081856-cf2c71a71aba // indirect
|
||||
github.com/sagernet/sing-openvpn v0.0.0-20260718013246-3cd8a7b83247 // indirect
|
||||
github.com/sagernet/sing-shadowtls v0.2.1 // indirect
|
||||
github.com/sagernet/sing-snell v0.0.0-20260710094516-a4e97ee24beb // indirect
|
||||
github.com/sagernet/sing-tun v0.8.12-0.20260717024008-39eed1f6361d // indirect
|
||||
github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb // indirect
|
||||
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 // indirect
|
||||
github.com/sagernet/smux v1.5.50-sing-box-mod.1 // indirect
|
||||
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260717024045-1edfbb9ee544 // indirect
|
||||
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260717155615-b353b93d194a // indirect
|
||||
github.com/sagernet/wireguard-go v0.0.5-0.20260717024847-6f5e8b1947ae // indirect
|
||||
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 // indirect
|
||||
github.com/smallstep/pkcs7 v0.1.1 // indirect
|
||||
|
||||
+8
-8
@@ -298,16 +298,16 @@ github.com/sagernet/nftables v0.3.0-mod.4 h1:vnOtcDYeSXv2e5RoRuGH0lrpttQFJ8iC4IC
|
||||
github.com/sagernet/nftables v0.3.0-mod.4/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ=
|
||||
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o=
|
||||
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4=
|
||||
github.com/sagernet/sing v0.8.12-0.20260717023913-84ab32b56cb8 h1:dyRIj+MZ2rc9JVzJoG04jxu+MpvHrLIZLJr0QjNAMGg=
|
||||
github.com/sagernet/sing v0.8.12-0.20260717023913-84ab32b56cb8/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA=
|
||||
github.com/sagernet/sing v0.8.12-0.20260717153536-4f1ed45a99a5 h1:WyL7xI7h+mNF6we/arqVNDEB+0iK2SwJRjim8rGydDM=
|
||||
github.com/sagernet/sing v0.8.12-0.20260717153536-4f1ed45a99a5/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA=
|
||||
github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 h1:3y6++yIa8XlDhxPkpR4p+7RUHVY2KTP9CPIGnWmOlO8=
|
||||
github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3/go.mod h1:XEqEDYRCAYLaoPjZ1ifVWJg5iWAJHL2gOAXe/PM28Cg=
|
||||
github.com/sagernet/sing-mux v0.3.5 h1:RHnhVEc+SFqkrK4xMygYjDwwLhzp2Bj3lztSukONfhI=
|
||||
github.com/sagernet/sing-mux v0.3.5/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
|
||||
github.com/sagernet/sing-openconnect v0.0.0-20260717061548-458a8732933e h1:Kgcf16uKnxBNJMsR8MaWlORtOV2/qi+6yH830+b7Yfc=
|
||||
github.com/sagernet/sing-openconnect v0.0.0-20260717061548-458a8732933e/go.mod h1:EIzh5HtImfQJxPKXFwS9lyMnmMy4aCQCx7ntQ4u41Gs=
|
||||
github.com/sagernet/sing-openvpn v0.0.0-20260717055507-7e569eca5e4d h1:KGvybsWqE+Qkd9Ns2AzrrBSyNfbBJ7IZZqgj+oWa6SM=
|
||||
github.com/sagernet/sing-openvpn v0.0.0-20260717055507-7e569eca5e4d/go.mod h1:CmTGnS5ijVSqFQV1dTq4WvFLUoz7bk9xasBPsX8NcYo=
|
||||
github.com/sagernet/sing-openconnect v0.0.0-20260717081856-cf2c71a71aba h1:S87Ej/jFssn0qhPF1ExF0YIV0USfGZF2If6kSjGLPt8=
|
||||
github.com/sagernet/sing-openconnect v0.0.0-20260717081856-cf2c71a71aba/go.mod h1:EIzh5HtImfQJxPKXFwS9lyMnmMy4aCQCx7ntQ4u41Gs=
|
||||
github.com/sagernet/sing-openvpn v0.0.0-20260718013246-3cd8a7b83247 h1:IfZqHohaWz13eqc6SAUHkmP9xhMpMBDIShl7OtgRd5Y=
|
||||
github.com/sagernet/sing-openvpn v0.0.0-20260718013246-3cd8a7b83247/go.mod h1:CmTGnS5ijVSqFQV1dTq4WvFLUoz7bk9xasBPsX8NcYo=
|
||||
github.com/sagernet/sing-quic v0.6.4-0.20260709034545-e23afe1172dc h1:zdc0fj4JdAdgAmQIoh7ZF+B/wPTEF2X75lYDqTmvlaw=
|
||||
github.com/sagernet/sing-quic v0.6.4-0.20260709034545-e23afe1172dc/go.mod h1:9k+dzGsWMttUGldBzq3dU792YHXzW6NgfbOGltnXq+0=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
|
||||
@@ -326,8 +326,8 @@ github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkV
|
||||
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY=
|
||||
github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478=
|
||||
github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8=
|
||||
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260717024045-1edfbb9ee544 h1:j2tab0dGHutfclhwZxrkSDMXwGXtozIo5BV4DgwS+1Q=
|
||||
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260717024045-1edfbb9ee544/go.mod h1:p8Ms8FbGlwQJyHb862XmdShTS50fFJ8C71VdO6xvWyk=
|
||||
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260717155615-b353b93d194a h1:mORXldIuzgU8Bk6n9KpOmLv1rV7Ta8us+lZ9JSv2eBw=
|
||||
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260717155615-b353b93d194a/go.mod h1:p8Ms8FbGlwQJyHb862XmdShTS50fFJ8C71VdO6xvWyk=
|
||||
github.com/sagernet/wireguard-go v0.0.5-0.20260717024847-6f5e8b1947ae h1:GmxlXWnRmeNfPE1tWXRZIFgKJd5BH5okoDHKZkkI5bw=
|
||||
github.com/sagernet/wireguard-go v0.0.5-0.20260717024847-6f5e8b1947ae/go.mod h1:hEqi4y5czEg6LYtX2Bpjg+lV0b/J1n+5rA885Z66Mx0=
|
||||
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
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"
|
||||
|
||||
@@ -125,6 +126,8 @@ func TestOpenConnectDockerInterop(t *testing.T) {
|
||||
require.Nil(subtest, status.AuthForm)
|
||||
err := exchangeOpenConnectTCPEcho(endpoint, 256*1024, 30*time.Second)
|
||||
require.NoError(subtest, err)
|
||||
err = exchangeOpenConnectUDPEcho(endpoint, 1400, 30*time.Second)
|
||||
require.NoError(subtest, err)
|
||||
})
|
||||
|
||||
t.Run("interactive_password_auth", func(subtest *testing.T) {
|
||||
@@ -179,6 +182,10 @@ func openConnectInstanceOptions(server string, certificateAuthorityPath string,
|
||||
Username: username,
|
||||
Password: password,
|
||||
NoUDP: true,
|
||||
UDPTimeout: badoption.Duration(time.Minute),
|
||||
UDPMapping: option.UDPNATBehaviorAddressDependent,
|
||||
UDPFiltering: option.UDPNATBehaviorAddressAndPortDependent,
|
||||
UDPNATMax: 128,
|
||||
TLS: option.OpenConnectTLSOptions{
|
||||
CertificateAuthorityPath: certificateAuthorityPath,
|
||||
},
|
||||
@@ -318,6 +325,38 @@ func exchangeOpenConnectTCPEcho(endpoint adapter.OpenConnectEndpoint, payloadSiz
|
||||
return nil
|
||||
}
|
||||
|
||||
func exchangeOpenConnectUDPEcho(endpoint adapter.OpenConnectEndpoint, payloadSize int, timeout time.Duration) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
conn, err := endpoint.DialContext(ctx, N.NetworkUDP, M.ParseSocksaddrHostPort(openConnectTunnelAddress, openConnectEchoPort))
|
||||
if err != nil {
|
||||
return E.Cause(err, "dial ocserv tunnel UDP echo")
|
||||
}
|
||||
defer conn.Close()
|
||||
err = conn.SetDeadline(time.Now().Add(timeout))
|
||||
if err != nil {
|
||||
return E.Cause(err, "set ocserv tunnel UDP echo deadline")
|
||||
}
|
||||
payload := make([]byte, payloadSize)
|
||||
_, err = rand.Read(payload)
|
||||
if err != nil {
|
||||
return E.Cause(err, "generate ocserv tunnel UDP echo payload")
|
||||
}
|
||||
_, err = conn.Write(payload)
|
||||
if err != nil {
|
||||
return E.Cause(err, "write ocserv tunnel UDP echo payload")
|
||||
}
|
||||
response := make([]byte, payloadSize+1)
|
||||
responseLength, err := conn.Read(response)
|
||||
if err != nil {
|
||||
return E.Cause(err, "read ocserv tunnel UDP echo payload")
|
||||
}
|
||||
if !bytes.Equal(response[:responseLength], payload) {
|
||||
return E.New("ocserv tunnel UDP echo payload mismatch")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func waitForOpenConnectTCPEcho(t *testing.T, endpoint adapter.OpenConnectEndpoint, timeout time.Duration) {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(timeout)
|
||||
|
||||
+13
-3
@@ -150,10 +150,16 @@ func runOpenVPNSelfToSelf(t *testing.T, testCase openVPNSelfCase) {
|
||||
ClientCertificatePath: certificates.caPath,
|
||||
},
|
||||
}
|
||||
serverOptions.UDPMapping = option.UDPNATBehaviorAddressDependent
|
||||
serverOptions.UDPFiltering = option.UDPNATBehaviorAddressAndPortDependent
|
||||
serverOptions.UDPNATMax = 128
|
||||
clientOptions := newOpenVPNTLSClientOptions(testCase.protocol, openVPNPort, certificates.caPath, certificates.clientCertPath, certificates.clientKeyPath)
|
||||
clientOptions.UDPMapping = option.UDPNATBehaviorAddressDependent
|
||||
clientOptions.UDPFiltering = option.UDPNATBehaviorAddressAndPortDependent
|
||||
clientOptions.UDPNATMax = 128
|
||||
if testCase.tlsCrypt {
|
||||
tlsCryptKeyPath := writeOpenVPNStaticKeyFile(t, createOpenVPNStaticKey(t))
|
||||
serverOptions.TLS.ControlWrap = &option.OpenVPNControlWrapOptions{
|
||||
serverOptions.TLS.ControlWrap = &option.OpenVPNInboundControlWrapOptions{
|
||||
Type: "tls_crypt",
|
||||
KeyPath: tlsCryptKeyPath,
|
||||
}
|
||||
@@ -538,8 +544,12 @@ func TestOpenVPNClientReconnectSelfToSelf(t *testing.T) {
|
||||
KeyPath: certificates.serverKeyPath,
|
||||
ClientCertificatePath: certificates.caPath,
|
||||
},
|
||||
KeepaliveInterval: badoption.Duration(time.Second),
|
||||
KeepaliveTimeout: badoption.Duration(2 * time.Second),
|
||||
PingInterval: badoption.Duration(time.Second),
|
||||
PingRestart: badoption.Duration(4 * time.Second),
|
||||
Push: &option.OpenVPNPushOptions{
|
||||
PingInterval: badoption.Duration(time.Second),
|
||||
PingRestart: badoption.Duration(2 * time.Second),
|
||||
},
|
||||
Users: []auth.User{
|
||||
{
|
||||
Username: openVPNTLSUsername,
|
||||
|
||||
+15
-5
@@ -12,11 +12,21 @@ def echo(connection):
|
||||
connection.sendall(data)
|
||||
|
||||
|
||||
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
listener.bind(("0.0.0.0", 18080))
|
||||
listener.listen()
|
||||
def echo_udp(connection):
|
||||
while True:
|
||||
data, address = connection.recvfrom(65536)
|
||||
connection.sendto(data, address)
|
||||
|
||||
|
||||
udp_listener = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
udp_listener.bind(("0.0.0.0", 18080))
|
||||
threading.Thread(target=echo_udp, args=(udp_listener,), daemon=True).start()
|
||||
|
||||
tcp_listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
tcp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
tcp_listener.bind(("0.0.0.0", 18080))
|
||||
tcp_listener.listen()
|
||||
print("openconnect echo ready", flush=True)
|
||||
while True:
|
||||
accepted, _ = listener.accept()
|
||||
accepted, _ = tcp_listener.accept()
|
||||
threading.Thread(target=echo, args=(accepted,), daemon=True).start()
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"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"
|
||||
@@ -42,6 +43,11 @@ type DeviceOptions struct {
|
||||
Handler tun.Handler
|
||||
UDPTimeout time.Duration
|
||||
ICMPTimeout time.Duration
|
||||
UDPMapping tun.NATMapping
|
||||
UDPFiltering tun.NATFiltering
|
||||
UDPNATMax uint32
|
||||
InterfaceFinder control.InterfaceFinder
|
||||
ExcludeInterface []string
|
||||
Name string
|
||||
MTU uint32
|
||||
Configuration Configuration
|
||||
|
||||
@@ -37,6 +37,7 @@ type stackDevice struct {
|
||||
endpoint *stackEndpoint
|
||||
inet4Address netip.Addr
|
||||
inet6Address netip.Addr
|
||||
udpForwarder *tun.UDPForwarder
|
||||
icmpForwarder *tun.ICMPForwarder
|
||||
closeOnce sync.Once
|
||||
}
|
||||
@@ -66,9 +67,17 @@ func newStackDevice(options DeviceOptions) (*stackDevice, error) {
|
||||
}
|
||||
if options.Handler != nil {
|
||||
ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tun.NewTCPForwarder(options.Context, ipStack, options.Handler).HandlePacket)
|
||||
ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, tun.NewUDPForwarder(options.Context, ipStack, options.Handler, tun.UDPNatOptions{
|
||||
udpForwarder := tun.NewUDPForwarder(options.Context, ipStack, options.Handler, tun.UDPNatOptions{
|
||||
Timeout: options.UDPTimeout,
|
||||
}).HandlePacket)
|
||||
Shared: true,
|
||||
Mapping: options.UDPMapping,
|
||||
Filtering: options.UDPFiltering,
|
||||
MaxSize: options.UDPNATMax,
|
||||
InterfaceFinder: options.InterfaceFinder,
|
||||
ExcludeInterface: options.ExcludeInterface,
|
||||
})
|
||||
ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket)
|
||||
device.udpForwarder = udpForwarder
|
||||
icmpForwarder := tun.NewICMPForwarder(ipStack, options.Handler, options.Logger)
|
||||
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket)
|
||||
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket)
|
||||
@@ -78,6 +87,12 @@ func newStackDevice(options DeviceOptions) (*stackDevice, error) {
|
||||
}
|
||||
|
||||
func (d *stackDevice) Start() error {
|
||||
if d.udpForwarder != nil {
|
||||
err := d.udpForwarder.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -232,6 +247,9 @@ func (d *stackDevice) PortMTU() uint32 {
|
||||
func (d *stackDevice) Close() error {
|
||||
d.closeOnce.Do(func() {
|
||||
close(d.endpoint.done)
|
||||
if d.udpForwarder != nil {
|
||||
d.udpForwarder.Close()
|
||||
}
|
||||
if d.icmpForwarder != nil {
|
||||
d.icmpForwarder.Close()
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ func newSystemStackDevice(options DeviceOptions) (*systemStackDevice, error) {
|
||||
}
|
||||
stackOptions := options
|
||||
stackOptions.System = false
|
||||
stackOptions.Name = system.options.Name
|
||||
stackOptions.ExcludeInterface = []string{system.options.Name}
|
||||
stackDevice, err := newStackDevice(stackOptions)
|
||||
if err != nil {
|
||||
system.Close()
|
||||
@@ -40,6 +42,15 @@ func (d *systemStackDevice) SetPacketWriter(writer PacketWriter) {
|
||||
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 {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"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"
|
||||
@@ -42,6 +43,11 @@ type DeviceOptions struct {
|
||||
Handler tun.Handler
|
||||
UDPTimeout time.Duration
|
||||
ICMPTimeout time.Duration
|
||||
UDPMapping tun.NATMapping
|
||||
UDPFiltering tun.NATFiltering
|
||||
UDPNATMax uint32
|
||||
InterfaceFinder control.InterfaceFinder
|
||||
ExcludeInterface []string
|
||||
Name string
|
||||
MTU uint32
|
||||
Configuration Configuration
|
||||
|
||||
@@ -37,6 +37,7 @@ type stackDevice struct {
|
||||
endpoint *stackEndpoint
|
||||
inet4Address netip.Addr
|
||||
inet6Address netip.Addr
|
||||
udpForwarder *tun.UDPForwarder
|
||||
icmpForwarder *tun.ICMPForwarder
|
||||
logRouteOptions bool
|
||||
closeOnce sync.Once
|
||||
@@ -68,9 +69,17 @@ func newStackDevice(options DeviceOptions) (*stackDevice, error) {
|
||||
}
|
||||
if options.Handler != nil {
|
||||
ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tun.NewTCPForwarder(options.Context, ipStack, options.Handler).HandlePacket)
|
||||
ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, tun.NewUDPForwarder(options.Context, ipStack, options.Handler, tun.UDPNatOptions{
|
||||
udpForwarder := tun.NewUDPForwarder(options.Context, ipStack, options.Handler, tun.UDPNatOptions{
|
||||
Timeout: options.UDPTimeout,
|
||||
}).HandlePacket)
|
||||
Shared: true,
|
||||
Mapping: options.UDPMapping,
|
||||
Filtering: options.UDPFiltering,
|
||||
MaxSize: options.UDPNATMax,
|
||||
InterfaceFinder: options.InterfaceFinder,
|
||||
ExcludeInterface: options.ExcludeInterface,
|
||||
})
|
||||
ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket)
|
||||
device.udpForwarder = udpForwarder
|
||||
icmpForwarder := tun.NewICMPForwarder(ipStack, options.Handler, options.Logger)
|
||||
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket)
|
||||
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket)
|
||||
@@ -80,6 +89,12 @@ func newStackDevice(options DeviceOptions) (*stackDevice, error) {
|
||||
}
|
||||
|
||||
func (d *stackDevice) Start() error {
|
||||
if d.udpForwarder != nil {
|
||||
err := d.udpForwarder.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -249,6 +264,9 @@ func (d *stackDevice) PortMTU() uint32 {
|
||||
func (d *stackDevice) Close() error {
|
||||
d.closeOnce.Do(func() {
|
||||
close(d.endpoint.done)
|
||||
if d.udpForwarder != nil {
|
||||
d.udpForwarder.Close()
|
||||
}
|
||||
if d.icmpForwarder != nil {
|
||||
d.icmpForwarder.Close()
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ func newSystemStackDevice(options DeviceOptions) (*systemStackDevice, error) {
|
||||
}
|
||||
stackOptions := options
|
||||
stackOptions.System = false
|
||||
stackOptions.Name = system.options.Name
|
||||
stackOptions.ExcludeInterface = []string{system.options.Name}
|
||||
stackDevice, err := newStackDevice(stackOptions)
|
||||
if err != nil {
|
||||
system.Close()
|
||||
@@ -41,6 +43,15 @@ func (d *systemStackDevice) SetPacketWriter(writer PacketWriter) {
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user