Improve OpenVPN & OpenConnect interoperability

This commit is contained in:
世界
2026-08-30 17:41:44 +08:00
parent b2f1f630d9
commit b9c7d52582
47 changed files with 3915 additions and 440 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ import "strings"
func LinkerFlags(version string, debug bool) string { func LinkerFlags(version string, debug bool) string {
flags := []string{ flags := []string{
"-X github.com/sagernet/sing-box/constant.Version=" + version, "-X github.com/sagernet/sing-box/constant.Version=" + version,
"-X internal/godebug.defaultGODEBUG=multipathtcp=0", "-X runtime.godebugDefault=multipathtcp=0,tlssha1=1,tlsunsafeekm=1",
"-checklinkname=0", "-checklinkname=0",
} }
if !debug { if !debug {
+17 -4
View File
@@ -155,6 +155,13 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
if options.DisableTCPKeepAlive { if options.DisableTCPKeepAlive {
dialer.KeepAlive = -1 dialer.KeepAlive = -1
dialer.KeepAliveConfig.Enable = false dialer.KeepAliveConfig.Enable = false
} else if options.TCPKeepAliveSystemDefaults {
dialer.KeepAliveConfig = net.KeepAliveConfig{
Enable: true,
Idle: -1,
Interval: -1,
Count: -1,
}
} else { } else {
keepIdle := time.Duration(options.TCPKeepAlive) keepIdle := time.Duration(options.TCPKeepAlive)
if keepIdle == 0 { if keepIdle == 0 {
@@ -188,8 +195,11 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
if options.Inet4BindAddress != nil { if options.Inet4BindAddress != nil {
bindAddr := options.Inet4BindAddress.Build(netip.IPv4Unspecified()) bindAddr := options.Inet4BindAddress.Build(netip.IPv4Unspecified())
dialer4.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()} dialer4.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()}
udpDialer4.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()} udpDialer4.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice(), Port: int(options.UDPBindPort)}
udpAddr4 = M.SocksaddrFrom(bindAddr, 0).String() udpAddr4 = M.SocksaddrFrom(bindAddr, options.UDPBindPort).String()
} else if options.UDPBindPort != 0 {
udpDialer4.LocalAddr = &net.UDPAddr{IP: net.IPv4zero, Port: int(options.UDPBindPort)}
udpAddr4 = M.SocksaddrFrom(netip.IPv4Unspecified(), options.UDPBindPort).String()
} }
var ( var (
dialer6 = dialer dialer6 = dialer
@@ -199,8 +209,11 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
if options.Inet6BindAddress != nil { if options.Inet6BindAddress != nil {
bindAddr := options.Inet6BindAddress.Build(netip.IPv6Unspecified()) bindAddr := options.Inet6BindAddress.Build(netip.IPv6Unspecified())
dialer6.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()} dialer6.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()}
udpDialer6.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()} udpDialer6.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice(), Port: int(options.UDPBindPort)}
udpAddr6 = M.SocksaddrFrom(bindAddr, 0).String() udpAddr6 = M.SocksaddrFrom(bindAddr, options.UDPBindPort).String()
} else if options.UDPBindPort != 0 {
udpDialer6.LocalAddr = &net.UDPAddr{IP: net.IPv6unspecified, Port: int(options.UDPBindPort)}
udpAddr6 = M.SocksaddrFrom(netip.IPv6Unspecified(), options.UDPBindPort).String()
} }
if options.TCPMultiPath { if options.TCPMultiPath {
dialer4.SetMultipathTCP(true) dialer4.SetMultipathTCP(true)
+15 -13
View File
@@ -15,19 +15,21 @@ const (
) )
const ( const (
DNSTypeLegacy = "legacy" DNSTypeLegacy = "legacy"
DNSTypeUDP = "udp" DNSTypeUDP = "udp"
DNSTypeTCP = "tcp" DNSTypeTCP = "tcp"
DNSTypeTLS = "tls" DNSTypeTLS = "tls"
DNSTypeHTTPS = "https" DNSTypeHTTPS = "https"
DNSTypeQUIC = "quic" DNSTypeQUIC = "quic"
DNSTypeHTTP3 = "h3" DNSTypeHTTP3 = "h3"
DNSTypeLocal = "local" DNSTypeLocal = "local"
DNSTypeHosts = "hosts" DNSTypeHosts = "hosts"
DNSTypeFakeIP = "fakeip" DNSTypeFakeIP = "fakeip"
DNSTypeDHCP = "dhcp" DNSTypeDHCP = "dhcp"
DNSTypeMDNS = "mdns" DNSTypeMDNS = "mdns"
DNSTypeTailscale = "tailscale" DNSTypeTailscale = "tailscale"
DNSTypeOpenConnect = "openconnect"
DNSTypeOpenVPN = "openvpn"
) )
const ( const (
+8 -7
View File
@@ -507,13 +507,14 @@ Match source device hostname from DHCP leases.
Match specified DNS servers' preferred domains. Match specified DNS servers' preferred domains.
| Type | Match | | Type | Match |
|-------------|------------------------------------------------------------------------------| |---------------|------------------------------------------------------------------------------|
| `hosts` | Match predefined entries and entries in hosts files | | `hosts` | Match predefined entries and entries in hosts files |
| `local` | Match hosts entries, neighbor-resolved hosts, and mDNS local domains | | `local` | Match hosts entries, neighbor-resolved hosts, and mDNS local domains |
| `mdns` | Match mDNS local domains (`*.local.` and IPv4/IPv6 link-local reverse zones) | | `mdns` | Match mDNS local domains (`*.local.` and IPv4/IPv6 link-local reverse zones) |
| `tailscale` | Match MagicDNS hosts and DNS route suffixes | | `tailscale` | Match MagicDNS hosts and DNS route suffixes |
| `resolved` | Match split DNS and search domains from systemd-resolved links | | `openconnect` | Match split DNS and search domains pushed by the VPN server |
| `resolved` | Match split DNS and search domains from systemd-resolved links |
#### wifi_ssid #### wifi_ssid
+8 -7
View File
@@ -499,13 +499,14 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
匹配指定 DNS 服务器的首选域名。 匹配指定 DNS 服务器的首选域名。
| 类型 | 匹配 | | 类型 | 匹配 |
|-------------|-------------------------------------------------------------| |---------------|-------------------------------------------------------------|
| `hosts` | 匹配预定义条目和 hosts 文件中的条目 | | `hosts` | 匹配预定义条目和 hosts 文件中的条目 |
| `local` | 匹配 hosts 中的条目、邻居解析得到的主机名以及 mDNS 本地域名 | | `local` | 匹配 hosts 中的条目、邻居解析得到的主机名以及 mDNS 本地域名 |
| `mdns` | 匹配 mDNS 本地域名(`*.local.` 以及 IPv4/IPv6 链路本地反向区域) | | `mdns` | 匹配 mDNS 本地域名(`*.local.` 以及 IPv4/IPv6 链路本地反向区域) |
| `tailscale` | 匹配 MagicDNS 主机和 DNS 路由后缀 | | `tailscale` | 匹配 MagicDNS 主机和 DNS 路由后缀 |
| `resolved` | 匹配 systemd-resolved 链路中的分流域名和搜索域 | | `openconnect` | 匹配 VPN 服务器推送的分流 DNS 和搜索域 |
| `resolved` | 匹配 systemd-resolved 链路中的分流域名和搜索域 |
#### wifi_ssid #### wifi_ssid
+2
View File
@@ -46,6 +46,8 @@ The type of the DNS server.
| `mdns` | [mDNS](./mdns/) | | `mdns` | [mDNS](./mdns/) |
| `fakeip` | [Fake IP](./fakeip/) | | `fakeip` | [Fake IP](./fakeip/) |
| `tailscale` | [Tailscale](./tailscale/) | | `tailscale` | [Tailscale](./tailscale/) |
| `openconnect` | [OpenConnect](./openconnect/) |
| `openvpn` | [OpenVPN](./openvpn/) |
| `resolved` | [Resolved](./resolved/) | | `resolved` | [Resolved](./resolved/) |
#### tag #### tag
@@ -46,6 +46,8 @@ DNS 服务器的类型。
| `mdns` | [mDNS](./mdns/) | | `mdns` | [mDNS](./mdns/) |
| `fakeip` | [Fake IP](./fakeip/) | | `fakeip` | [Fake IP](./fakeip/) |
| `tailscale` | [Tailscale](./tailscale/) | | `tailscale` | [Tailscale](./tailscale/) |
| `openconnect` | [OpenConnect](./openconnect/) |
| `openvpn` | [OpenVPN](./openvpn/) |
| `resolved` | [Resolved](./resolved/) | | `resolved` | [Resolved](./resolved/) |
#### tag #### tag
@@ -0,0 +1,97 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.14.0"
# OpenConnect
### Structure
```json
{
"dns": {
"servers": [
{
"type": "openconnect",
"tag": "",
"endpoint": "oc-client",
"accept_default_resolvers": false,
"accept_search_domain": false
}
]
}
}
```
### Fields
#### endpoint
==Required==
The tag of the [OpenConnect Endpoint](/configuration/endpoint/openconnect).
DNS queries are sent to the resolvers pushed by the VPN server through the OpenConnect endpoint. Pushed split-DNS rules use their dedicated resolvers, while pushed split-DNS and search-domain suffixes use the general pushed resolvers. The most specific matching suffix takes precedence.
Pushed DNS settings are not installed into the operating system.
#### accept_default_resolvers
Accept the general resolvers pushed by the VPN server for unmatched queries.
When enabled, the general resolvers are used as the default only if the server requests all DNS through the tunnel, or if it does not provide split-DNS rules or suffixes. Otherwise, unmatched queries return `NXDOMAIN`.
#### accept_search_domain
When enabled and pushed search domains are available, single-label queries (for example, `intranet`) are retried with each search domain until one resolves.
If every search-domain expansion returns `NXDOMAIN`, the original unqualified name follows normal default-resolver behavior.
### Examples
=== "Split DNS only"
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
},
{
"type": "openconnect",
"tag": "oc",
"endpoint": "oc-client"
}
],
"rules": [
{
"preferred_by": "oc",
"action": "route",
"server": "oc"
}
],
"final": "local"
}
}
```
=== "Accept pushed default resolvers"
```json
{
"dns": {
"servers": [
{
"type": "openconnect",
"endpoint": "oc-client",
"accept_default_resolvers": true,
"accept_search_domain": true
}
]
}
}
```
@@ -0,0 +1,97 @@
---
icon: material/new-box
---
!!! question "自 sing-box 1.14.0 起"
# OpenConnect
### 结构
```json
{
"dns": {
"servers": [
{
"type": "openconnect",
"tag": "",
"endpoint": "oc-client",
"accept_default_resolvers": false,
"accept_search_domain": false
}
]
}
}
```
### 字段
#### endpoint
==必填==
[OpenConnect 端点](/zh/configuration/endpoint/openconnect) 的标签。
DNS 查询会通过 OpenConnect 端点发送到 VPN 服务器推送的解析器。推送的分流 DNS 规则使用各自的专用解析器,推送的分流 DNS 和搜索域后缀则使用通用推送解析器。匹配时优先使用最具体的后缀。
推送的 DNS 设置不会安装到操作系统中。
#### accept_default_resolvers
接受 VPN 服务器推送的通用解析器,用于未匹配的查询。
启用时,仅当服务器要求所有 DNS 通过隧道,或未提供分流 DNS 规则及后缀时,通用解析器才会作为默认解析器。否则,未匹配的查询将返回 `NXDOMAIN`
#### accept_search_domain
启用且存在推送的搜索域时,单标签查询(例如 `intranet`)会依次附加各个搜索域进行重试,直到其中一个解析成功。
如果所有搜索域扩展均返回 `NXDOMAIN`,原始未限定名称将按普通默认解析器行为处理。
### 示例
=== "仅分流 DNS"
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
},
{
"type": "openconnect",
"tag": "oc",
"endpoint": "oc-client"
}
],
"rules": [
{
"preferred_by": "oc",
"action": "route",
"server": "oc"
}
],
"final": "local"
}
}
```
=== "接受推送的默认解析器"
```json
{
"dns": {
"servers": [
{
"type": "openconnect",
"endpoint": "oc-client",
"accept_default_resolvers": true,
"accept_search_domain": true
}
]
}
}
```
+82
View File
@@ -0,0 +1,82 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.14.0"
# OpenVPN
### Structure
```json
{
"dns": {
"servers": [
{
"type": "openvpn",
"tag": "",
"endpoint": "ovpn-client",
"accept_default_resolvers": false,
"accept_search_domain": false
}
]
}
}
```
### Fields
#### endpoint
==Required==
The tag of the [OpenVPN Client Endpoint](/configuration/endpoint/openvpn-client).
DNS queries are sent through the endpoint to resolvers pushed by the OpenVPN server. Modern OpenVPN `dns server` options support plain DNS, DNS over TLS, DNS over HTTPS, custom ports, SNI, and `resolve-domains`. Only the server group with the lowest priority number is active. Legacy `dhcp-option DNS`/`DNS6` and `DOMAIN-ROUTE` are used when no modern server group is present.
A modern server group overrides legacy DHCP DNS resolver and domain options. A standalone modern `dns search-domains` option does not remove legacy resolvers. Required DNSSEC validation (`dnssec yes`) is rejected because this transport does not provide DNSSEC validation.
Pushed DNS settings are not installed into the operating system.
#### accept_default_resolvers
Use pushed resolvers for queries that do not match a pushed `resolve-domains`, `DOMAIN-ROUTE`, or search-domain suffix.
When disabled, unmatched queries return `NXDOMAIN`.
#### accept_search_domain
When enabled and pushed search domains are available, single-label queries (for example, `intranet`) are retried with each search domain until one resolves.
If no search domain is available, the original single-label query follows normal default-resolver behavior.
### Example
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
},
{
"type": "openvpn",
"tag": "ovpn-dns",
"endpoint": "ovpn-client",
"accept_default_resolvers": true,
"accept_search_domain": true
}
],
"rules": [
{
"preferred_by": "ovpn-dns",
"action": "route",
"server": "ovpn-dns"
}
],
"final": "local"
}
}
```
@@ -0,0 +1,82 @@
---
icon: material/new-box
---
!!! question "自 sing-box 1.14.0 起"
# OpenVPN
### 结构
```json
{
"dns": {
"servers": [
{
"type": "openvpn",
"tag": "",
"endpoint": "ovpn-client",
"accept_default_resolvers": false,
"accept_search_domain": false
}
]
}
}
```
### 字段
#### endpoint
==必填==
[OpenVPN 客户端端点](/zh/configuration/endpoint/openvpn-client) 的标签。
DNS 查询会通过该端点发送到 OpenVPN 服务器推送的解析器。现代 OpenVPN `dns server` 选项支持普通 DNS、DNS over TLS、DNS over HTTPS、自定义端口、SNI 和 `resolve-domains`。只有优先级数字最低的服务器组会生效。没有现代服务器组时,使用传统的 `dhcp-option DNS`/`DNS6``DOMAIN-ROUTE`
现代服务器组会覆盖传统 DHCP DNS 解析器及相关域选项。只有现代 `dns search-domains` 而没有现代服务器组时,不会移除传统解析器。由于此传输不提供 DNSSEC 验证,需要强制验证的 `dnssec yes` 会被拒绝。
推送的 DNS 设置不会安装到操作系统中。
#### accept_default_resolvers
对未匹配推送的 `resolve-domains``DOMAIN-ROUTE` 或搜索域后缀的查询使用推送解析器。
禁用时,未匹配查询返回 `NXDOMAIN`
#### accept_search_domain
启用且存在推送的搜索域时,单标签查询(例如 `intranet`)会依次附加各个搜索域重试,直到其中一个解析成功。
不存在搜索域时,原始单标签查询按普通默认解析器规则处理。
### 示例
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
},
{
"type": "openvpn",
"tag": "ovpn-dns",
"endpoint": "ovpn-client",
"accept_default_resolvers": true,
"accept_search_domain": true
}
],
"rules": [
{
"preferred_by": "ovpn-dns",
"action": "route",
"server": "ovpn-dns"
}
],
"final": "local"
}
}
```
+237 -8
View File
@@ -21,9 +21,11 @@
"username": "", "username": "",
"password": "", "password": "",
"auth_group": "", "auth_group": "",
"cookie": "",
"token": { "token": {
"mode": "", "mode": "",
"secret": "", "secret": "",
"secret_path": "",
"pin": "", "pin": "",
"password": "", "password": "",
"device_id": "", "device_id": "",
@@ -31,6 +33,13 @@
}, },
"reported_os": "", "reported_os": "",
"user_agent": "", "user_agent": "",
"version": "",
"local_hostname": "",
"mobile": {
"platform_version": "",
"device_type": "",
"device_unique_id": ""
},
"csd": { "csd": {
"wrapper_path": "" "wrapper_path": ""
}, },
@@ -50,8 +59,28 @@
] ]
}, },
"no_udp": false, "no_udp": false,
"dtls_local_port": 0,
"compression_disabled": false,
"compression_mode": "",
"ipv6_disabled": false,
"http_keepalive_disabled": false,
"xml_post_disabled": false,
"external_auth_disabled": false,
"password_authentication_disabled": false,
"tcp_keep_alive_enabled": false,
"pfs": false,
"mtu": 0,
"base_mtu": 0,
"dpd_interval": "",
"reconnect_timeout": "",
"trojan_interval": "",
"queue_length": 0,
"allow_insecure_crypto": false, "allow_insecure_crypto": false,
"tls": { "tls": {
"insecure": false,
"server_name": "",
"peer_fingerprint": [],
"system_trust_disabled": false,
"certificate_authority": [], "certificate_authority": [],
"certificate_authority_path": "", "certificate_authority_path": "",
"client_certificate": [], "client_certificate": [],
@@ -125,30 +154,56 @@ Password used to fill matching authentication form fields.
Authentication group used to preselect a matching group, realm, domain, or gateway choice when supported by the selected flavor. Authentication group used to preselect a matching group, realm, domain, or gateway choice when supported by the selected flavor.
### cookie
Existing authentication session used to connect without first prompting for credentials.
The accepted format depends on `flavor`:
- `anyconnect`: A `webvpn` value, or a semicolon-separated cookie list containing `webvpn`.
- `gp`: The complete authenticated query string returned by GlobalProtect authentication.
- `nc`: A `DSID` value, or a semicolon-separated cookie list containing `DSID`.
- `pulse`: The raw Pulse authentication cookie value.
- `f5`: An `MRHSession` value, or a semicolon-separated cookie list containing `MRHSession` and optionally `F5_ST`.
- `fortinet`: An `SVPNCOOKIE` value, or a semicolon-separated cookie list containing `SVPNCOOKIE`.
If the server rejects the supplied session, normal authentication is attempted.
### token ### token
Software token configuration for automatically answering matching token fields. Token configuration for automatically answering matching token fields or HTTP Bearer authentication.
One of `token.secret` or `token.secret_path` is required.
### token.mode ### token.mode
==Required== ==Required==
Software token mode, one of: Token mode, one of:
- `totp`: Time-based One-Time Password. - `totp`: Time-based One-Time Password.
- `hotp`: HMAC-based One-Time Password. - `hotp`: HMAC-based One-Time Password.
- `stoken`: RSA SecurID software token. - `stoken`: RSA SecurID software token.
- `oidc`: OIDC access token used for HTTP Bearer authentication.
### token.secret ### token.secret
==Required==
Software token secret. Software token secret.
For `totp` and `hotp`, this can be a Base32 secret, a `base32:`-prefixed secret, or an `otpauth://` URI of the matching type. For `totp` and `hotp`, this can be a Base32 secret, a `base32:`-prefixed secret, or an `otpauth://` URI of the matching type.
For `stoken`, this is the encoded RSA SecurID CTF token content. For `stoken`, this is the encoded RSA SecurID CTF token content.
For `oidc`, this is the access token value. It is sent only after the VPN server requests HTTP Bearer authentication.
Conflict with `token.secret_path`.
### token.secret_path
Path to the software token secret or OIDC access token.
Conflict with `token.secret`.
### token.pin ### token.pin
RSA SecurID PIN for `stoken` mode. RSA SecurID PIN for `stoken` mode.
@@ -173,13 +228,41 @@ Operating system identity reported to the VPN server when supported by the selec
For `anyconnect`, `gp`, and `pulse`, the supported values are `linux`, `linux-64`, `win`, `mac-intel`, `android`, and `apple-ios`. For `anyconnect`, `gp`, and `pulse`, the supported values are `linux`, `linux-64`, `win`, `mac-intel`, `android`, and `apple-ios`.
`anyconnect` uses `linux-64` by default. `gp` and `pulse` select a value based on the system platform by default. The default is selected from the system platform: `win` on Windows, `mac-intel` on macOS, `android` on Android, `apple-ios` on iOS, and `linux-64` or `linux` on other 64-bit or 32-bit systems.
### user_agent ### user_agent
User agent reported to the VPN server when supported by the selected flavor. User agent reported to the VPN server when supported by the selected flavor.
The default is flavor-specific. The default is flavor-specific. AnyConnect, Network Connect, Pulse, and F5 use `AnyConnect-compatible OpenConnect VPN Agent v9.21`; GlobalProtect uses `PAN GlobalProtect`; Fortinet uses `Mozilla/5.0 SV1`.
### version
Client version reported separately from `user_agent` when supported by the selected flavor.
`v9.21` is used by default. Currently used by AnyConnect XML authentication.
### local_hostname
Local hostname reported to the VPN server when supported by the selected flavor.
The system hostname is used by default, or `localhost` if it is unavailable.
### mobile
AnyConnect mobile client identity. When configured, all three fields are required and are reported during XML authentication and tunnel establishment.
### mobile.platform_version
Mobile operating system version reported to the AnyConnect server.
### mobile.device_type
Mobile device model or type reported to the AnyConnect server.
### mobile.device_unique_id
Mobile device identifier reported to the AnyConnect server.
### csd ### csd
@@ -263,16 +346,158 @@ Conflict with `tncc.certificates.certificate`.
Disable the DTLS or ESP secondary data channel and use the TLS data channel only. Disable the DTLS or ESP secondary data channel and use the TLS data channel only.
### dtls_local_port
Local UDP port used by the direct DTLS or ESP secondary data channel.
An automatically selected ephemeral port is used by default.
### compression_disabled
Disable AnyConnect compression negotiation.
By default, stateless `oc-lz4` and `lzs` compression is negotiated for CSTP and DTLS when supported by the server.
Compression can weaken traffic confidentiality when an attacker can influence plaintext sent through the VPN tunnel.
Conflict with `compression_mode` set to `all`.
### compression_mode
AnyConnect compression mode, one of:
- `stateless`: Advertise stateless `oc-lz4` and `lzs` compression.
- `all`: Additionally advertise stateful `deflate` compression for CSTP.
`stateless` is used by default. DTLS always uses stateless compression, including when `all` is selected.
Stateful compression has additional traffic confidentiality risks and should only be enabled when required by the VPN server.
### ipv6_disabled
Disable requesting and using IPv6 tunnel configuration.
### http_keepalive_disabled
Disable HTTP connection reuse during authentication and configuration requests.
### xml_post_disabled
Disable AnyConnect XML POST authentication and start authentication with the legacy GET flow.
### external_auth_disabled
Disable external browser authentication such as SSO and SAML for AnyConnect and GlobalProtect.
When enabled, external authentication is not advertised to the server and an unexpected external authentication request is rejected.
### password_authentication_disabled
Abort AnyConnect authentication if the server returns a non-success authentication form, matching OpenConnect `--no-passwd` behavior.
This does not affect the other flavors or a session supplied by `cookie`.
### tcp_keep_alive_enabled
Enable TCP keep alive for direct VPN server connections.
Disabled by default to match OpenConnect. Setting `tcp_keep_alive` or `tcp_keep_alive_interval` also enables it without requiring this field. When enabled without either duration, the operating system TCP keep alive timing is retained.
Conflict with `disable_tcp_keep_alive`.
### pfs
Require forward-secret TLS cipher suites for TLS 1.2 and earlier.
Disabled by default for compatibility with VPN servers that require RSA key exchange. This does not enable deprecated cipher suites; see `allow_insecure_crypto` for legacy crypto support.
### mtu
Preferred tunnel MTU.
The negotiated MTU is limited to this value for all flavors. For AnyConnect, this value is also sent to the server. GlobalProtect, F5, and Fortinet remove their protocol overhead before using it as the tunnel MTU.
Non-zero values below `576` are treated as `576`. The maximum value is `65535`.
### base_mtu
Base path MTU used to calculate the AnyConnect, GlobalProtect, F5, and Fortinet tunnel MTU after outer IP, transport, and protocol overhead.
`1406` is used by default.
These flavors treat values below `1280` as `1280`. The maximum value is `65535`.
### dpd_interval
Override the Dead Peer Detection interval.
The server-provided or flavor-specific interval is used by default.
Positive values below `2s` are treated as `2s`. The value must not be negative.
### reconnect_timeout
Maximum accumulated backoff time after failed reconnect attempts. The first reconnect attempt starts immediately, and this timeout does not cancel an attempt already in progress.
`300s` is used by default.
The value must not be negative.
### trojan_interval
Override the interval between GlobalProtect HIP reports or Network Connect TNCC checks.
The server-provided interval is used by default. GlobalProtect uses `1h` when the server does not provide one.
The value must not be negative.
### queue_length
Inbound and outbound packet queue length between the VPN transport and the tunnel interface.
`32` is used by default. A full queue applies backpressure until its consumer makes room; queued packets are not discarded.
### allow_insecure_crypto ### allow_insecure_crypto
Allow deprecated TLS and DTLS versions and cipher suites required by legacy VPN servers. Enable weak TLS and DTLS cipher suites and TLS 1.0 compatibility required by legacy VPN servers.
Disabled by default. This option does not disable server certificate verification. Disabled by default; TLS versions below 1.2 are otherwise rejected. This option does not disable server certificate verification.
### tls ### tls
OpenConnect TLS configuration. OpenConnect TLS configuration.
### tls.insecure
Disable verification of the VPN server certificate and hostname.
Disabled by default. Enabling this permits an active attacker to impersonate the VPN server. Prefer `tls.certificate_authority` or `tls.peer_fingerprint` when possible.
### tls.server_name
Server name used for TLS SNI and certificate hostname verification.
The hostname from `server` is used by default.
### tls.peer_fingerprint
Allowed server certificate fingerprints. A single string or a list can be specified.
Supported formats:
- An unprefixed SHA-1 certificate fingerprint compatible with OpenConnect `--servercert`.
- `sha1:<hex>`: SHA-1 SPKI fingerprint.
- `sha256:<hex>`: SHA-256 SPKI fingerprint.
- `pin-sha256:<base64>`: Base64-encoded SHA-256 SPKI pin.
The encoded fingerprint in every format can be abbreviated to a prefix of at least four characters. When configured, the peer certificate must match one of these fingerprints; a match can authorize a certificate that is not otherwise trusted.
### tls.system_trust_disabled
Disable the system CA certificate pool.
Use `tls.certificate_authority` or `tls.peer_fingerprint` to establish trust when enabled.
### tls.certificate_authority ### tls.certificate_authority
Additional trusted CA certificate content in PEM format. Additional trusted CA certificate content in PEM format.
@@ -392,3 +617,7 @@ See [Dial Fields](/configuration/shared/dial/) for details.
## Interactive authentication ## Interactive authentication
Use `Tools` > `Endpoints` in the sing-box dashboard or any sing-box graphical client to authenticate and manage the endpoint. Use `Tools` > `Endpoints` in the sing-box dashboard or any sing-box graphical client to authenticate and manage the endpoint.
## DNS
Pushed DNS settings are not installed into the operating system. Configure an [OpenConnect DNS server](/configuration/dns/server/openconnect/) to use them through sing-box.
+237 -8
View File
@@ -21,9 +21,11 @@
"username": "", "username": "",
"password": "", "password": "",
"auth_group": "", "auth_group": "",
"cookie": "",
"token": { "token": {
"mode": "", "mode": "",
"secret": "", "secret": "",
"secret_path": "",
"pin": "", "pin": "",
"password": "", "password": "",
"device_id": "", "device_id": "",
@@ -31,6 +33,13 @@
}, },
"reported_os": "", "reported_os": "",
"user_agent": "", "user_agent": "",
"version": "",
"local_hostname": "",
"mobile": {
"platform_version": "",
"device_type": "",
"device_unique_id": ""
},
"csd": { "csd": {
"wrapper_path": "" "wrapper_path": ""
}, },
@@ -50,8 +59,28 @@
] ]
}, },
"no_udp": false, "no_udp": false,
"dtls_local_port": 0,
"compression_disabled": false,
"compression_mode": "",
"ipv6_disabled": false,
"http_keepalive_disabled": false,
"xml_post_disabled": false,
"external_auth_disabled": false,
"password_authentication_disabled": false,
"tcp_keep_alive_enabled": false,
"pfs": false,
"mtu": 0,
"base_mtu": 0,
"dpd_interval": "",
"reconnect_timeout": "",
"trojan_interval": "",
"queue_length": 0,
"allow_insecure_crypto": false, "allow_insecure_crypto": false,
"tls": { "tls": {
"insecure": false,
"server_name": "",
"peer_fingerprint": [],
"system_trust_disabled": false,
"certificate_authority": [], "certificate_authority": [],
"certificate_authority_path": "", "certificate_authority_path": "",
"client_certificate": [], "client_certificate": [],
@@ -125,30 +154,56 @@ OpenConnect 协议 flavor,可选值为 `anyconnect`、`gp`、`fortinet`、`f5`
认证组,用于在所选 flavor 支持时预选匹配的组、realm、domain 或 gateway 选项。 认证组,用于在所选 flavor 支持时预选匹配的组、realm、domain 或 gateway 选项。
### cookie
用于跳过凭据提示并直接连接的现有认证会话。
接受的格式取决于 `flavor`
- `anyconnect``webvpn` 值,或包含 `webvpn` 的分号分隔 cookie 列表。
- `gp`GlobalProtect 认证返回的完整 authenticated query string。
- `nc``DSID` 值,或包含 `DSID` 的分号分隔 cookie 列表。
- `pulse`:原始 Pulse 认证 cookie 值。
- `f5``MRHSession` 值,或包含 `MRHSession` 及可选 `F5_ST` 的分号分隔 cookie 列表。
- `fortinet``SVPNCOOKIE` 值,或包含 `SVPNCOOKIE` 的分号分隔 cookie 列表。
如果服务器拒绝提供的会话,将尝试正常认证。
### token ### token
用于自动回答匹配 token 字段的软件 token 配置。 用于自动回答匹配 token 字段或进行 HTTP Bearer 认证的 token 配置。
必须设置 `token.secret``token.secret_path` 之一。
### token.mode ### token.mode
==必填== ==必填==
软件 token 模式,可选值为: Token 模式,可选值为:
- `totp`:基于时间的一次性密码。 - `totp`:基于时间的一次性密码。
- `hotp`:基于 HMAC 的一次性密码。 - `hotp`:基于 HMAC 的一次性密码。
- `stoken`RSA SecurID 软件 token。 - `stoken`RSA SecurID 软件 token。
- `oidc`:用于 HTTP Bearer 认证的 OIDC access token。
### token.secret ### token.secret
==必填==
软件 token 密钥。 软件 token 密钥。
对于 `totp``hotp`,可以是 Base32 密钥、带 `base32:` 前缀的密钥或类型匹配的 `otpauth://` URI。 对于 `totp``hotp`,可以是 Base32 密钥、带 `base32:` 前缀的密钥或类型匹配的 `otpauth://` URI。
对于 `stoken`,这是编码后的 RSA SecurID CTF token 内容。 对于 `stoken`,这是编码后的 RSA SecurID CTF token 内容。
对于 `oidc`,这是 access token 值。仅在 VPN 服务器请求 HTTP Bearer 认证后发送。
`token.secret_path` 冲突。
### token.secret_path
软件 token 密钥或 OIDC access token 的路径。
`token.secret` 冲突。
### token.pin ### token.pin
`stoken` 模式的 RSA SecurID PIN。 `stoken` 模式的 RSA SecurID PIN。
@@ -173,13 +228,41 @@ OpenConnect 协议 flavor,可选值为 `anyconnect`、`gp`、`fortinet`、`f5`
对于 `anyconnect``gp``pulse`,支持的值为 `linux``linux-64``win``mac-intel``android``apple-ios` 对于 `anyconnect``gp``pulse`,支持的值为 `linux``linux-64``win``mac-intel``android``apple-ios`
`anyconnect` 默认使用 `linux-64``gp``pulse` 默认根据系统平台选择值 默认值根据系统平台选择:Windows 使用 `win`macOS 使用 `mac-intel`Android 使用 `android`iOS 使用 `apple-ios`,其他 64 位或 32 位系统使用 `linux-64``linux`
### user_agent ### user_agent
所选 flavor 支持时向 VPN 服务器报告的 User-Agent。 所选 flavor 支持时向 VPN 服务器报告的 User-Agent。
默认值由 flavor 决定。 默认值由 flavor 决定。AnyConnect、Network Connect、Pulse 和 F5 使用 `AnyConnect-compatible OpenConnect VPN Agent v9.21`GlobalProtect 使用 `PAN GlobalProtect`Fortinet 使用 `Mozilla/5.0 SV1`
### version
所选 flavor 支持时,与 `user_agent` 分开报告的客户端版本。
默认使用 `v9.21`。当前用于 AnyConnect XML 认证。
### local_hostname
所选 flavor 支持时向 VPN 服务器报告的本地主机名。
默认使用系统主机名;无法获取时使用 `localhost`
### mobile
AnyConnect 移动客户端身份。配置时三个字段均为必填,并会在 XML 认证和隧道建立阶段报告。
### mobile.platform_version
向 AnyConnect 服务器报告的移动操作系统版本。
### mobile.device_type
向 AnyConnect 服务器报告的移动设备型号或类型。
### mobile.device_unique_id
向 AnyConnect 服务器报告的移动设备标识符。
### csd ### csd
@@ -263,16 +346,158 @@ PEM 格式的 TNCC 机器证书路径。
禁用 DTLS 或 ESP 辅助数据通道,仅使用 TLS 数据通道。 禁用 DTLS 或 ESP 辅助数据通道,仅使用 TLS 数据通道。
### dtls_local_port
直连 DTLS 或 ESP 辅助数据通道使用的本地 UDP 端口。
默认自动选择临时端口。
### compression_disabled
禁用 AnyConnect 压缩协商。
默认情况下,当服务器支持时,CSTP 和 DTLS 会协商无状态 `oc-lz4``lzs` 压缩。
当攻击者能够影响通过 VPN 隧道发送的明文时,压缩可能削弱流量机密性。
与设置为 `all``compression_mode` 冲突。
### compression_mode
AnyConnect 压缩模式,可选值为:
- `stateless`:声明支持无状态 `oc-lz4``lzs` 压缩。
- `all`:额外声明支持 CSTP 有状态 `deflate` 压缩。
默认使用 `stateless`。即使选择 `all`DTLS 也始终使用无状态压缩。
有状态压缩存在额外的流量机密性风险,仅应在 VPN 服务器需要时启用。
### ipv6_disabled
禁用请求和使用 IPv6 隧道配置。
### http_keepalive_disabled
在认证和配置请求中禁用 HTTP 连接复用。
### xml_post_disabled
禁用 AnyConnect XML POST 认证,并直接使用旧版 GET 流程开始认证。
### external_auth_disabled
禁用 AnyConnect 和 GlobalProtect 的 SSO、SAML 等外部浏览器认证。
启用时不会向服务器声明外部认证支持,并会拒绝意外收到的外部认证请求。
### password_authentication_disabled
如果服务器返回非成功的认证表单,则中止 AnyConnect 认证,与 OpenConnect `--no-passwd` 行为一致。
此选项不影响其他 flavor,也不影响由 `cookie` 提供的会话。
### tcp_keep_alive_enabled
为直接 VPN 服务器连接启用 TCP keep alive。
默认禁用以匹配 OpenConnect。设置 `tcp_keep_alive``tcp_keep_alive_interval` 也会启用,无需同时设置此字段。启用但未设置这两个时间值时,保留操作系统的 TCP keep alive 时间设置。
`disable_tcp_keep_alive` 冲突。
### pfs
要求 TLS 1.2 及更早版本使用具有前向保密性的 TLS 密码套件。
默认禁用,以兼容需要 RSA 密钥交换的 VPN 服务器。此选项不会启用已弃用的密码套件;旧版加密支持参阅 `allow_insecure_crypto`
### mtu
首选隧道 MTU。
所有 flavor 协商的 MTU 都不会超过此值。对于 AnyConnect,此值还会发送给服务器。GlobalProtect、F5 和 Fortinet 会先扣除各自的协议开销,再将结果作为隧道 MTU。
非零值小于 `576` 时按 `576` 处理。最大值为 `65535`
### base_mtu
扣除外层 IP、传输和协议开销后,用于计算 AnyConnect、GlobalProtect、F5 和 Fortinet 隧道 MTU 的基础路径 MTU。
默认使用 `1406`
这些 flavor 会将小于 `1280` 的值按 `1280` 处理。最大值为 `65535`
### dpd_interval
覆盖 Dead Peer Detection 间隔。
默认使用服务器提供或 flavor 特定的间隔。
大于零且小于 `2s` 的值按 `2s` 处理。值不得为负数。
### reconnect_timeout
重连尝试失败后允许累计使用的最大退避时间。断线后的第一次重连会立即开始,且此超时不会取消已经进行中的尝试。
默认使用 `300s`
值不得为负数。
### trojan_interval
覆盖 GlobalProtect HIP report 或 Network Connect TNCC check 的执行间隔。
默认使用服务器提供的间隔。服务器未提供时,GlobalProtect 使用 `1h`
值不得为负数。
### queue_length
VPN transport 与隧道接口之间的入站和出站数据包队列长度。
默认使用 `32`。队列已满时会施加反压并等待消费者腾出空间,不会丢弃已排队的数据包。
### allow_insecure_crypto ### allow_insecure_crypto
允许旧版 VPN 服务器所需的已弃用 TLS 和 DTLS 版本及密码套件。 启用旧版 VPN 服务器所需的 TLS 和 DTLS 密码套件及 TLS 1.0 兼容性
默认禁用。此选项不会禁用服务器证书验证。 默认禁用;未启用时会拒绝低于 TLS 1.2 的版本。此选项不会禁用服务器证书验证。
### tls ### tls
OpenConnect TLS 配置。 OpenConnect TLS 配置。
### tls.insecure
禁用 VPN 服务器证书和主机名验证。
默认禁用。启用后,主动攻击者可以冒充 VPN 服务器。应尽可能使用 `tls.certificate_authority``tls.peer_fingerprint`
### tls.server_name
用于 TLS SNI 和证书主机名验证的服务器名称。
默认使用 `server` 中的主机名。
### tls.peer_fingerprint
允许的服务器证书指纹。可以指定单个字符串或列表。
支持的格式:
- 与 OpenConnect `--servercert` 兼容的无前缀 SHA-1 证书指纹。
- `sha1:<hex>`SHA-1 SPKI 指纹。
- `sha256:<hex>`SHA-256 SPKI 指纹。
- `pin-sha256:<base64>`Base64 编码的 SHA-256 SPKI pin。
每种格式的编码指纹均可缩写为至少四个字符的前缀。配置后,对端证书必须匹配其中一个指纹;匹配的指纹可以授权未通过其他方式信任的证书。
### tls.system_trust_disabled
禁用系统 CA 证书池。
启用时,使用 `tls.certificate_authority``tls.peer_fingerprint` 建立信任。
### tls.certificate_authority ### tls.certificate_authority
PEM 格式的附加受信任 CA 证书内容。 PEM 格式的附加受信任 CA 证书内容。
@@ -392,3 +617,7 @@ MCA 证书和私钥必须同时设置或同时为空。
## 交互式认证 ## 交互式认证
在 sing-box dashboard 或任意 sing-box 图形客户端的 `工具` > `端点` 中认证和管理 endpoint。 在 sing-box dashboard 或任意 sing-box 图形客户端的 `工具` > `端点` 中认证和管理 endpoint。
## DNS
推送的 DNS 设置不会安装到操作系统中。配置 [OpenConnect DNS 服务器](/zh/configuration/dns/server/openconnect/) 以通过 sing-box 使用这些设置。
+213 -5
View File
@@ -9,6 +9,7 @@
"type": "openvpn-client", "type": "openvpn-client",
"tag": "ovpn-client", "tag": "ovpn-client",
"mode": "tls",
"server": "127.0.0.1", "server": "127.0.0.1",
"server_port": 1194, "server_port": 1194,
"servers": [ "servers": [
@@ -20,11 +21,18 @@
], ],
"remote_random": false, "remote_random": false,
"network": "udp", "network": "udp",
"address": [],
"peer_address": "",
"peer_address_ipv6": "",
"topology": "",
"username": "", "username": "",
"password": "", "password": "",
"auth_retry": "none", "auth_retry": "none",
"static_challenge": "", "static_challenge": "",
"static_challenge_echo": false, "static_challenge_echo": false,
"static_key": [],
"static_key_path": "",
"key_direction": "",
"tls": { "tls": {
"server_name": "", "server_name": "",
"server_name_type": "name", "server_name_type": "name",
@@ -40,6 +48,7 @@
"remote_certificate_eku": "", "remote_certificate_eku": "",
"remote_certificate_tls": "", "remote_certificate_tls": "",
"certificate_profile": "", "certificate_profile": "",
"ns_certificate_type": "",
"version_min": "1.2", "version_min": "1.2",
"version_max": "", "version_max": "",
"cipher": "", "cipher": "",
@@ -51,11 +60,16 @@
"direction": "" "direction": ""
} }
}, },
"cipher": "",
"data_ciphers": [], "data_ciphers": [],
"data_ciphers_fallback": "", "data_ciphers_fallback": "",
"auth": "", "auth": "",
"mss_fix": 0, "mss_fix": 0,
"mss_fix_disabled": false,
"mss_fix_mode": "",
"fragment": 0, "fragment": 0,
"replay_window": 0,
"replay_window_time": "",
"compression": "", "compression": "",
"compression_lzo": "", "compression_lzo": "",
"allow_compression": "no", "allow_compression": "no",
@@ -71,9 +85,17 @@
"route_metric": 0, "route_metric": 0,
"redirect_gateway": false, "redirect_gateway": false,
"redirect_gateway_flags": [], "redirect_gateway_flags": [],
"redirect_private": false,
"block_ipv6": false,
"ping_interval": "", "ping_interval": "",
"ping_restart": "", "ping_restart": "",
"ping_restart_disabled": false,
"renegotiate_interval": "", "renegotiate_interval": "",
"renegotiate_disabled": false,
"renegotiate_bytes": 0,
"renegotiate_packets": 0,
"tls_timeout": "",
"handshake_window": "",
"explicit_exit_notify": 0, "explicit_exit_notify": 0,
"system": false, "system": false,
"name": "", "name": "",
@@ -91,6 +113,17 @@
## Fields ## Fields
### mode
OpenVPN session mode, one of `tls` or `static_key`.
`tls` is used by default.
`static_key` is a deprecated OpenVPN mode without a TLS control channel or
forward secrecy. It is retained as an explicit compatibility option for
immutable enterprise VPN servers. It does not use `tls`, username/password
authentication, pull options, or TLS renegotiation options.
### server ### server
OpenVPN server address. OpenVPN server address.
@@ -147,10 +180,38 @@ Default OpenVPN transport network, one of `udp` or `tcp`.
This value applies to `server` and to `servers` entries without their own `network`. This value applies to `server` and to `servers` entries without their own `network`.
### address
Local IPv4 and IPv6 tunnel prefixes.
At least one address is required in `static_key` mode. In TLS mode these
addresses are optional and can be replaced by addresses pulled from the
server.
### peer_address
IPv4 tunnel peer address and VPN gateway.
Required when an IPv4 `address` is configured in `static_key` mode.
### peer_address_ipv6
IPv6 tunnel peer address and VPN gateway.
Required when an IPv6 `address` is configured in `static_key` mode.
### topology
Tunnel topology, one of `net30`, `p2p`, or `subnet`.
The topology pulled from the server is used when empty in TLS mode.
### username ### username
Username for OpenVPN username/password authentication. Username for OpenVPN username/password authentication.
Only available in TLS mode.
### password ### password
Password for OpenVPN username/password authentication. Password for OpenVPN username/password authentication.
@@ -171,9 +232,31 @@ Static challenge text shown when requesting an authentication response.
Show the static challenge response as plain text. Show the static challenge response as plain text.
### static_key
OpenVPN static key content.
Required in `static_key` mode.
Conflict with `static_key_path`.
### static_key_path
OpenVPN static key path.
Required in `static_key` mode when `static_key` is not set.
Conflict with `static_key`.
### key_direction
Static key direction, one of `server` or `client`.
The key is used bidirectionally if empty. Only available in `static_key` mode.
### tls ### tls
==Required== Required in TLS mode.
OpenVPN control channel TLS configuration. OpenVPN control channel TLS configuration.
@@ -283,6 +366,19 @@ Certificate profile, one of `insecure`, `legacy`, `preferred`, or `suiteb`.
`legacy` is used by default. `legacy` is used by default.
`insecure` accepts MD5- and SHA-1-signed certificate chains and smaller legacy
keys for compatibility with immutable peers. Use it only when the peer cannot
be upgraded. `legacy` accepts SHA-1 but rejects MD5 signatures; `preferred`
requires stronger signatures and keys.
When `suiteb` is selected and `tls.cipher` is empty, the TLS 1.2 cipher list defaults to the Suite B ECDHE-ECDSA AES-GCM suites. Explicit `tls.cipher` and `tls.groups` values are not restricted by the profile.
### tls.ns_certificate_type
Deprecated Netscape certificate type check, one of `server` or `client`.
Disabled by default. Prefer `tls.remote_certificate_tls`.
### tls.version_min ### tls.version_min
Minimum TLS version, one of `1.0`, `1.1`, `1.2`, or `1.3`. Minimum TLS version, one of `1.0`, `1.1`, `1.2`, or `1.3`.
@@ -341,24 +437,49 @@ Conflict with `tls.control_wrap.key`.
Only available when `tls.control_wrap.type` is `tls_auth`. The key is used bidirectionally if empty. Only available when `tls.control_wrap.type` is `tls_auth`. The key is used bidirectionally if empty.
### cipher
Data-channel cipher used in `static_key` mode.
The upstream static-key default `BF-CBC` is used when empty. `BF-CBC` is a
legacy cipher with a 64-bit block size; configure the cipher required by the
server explicitly whenever possible. Static-key ciphers include `BF-CBC`,
`CAST5-CBC`, `DES-CBC`, `DES-EDE-CBC`, `DES-EDE3-CBC`, the AES-CBC,
ARIA-CBC, and Camellia-CBC families, `SEED-CBC`, `SM4-CBC`, and `NONE`.
Only available in `static_key` mode. `NONE` provides no confidentiality.
### data_ciphers ### data_ciphers
Allowed OpenVPN data channel ciphers. Allowed OpenVPN data channel ciphers.
Only available in TLS mode.
`AES-256-GCM`, `AES-128-GCM`, and `CHACHA20-POLY1305` are used by default. `AES-256-GCM`, `AES-128-GCM`, and `CHACHA20-POLY1305` are used by default.
The AES-GCM family includes `AES-192-GCM`. Retained ciphers include the CBC,
CFB, and OFB forms of AES, ARIA, Camellia, DES, Blowfish, and CAST5, the CBC,
CFB, and OFB forms of SEED and SM4, and `NONE`. CFB and OFB are available only
in TLS mode. Legacy ciphers provide weaker or no confidentiality and are not
enabled by default.
### data_ciphers_fallback ### data_ciphers_fallback
Data channel cipher for peers that do not support cipher negotiation. Data channel cipher for peers that do not support cipher negotiation.
Disabled by default. Disabled by default.
Only available in TLS mode.
### auth ### auth
OpenVPN data channel authentication digest. OpenVPN data channel authentication digest.
`SHA1` is used by default. It only applies to non-AEAD data ciphers and `tls_auth`. `SHA1` is used by default. It only applies to non-AEAD data ciphers and `tls_auth`.
Legacy digests including `MD5` and `RIPEMD160` remain available when explicitly
configured for compatibility.
### mss_fix ### mss_fix
Maximum OpenVPN UDP packet size used to clamp the MSS of TCP connections sent through the tunnel. Maximum OpenVPN UDP packet size used to clamp the MSS of TCP connections sent through the tunnel.
@@ -368,6 +489,20 @@ This prevents TCP packets from exceeding the path MTU after OpenVPN encapsulatio
When empty, the upstream OpenVPN default is used: `fragment` when configured, When empty, the upstream OpenVPN default is used: `fragment` when configured,
otherwise `1492` for the default tunnel MTU or the configured tunnel MTU. otherwise `1492` for the default tunnel MTU or the configured tunnel MTU.
### mss_fix_disabled
Disable MSS clamping, including the default clamp.
Conflict with `mss_fix` and `mss_fix_mode`.
### mss_fix_mode
OpenVPN MSS calculation mode for an explicit `mss_fix`, one of `mtu` or `fixed`.
An empty value uses the normal OpenVPN encapsulation-aware calculation. `mtu` also accounts for the outer IP and UDP/TCP transport headers. `fixed` treats `mss_fix` as an inner IPv4 packet size.
Requires `mss_fix`.
### fragment ### fragment
Maximum OpenVPN UDP packet size used for OpenVPN data channel fragmentation. Maximum OpenVPN UDP packet size used for OpenVPN data channel fragmentation.
@@ -376,6 +511,18 @@ Disabled when `0`. A non-zero value must be at least `68`.
Conflict with TCP transport. Conflict with TCP transport.
### replay_window
UDP data-channel replay window size. `64` is used by default. The maximum is `65536`.
TCP always requires strictly consecutive packet IDs.
### replay_window_time
UDP data-channel replay window duration. `15s` is used by default and the maximum is `10m`.
The value must use whole seconds.
### compression ### compression
OpenVPN `compress` framing mode, one of `none`, `no`, `lz4`, `lz4-v2`, `stub`, `stub-v2`, `disabled`, or `off`. OpenVPN `compress` framing mode, one of `none`, `no`, `lz4`, `lz4-v2`, `stub`, `stub-v2`, `disabled`, or `off`.
@@ -396,7 +543,7 @@ Compression can weaken traffic confidentiality. Enable it only when required by
Policy for compression pushed by the server, one of `no`, `asym`, or `yes`. Policy for compression pushed by the server, one of `no`, `asym`, or `yes`.
`no` is used by default and permits only compression stub framing. `asym` accepts compressed packets from the server but does not compress outgoing packets. `yes` permits compression in both directions. `no` is used by default and permits only compression stub framing. `asym` accepts compressed packets from the server but does not compress outgoing packets. For OpenVPN 2.7 compatibility, `yes` is accepted as a legacy alias for `asym`; the client never sends compressed packets.
Conflict with non-stub compression enabled by `compression` or `compression_lzo` when set to `no`. Conflict with non-stub compression enabled by `compression` or `compression_lzo` when set to `no`.
@@ -433,36 +580,66 @@ For example, `route ` matches pushed IPv4 route options without matching `route-
### routes ### routes
IPv4 and IPv6 route prefixes routed through the OpenVPN endpoint. IPv4 and IPv6 prefixes preferred by sing-box routing for this OpenVPN endpoint.
These routes are used in addition to routes accepted from the server. These routes are used in addition to routes accepted from the server.
They do not install operating-system routes. Select the endpoint through
sing-box route rules or its preferred-route behavior.
### route_gateway ### route_gateway
IPv4 gateway for routes through the OpenVPN endpoint. IPv4 gateway for routes through the OpenVPN endpoint.
When empty, the VPN gateway received from the server is used. When empty, the VPN gateway received from the server is used.
The value is retained for OpenVPN configuration compatibility; endpoint route
preference is prefix-based and does not install a system gateway route.
### route_metric ### route_metric
Default metric for routes through the OpenVPN endpoint. Default metric for routes through the OpenVPN endpoint.
The platform default is used when `0`. The platform default is used when `0`.
The value is retained for OpenVPN configuration compatibility and does not
install a system route.
### redirect_gateway ### redirect_gateway
Route all IPv4 traffic through the OpenVPN endpoint. Prefer the OpenVPN endpoint for all IPv4 destinations in sing-box routing.
Disabled by default. Disabled by default.
This does not install an operating-system default route.
### redirect_gateway_flags ### redirect_gateway_flags
OpenVPN `redirect-gateway` flags. OpenVPN `redirect-gateway` flags.
`!ipv4` disables the IPv4 default route, and `ipv6` also routes all IPv6 traffic through the endpoint. Other OpenVPN flags are accepted for compatibility but do not change endpoint routing. `!ipv4` disables IPv4 preference, `def1` represents it with two `/1`
prefixes, and `ipv6` also prefers the upstream-specific IPv6 prefixes. The
OpenVPN control connection always uses its configured outbound dialer rather
than endpoint routes, so `local` and `autolocal` require no system-route
exception. `bypass-dhcp` and `bypass-dns` are not applicable because sing-box
does not install pushed DHCP or DNS settings into the operating system.
`block-local` is unsupported because the endpoint has no cross-platform source
for the physical default gateway needed to preserve the gateway exception.
Empty by default. Empty by default.
### redirect_private
Accept `redirect_gateway_flags` without adding a default-route preference. Routes pushed or configured separately still affect the endpoint's preferred addresses, but no operating-system routes are installed.
Disabled by default.
### block_ipv6
Reject IPv6 traffic locally instead of sending it through the VPN.
Disabled by default.
### ping_interval ### ping_interval
Interval after which the client sends a data-channel ping when no packet has been sent to the server. Interval after which the client sends a data-channel ping when no packet has been sent to the server.
@@ -484,12 +661,40 @@ The value must use whole seconds.
When empty, `120s` is used for UDP connections with pull enabled until the 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. server pushes another value. No default receive timeout is used for TCP.
### ping_restart_disabled
Disable the initial `120s` UDP pull timeout and any locally configured ping restart timeout.
Conflict with `ping_restart`.
### renegotiate_interval ### renegotiate_interval
OpenVPN TLS renegotiation interval. OpenVPN TLS renegotiation interval.
When empty, the OpenVPN default `1h` is used. When empty, the OpenVPN default `1h` is used.
### renegotiate_disabled
Disable time-based TLS renegotiation, including the default interval.
Conflict with `renegotiate_interval`.
### renegotiate_bytes
Renegotiate data-channel keys after this many bytes. `0` uses the cipher-dependent OpenVPN default.
### renegotiate_packets
Renegotiate data-channel keys after this many packets. `0` uses the cipher-dependent OpenVPN default.
### tls_timeout
Initial retransmission timeout for TLS control packets. The OpenVPN default `2s` is used when empty.
### handshake_window
Maximum time allowed for the initial TLS handshake and each renegotiation. The OpenVPN default `1m` is used when empty.
### explicit_exit_notify ### explicit_exit_notify
Number of OpenVPN exit notifications sent when closing a UDP connection. Number of OpenVPN exit notifications sent when closing a UDP connection.
@@ -502,6 +707,9 @@ Use a system interface.
Requires privilege and cannot conflict with existing system interfaces. Requires privilege and cannot conflict with existing system interfaces.
The endpoint configures interface addresses and MTU but does not install
operating-system routes or DNS settings.
If disabled, sing-box uses the internal network stack. If disabled, sing-box uses the internal network stack.
### name ### name
@@ -9,6 +9,7 @@
"type": "openvpn-client", "type": "openvpn-client",
"tag": "ovpn-client", "tag": "ovpn-client",
"mode": "tls",
"server": "127.0.0.1", "server": "127.0.0.1",
"server_port": 1194, "server_port": 1194,
"servers": [ "servers": [
@@ -20,11 +21,18 @@
], ],
"remote_random": false, "remote_random": false,
"network": "udp", "network": "udp",
"address": [],
"peer_address": "",
"peer_address_ipv6": "",
"topology": "",
"username": "", "username": "",
"password": "", "password": "",
"auth_retry": "none", "auth_retry": "none",
"static_challenge": "", "static_challenge": "",
"static_challenge_echo": false, "static_challenge_echo": false,
"static_key": [],
"static_key_path": "",
"key_direction": "",
"tls": { "tls": {
"server_name": "", "server_name": "",
"server_name_type": "name", "server_name_type": "name",
@@ -40,6 +48,7 @@
"remote_certificate_eku": "", "remote_certificate_eku": "",
"remote_certificate_tls": "", "remote_certificate_tls": "",
"certificate_profile": "", "certificate_profile": "",
"ns_certificate_type": "",
"version_min": "1.2", "version_min": "1.2",
"version_max": "", "version_max": "",
"cipher": "", "cipher": "",
@@ -51,11 +60,16 @@
"direction": "" "direction": ""
} }
}, },
"cipher": "",
"data_ciphers": [], "data_ciphers": [],
"data_ciphers_fallback": "", "data_ciphers_fallback": "",
"auth": "", "auth": "",
"mss_fix": 0, "mss_fix": 0,
"mss_fix_disabled": false,
"mss_fix_mode": "",
"fragment": 0, "fragment": 0,
"replay_window": 0,
"replay_window_time": "",
"compression": "", "compression": "",
"compression_lzo": "", "compression_lzo": "",
"allow_compression": "no", "allow_compression": "no",
@@ -71,9 +85,17 @@
"route_metric": 0, "route_metric": 0,
"redirect_gateway": false, "redirect_gateway": false,
"redirect_gateway_flags": [], "redirect_gateway_flags": [],
"redirect_private": false,
"block_ipv6": false,
"ping_interval": "", "ping_interval": "",
"ping_restart": "", "ping_restart": "",
"ping_restart_disabled": false,
"renegotiate_interval": "", "renegotiate_interval": "",
"renegotiate_disabled": false,
"renegotiate_bytes": 0,
"renegotiate_packets": 0,
"tls_timeout": "",
"handshake_window": "",
"explicit_exit_notify": 0, "explicit_exit_notify": 0,
"system": false, "system": false,
"name": "", "name": "",
@@ -91,6 +113,16 @@
## 字段 ## 字段
### mode
OpenVPN 会话模式,可选值为 `tls``static_key`
默认使用 `tls`
`static_key` 是已弃用的 OpenVPN 模式,不使用 TLS 控制通道且不提供前向保密。
为兼容无法修改的企业 VPN 服务器,此模式仍作为显式兼容选项保留。该模式不使用
`tls`、用户名/密码认证、拉取选项或 TLS 重协商选项。
### server ### server
OpenVPN 服务器地址。 OpenVPN 服务器地址。
@@ -147,10 +179,36 @@ OpenVPN 服务器端口。
该值应用于 `server` 和未单独设置 `network``servers` 条目。 该值应用于 `server` 和未单独设置 `network``servers` 条目。
### address
本地 IPv4 和 IPv6 隧道前缀。
`static_key` 模式至少需要一个地址。在 TLS 模式下该字段可选,并可被服务器推送的地址替换。
### peer_address
IPv4 隧道对端地址及 VPN 网关。
`static_key` 模式下配置 IPv4 `address` 时必填。
### peer_address_ipv6
IPv6 隧道对端地址及 VPN 网关。
`static_key` 模式下配置 IPv6 `address` 时必填。
### topology
隧道拓扑,可选值为 `net30``p2p``subnet`
TLS 模式下为空时使用服务器推送的拓扑。
### username ### username
OpenVPN 用户名/密码认证的用户名。 OpenVPN 用户名/密码认证的用户名。
仅在 TLS 模式下可用。
### password ### password
OpenVPN 用户名/密码认证的密码。 OpenVPN 用户名/密码认证的密码。
@@ -171,9 +229,31 @@ OpenVPN 用户名/密码认证的密码。
以明文显示静态质询响应。 以明文显示静态质询响应。
### static_key
OpenVPN 静态密钥内容。
`static_key` 模式下必填。
`static_key_path` 冲突。
### static_key_path
OpenVPN 静态密钥路径。
`static_key` 模式下未设置 `static_key` 时必填。
`static_key` 冲突。
### key_direction
静态密钥方向,可选值为 `server``client`
为空时双向使用密钥。仅在 `static_key` 模式下可用。
### tls ### tls
==必填== 在 TLS 模式下必填。
OpenVPN 控制通道 TLS 配置。 OpenVPN 控制通道 TLS 配置。
@@ -283,6 +363,16 @@ OpenVPN 控制通道 TLS 配置。
默认使用 `legacy` 默认使用 `legacy`
`insecure` 为兼容不可变对端而接受使用 MD5 或 SHA-1 签名的证书链和较小的旧密钥,仅应在对端无法升级时使用。`legacy` 接受 SHA-1 但拒绝 MD5 签名;`preferred` 要求更强的签名和密钥。
选择 `suiteb``tls.cipher` 为空时,TLS 1.2 cipher 列表默认使用 Suite B ECDHE-ECDSA AES-GCM 套件。该 profile 不限制显式配置的 `tls.cipher``tls.groups`
### tls.ns_certificate_type
已弃用的 Netscape 证书类型检查,`server``client` 之一。
默认禁用。请优先使用 `tls.remote_certificate_tls`
### tls.version_min ### tls.version_min
最低 TLS 版本,可选值为 `1.0``1.1``1.2``1.3` 最低 TLS 版本,可选值为 `1.0``1.1``1.2``1.3`
@@ -341,24 +431,43 @@ OpenVPN 控制通道封装。
仅当 `tls.control_wrap.type``tls_auth` 时可用。为空时双向使用密钥。 仅当 `tls.control_wrap.type``tls_auth` 时可用。为空时双向使用密钥。
### cipher
`static_key` 模式使用的数据通道 cipher。
为空时使用上游静态密钥模式的默认值 `BF-CBC``BF-CBC` 是采用 64 位 block size
的旧 cipher;应尽可能显式配置服务器要求的 cipher。静态密钥 cipher 包括
`BF-CBC``CAST5-CBC``DES-CBC``DES-EDE-CBC``DES-EDE3-CBC`
AES-CBC、ARIA-CBC、Camellia-CBC 系列,以及 `SEED-CBC``SM4-CBC``NONE`
仅在 `static_key` 模式下可用。`NONE` 不提供机密性。
### data_ciphers ### data_ciphers
允许的 OpenVPN 数据通道 cipher。 允许的 OpenVPN 数据通道 cipher。
仅在 TLS 模式下可用。
默认使用 `AES-256-GCM``AES-128-GCM``CHACHA20-POLY1305` 默认使用 `AES-256-GCM``AES-128-GCM``CHACHA20-POLY1305`
AES-GCM 系列还包括 `AES-192-GCM`。保留的 cipher 包括 AES、ARIA、Camellia、DES、Blowfish、CAST5、SEED 和 SM4 的 CBC、CFB、OFB 形式,以及 `NONE`。CFB 和 OFB 仅可用于 TLS 模式。旧 cipher 只能提供较弱的机密性或完全不加密,因此默认不启用。
### data_ciphers_fallback ### data_ciphers_fallback
用于不支持 cipher 协商的对端的数据通道 cipher。 用于不支持 cipher 协商的对端的数据通道 cipher。
默认禁用。 默认禁用。
仅在 TLS 模式下可用。
### auth ### auth
OpenVPN 数据通道认证摘要。 OpenVPN 数据通道认证摘要。
默认使用 `SHA1`,仅应用于非 AEAD 数据 cipher 和 `tls_auth` 默认使用 `SHA1`,仅应用于非 AEAD 数据 cipher 和 `tls_auth`
为兼容既有服务器,显式配置时仍可使用 `MD5``RIPEMD160` 等旧摘要。
### mss_fix ### mss_fix
OpenVPN UDP packet 的最大大小,用于限制通过隧道发送的 TCP 连接 MSS。 OpenVPN UDP packet 的最大大小,用于限制通过隧道发送的 TCP 连接 MSS。
@@ -367,6 +476,20 @@ OpenVPN UDP packet 的最大大小,用于限制通过隧道发送的 TCP 连
为空时使用上游 OpenVPN 默认值:配置了 `fragment` 时使用其值;否则默认 tunnel MTU 使用 `1492`,自定义 tunnel MTU 使用该 MTU。 为空时使用上游 OpenVPN 默认值:配置了 `fragment` 时使用其值;否则默认 tunnel MTU 使用 `1492`,自定义 tunnel MTU 使用该 MTU。
### mss_fix_disabled
禁用 MSS 限制,包括默认限制。
`mss_fix``mss_fix_mode` 冲突。
### mss_fix_mode
显式 `mss_fix` 的 OpenVPN MSS 计算模式,`mtu``fixed` 之一。
空值使用普通的 OpenVPN 封装开销计算。`mtu` 还会计算外层 IP 和 UDP/TCP 传输头;`fixed``mss_fix` 视为内层 IPv4 数据包大小。
需要 `mss_fix`
### fragment ### fragment
用于 OpenVPN 数据通道 fragmentation 的最大 OpenVPN UDP packet 大小。 用于 OpenVPN 数据通道 fragmentation 的最大 OpenVPN UDP packet 大小。
@@ -375,6 +498,18 @@ OpenVPN UDP packet 的最大大小,用于限制通过隧道发送的 TCP 连
与 TCP 传输冲突。 与 TCP 传输冲突。
### replay_window
UDP 数据通道重放窗口大小。默认使用 `64`,最大值为 `65536`
TCP 始终要求数据包 ID 严格连续。
### replay_window_time
UDP 数据通道重放窗口时长。默认使用 `15s`,最大值为 `10m`
该值必须使用整秒。
### compression ### compression
OpenVPN `compress` framing 模式,可选值为 `none``no``lz4``lz4-v2``stub``stub-v2``disabled``off` OpenVPN `compress` framing 模式,可选值为 `none``no``lz4``lz4-v2``stub``stub-v2``disabled``off`
@@ -395,7 +530,7 @@ Compression 可能削弱流量机密性。仅在服务器要求时启用。
服务器推送的 compression 策略,可选值为 `no``asym``yes` 服务器推送的 compression 策略,可选值为 `no``asym``yes`
默认使用 `no`,仅允许 compression stub framing。`asym` 接受来自服务器的 compressed packet,但不压缩出站 packet。`yes` 允许双向 compression 默认使用 `no`,仅允许 compression stub framing。`asym` 接受来自服务器的 compressed packet,但不压缩出站 packet。为兼容 OpenVPN 2.7`yes` 作为 `asym` 的旧别名接受;客户端绝不会发送 compressed packet
当设为 `no` 时,与通过 `compression``compression_lzo` 启用的非 stub compression 冲突。 当设为 `no` 时,与通过 `compression``compression_lzo` 启用的非 stub compression 冲突。
@@ -432,36 +567,56 @@ Filter action,可选值为 `accept`、`ignore` 或 `reject`。
### routes ### routes
通过 OpenVPN endpoint 路由的 IPv4 和 IPv6 route prefix sing-box 路由优先选择此 OpenVPN endpoint 的 IPv4 和 IPv6 前缀
这些 route 会与从服务器接受的 route 一起使用。 这些 route 会与从服务器接受的 route 一起使用。
它们不会安装操作系统路由。请通过 sing-box 路由规则或 endpoint 的首选路由行为选择此 endpoint。
### route_gateway ### route_gateway
通过 OpenVPN endpoint 路由的 IPv4 gateway。 通过 OpenVPN endpoint 路由的 IPv4 gateway。
为空时使用从服务器接收的 VPN gateway。 为空时使用从服务器接收的 VPN gateway。
该值仅为兼容 OpenVPN 配置而保留;endpoint 的路由偏好只按前缀判断,不会安装系统 gateway 路由。
### route_metric ### route_metric
通过 OpenVPN endpoint 路由的默认 metric。 通过 OpenVPN endpoint 路由的默认 metric。
设为 `0` 时使用平台默认值。 设为 `0` 时使用平台默认值。
该值仅为兼容 OpenVPN 配置而保留,不会安装系统路由。
### redirect_gateway ### redirect_gateway
通过 OpenVPN endpoint 路由所有 IPv4 流量 在 sing-box 路由中对所有 IPv4 目的地优先选择 OpenVPN endpoint。
默认禁用。 默认禁用。
这不会安装操作系统默认路由。
### redirect_gateway_flags ### redirect_gateway_flags
OpenVPN `redirect-gateway` flag。 OpenVPN `redirect-gateway` flag。
`!ipv4` 禁用 IPv4 default route`ipv6` 还会通过 endpoint 路由所有 IPv6 流量。接受其他 OpenVPN flag 以兼容配置,但它们不会改变 endpoint 路由 `!ipv4` 禁用 IPv4 偏好,`def1` 使用两个 `/1` 前缀表示,`ipv6` 还会优先选择上游特定的 IPv6 前缀。OpenVPN 控制连接始终使用其配置的出站拨号器,不经过 endpoint 路由,因此 `local``autolocal` 不需要系统路由例外。由于 sing-box 不会把推送的 DHCP 或 DNS 设置安装到操作系统,`bypass-dhcp``bypass-dns` 不适用。`block-local` 不受支持,因为 endpoint 没有可跨平台获取物理默认网关的来源,无法保留网关例外
默认为空。 默认为空。
### redirect_private
接受 `redirect_gateway_flags`,但不添加默认路由偏好。单独推送或配置的路由仍会影响 endpoint 的首选地址,但不会安装操作系统路由。
默认禁用。
### block_ipv6
在本地拒绝 IPv6 流量,而不是通过 VPN 发送。
默认禁用。
### ping_interval ### ping_interval
客户端未向服务器发送任何 packet 时,发送 data channel ping 的间隔。 客户端未向服务器发送任何 packet 时,发送 data channel ping 的间隔。
@@ -482,12 +637,40 @@ OpenVPN `redirect-gateway` flag。
为空时,启用了 pull 的 UDP 连接会使用 `120s`,直到服务器推送其他值。TCP 不使用默认接收超时。 为空时,启用了 pull 的 UDP 连接会使用 `120s`,直到服务器推送其他值。TCP 不使用默认接收超时。
### ping_restart_disabled
禁用初始 `120s` UDP 拉取超时和本地配置的 ping 重启超时。
`ping_restart` 冲突。
### renegotiate_interval ### renegotiate_interval
OpenVPN TLS 重新协商间隔。 OpenVPN TLS 重新协商间隔。
为空时使用 OpenVPN 默认值 `1h` 为空时使用 OpenVPN 默认值 `1h`
### renegotiate_disabled
禁用基于时间的 TLS 重新协商,包括默认间隔。
`renegotiate_interval` 冲突。
### renegotiate_bytes
传输指定字节数后重新协商数据通道密钥。`0` 使用与密码算法相关的 OpenVPN 默认值。
### renegotiate_packets
传输指定数据包数后重新协商数据通道密钥。`0` 使用与密码算法相关的 OpenVPN 默认值。
### tls_timeout
TLS 控制数据包的初始重传超时。为空时使用 OpenVPN 默认值 `2s`
### handshake_window
初始 TLS 握手及每次重新协商的最长允许时间。为空时使用 OpenVPN 默认值 `1m`
### explicit_exit_notify ### explicit_exit_notify
关闭 UDP 连接时发送的 OpenVPN exit notification 数量。 关闭 UDP 连接时发送的 OpenVPN exit notification 数量。
@@ -500,6 +683,8 @@ Notification 之间间隔一秒。设为 `0` 时禁用。
需要权限,且不能与现有系统接口冲突。 需要权限,且不能与现有系统接口冲突。
endpoint 会配置接口地址和 MTU,但不会安装操作系统路由或 DNS 设置。
禁用时,sing-box 使用内部网络栈。 禁用时,sing-box 使用内部网络栈。
### name ### name
+248 -4
View File
@@ -14,9 +14,14 @@
"system": false, "system": false,
"name": "", "name": "",
"mtu": 1500, "mtu": 1500,
"mode": "tls",
"network": "udp", "network": "udp",
"remote": "",
"remote_port": 0,
"max_clients": 1024, "max_clients": 1024,
"address": [], "address": [],
"peer_address": "",
"peer_address_ipv6": "",
"topology": "subnet", "topology": "subnet",
"duplicate_cn": false, "duplicate_cn": false,
"users": [ "users": [
@@ -25,6 +30,9 @@
"password": "" "password": ""
} }
], ],
"static_key": [],
"static_key_path": "",
"key_direction": "",
"tls": { "tls": {
"certificate": [], "certificate": [],
"certificate_path": "", "certificate_path": "",
@@ -33,7 +41,19 @@
"client_certificate": [], "client_certificate": [],
"client_certificate_path": "", "client_certificate_path": "",
"verify_client_certificate": "require", "verify_client_certificate": "require",
"client_name": "",
"client_name_type": "name",
"peer_fingerprint": [],
"crl_path": "",
"remote_certificate_ku": [],
"remote_certificate_eku": "",
"remote_certificate_tls": "",
"certificate_profile": "", "certificate_profile": "",
"ns_certificate_type": "",
"version_min": "1.2",
"version_max": "",
"cipher": "",
"groups": "",
"control_wrap": { "control_wrap": {
"type": "tls_crypt", "type": "tls_crypt",
"key": [], "key": [],
@@ -42,12 +62,21 @@
"force_cookie": false "force_cookie": false
} }
}, },
"cipher": "",
"data_ciphers": [], "data_ciphers": [],
"data_ciphers_fallback": "", "data_ciphers_fallback": "",
"auth": "", "auth": "",
"mss_fix": 0,
"mss_fix_disabled": false,
"mss_fix_mode": "",
"replay_window": 0,
"replay_window_time": "",
"push": { "push": {
"routes": [], "routes": [],
"dns": [], "dns": [],
"dns_servers": [],
"search_domains": [],
"dhcp_options": [],
"redirect_gateway": false, "redirect_gateway": false,
"redirect_gateway_flags": [], "redirect_gateway_flags": [],
"block_outside_dns": false, "block_outside_dns": false,
@@ -57,6 +86,9 @@
"ping_interval": "", "ping_interval": "",
"ping_restart": "", "ping_restart": "",
"renegotiate_interval": "", "renegotiate_interval": "",
"renegotiate_disabled": false,
"renegotiate_bytes": 0,
"renegotiate_packets": 0,
"handshake_window": "1m", "handshake_window": "1m",
... // UDP NAT Fields ... // UDP NAT Fields
@@ -79,6 +111,9 @@ Use system interface.
Requires privilege and cannot conflict with existing system interfaces. Requires privilege and cannot conflict with existing system interfaces.
The endpoint configures interface addresses and MTU but does not install
operating-system routes or DNS settings.
If disabled, sing-box uses the internal network stack. If disabled, sing-box uses the internal network stack.
### name ### name
@@ -93,6 +128,16 @@ OpenVPN interface MTU.
`1500` will be used by default. `1500` will be used by default.
### mode
OpenVPN session mode, one of `tls` or `static_key`.
`tls` is used by default.
`static_key` serves one peer without a TLS control channel or forward secrecy.
It is retained as an explicit compatibility option for immutable deployments.
It does not use `tls`, `users`, push options, or TLS renegotiation options.
### network ### network
OpenVPN transport network, one of `udp` or `tcp`. OpenVPN transport network, one of `udp` or `tcp`.
@@ -103,12 +148,27 @@ Only one transport network is served per endpoint; to serve both TCP and UDP,
configure two endpoints with separate `address` subnets, configure two endpoints with separate `address` subnets,
matching upstream OpenVPN which requires two server processes. matching upstream OpenVPN which requires two server processes.
### remote
Fixed remote peer address for a UDP `static_key` server.
Required with `remote_port` in UDP `static_key` mode. TCP servers accept the
single peer from the listening socket and do not use this field.
### remote_port
Fixed remote peer port for a UDP `static_key` server.
Required with `remote` in UDP `static_key` mode.
### max_clients ### max_clients
Maximum number of established and pending TLS client sessions. Maximum number of established and pending TLS client sessions.
`1024` is used by default. The value must be smaller than `16777216`, the size of the OpenVPN peer-id space. `1024` is used by default. The value must be smaller than `16777216`, the size of the OpenVPN peer-id space.
`static_key` mode supports one peer, so this value must be `0` or `1`.
### address ### address
==Required== ==Required==
@@ -121,11 +181,26 @@ The prefix address is assigned to the server interface. The masked prefix is use
The first IPv4 and IPv6 prefix addresses are used as the endpoint's local addresses. The first IPv4 and IPv6 prefix addresses are used as the endpoint's local addresses.
In `static_key` mode these are the local tunnel prefixes rather than address pools.
### peer_address
IPv4 tunnel peer address.
Required when an IPv4 `address` is configured in `static_key` mode.
### peer_address_ipv6
IPv6 tunnel peer address.
Required when an IPv6 `address` is configured in `static_key` mode.
### topology ### topology
OpenVPN topology pushed to clients, one of `subnet`, `p2p` or `net30`. OpenVPN topology pushed to clients, one of `subnet`, `p2p` or `net30`.
`subnet` will be used by default. `subnet` is used by default in TLS mode. `p2p` is used by default in
`static_key` mode.
### duplicate_cn ### duplicate_cn
@@ -135,12 +210,16 @@ When disabled, a newly authenticated session replaces the existing session with
Disabled by default. Disabled by default.
Only available in TLS mode.
### users ### users
List of OpenVPN username/password users. List of OpenVPN username/password users.
If set, clients must pass username/password authentication in addition to any certificate policy configured by `tls.verify_client_certificate`. If set, clients must pass username/password authentication in addition to any certificate policy configured by `tls.verify_client_certificate`.
Only available in TLS mode.
### users.username ### users.username
Username. Username.
@@ -149,9 +228,34 @@ Username.
Password. Password.
### static_key
OpenVPN static key content.
Required in `static_key` mode.
Conflict with `static_key_path`.
### static_key_path
OpenVPN static key path.
Required in `static_key` mode when `static_key` is not set.
Conflict with `static_key`.
### key_direction
Static key direction, one of `server` or `client`.
The key is used bidirectionally if empty. Conventionally the server uses
`server` and the peer uses `client`.
Only available in `static_key` mode.
### tls ### tls
==Required== Required in TLS mode.
OpenVPN control channel TLS configuration. OpenVPN control channel TLS configuration.
@@ -191,7 +295,7 @@ Conflict with `tls.key`.
TLS CA certificate content, used to verify client certificates. TLS CA certificate content, used to verify client certificates.
Either `tls.client_certificate` or `tls.client_certificate_path` is required. One of `tls.client_certificate`, `tls.client_certificate_path`, or `tls.peer_fingerprint` is required when `tls.verify_client_certificate` is `require` or `optional`.
Conflict with `tls.client_certificate_path`. Conflict with `tls.client_certificate_path`.
@@ -199,7 +303,7 @@ Conflict with `tls.client_certificate_path`.
TLS CA certificate path, used to verify client certificates. TLS CA certificate path, used to verify client certificates.
Either `tls.client_certificate` or `tls.client_certificate_path` is required. One of `tls.client_certificate`, `tls.client_certificate_path`, or `tls.peer_fingerprint` is required when `tls.verify_client_certificate` is `require` or `optional`.
Conflict with `tls.client_certificate`. Conflict with `tls.client_certificate`.
@@ -215,12 +319,71 @@ If set to `none`, client certificates are not requested.
This field does not replace `users`; when `users` is set, username/password authentication is still required. This field does not replace `users`; when `users` is set, username/password authentication is still required.
### tls.client_name
Expected client certificate name. Disabled when empty.
### tls.client_name_type
Certificate field matched by `tls.client_name`, one of `subject`, `name`, or `name-prefix`.
`name` is used by default when `tls.client_name` is configured.
### tls.peer_fingerprint
Allowed SHA-256 fingerprints of client leaf certificates. Fingerprint-only verification can be used without a client CA.
### tls.crl_path
Path to a certificate revocation list used to reject revoked client certificates.
### tls.remote_certificate_ku
Required client certificate key usage masks in OpenVPN `remote-cert-ku` format.
### tls.remote_certificate_eku
Required client certificate extended key usage. Conflict with an explicitly configured `tls.remote_certificate_tls`.
### tls.remote_certificate_tls
Client certificate purpose check, one of `server`, `client`, or `none`. `client` is used by default.
### tls.certificate_profile ### tls.certificate_profile
Certificate profile, one of `insecure`, `legacy`, `preferred`, or `suiteb`. Certificate profile, one of `insecure`, `legacy`, `preferred`, or `suiteb`.
`legacy` is used by default. `legacy` is used by default.
`insecure` accepts MD5- and SHA-1-signed certificate chains and smaller legacy
keys for compatibility with immutable peers. Use it only when the peer cannot
be upgraded. `legacy` accepts SHA-1 but rejects MD5 signatures; `preferred`
requires stronger signatures and keys.
When `suiteb` is selected and `tls.cipher` is empty, the TLS 1.2 cipher list defaults to the Suite B ECDHE-ECDSA AES-GCM suites. Explicit `tls.cipher` and `tls.groups` values are not restricted by the profile.
### tls.ns_certificate_type
Deprecated Netscape certificate type check, one of `server` or `client`.
### tls.version_min
Minimum TLS version. `1.2` is used by default.
### tls.version_max
Maximum TLS version. The maximum supported version is used by default.
### tls.cipher
Colon-separated OpenSSL cipher suite names allowed for TLS 1.2 and earlier.
The default TLS cipher suites are used when empty. TLS 1.3 cipher suites are not controlled by this field.
### tls.groups
Colon-separated TLS key exchange groups in preference order.
### tls.control_wrap ### tls.control_wrap
OpenVPN control channel wrapping. OpenVPN control channel wrapping.
@@ -272,12 +435,30 @@ clients without cookie support are accepted using the upstream `allow-noncookie`
Disabled by default. Disabled by default.
### cipher
Data-channel cipher used in `static_key` mode.
The upstream static-key default `BF-CBC` is used when empty. Supported
static-key ciphers are the AES-CBC, ARIA-CBC, Camellia-CBC, DES-CBC,
Blowfish-CBC, CAST5-CBC families, `SEED-CBC`, `SM4-CBC`, and `NONE`.
Only available in `static_key` mode. `NONE` provides no confidentiality.
### data_ciphers ### data_ciphers
Allowed OpenVPN data channel ciphers. Allowed OpenVPN data channel ciphers.
`AES-256-GCM`, `AES-128-GCM` and `CHACHA20-POLY1305` are used by default. `AES-256-GCM`, `AES-128-GCM` and `CHACHA20-POLY1305` are used by default.
The AES-GCM family includes `AES-192-GCM`. Retained ciphers include the CBC,
CFB, and OFB forms of AES, ARIA, Camellia, DES, Blowfish, and CAST5, the CBC,
CFB, and OFB forms of SEED and SM4, and `NONE`. CFB and OFB are available only
in TLS mode. Legacy ciphers provide weaker or no confidentiality and are not
enabled by default.
Only available in TLS mode.
### data_ciphers_fallback ### data_ciphers_fallback
OpenVPN data channel cipher for legacy clients that do not support cipher negotiation. OpenVPN data channel cipher for legacy clients that do not support cipher negotiation.
@@ -286,12 +467,37 @@ Equivalent to OpenVPN `data-ciphers-fallback`.
Disabled by default. Disabled by default.
Only available in TLS mode.
### auth ### auth
OpenVPN data channel authentication digest. OpenVPN data channel authentication digest.
`SHA1` will be used by default, matching the upstream default; it only applies to non-AEAD data ciphers and `tls_auth`. `SHA1` will be used by default, matching the upstream default; it only applies to non-AEAD data ciphers and `tls_auth`.
Legacy digests including `MD5` and `RIPEMD160` remain available when explicitly
configured for compatibility.
### mss_fix
Maximum encapsulated packet size used to clamp TCP MSS. The upstream default calculation uses `1492` with the default MTU.
### mss_fix_disabled
Disable MSS clamping, including the default clamp.
### mss_fix_mode
Calculation mode for an explicit `mss_fix`, one of `mtu` or `fixed`. Requires `mss_fix`.
### replay_window
UDP data-channel replay window size. `64` is used by default; TCP packet IDs remain strictly consecutive.
### replay_window_time
UDP replay window duration. `15s` is used by default. The value must use whole seconds.
### push ### push
Options pushed to clients. Options pushed to clients.
@@ -306,6 +512,22 @@ IPv4 and IPv6 prefixes can be mixed.
DNS server addresses to push to clients. DNS server addresses to push to clients.
Uses legacy `dhcp-option DNS`/`DNS6`. A pushed modern DNS server group overrides these addresses on compatible clients.
### push.dns_servers
Modern OpenVPN DNS server groups to push. Each entry contains `priority`, `addresses`, optional `resolve_domains`, `dnssec`, `transport`, and `sni`.
Addresses accept an IP address or `IP:port` (IPv6 ports use `[IPv6]:port`). `transport` is one of `plain`, `dot`, or `doh`; `dnssec` is one of `yes`, `optional`, or `no`. OpenVPN clients apply only the group with the lowest priority number.
### push.search_domains
Modern OpenVPN search domains to push.
### push.dhcp_options
Additional legacy `dhcp-option` values to push, without the `dhcp-option` prefix.
### push.redirect_gateway ### push.redirect_gateway
Push `redirect-gateway` to clients, which routes client traffic through the VPN according to `push.redirect_gateway_flags`. Push `redirect-gateway` to clients, which routes client traffic through the VPN according to `push.redirect_gateway_flags`.
@@ -372,12 +594,34 @@ OpenVPN TLS renegotiation interval.
When empty, the OpenVPN default `1h` is used. When empty, the OpenVPN default `1h` is used.
Only available in TLS mode.
### renegotiate_disabled
Disable time-based TLS renegotiation, including the default interval.
Only available in TLS mode.
### renegotiate_bytes
Renegotiate data-channel keys after this many bytes. `0` uses the cipher-dependent OpenVPN default.
Only available in TLS mode.
### renegotiate_packets
Renegotiate data-channel keys after this many packets. `0` uses the cipher-dependent OpenVPN default.
Only available in TLS mode.
### handshake_window ### handshake_window
Maximum time allowed for the initial TLS handshake and each TLS renegotiation. Maximum time allowed for the initial TLS handshake and each TLS renegotiation.
`1m` is used by default. `1m` is used by default.
Only available in TLS mode.
## UDP NAT Fields ## UDP NAT Fields
These fields configure UDP sessions for traffic through the OpenVPN interface. These fields configure UDP sessions for traffic through the OpenVPN interface.
@@ -14,9 +14,14 @@
"system": false, "system": false,
"name": "", "name": "",
"mtu": 1500, "mtu": 1500,
"mode": "tls",
"network": "udp", "network": "udp",
"remote": "",
"remote_port": 0,
"max_clients": 1024, "max_clients": 1024,
"address": [], "address": [],
"peer_address": "",
"peer_address_ipv6": "",
"topology": "subnet", "topology": "subnet",
"duplicate_cn": false, "duplicate_cn": false,
"users": [ "users": [
@@ -25,6 +30,9 @@
"password": "" "password": ""
} }
], ],
"static_key": [],
"static_key_path": "",
"key_direction": "",
"tls": { "tls": {
"certificate": [], "certificate": [],
"certificate_path": "", "certificate_path": "",
@@ -33,7 +41,19 @@
"client_certificate": [], "client_certificate": [],
"client_certificate_path": "", "client_certificate_path": "",
"verify_client_certificate": "require", "verify_client_certificate": "require",
"client_name": "",
"client_name_type": "name",
"peer_fingerprint": [],
"crl_path": "",
"remote_certificate_ku": [],
"remote_certificate_eku": "",
"remote_certificate_tls": "",
"certificate_profile": "", "certificate_profile": "",
"ns_certificate_type": "",
"version_min": "1.2",
"version_max": "",
"cipher": "",
"groups": "",
"control_wrap": { "control_wrap": {
"type": "tls_crypt", "type": "tls_crypt",
"key": [], "key": [],
@@ -42,12 +62,21 @@
"force_cookie": false "force_cookie": false
} }
}, },
"cipher": "",
"data_ciphers": [], "data_ciphers": [],
"data_ciphers_fallback": "", "data_ciphers_fallback": "",
"auth": "", "auth": "",
"mss_fix": 0,
"mss_fix_disabled": false,
"mss_fix_mode": "",
"replay_window": 0,
"replay_window_time": "",
"push": { "push": {
"routes": [], "routes": [],
"dns": [], "dns": [],
"dns_servers": [],
"search_domains": [],
"dhcp_options": [],
"redirect_gateway": false, "redirect_gateway": false,
"redirect_gateway_flags": [], "redirect_gateway_flags": [],
"block_outside_dns": false, "block_outside_dns": false,
@@ -57,6 +86,9 @@
"ping_interval": "", "ping_interval": "",
"ping_restart": "", "ping_restart": "",
"renegotiate_interval": "", "renegotiate_interval": "",
"renegotiate_disabled": false,
"renegotiate_bytes": 0,
"renegotiate_packets": 0,
"handshake_window": "1m", "handshake_window": "1m",
... // UDP NAT 字段 ... // UDP NAT 字段
@@ -79,6 +111,8 @@
需要特权且不能与已有系统接口冲突。 需要特权且不能与已有系统接口冲突。
endpoint 会配置接口地址和 MTU,但不会安装操作系统路由或 DNS 设置。
如果禁用,sing-box 将使用内部网络栈。 如果禁用,sing-box 将使用内部网络栈。
### name ### name
@@ -93,6 +127,14 @@ OpenVPN 接口 MTU。
默认使用 `1500` 默认使用 `1500`
### mode
OpenVPN 会话模式,`tls``static_key` 之一。
默认使用 `tls`
`static_key` 在没有 TLS 控制信道和前向保密的情况下服务一个对端,仅作为不可变部署的显式兼容选项保留。该模式不使用 `tls``users`、推送选项或 TLS 重协商选项。
### network ### network
OpenVPN 传输网络,`udp``tcp` 之一。 OpenVPN 传输网络,`udp``tcp` 之一。
@@ -103,12 +145,26 @@ OpenVPN 传输网络,`udp` 或 `tcp` 之一。
需要配置两个端点并使用互不重叠的 `address` 子网, 需要配置两个端点并使用互不重叠的 `address` 子网,
与上游 OpenVPN 需要两个服务进程一致。 与上游 OpenVPN 需要两个服务进程一致。
### remote
UDP `static_key` 服务器的固定远端地址。
在 UDP `static_key` 模式下与 `remote_port` 一起必填。TCP 服务器从监听套接字接受单个对端,不使用此字段。
### remote_port
UDP `static_key` 服务器的固定远端端口。
在 UDP `static_key` 模式下与 `remote` 一起必填。
### max_clients ### max_clients
已建立与握手中的 TLS 客户端会话的最大数量。 已建立与握手中的 TLS 客户端会话的最大数量。
默认使用 `1024`。该值必须小于 OpenVPN peer-id 空间的大小 `16777216` 默认使用 `1024`。该值必须小于 OpenVPN peer-id 空间的大小 `16777216`
`static_key` 模式仅支持一个对端,因此此值必须为 `0``1`
### address ### address
==必填== ==必填==
@@ -121,11 +177,25 @@ OpenVPN 服务器地址前缀列表。
第一个 IPv4 和 IPv6 前缀地址用作端点的本地地址。 第一个 IPv4 和 IPv6 前缀地址用作端点的本地地址。
`static_key` 模式下,这些地址是本地隧道前缀,而不是地址池。
### peer_address
IPv4 隧道对端地址。
`static_key` 模式下配置 IPv4 `address` 时必填。
### peer_address_ipv6
IPv6 隧道对端地址。
`static_key` 模式下配置 IPv6 `address` 时必填。
### topology ### topology
推送给客户端的 OpenVPN topology`subnet``p2p``net30` 之一。 推送给客户端的 OpenVPN topology`subnet``p2p``net30` 之一。
默认使用 `subnet` TLS 模式默认使用 `subnet``static_key` 模式默认使用 `p2p`
### duplicate_cn ### duplicate_cn
@@ -135,12 +205,16 @@ OpenVPN 服务器地址前缀列表。
默认禁用。 默认禁用。
仅在 TLS 模式下可用。
### users ### users
OpenVPN 用户名/密码用户列表。 OpenVPN 用户名/密码用户列表。
如果设置,客户端除了通过 `tls.verify_client_certificate` 配置的证书策略外,还必须通过用户名/密码认证。 如果设置,客户端除了通过 `tls.verify_client_certificate` 配置的证书策略外,还必须通过用户名/密码认证。
仅在 TLS 模式下可用。
### users.username ### users.username
用户名。 用户名。
@@ -149,9 +223,33 @@ OpenVPN 用户名/密码用户列表。
密码。 密码。
### static_key
OpenVPN 静态密钥内容。
`static_key` 模式下必填。
`static_key_path` 冲突。
### static_key_path
OpenVPN 静态密钥路径。
`static_key` 模式下未设置 `static_key` 时必填。
`static_key` 冲突。
### key_direction
静态密钥方向,`server``client` 之一。
为空时双向使用密钥。按照惯例,服务器使用 `server`,对端使用 `client`
仅在 `static_key` 模式下可用。
### tls ### tls
==必填== 在 TLS 模式下必填。
OpenVPN 控制信道 TLS 配置。 OpenVPN 控制信道 TLS 配置。
@@ -191,7 +289,7 @@ TLS 服务器私钥路径。
TLS CA 证书内容,用于验证客户端证书。 TLS CA 证书内容,用于验证客户端证书。
`tls.client_certificate` `tls.client_certificate_path` 必填其一。 `tls.verify_client_certificate` `require``optional` 时,`tls.client_certificate``tls.client_certificate_path``tls.peer_fingerprint` 必填其一。
`tls.client_certificate_path` 冲突。 `tls.client_certificate_path` 冲突。
@@ -199,7 +297,7 @@ TLS CA 证书内容,用于验证客户端证书。
TLS CA 证书路径,用于验证客户端证书。 TLS CA 证书路径,用于验证客户端证书。
`tls.client_certificate` `tls.client_certificate_path` 必填其一。 `tls.verify_client_certificate` `require``optional` 时,`tls.client_certificate``tls.client_certificate_path``tls.peer_fingerprint` 必填其一。
`tls.client_certificate` 冲突。 `tls.client_certificate` 冲突。
@@ -215,12 +313,68 @@ OpenVPN 客户端证书策略,`require`、`optional` 或 `none` 之一。
该字段不替代 `users`;设置 `users` 后仍然要求用户名/密码认证。 该字段不替代 `users`;设置 `users` 后仍然要求用户名/密码认证。
### tls.client_name
期望的客户端证书名称。为空时禁用。
### tls.client_name_type
`tls.client_name` 匹配的证书字段,`subject``name``name-prefix` 之一。
配置 `tls.client_name` 时默认使用 `name`
### tls.peer_fingerprint
允许的客户端叶证书 SHA-256 指纹。可以在没有客户端 CA 时仅使用指纹验证。
### tls.crl_path
用于拒绝已吊销客户端证书的证书吊销列表路径。
### tls.remote_certificate_ku
OpenVPN `remote-cert-ku` 格式的客户端证书 Key Usage mask。
### tls.remote_certificate_eku
客户端证书所需的 Extended Key Usage。与显式配置的 `tls.remote_certificate_tls` 冲突。
### tls.remote_certificate_tls
客户端证书用途检查,`server``client``none` 之一。默认使用 `client`
### tls.certificate_profile ### tls.certificate_profile
证书 profile,可选值为 `insecure``legacy``preferred``suiteb` 证书 profile,可选值为 `insecure``legacy``preferred``suiteb`
默认使用 `legacy` 默认使用 `legacy`
`insecure` 为兼容不可变对端而接受使用 MD5 或 SHA-1 签名的证书链和较小的旧密钥,仅应在对端无法升级时使用。`legacy` 接受 SHA-1 但拒绝 MD5 签名;`preferred` 要求更强的签名和密钥。
选择 `suiteb``tls.cipher` 为空时,TLS 1.2 cipher 列表默认使用 Suite B ECDHE-ECDSA AES-GCM 套件。该 profile 不限制显式配置的 `tls.cipher``tls.groups`
### tls.ns_certificate_type
已弃用的 Netscape 证书类型检查,`server``client` 之一。
### tls.version_min
最低 TLS 版本。默认使用 `1.2`
### tls.version_max
最高 TLS 版本。默认使用支持的最高版本。
### tls.cipher
TLS 1.2 及更低版本允许的 OpenSSL cipher suite 名称,以冒号分隔。
为空时使用默认 TLS cipher suite。该字段不控制 TLS 1.3 cipher suite。
### tls.groups
按偏好顺序排列的 TLS key exchange group,以冒号分隔。
### tls.control_wrap ### tls.control_wrap
OpenVPN 控制信道包装。 OpenVPN 控制信道包装。
@@ -271,12 +425,24 @@ OpenVPN `tls-auth` 密钥方向,`server` 或 `client` 之一。
默认禁用。 默认禁用。
### cipher
`static_key` 模式使用的数据信道加密方式。
为空时使用上游静态密钥模式的默认值 `BF-CBC`。支持 AES-CBC、ARIA-CBC、Camellia-CBC、DES-CBC、Blowfish-CBC、CAST5-CBC 系列,以及 `SEED-CBC``SM4-CBC``NONE`
仅在 `static_key` 模式下可用。`NONE` 不提供机密性。
### data_ciphers ### data_ciphers
允许的 OpenVPN 数据信道加密方式。 允许的 OpenVPN 数据信道加密方式。
默认使用 `AES-256-GCM``AES-128-GCM``CHACHA20-POLY1305` 默认使用 `AES-256-GCM``AES-128-GCM``CHACHA20-POLY1305`
AES-GCM 系列还包括 `AES-192-GCM`。保留的 cipher 包括 AES、ARIA、Camellia、DES、Blowfish、CAST5、SEED 和 SM4 的 CBC、CFB、OFB 形式,以及 `NONE`。CFB 和 OFB 仅可用于 TLS 模式。旧 cipher 只能提供较弱的机密性或完全不加密,因此默认不启用。
仅在 TLS 模式下可用。
### data_ciphers_fallback ### data_ciphers_fallback
用于不支持加密方式协商的遗留客户端的 OpenVPN 数据信道加密方式。 用于不支持加密方式协商的遗留客户端的 OpenVPN 数据信道加密方式。
@@ -285,12 +451,36 @@ OpenVPN `tls-auth` 密钥方向,`server` 或 `client` 之一。
默认禁用。 默认禁用。
仅在 TLS 模式下可用。
### auth ### auth
OpenVPN 数据信道认证摘要。 OpenVPN 数据信道认证摘要。
默认使用 `SHA1`,与上游默认值一致;仅对非 AEAD 数据信道加密方式和 `tls_auth` 生效。 默认使用 `SHA1`,与上游默认值一致;仅对非 AEAD 数据信道加密方式和 `tls_auth` 生效。
为兼容既有客户端,显式配置时仍可使用 `MD5``RIPEMD160` 等旧摘要。
### mss_fix
用于限制 TCP MSS 的最大封装数据包大小。默认 MTU 下使用上游默认值 `1492` 计算。
### mss_fix_disabled
禁用 MSS 限制,包括默认限制。
### mss_fix_mode
显式 `mss_fix` 的计算模式,`mtu``fixed` 之一。需要 `mss_fix`
### replay_window
UDP 数据通道重放窗口大小。默认使用 `64`TCP 数据包 ID 始终严格连续。
### replay_window_time
UDP 重放窗口时长。默认使用 `15s`。该值必须使用整秒。
### push ### push
推送给客户端的选项。 推送给客户端的选项。
@@ -305,6 +495,22 @@ IPv4 和 IPv6 前缀可以混用。
推送给客户端的 DNS 服务器地址。 推送给客户端的 DNS 服务器地址。
使用传统的 `dhcp-option DNS`/`DNS6`。兼容客户端收到现代 DNS 服务器组时会覆盖这些地址。
### push.dns_servers
推送的现代 OpenVPN DNS 服务器组。每项包含 `priority``addresses`,以及可选的 `resolve_domains``dnssec``transport``sni`
地址接受 IP 或 `IP:port`(带端口的 IPv6 使用 `[IPv6]:port`)。`transport``plain``dot``doh` 之一;`dnssec``yes``optional``no` 之一。OpenVPN 客户端仅应用优先级数字最低的服务器组。
### push.search_domains
推送的现代 OpenVPN 搜索域。
### push.dhcp_options
推送的额外传统 `dhcp-option` 值,不包含 `dhcp-option` 前缀。
### push.redirect_gateway ### push.redirect_gateway
向客户端推送 `redirect-gateway`,根据 `push.redirect_gateway_flags` 通过 VPN 路由客户端流量。 向客户端推送 `redirect-gateway`,根据 `push.redirect_gateway_flags` 通过 VPN 路由客户端流量。
@@ -371,12 +577,34 @@ OpenVPN TLS 重协商间隔。
为空时使用 OpenVPN 默认值 `1h` 为空时使用 OpenVPN 默认值 `1h`
仅在 TLS 模式下可用。
### renegotiate_disabled
禁用基于时间的 TLS 重协商,包括默认间隔。
仅在 TLS 模式下可用。
### renegotiate_bytes
传输指定字节数后重新协商数据通道密钥。`0` 使用与密码算法相关的 OpenVPN 默认值。
仅在 TLS 模式下可用。
### renegotiate_packets
传输指定数据包数后重新协商数据通道密钥。`0` 使用与密码算法相关的 OpenVPN 默认值。
仅在 TLS 模式下可用。
### handshake_window ### handshake_window
初始 TLS 握手和每次 TLS 重协商允许使用的最长时间。 初始 TLS 握手和每次 TLS 重协商允许使用的最长时间。
默认使用 `1m` 默认使用 `1m`
仅在 TLS 模式下可用。
## UDP NAT 字段 ## UDP NAT 字段
这些字段配置通过 OpenVPN 接口的流量的 UDP 会话。 这些字段配置通过 OpenVPN 接口的流量的 UDP 会话。
+1 -2
View File
@@ -69,11 +69,10 @@ It is not recommended to change the default build tag list unless you really kno
## :material-wrench: Linker Flags ## :material-wrench: Linker Flags
The following `-ldflags` are used in official builds: The required linker flags for official builds are maintained in `release/LDFLAGS`. Downstream builds should use that file unchanged.
| Flag | Description | | Flag | Description |
|-------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |-------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `-X 'internal/godebug.defaultGODEBUG=multipathtcp=0'` | Go 1.24 enabled Multipath TCP for listeners by default (`multipathtcp=2`). This may cause errors on low-level sockets, and sing-box has its own MPTCP control (`tcp_multi_path` option). This flag disables the Go default. |
| `-checklinkname=0` | Go 1.23+ linker rejects unauthorized `go:linkname` usage. This flag disables the check, required together with the `badlinkname` build tag. | | `-checklinkname=0` | Go 1.23+ linker rejects unauthorized `go:linkname` usage. This flag disables the check, required together with the `badlinkname` build tag. |
## :material-package-variant: For Downstream Packagers ## :material-package-variant: For Downstream Packagers
+1 -2
View File
@@ -73,11 +73,10 @@ go build -tags "tag_a tag_b" ./cmd/sing-box
## :material-wrench: 链接器标志 ## :material-wrench: 链接器标志
以下 `-ldflags` 在官方构建中使用: 官方构建所需的链接器标志维护在 `release/LDFLAGS` 中。下游构建应原样使用该文件。
| 标志 | 说明 | | 标志 | 说明 |
|-------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------| |-------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `-X 'internal/godebug.defaultGODEBUG=multipathtcp=0'` | Go 1.24 默认为监听器启用 Multipath TCP`multipathtcp=2`)。这可能在底层 socket 上导致错误,且 sing-box 有自己的 MPTCP 控制(`tcp_multi_path` 选项)。此标志禁用 Go 的默认行为。 |
| `-checklinkname=0` | Go 1.23+ 链接器拒绝未授权的 `go:linkname` 使用。此标志禁用该检查,需要与 `badlinkname` 构建标记一起使用。 | | `-checklinkname=0` | Go 1.23+ 链接器拒绝未授权的 `go:linkname` 使用。此标志禁用该检查,需要与 `badlinkname` 构建标记一起使用。 |
## :material-package-variant: 下游打包者 ## :material-package-variant: 下游打包者
+4 -4
View File
@@ -44,7 +44,7 @@
"ts_omit_synology", "ts_omit_synology",
"ts_omit_bird" "ts_omit_bird"
], ],
"ldflags": "-X github.com/sagernet/sing-box/constant.Version=${VERSION} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0", "ldflags": "-X github.com/sagernet/sing-box/constant.Version=${VERSION} -X runtime.godebugDefault=multipathtcp=0,tlssha1=1,tlsunsafeekm=1 -s -w -buildid= -checklinkname=0",
"trimpath": true "trimpath": true
} }
}, },
@@ -73,7 +73,7 @@
"ts_omit_synology", "ts_omit_synology",
"ts_omit_bird" "ts_omit_bird"
], ],
"ldflags": "-X github.com/sagernet/sing-box/constant.Version=${VERSION} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0", "ldflags": "-X github.com/sagernet/sing-box/constant.Version=${VERSION} -X runtime.godebugDefault=multipathtcp=0,tlssha1=1,tlsunsafeekm=1 -s -w -buildid= -checklinkname=0",
"trimpath": true "trimpath": true
} }
}, },
@@ -105,7 +105,7 @@
"ts_omit_synology", "ts_omit_synology",
"ts_omit_bird" "ts_omit_bird"
], ],
"ldflags": "-X github.com/sagernet/sing-box/constant.Version=${VERSION} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0", "ldflags": "-X github.com/sagernet/sing-box/constant.Version=${VERSION} -X runtime.godebugDefault=multipathtcp=0,tlssha1=1,tlsunsafeekm=1 -s -w -buildid= -checklinkname=0",
"trimpath": true "trimpath": true
}, },
"overrides": [ "overrides": [
@@ -146,7 +146,7 @@
"ts_omit_synology", "ts_omit_synology",
"ts_omit_bird" "ts_omit_bird"
], ],
"ldflags": "-X github.com/sagernet/sing-box/constant.Version=${VERSION} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0", "ldflags": "-X github.com/sagernet/sing-box/constant.Version=${VERSION} -X runtime.godebugDefault=multipathtcp=0,tlssha1=1,tlsunsafeekm=1 -s -w -buildid= -checklinkname=0",
"trimpath": true "trimpath": true
} }
} }
+5 -2
View File
@@ -47,8 +47,8 @@ require (
github.com/sagernet/sing v0.9.0-beta.3 github.com/sagernet/sing v0.9.0-beta.3
github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3
github.com/sagernet/sing-mux v0.3.5 github.com/sagernet/sing-mux v0.3.5
github.com/sagernet/sing-openconnect v0.0.0-20260720032640-bf28b6a6f10e github.com/sagernet/sing-openconnect v0.0.0-20260721013312-6c25fa7e089a
github.com/sagernet/sing-openvpn v0.0.0-20260720132803-a5e407d00242 github.com/sagernet/sing-openvpn v0.0.0-20260721005523-64b754d1c277
github.com/sagernet/sing-quic v0.6.4-0.20260803041931-6c84c468bea2 github.com/sagernet/sing-quic v0.6.4-0.20260803041931-6c84c468bea2
github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks v0.2.8
github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowsocks2 v0.2.1
@@ -83,6 +83,7 @@ require (
require ( require (
filippo.io/edwards25519 v1.1.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect
filippo.io/hpke v0.4.0 // indirect filippo.io/hpke v0.4.0 // indirect
github.com/RyuaNerin/go-krypto v1.3.0 // indirect
github.com/ajg/form v1.5.1 // indirect github.com/ajg/form v1.5.1 // indirect
github.com/akutz/memconn v0.1.0 // indirect github.com/akutz/memconn v0.1.0 // indirect
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
@@ -95,6 +96,7 @@ require (
github.com/database64128/netx-go v0.1.1 // indirect github.com/database64128/netx-go v0.1.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 // indirect github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 // indirect
github.com/dgryski/go-camellia v0.0.0-20191119043421-69a8a13fb23d // indirect
github.com/ebitengine/purego v0.10.0 // indirect github.com/ebitengine/purego v0.10.0 // indirect
github.com/florianl/go-nfqueue/v2 v2.1.0 // indirect github.com/florianl/go-nfqueue/v2 v2.1.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect
@@ -176,6 +178,7 @@ require (
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect github.com/tidwall/sjson v1.2.5 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
+72 -4
View File
@@ -1,5 +1,6 @@
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd h1:ZLsPO6WdZ5zatV4UfVpr7oAwLGRZ+sebTUruuM4Ra3M= c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd h1:ZLsPO6WdZ5zatV4UfVpr7oAwLGRZ+sebTUruuM4Ra3M=
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo= c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE= code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE=
code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM= code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM=
filippo.io/age v1.3.1 h1:hbzdQOJkuaMEpRCLSN1/C5DX74RPcNCk6oqhKMXmZi0= filippo.io/age v1.3.1 h1:hbzdQOJkuaMEpRCLSN1/C5DX74RPcNCk6oqhKMXmZi0=
@@ -8,6 +9,11 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A= filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A=
filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY= filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/RyuaNerin/go-krypto v1.3.0 h1:smavTzSMAx8iuVlGb4pEwl9MD2qicqMzuXR2QWp2/Pg=
github.com/RyuaNerin/go-krypto v1.3.0/go.mod h1:9R9TU936laAIqAmjcHo/LsaXYOZlymudOAxjaBf62UM=
github.com/RyuaNerin/testingutil v0.1.0 h1:IYT6JL57RV3U2ml3dLHZsVtPOP6yNK7WUVdzzlpNrss=
github.com/RyuaNerin/testingutil v0.1.0/go.mod h1:yTqj6Ta/ycHMPJHRyO12Mz3VrvTloWOsy23WOZH19AA=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A= github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
@@ -30,10 +36,13 @@ github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7l
github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk= github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk=
github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso= github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0= github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0=
@@ -56,10 +65,15 @@ github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbww
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ= github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ=
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 h1:CaO/zOnF8VvUfEbhRatPcwKVWamvbYd8tQGRWacE9kU= github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 h1:CaO/zOnF8VvUfEbhRatPcwKVWamvbYd8tQGRWacE9kU=
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4= github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4=
github.com/dgryski/go-camellia v0.0.0-20191119043421-69a8a13fb23d h1:CPqTNIigGweVPT4CYb+OO2E6XyRKFOmvTHwWRLgCAlE=
github.com/dgryski/go-camellia v0.0.0-20191119043421-69a8a13fb23d/go.mod h1:QX5ZVULjAfZJux/W62Y91HvCh9hyW6enAwcrrv/sLj0=
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/florianl/go-nfqueue/v2 v2.1.0 h1:Fywt30TY/evxyDySpXjxQ1jsRW7nQbLpOhELqpr4068= github.com/florianl/go-nfqueue/v2 v2.1.0 h1:Fywt30TY/evxyDySpXjxQ1jsRW7nQbLpOhELqpr4068=
github.com/florianl/go-nfqueue/v2 v2.1.0/go.mod h1:8PKUM5rYoVFO5IZV1bifx4/b0jHAglKkHXr9PRwzi4Y= github.com/florianl/go-nfqueue/v2 v2.1.0/go.mod h1:8PKUM5rYoVFO5IZV1bifx4/b0jHAglKkHXr9PRwzi4Y=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
@@ -90,14 +104,29 @@ github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0= github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0=
github.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/certificate-transparency-go v1.3.2 h1:9ahSNZF2o7SYMaKaXhAumVEzXB2QaayzII9C8rv7v+A= github.com/google/certificate-transparency-go v1.3.2 h1:9ahSNZF2o7SYMaKaXhAumVEzXB2QaayzII9C8rv7v+A=
github.com/google/certificate-transparency-go v1.3.2/go.mod h1:H5FpMUaGa5Ab2+KCYsxg6sELw3Flkl7pGZzWdBoYLXs= github.com/google/certificate-transparency-go v1.3.2/go.mod h1:H5FpMUaGa5Ab2+KCYsxg6sELw3Flkl7pGZzWdBoYLXs=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -194,6 +223,7 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4= github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4=
github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4= github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -287,10 +317,10 @@ github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 h1:3y6
github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3/go.mod h1:XEqEDYRCAYLaoPjZ1ifVWJg5iWAJHL2gOAXe/PM28Cg= 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 h1:RHnhVEc+SFqkrK4xMygYjDwwLhzp2Bj3lztSukONfhI=
github.com/sagernet/sing-mux v0.3.5/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= github.com/sagernet/sing-mux v0.3.5/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
github.com/sagernet/sing-openconnect v0.0.0-20260720032640-bf28b6a6f10e h1:z0QFO/Bj3ikw4qDM7iwlEzSRdt/04Mh1s1pPeI/LRs0= github.com/sagernet/sing-openconnect v0.0.0-20260721013312-6c25fa7e089a h1:DafepSfytV5uShjQctZ0Cnw+Q9cOBIMT88p8jU2DSck=
github.com/sagernet/sing-openconnect v0.0.0-20260720032640-bf28b6a6f10e/go.mod h1:EIzh5HtImfQJxPKXFwS9lyMnmMy4aCQCx7ntQ4u41Gs= github.com/sagernet/sing-openconnect v0.0.0-20260721013312-6c25fa7e089a/go.mod h1:4AKZLVcvY3r54UaK2Gbnm7aN8pOwdLz+y4EP0QFZ5Eg=
github.com/sagernet/sing-openvpn v0.0.0-20260720132803-a5e407d00242 h1:0ZvKyBmBIlAuZ9G+zAWdGh6SrmXIXN7NWENHDrptxtI= github.com/sagernet/sing-openvpn v0.0.0-20260721005523-64b754d1c277 h1:4H38L3OxOx1fGEuH4n9lh/5O7XtZTgQ/1V/gdQ+b+Es=
github.com/sagernet/sing-openvpn v0.0.0-20260720132803-a5e407d00242/go.mod h1:mK4GzZyUIhG751Mt1MSvSqLGYOR7DFJd8q5QfgFjE2Y= github.com/sagernet/sing-openvpn v0.0.0-20260721005523-64b754d1c277/go.mod h1:PWX7WygD8jpwfqfaGNySXpJYTn0SOwjBI1BKHHC2+Bw=
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 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-quic v0.6.4-0.20260803041931-6c84c468bea2/go.mod h1:9k+dzGsWMttUGldBzq3dU792YHXzW6NgfbOGltnXq+0=
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE= github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
@@ -353,6 +383,8 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s= github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM= github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
@@ -396,6 +428,8 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
@@ -404,10 +438,14 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w= golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g= golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -417,8 +455,13 @@ golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -429,8 +472,11 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -441,10 +487,12 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -486,6 +534,10 @@ golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
@@ -507,10 +559,24 @@ golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
@@ -521,6 +587,8 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM= howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
+5
View File
@@ -4,9 +4,14 @@ package include
import ( import (
"github.com/sagernet/sing-box/adapter/endpoint" "github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/protocol/openconnect" "github.com/sagernet/sing-box/protocol/openconnect"
) )
func registerOpenConnectEndpoint(registry *endpoint.Registry) { func registerOpenConnectEndpoint(registry *endpoint.Registry) {
openconnect.RegisterEndpoint(registry) openconnect.RegisterEndpoint(registry)
} }
func registerOpenConnectDNSTransport(registry *dns.TransportRegistry) {
openconnect.RegisterDNSTransport(registry)
}
+7
View File
@@ -8,6 +8,7 @@ import (
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/endpoint" "github.com/sagernet/sing-box/adapter/endpoint"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
@@ -21,3 +22,9 @@ func registerOpenConnectEndpoint(registry *endpoint.Registry) {
return nil, E.New(`OpenConnect is not included in this build, rebuild with -tags with_openconnect`) return nil, E.New(`OpenConnect is not included in this build, rebuild with -tags with_openconnect`)
}) })
} }
func registerOpenConnectDNSTransport(registry *dns.TransportRegistry) {
dns.RegisterTransport[option.OpenConnectDNSServerOptions](registry, C.DNSTypeOpenConnect, func(ctx context.Context, logger log.ContextLogger, tag string, options option.OpenConnectDNSServerOptions) (adapter.DNSTransport, error) {
return nil, E.New(`OpenConnect is not included in this build, rebuild with -tags with_openconnect`)
})
}
+5
View File
@@ -4,9 +4,14 @@ package include
import ( import (
"github.com/sagernet/sing-box/adapter/endpoint" "github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/protocol/openvpn" "github.com/sagernet/sing-box/protocol/openvpn"
) )
func registerOpenVPNEndpoints(registry *endpoint.Registry) { func registerOpenVPNEndpoints(registry *endpoint.Registry) {
openvpn.RegisterEndpoint(registry) openvpn.RegisterEndpoint(registry)
} }
func registerOpenVPNDNSTransport(registry *dns.TransportRegistry) {
openvpn.RegisterDNSTransport(registry)
}
+7
View File
@@ -8,6 +8,7 @@ import (
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/endpoint" "github.com/sagernet/sing-box/adapter/endpoint"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
@@ -27,3 +28,9 @@ func registerOpenVPNEndpoints(registry *endpoint.Registry) {
return nil, E.New(`OpenVPN is not included in this build, rebuild with -tags with_openvpn`) return nil, E.New(`OpenVPN is not included in this build, rebuild with -tags with_openvpn`)
}) })
} }
func registerOpenVPNDNSTransport(registry *dns.TransportRegistry) {
dns.RegisterTransport[option.OpenVPNDNSServerOptions](registry, C.DNSTypeOpenVPN, func(ctx context.Context, logger log.ContextLogger, tag string, options option.OpenVPNDNSServerOptions) (adapter.DNSTransport, error) {
return nil, E.New(`OpenVPN is not included in this build, rebuild with -tags with_openvpn`)
})
}
+2
View File
@@ -134,6 +134,8 @@ func DNSTransportRegistry() *dns.TransportRegistry {
registerQUICTransports(registry) registerQUICTransports(registry)
registerDHCPTransport(registry) registerDHCPTransport(registry)
registerTailscaleTransport(registry) registerTailscaleTransport(registry)
registerOpenConnectDNSTransport(registry)
registerOpenVPNDNSTransport(registry)
return registry return registry
} }
+2
View File
@@ -100,6 +100,8 @@ nav:
- mDNS: configuration/dns/server/mdns.md - mDNS: configuration/dns/server/mdns.md
- FakeIP: configuration/dns/server/fakeip.md - FakeIP: configuration/dns/server/fakeip.md
- Tailscale: configuration/dns/server/tailscale.md - Tailscale: configuration/dns/server/tailscale.md
- OpenConnect: configuration/dns/server/openconnect.md
- OpenVPN: configuration/dns/server/openvpn.md
- Resolved: configuration/dns/server/resolved.md - Resolved: configuration/dns/server/resolved.md
- DNS Rule: configuration/dns/rule.md - DNS Rule: configuration/dns/rule.md
- DNS Rule Action: configuration/dns/rule_action.md - DNS Rule Action: configuration/dns/rule_action.md
+64 -27
View File
@@ -4,36 +4,63 @@ import "github.com/sagernet/sing/common/json/badoption"
type OpenConnectEndpointOptions struct { type OpenConnectEndpointOptions struct {
DialerOptions DialerOptions
System bool `json:"system,omitempty"` System bool `json:"system,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"` UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"`
UDPMapping UDPNATBehavior `json:"udp_mapping,omitempty"` UDPMapping UDPNATBehavior `json:"udp_mapping,omitempty"`
UDPFiltering UDPNATBehavior `json:"udp_filtering,omitempty"` UDPFiltering UDPNATBehavior `json:"udp_filtering,omitempty"`
UDPNATMax uint32 `json:"udp_nat_max,omitempty"` UDPNATMax uint32 `json:"udp_nat_max,omitempty"`
Server string `json:"server"` Server string `json:"server"`
Flavor string `json:"flavor,omitempty"` Flavor string `json:"flavor,omitempty"`
Username string `json:"username,omitempty"` Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"` Password string `json:"password,omitempty"`
AuthGroup string `json:"auth_group,omitempty"` AuthGroup string `json:"auth_group,omitempty"`
Token *OpenConnectTokenOptions `json:"token,omitempty"` Cookie string `json:"cookie,omitempty"`
ReportedOS string `json:"reported_os,omitempty"` Token *OpenConnectTokenOptions `json:"token,omitempty"`
UserAgent string `json:"user_agent,omitempty"` ReportedOS string `json:"reported_os,omitempty"`
CSD *OpenConnectCSDOptions `json:"csd,omitempty"` UserAgent string `json:"user_agent,omitempty"`
HIP *OpenConnectHIPOptions `json:"hip,omitempty"` Version string `json:"version,omitempty"`
TNCC *OpenConnectTNCCOptions `json:"tncc,omitempty"` LocalHostname string `json:"local_hostname,omitempty"`
NoUDP bool `json:"no_udp,omitempty"` Mobile *OpenConnectMobileOptions `json:"mobile,omitempty"`
AllowInsecureCrypto bool `json:"allow_insecure_crypto,omitempty"` CSD *OpenConnectCSDOptions `json:"csd,omitempty"`
TLS OpenConnectTLSOptions `json:"tls,omitempty"` HIP *OpenConnectHIPOptions `json:"hip,omitempty"`
FormEntries []OpenConnectFormEntryOptions `json:"form_entries,omitempty"` TNCC *OpenConnectTNCCOptions `json:"tncc,omitempty"`
NoUDP bool `json:"no_udp,omitempty"`
DTLSLocalPort uint16 `json:"dtls_local_port,omitempty"`
CompressionDisabled bool `json:"compression_disabled,omitempty"`
CompressionMode string `json:"compression_mode,omitempty"`
IPv6Disabled bool `json:"ipv6_disabled,omitempty"`
HTTPKeepAliveDisabled bool `json:"http_keepalive_disabled,omitempty"`
XMLPostDisabled bool `json:"xml_post_disabled,omitempty"`
ExternalAuthDisabled bool `json:"external_auth_disabled,omitempty"`
PasswordAuthenticationDisabled bool `json:"password_authentication_disabled,omitempty"`
TCPKeepAliveEnabled bool `json:"tcp_keep_alive_enabled,omitempty"`
PFS bool `json:"pfs,omitempty"`
MTU uint32 `json:"mtu,omitempty"`
BaseMTU uint32 `json:"base_mtu,omitempty"`
DPDInterval badoption.Duration `json:"dpd_interval,omitempty"`
ReconnectTimeout badoption.Duration `json:"reconnect_timeout,omitempty"`
TrojanInterval badoption.Duration `json:"trojan_interval,omitempty"`
QueueLength uint32 `json:"queue_length,omitempty"`
AllowInsecureCrypto bool `json:"allow_insecure_crypto,omitempty"`
TLS OpenConnectTLSOptions `json:"tls,omitempty"`
FormEntries []OpenConnectFormEntryOptions `json:"form_entries,omitempty"`
} }
type OpenConnectTokenOptions struct { type OpenConnectTokenOptions struct {
Mode string `json:"mode,omitempty"` Mode string `json:"mode,omitempty"`
Secret string `json:"secret,omitempty"` Secret string `json:"secret,omitempty"`
PIN string `json:"pin,omitempty"` SecretPath string `json:"secret_path,omitempty"`
Password string `json:"password,omitempty"` PIN string `json:"pin,omitempty"`
DeviceID string `json:"device_id,omitempty"` Password string `json:"password,omitempty"`
Counter uint64 `json:"counter,omitempty"` DeviceID string `json:"device_id,omitempty"`
Counter uint64 `json:"counter,omitempty"`
}
type OpenConnectMobileOptions struct {
PlatformVersion string `json:"platform_version"`
DeviceType string `json:"device_type"`
DeviceUniqueID string `json:"device_unique_id"`
} }
type OpenConnectCSDOptions struct { type OpenConnectCSDOptions struct {
@@ -58,6 +85,10 @@ type OpenConnectTNCCCertificateOptions struct {
} }
type OpenConnectTLSOptions struct { type OpenConnectTLSOptions struct {
Insecure bool `json:"insecure,omitempty"`
ServerName string `json:"server_name,omitempty"`
PeerFingerprint badoption.Listable[string] `json:"peer_fingerprint,omitempty"`
SystemTrustDisabled bool `json:"system_trust_disabled,omitempty"`
CertificateAuthority badoption.Listable[string] `json:"certificate_authority,omitempty"` CertificateAuthority badoption.Listable[string] `json:"certificate_authority,omitempty"`
CertificateAuthorityPath string `json:"certificate_authority_path,omitempty"` CertificateAuthorityPath string `json:"certificate_authority_path,omitempty"`
ClientCertificate badoption.Listable[string] `json:"client_certificate,omitempty"` ClientCertificate badoption.Listable[string] `json:"client_certificate,omitempty"`
@@ -79,3 +110,9 @@ type OpenConnectFormEntryOptions struct {
Value string `json:"value,omitempty"` Value string `json:"value,omitempty"`
Promote bool `json:"promote,omitempty"` Promote bool `json:"promote,omitempty"`
} }
type OpenConnectDNSServerOptions struct {
Endpoint string `json:"endpoint,omitempty"`
AcceptDefaultResolvers bool `json:"accept_default_resolvers,omitempty"`
AcceptSearchDomain bool `json:"accept_search_domain,omitempty"`
}
+69
View File
@@ -20,20 +20,33 @@ type OpenVPNClientEndpointOptions struct {
DialerOptions DialerOptions
ServerOptions ServerOptions
OpenVPNEndpointOptions OpenVPNEndpointOptions
Mode string `json:"mode,omitempty"`
Network string `json:"network,omitempty"` Network string `json:"network,omitempty"`
Servers []OpenVPNRemoteOptions `json:"servers,omitempty"` Servers []OpenVPNRemoteOptions `json:"servers,omitempty"`
RemoteRandom bool `json:"remote_random,omitempty"` RemoteRandom bool `json:"remote_random,omitempty"`
Address badoption.Listable[netip.Prefix] `json:"address,omitempty"`
PeerAddress badoption.Addr `json:"peer_address,omitempty"`
PeerAddressIPv6 badoption.Addr `json:"peer_address_ipv6,omitempty"`
Topology string `json:"topology,omitempty"`
Username string `json:"username,omitempty"` Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"` Password string `json:"password,omitempty"`
AuthRetry string `json:"auth_retry,omitempty"` AuthRetry string `json:"auth_retry,omitempty"`
StaticChallenge string `json:"static_challenge,omitempty"` StaticChallenge string `json:"static_challenge,omitempty"`
StaticChallengeEcho bool `json:"static_challenge_echo,omitempty"` StaticChallengeEcho bool `json:"static_challenge_echo,omitempty"`
StaticKey badoption.Listable[string] `json:"static_key,omitempty"`
StaticKeyPath string `json:"static_key_path,omitempty"`
KeyDirection string `json:"key_direction,omitempty"`
TLS *OpenVPNOutboundTLSOptions `json:"tls,omitempty"` TLS *OpenVPNOutboundTLSOptions `json:"tls,omitempty"`
Cipher string `json:"cipher,omitempty"`
DataCiphers badoption.Listable[string] `json:"data_ciphers,omitempty"` DataCiphers badoption.Listable[string] `json:"data_ciphers,omitempty"`
DataCiphersFallback string `json:"data_ciphers_fallback,omitempty"` DataCiphersFallback string `json:"data_ciphers_fallback,omitempty"`
Auth string `json:"auth,omitempty"` Auth string `json:"auth,omitempty"`
MSSFix uint32 `json:"mss_fix,omitempty"` MSSFix uint32 `json:"mss_fix,omitempty"`
MSSFixDisabled bool `json:"mss_fix_disabled,omitempty"`
MSSFixMode string `json:"mss_fix_mode,omitempty"`
Fragment uint32 `json:"fragment,omitempty"` Fragment uint32 `json:"fragment,omitempty"`
ReplayWindow uint32 `json:"replay_window,omitempty"`
ReplayWindowTime badoption.Duration `json:"replay_window_time,omitempty"`
Compression string `json:"compression,omitempty"` Compression string `json:"compression,omitempty"`
CompressionLZO string `json:"compression_lzo,omitempty"` CompressionLZO string `json:"compression_lzo,omitempty"`
AllowCompression string `json:"allow_compression,omitempty"` AllowCompression string `json:"allow_compression,omitempty"`
@@ -44,9 +57,17 @@ type OpenVPNClientEndpointOptions struct {
RouteMetric int `json:"route_metric,omitempty"` RouteMetric int `json:"route_metric,omitempty"`
RedirectGateway bool `json:"redirect_gateway,omitempty"` RedirectGateway bool `json:"redirect_gateway,omitempty"`
RedirectGatewayFlags badoption.Listable[string] `json:"redirect_gateway_flags,omitempty"` RedirectGatewayFlags badoption.Listable[string] `json:"redirect_gateway_flags,omitempty"`
RedirectPrivate bool `json:"redirect_private,omitempty"`
BlockIPv6 bool `json:"block_ipv6,omitempty"`
PingInterval badoption.Duration `json:"ping_interval,omitempty"` PingInterval badoption.Duration `json:"ping_interval,omitempty"`
PingRestart badoption.Duration `json:"ping_restart,omitempty"` PingRestart badoption.Duration `json:"ping_restart,omitempty"`
PingRestartDisabled bool `json:"ping_restart_disabled,omitempty"`
RenegotiateInterval badoption.Duration `json:"renegotiate_interval,omitempty"` RenegotiateInterval badoption.Duration `json:"renegotiate_interval,omitempty"`
RenegotiateDisabled bool `json:"renegotiate_disabled,omitempty"`
RenegotiateBytes uint64 `json:"renegotiate_bytes,omitempty"`
RenegotiatePackets uint64 `json:"renegotiate_packets,omitempty"`
TLSTimeout badoption.Duration `json:"tls_timeout,omitempty"`
HandshakeWindow badoption.Duration `json:"handshake_window,omitempty"`
ExplicitExitNotify uint32 `json:"explicit_exit_notify,omitempty"` ExplicitExitNotify uint32 `json:"explicit_exit_notify,omitempty"`
UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"` UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"`
} }
@@ -54,20 +75,37 @@ type OpenVPNClientEndpointOptions struct {
type OpenVPNServerEndpointOptions struct { type OpenVPNServerEndpointOptions struct {
ListenOptions ListenOptions
OpenVPNEndpointOptions OpenVPNEndpointOptions
Mode string `json:"mode,omitempty"`
Network string `json:"network,omitempty"` Network string `json:"network,omitempty"`
Remote string `json:"remote,omitempty"`
RemotePort uint16 `json:"remote_port,omitempty"`
MaxClients int `json:"max_clients,omitempty"` MaxClients int `json:"max_clients,omitempty"`
Address badoption.Listable[netip.Prefix] `json:"address"` Address badoption.Listable[netip.Prefix] `json:"address"`
PeerAddress badoption.Addr `json:"peer_address,omitempty"`
PeerAddressIPv6 badoption.Addr `json:"peer_address_ipv6,omitempty"`
Topology string `json:"topology,omitempty"` Topology string `json:"topology,omitempty"`
DuplicateCN bool `json:"duplicate_cn,omitempty"` DuplicateCN bool `json:"duplicate_cn,omitempty"`
Users []auth.User `json:"users,omitempty"` Users []auth.User `json:"users,omitempty"`
StaticKey badoption.Listable[string] `json:"static_key,omitempty"`
StaticKeyPath string `json:"static_key_path,omitempty"`
KeyDirection string `json:"key_direction,omitempty"`
TLS *OpenVPNInboundTLSOptions `json:"tls,omitempty"` TLS *OpenVPNInboundTLSOptions `json:"tls,omitempty"`
Cipher string `json:"cipher,omitempty"`
DataCiphers badoption.Listable[string] `json:"data_ciphers,omitempty"` DataCiphers badoption.Listable[string] `json:"data_ciphers,omitempty"`
DataCiphersFallback string `json:"data_ciphers_fallback,omitempty"` DataCiphersFallback string `json:"data_ciphers_fallback,omitempty"`
Auth string `json:"auth,omitempty"` Auth string `json:"auth,omitempty"`
MSSFix uint32 `json:"mss_fix,omitempty"`
MSSFixDisabled bool `json:"mss_fix_disabled,omitempty"`
MSSFixMode string `json:"mss_fix_mode,omitempty"`
ReplayWindow uint32 `json:"replay_window,omitempty"`
ReplayWindowTime badoption.Duration `json:"replay_window_time,omitempty"`
Push *OpenVPNPushOptions `json:"push,omitempty"` Push *OpenVPNPushOptions `json:"push,omitempty"`
PingInterval badoption.Duration `json:"ping_interval,omitempty"` PingInterval badoption.Duration `json:"ping_interval,omitempty"`
PingRestart badoption.Duration `json:"ping_restart,omitempty"` PingRestart badoption.Duration `json:"ping_restart,omitempty"`
RenegotiateInterval badoption.Duration `json:"renegotiate_interval,omitempty"` RenegotiateInterval badoption.Duration `json:"renegotiate_interval,omitempty"`
RenegotiateDisabled bool `json:"renegotiate_disabled,omitempty"`
RenegotiateBytes uint64 `json:"renegotiate_bytes,omitempty"`
RenegotiatePackets uint64 `json:"renegotiate_packets,omitempty"`
HandshakeWindow badoption.Duration `json:"handshake_window,omitempty"` HandshakeWindow badoption.Duration `json:"handshake_window,omitempty"`
} }
@@ -96,6 +134,7 @@ type OpenVPNOutboundTLSOptions struct {
RemoteCertificateEKU string `json:"remote_certificate_eku,omitempty"` RemoteCertificateEKU string `json:"remote_certificate_eku,omitempty"`
RemoteCertificateTLS string `json:"remote_certificate_tls,omitempty"` RemoteCertificateTLS string `json:"remote_certificate_tls,omitempty"`
CertificateProfile string `json:"certificate_profile,omitempty"` CertificateProfile string `json:"certificate_profile,omitempty"`
NSCertificateType string `json:"ns_certificate_type,omitempty"`
VersionMin string `json:"version_min,omitempty"` VersionMin string `json:"version_min,omitempty"`
VersionMax string `json:"version_max,omitempty"` VersionMax string `json:"version_max,omitempty"`
Cipher string `json:"cipher,omitempty"` Cipher string `json:"cipher,omitempty"`
@@ -111,7 +150,19 @@ type OpenVPNInboundTLSOptions struct {
ClientCertificate badoption.Listable[string] `json:"client_certificate,omitempty"` ClientCertificate badoption.Listable[string] `json:"client_certificate,omitempty"`
ClientCertificatePath string `json:"client_certificate_path,omitempty"` ClientCertificatePath string `json:"client_certificate_path,omitempty"`
VerifyClientCertificate string `json:"verify_client_certificate,omitempty"` VerifyClientCertificate string `json:"verify_client_certificate,omitempty"`
ClientName string `json:"client_name,omitempty"`
ClientNameType string `json:"client_name_type,omitempty"`
PeerFingerprint badoption.Listable[string] `json:"peer_fingerprint,omitempty"`
CRLPath string `json:"crl_path,omitempty"`
RemoteCertificateKU badoption.Listable[string] `json:"remote_certificate_ku,omitempty"`
RemoteCertificateEKU string `json:"remote_certificate_eku,omitempty"`
RemoteCertificateTLS string `json:"remote_certificate_tls,omitempty"`
CertificateProfile string `json:"certificate_profile,omitempty"` CertificateProfile string `json:"certificate_profile,omitempty"`
NSCertificateType string `json:"ns_certificate_type,omitempty"`
VersionMin string `json:"version_min,omitempty"`
VersionMax string `json:"version_max,omitempty"`
Cipher string `json:"cipher,omitempty"`
Groups string `json:"groups,omitempty"`
ControlWrap *OpenVPNInboundControlWrapOptions `json:"control_wrap,omitempty"` ControlWrap *OpenVPNInboundControlWrapOptions `json:"control_wrap,omitempty"`
} }
@@ -133,9 +184,27 @@ type OpenVPNInboundControlWrapOptions struct {
type OpenVPNPushOptions struct { type OpenVPNPushOptions struct {
Routes badoption.Listable[netip.Prefix] `json:"routes,omitempty"` Routes badoption.Listable[netip.Prefix] `json:"routes,omitempty"`
DNS badoption.Listable[netip.Addr] `json:"dns,omitempty"` DNS badoption.Listable[netip.Addr] `json:"dns,omitempty"`
DNSServers []OpenVPNPushDNSServerOptions `json:"dns_servers,omitempty"`
SearchDomains badoption.Listable[string] `json:"search_domains,omitempty"`
DHCPOptions badoption.Listable[string] `json:"dhcp_options,omitempty"`
RedirectGateway bool `json:"redirect_gateway,omitempty"` RedirectGateway bool `json:"redirect_gateway,omitempty"`
RedirectGatewayFlags badoption.Listable[string] `json:"redirect_gateway_flags,omitempty"` RedirectGatewayFlags badoption.Listable[string] `json:"redirect_gateway_flags,omitempty"`
BlockOutsideDNS bool `json:"block_outside_dns,omitempty"` BlockOutsideDNS bool `json:"block_outside_dns,omitempty"`
PingInterval badoption.Duration `json:"ping_interval,omitempty"` PingInterval badoption.Duration `json:"ping_interval,omitempty"`
PingRestart badoption.Duration `json:"ping_restart,omitempty"` PingRestart badoption.Duration `json:"ping_restart,omitempty"`
} }
type OpenVPNPushDNSServerOptions struct {
Priority int `json:"priority"`
Addresses badoption.Listable[string] `json:"addresses"`
ResolveDomains badoption.Listable[string] `json:"resolve_domains,omitempty"`
DNSSEC string `json:"dnssec,omitempty"`
Transport string `json:"transport,omitempty"`
SNI string `json:"sni,omitempty"`
}
type OpenVPNDNSServerOptions struct {
Endpoint string `json:"endpoint,omitempty"`
AcceptDefaultResolvers bool `json:"accept_default_resolvers,omitempty"`
AcceptSearchDomain bool `json:"accept_search_domain,omitempty"`
}
+24 -22
View File
@@ -65,28 +65,30 @@ type DialerOptionsWrapper interface {
} }
type DialerOptions struct { type DialerOptions struct {
Detour string `json:"detour,omitempty"` Detour string `json:"detour,omitempty"`
BindInterface string `json:"bind_interface,omitempty"` BindInterface string `json:"bind_interface,omitempty"`
Inet4BindAddress *badoption.Addr `json:"inet4_bind_address,omitempty"` Inet4BindAddress *badoption.Addr `json:"inet4_bind_address,omitempty"`
Inet6BindAddress *badoption.Addr `json:"inet6_bind_address,omitempty"` Inet6BindAddress *badoption.Addr `json:"inet6_bind_address,omitempty"`
BindAddressNoPort bool `json:"bind_address_no_port,omitempty"` BindAddressNoPort bool `json:"bind_address_no_port,omitempty"`
ProtectPath string `json:"protect_path,omitempty"` ProtectPath string `json:"protect_path,omitempty"`
RoutingMark FwMark `json:"routing_mark,omitempty"` RoutingMark FwMark `json:"routing_mark,omitempty"`
ReuseAddr bool `json:"reuse_addr,omitempty"` ReuseAddr bool `json:"reuse_addr,omitempty"`
NetNs string `json:"netns,omitempty"` NetNs string `json:"netns,omitempty"`
ConnectTimeout badoption.Duration `json:"connect_timeout,omitempty"` ConnectTimeout badoption.Duration `json:"connect_timeout,omitempty"`
TCPFastOpen bool `json:"tcp_fast_open,omitempty"` TCPFastOpen bool `json:"tcp_fast_open,omitempty"`
TCPMultiPath bool `json:"tcp_multi_path,omitempty"` TCPMultiPath bool `json:"tcp_multi_path,omitempty"`
DisableTCPKeepAlive bool `json:"disable_tcp_keep_alive,omitempty"` DisableTCPKeepAlive bool `json:"disable_tcp_keep_alive,omitempty"`
TCPKeepAlive badoption.Duration `json:"tcp_keep_alive,omitempty"` TCPKeepAlive badoption.Duration `json:"tcp_keep_alive,omitempty"`
TCPKeepAliveInterval badoption.Duration `json:"tcp_keep_alive_interval,omitempty"` TCPKeepAliveInterval badoption.Duration `json:"tcp_keep_alive_interval,omitempty"`
UDPFragment *bool `json:"udp_fragment,omitempty"` TCPKeepAliveSystemDefaults bool `json:"-"`
UDPFragmentDefault bool `json:"-"` UDPBindPort uint16 `json:"-"`
DomainResolver *DomainResolveOptions `json:"domain_resolver,omitempty"` UDPFragment *bool `json:"udp_fragment,omitempty"`
NetworkStrategy *NetworkStrategy `json:"network_strategy,omitempty"` UDPFragmentDefault bool `json:"-"`
NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"` DomainResolver *DomainResolveOptions `json:"domain_resolver,omitempty"`
FallbackNetworkType badoption.Listable[InterfaceType] `json:"fallback_network_type,omitempty"` NetworkStrategy *NetworkStrategy `json:"network_strategy,omitempty"`
FallbackDelay badoption.Duration `json:"fallback_delay,omitempty"` NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"`
FallbackNetworkType badoption.Listable[InterfaceType] `json:"fallback_network_type,omitempty"`
FallbackDelay badoption.Duration `json:"fallback_delay,omitempty"`
// Deprecated: migrated to domain resolver // Deprecated: migrated to domain resolver
DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"` DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"`
+100 -32
View File
@@ -2,6 +2,7 @@ package openconnect
import ( import (
"context" "context"
"crypto/tls"
"net" "net"
"net/netip" "net/netip"
"net/url" "net/url"
@@ -50,6 +51,8 @@ type Endpoint struct {
flavor string flavor string
stateAccess sync.Mutex stateAccess sync.Mutex
state atomic.Pointer[clientState] state atomic.Pointer[clientState]
dnsTransportAccess sync.Mutex
dnsTransport *DNSTransport
deviceStarted bool deviceStarted bool
readLoopDone chan struct{} readLoopDone chan struct{}
statusAccess sync.Mutex statusAccess sync.Mutex
@@ -65,10 +68,22 @@ type clientState struct {
tunnelConfigured bool tunnelConfigured bool
localAddresses []netip.Prefix localAddresses []netip.Prefix
routeSet *netipx.IPSet routeSet *netipx.IPSet
preferredDomains map[string]bool
configuration openconnecttransport.Configuration
tunnelInfo adapter.OpenConnectTunnelInfo tunnelInfo adapter.OpenConnectTunnelInfo
} }
func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.OpenConnectEndpointOptions) (adapter.Endpoint, error) { func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.OpenConnectEndpointOptions) (adapter.Endpoint, error) {
tcpKeepAliveEnabled := options.TCPKeepAliveEnabled || options.TCPKeepAlive != 0 || options.TCPKeepAliveInterval != 0
if tcpKeepAliveEnabled && options.DisableTCPKeepAlive {
return nil, E.New("tcp_keep_alive_enabled conflicts with disable_tcp_keep_alive")
}
if !tcpKeepAliveEnabled {
options.DisableTCPKeepAlive = true
} else if options.TCPKeepAlive == 0 && options.TCPKeepAliveInterval == 0 {
options.TCPKeepAliveSystemDefaults = true
}
options.UDPBindPort = options.DTLSLocalPort
loopContext, cancelLoop := context.WithCancel(ctx) loopContext, cancelLoop := context.WithCancel(ctx)
openConnectEndpoint := &Endpoint{ openConnectEndpoint := &Endpoint{
endpointBase: endpointBase{ endpointBase: endpointBase{
@@ -98,7 +113,7 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
} }
serverURL, err := url.Parse(server) serverURL, err := url.Parse(server)
if err != nil { if err != nil {
return nil, E.Cause(err, "parse OpenConnect server") return nil, E.Cause(err, "parse server")
} }
serverPort := serverURL.Port() serverPort := serverURL.Port()
if serverPort == "" { if serverPort == "" {
@@ -162,6 +177,10 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
} }
func (e *Endpoint) buildClientOptions(options option.OpenConnectEndpointOptions, outboundDialer N.Dialer) (openconnect.ClientOptions, error) { func (e *Endpoint) buildClientOptions(options option.OpenConnectEndpointOptions, outboundDialer N.Dialer) (openconnect.ClientOptions, error) {
var tlsConfig *tls.Config
if options.TLS.Insecure {
tlsConfig = &tls.Config{InsecureSkipVerify: true}
}
certificateAuthority, err := materialSource("tls.certificate_authority", options.TLS.CertificateAuthority, options.TLS.CertificateAuthorityPath) certificateAuthority, err := materialSource("tls.certificate_authority", options.TLS.CertificateAuthority, options.TLS.CertificateAuthorityPath)
if err != nil { if err != nil {
return openconnect.ClientOptions{}, err return openconnect.ClientOptions{}, err
@@ -185,12 +204,13 @@ func (e *Endpoint) buildClientOptions(options option.OpenConnectEndpointOptions,
var tokenOptions *openconnect.TokenOptions var tokenOptions *openconnect.TokenOptions
if options.Token != nil { if options.Token != nil {
tokenOptions = &openconnect.TokenOptions{ tokenOptions = &openconnect.TokenOptions{
Mode: options.Token.Mode, Mode: options.Token.Mode,
Secret: options.Token.Secret, Secret: options.Token.Secret,
PIN: options.Token.PIN, SecretPath: options.Token.SecretPath,
Password: options.Token.Password, PIN: options.Token.PIN,
DeviceID: options.Token.DeviceID, Password: options.Token.Password,
Counter: options.Token.Counter, DeviceID: options.Token.DeviceID,
Counter: options.Token.Counter,
} }
if tokenOptions.Mode == openconnect.TokenModeHOTP { if tokenOptions.Mode == openconnect.TokenModeHOTP {
e.hotpCounter.Store(tokenOptions.Counter) e.hotpCounter.Store(tokenOptions.Counter)
@@ -201,6 +221,14 @@ func (e *Endpoint) buildClientOptions(options option.OpenConnectEndpointOptions,
} }
} }
var csdOptions *openconnect.CSDOptions var csdOptions *openconnect.CSDOptions
var mobileOptions *openconnect.MobileOptions
if options.Mobile != nil {
mobileOptions = &openconnect.MobileOptions{
PlatformVersion: options.Mobile.PlatformVersion,
DeviceType: options.Mobile.DeviceType,
DeviceUniqueID: options.Mobile.DeviceUniqueID,
}
}
if options.CSD != nil { if options.CSD != nil {
csdOptions = &openconnect.CSDOptions{WrapperPath: options.CSD.WrapperPath} csdOptions = &openconnect.CSDOptions{WrapperPath: options.CSD.WrapperPath}
} }
@@ -236,21 +264,44 @@ func (e *Endpoint) buildClientOptions(options option.OpenConnectEndpointOptions,
} }
}) })
return openconnect.ClientOptions{ return openconnect.ClientOptions{
Context: e.loopContext, Context: e.loopContext,
Server: options.Server, Server: options.Server,
Flavor: options.Flavor, Flavor: options.Flavor,
Username: options.Username, Username: options.Username,
Password: options.Password, Password: options.Password,
AuthGroup: options.AuthGroup, AuthGroup: options.AuthGroup,
Token: tokenOptions, Cookie: options.Cookie,
ReportedOS: options.ReportedOS, Token: tokenOptions,
UserAgent: options.UserAgent, ReportedOS: options.ReportedOS,
CSD: csdOptions, UserAgent: options.UserAgent,
HIP: hipOptions, Version: options.Version,
TNCC: tnccOptions, LocalHostname: options.LocalHostname,
NoUDP: options.NoUDP, Mobile: mobileOptions,
AllowInsecureCrypto: options.AllowInsecureCrypto, CSD: csdOptions,
HIP: hipOptions,
TNCC: tnccOptions,
NoUDP: options.NoUDP,
DTLSLocalPort: options.DTLSLocalPort,
CompressionDisabled: options.CompressionDisabled,
CompressionMode: options.CompressionMode,
IPv6Disabled: options.IPv6Disabled,
HTTPKeepAliveDisabled: options.HTTPKeepAliveDisabled,
XMLPostDisabled: options.XMLPostDisabled,
ExternalAuthDisabled: options.ExternalAuthDisabled,
PasswordAuthenticationDisabled: options.PasswordAuthenticationDisabled,
PFS: options.PFS,
MTU: options.MTU,
BaseMTU: options.BaseMTU,
DPDInterval: time.Duration(options.DPDInterval),
ReconnectTimeout: time.Duration(options.ReconnectTimeout),
TrojanInterval: time.Duration(options.TrojanInterval),
QueueLength: options.QueueLength,
AllowInsecureCrypto: options.AllowInsecureCrypto,
TLSConfig: openconnect.ClientTLSOptions{ TLSConfig: openconnect.ClientTLSOptions{
Config: tlsConfig,
ServerName: options.TLS.ServerName,
PeerFingerprints: options.TLS.PeerFingerprint,
SystemTrustDisabled: options.TLS.SystemTrustDisabled,
CertificateAuthority: certificateAuthority, CertificateAuthority: certificateAuthority,
Certificate: clientCertificate, Certificate: clientCertificate,
Key: clientKey, Key: clientKey,
@@ -274,7 +325,14 @@ func (e *Endpoint) handleTunnelConfiguration(event openconnect.TunnelConfigurati
e.updateState(func(state *clientState) { e.updateState(func(state *clientState) {
state.tunnelConfigured = false state.tunnelConfigured = false
}) })
err := e.device.UpdateConfiguration(configuration) routeSet, err := buildIPSet(configuration.Routes, configuration.ExcludedRoutes)
if err != nil {
return E.Cause(err, "build route set")
}
err = e.device.UpdateConfiguration(openconnecttransport.Configuration{
MTU: configuration.MTU,
Addresses: configuration.Addresses,
})
if err != nil { if err != nil {
return E.Cause(err, "update device configuration") return E.Cause(err, "update device configuration")
} }
@@ -285,10 +343,7 @@ func (e *Endpoint) handleTunnelConfiguration(event openconnect.TunnelConfigurati
} }
e.deviceStarted = true e.deviceStarted = true
} }
routeSet, err := buildIPSet(configuration.Routes, configuration.ExcludedRoutes) preferredDomains := buildPreferredDomains(configuration)
if err != nil {
return E.Cause(err, "build route set")
}
var ipv4Addresses []netip.Prefix var ipv4Addresses []netip.Prefix
var ipv6Addresses []netip.Prefix var ipv6Addresses []netip.Prefix
for _, address := range configuration.Addresses { for _, address := range configuration.Addresses {
@@ -308,6 +363,8 @@ func (e *Endpoint) handleTunnelConfiguration(event openconnect.TunnelConfigurati
state.tunnelConfigured = true state.tunnelConfigured = true
state.localAddresses = configuration.Addresses state.localAddresses = configuration.Addresses
state.routeSet = routeSet state.routeSet = routeSet
state.preferredDomains = preferredDomains
state.configuration = configuration
state.tunnelInfo = adapter.OpenConnectTunnelInfo{ state.tunnelInfo = adapter.OpenConnectTunnelInfo{
Server: e.server, Server: e.server,
Flavor: e.flavor, Flavor: e.flavor,
@@ -319,6 +376,12 @@ func (e *Endpoint) handleTunnelConfiguration(event openconnect.TunnelConfigurati
ConnectedSince: connectedSince, ConnectedSince: connectedSince,
} }
}) })
e.dnsTransportAccess.Lock()
dnsTransport := e.dnsTransport
e.dnsTransportAccess.Unlock()
if dnsTransport != nil {
dnsTransport.updateConfiguration(configuration)
}
return nil return nil
} }
@@ -358,7 +421,7 @@ func (e *Endpoint) readLoop() {
if E.IsClosedOrCanceled(err) || e.loopContext.Err() != nil { if E.IsClosedOrCanceled(err) || e.loopContext.Err() != nil {
return return
} }
e.logger.Error(E.Cause(err, "OpenConnect client terminated")) e.logger.Error(E.Cause(err, "client terminated"))
e.setTerminalError(err) e.setTerminalError(err)
return return
} }
@@ -436,11 +499,11 @@ func (e *Endpoint) ready() bool {
func (e *Endpoint) WritePackets(packets [][]byte) error { func (e *Endpoint) WritePackets(packets [][]byte) error {
if !e.ready() { if !e.ready() {
return E.New("OpenConnect client is not ready yet") return E.New("endpoint is not ready yet")
} }
err := e.client.WriteDataPackets(packets) err := e.client.WriteDataPackets(packets)
if E.IsMulti(err, openconnect.ErrDataChannelNotReady) { if E.IsMulti(err, openconnect.ErrDataChannelNotReady) {
return E.New("OpenConnect client is not ready yet") return E.New("endpoint is not ready yet")
} }
return err return err
} }
@@ -473,7 +536,7 @@ func (e *Endpoint) DialContext(ctx context.Context, network string, destination
e.logger.InfoContext(ctx, "outbound packet connection to ", destination) e.logger.InfoContext(ctx, "outbound packet connection to ", destination)
} }
if !e.ready() || !e.client.Ready() { if !e.ready() || !e.client.Ready() {
return nil, E.New("OpenConnect client is not ready yet") return nil, E.New("endpoint is not ready yet")
} }
if destination.IsDomain() { if destination.IsDomain() {
destinationAddresses, err := e.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{}) destinationAddresses, err := e.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{})
@@ -491,7 +554,7 @@ func (e *Endpoint) DialContext(ctx context.Context, network string, destination
func (e *Endpoint) ListenPacketWithDestination(ctx context.Context, destination M.Socksaddr) (net.PacketConn, netip.Addr, error) { func (e *Endpoint) ListenPacketWithDestination(ctx context.Context, destination M.Socksaddr) (net.PacketConn, netip.Addr, error) {
e.logger.InfoContext(ctx, "outbound packet connection to ", destination) e.logger.InfoContext(ctx, "outbound packet connection to ", destination)
if !e.ready() || !e.client.Ready() { if !e.ready() || !e.client.Ready() {
return nil, netip.Addr{}, E.New("OpenConnect client is not ready yet") return nil, netip.Addr{}, E.New("endpoint is not ready yet")
} }
if destination.IsDomain() { if destination.IsDomain() {
destinationAddresses, err := e.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{}) destinationAddresses, err := e.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{})
@@ -522,7 +585,12 @@ func (e *Endpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (n
} }
func (e *Endpoint) PreferredDomain(metadata *adapter.InboundContext, domain string) bool { func (e *Endpoint) PreferredDomain(metadata *adapter.InboundContext, domain string) bool {
return false state := e.state.Load()
if !state.started || !state.tunnelConfigured || !e.client.Ready() {
return false
}
canonicalDomain := canonicalOpenConnectDomain(domain)
return openConnectDomainMatchesAny(canonicalDomain, state.preferredDomains)
} }
func (e *Endpoint) PreferredAddress(metadata *adapter.InboundContext, address netip.Addr) bool { func (e *Endpoint) PreferredAddress(metadata *adapter.InboundContext, address netip.Addr) bool {
+373
View File
@@ -0,0 +1,373 @@
package openconnect
import (
"context"
"net/netip"
"os"
"strings"
"sync"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/dns/transport"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
openconnecttransport "github.com/sagernet/sing-box/transport/openconnect"
"github.com/sagernet/sing/common"
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"
"github.com/sagernet/sing/service"
mDNS "github.com/miekg/dns"
)
func RegisterDNSTransport(registry *dns.TransportRegistry) {
dns.RegisterTransport[option.OpenConnectDNSServerOptions](registry, C.DNSTypeOpenConnect, NewDNSTransport)
}
type DNSTransport struct {
dns.TransportAdapter
logger logger.ContextLogger
endpointTag string
acceptDefaultResolvers bool
acceptSearchDomain bool
endpointManager adapter.EndpointManager
endpoint *Endpoint
dialer N.Dialer
access sync.RWMutex
closed bool
routes []openConnectDNSRoute
searchDomains []string
defaultResolvers []adapter.DNSTransport
}
type openConnectDNSRoute struct {
domain string
resolvers []adapter.DNSTransport
}
func NewDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.OpenConnectDNSServerOptions) (adapter.DNSTransport, error) {
if options.Endpoint == "" {
return nil, E.New("missing endpoint tag")
}
return &DNSTransport{
TransportAdapter: dns.NewTransportAdapter(C.DNSTypeOpenConnect, tag, nil),
logger: logger,
endpointTag: options.Endpoint,
acceptDefaultResolvers: options.AcceptDefaultResolvers,
acceptSearchDomain: options.AcceptSearchDomain,
endpointManager: service.FromContext[adapter.EndpointManager](ctx),
}, nil
}
func (t *DNSTransport) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateInitialize {
return nil
}
rawEndpoint, loaded := t.endpointManager.Get(t.endpointTag)
if !loaded {
return E.New("endpoint not found: ", t.endpointTag)
}
openConnectEndpoint, isOpenConnect := rawEndpoint.(*Endpoint)
if !isOpenConnect {
return E.New("endpoint is not OpenConnect: ", t.endpointTag)
}
openConnectEndpoint.dnsTransportAccess.Lock()
if openConnectEndpoint.dnsTransport != nil && openConnectEndpoint.dnsTransport.Tag() != t.Tag() {
openConnectEndpoint.dnsTransportAccess.Unlock()
return E.New("only one DNS server is allowed for an endpoint")
}
openConnectEndpoint.dnsTransport = t
t.endpoint = openConnectEndpoint
t.dialer = openConnectEndpoint
state := openConnectEndpoint.state.Load()
if state.started && state.tunnelConfigured && openConnectEndpoint.client.Ready() {
t.updateConfiguration(state.configuration)
}
openConnectEndpoint.dnsTransportAccess.Unlock()
return nil
}
func (t *DNSTransport) updateConfiguration(configuration openconnecttransport.Configuration) {
resolverByAddress := make(map[netip.Addr]adapter.DNSTransport)
resolverFor := func(address netip.Addr) adapter.DNSTransport {
if !address.IsValid() {
return nil
}
resolver, loaded := resolverByAddress[address]
if loaded {
return resolver
}
resolver = transport.NewUDPRaw(
t.logger,
dns.NewTransportAdapter(C.DNSTypeUDP, t.Tag()+"/"+address.String(), nil),
t.dialer,
M.SocksaddrFrom(address, 53),
)
resolverByAddress[address] = resolver
return resolver
}
resolversFor := func(addresses []netip.Addr) []adapter.DNSTransport {
resolvers := make([]adapter.DNSTransport, 0, len(addresses))
resolverSet := make(map[adapter.DNSTransport]bool)
for _, address := range addresses {
resolver := resolverFor(address)
if resolver != nil && !resolverSet[resolver] {
resolverSet[resolver] = true
resolvers = append(resolvers, resolver)
}
}
return resolvers
}
defaultResolvers := resolversFor(configuration.DNS)
routes := make([]openConnectDNSRoute, 0, len(configuration.SplitDNS)+len(configuration.SearchDomains)+len(configuration.SplitDNSRules))
routeIndex := make(map[string]int)
for _, rule := range configuration.SplitDNSRules {
resolvers := resolversFor(rule.Servers)
for _, domain := range rule.Domains {
canonicalDomain := canonicalOpenConnectDomain(domain)
if canonicalDomain != "" {
fqdn := mDNS.Fqdn(canonicalDomain)
index, loaded := routeIndex[fqdn]
if loaded {
resolverSet := make(map[adapter.DNSTransport]bool)
for _, resolver := range routes[index].resolvers {
resolverSet[resolver] = true
}
for _, resolver := range resolvers {
if !resolverSet[resolver] {
routes[index].resolvers = append(routes[index].resolvers, resolver)
}
}
} else {
routeIndex[fqdn] = len(routes)
routes = append(routes, openConnectDNSRoute{domain: fqdn, resolvers: resolvers})
}
}
}
}
for _, domain := range append(append([]string(nil), configuration.SplitDNS...), configuration.SearchDomains...) {
canonicalDomain := canonicalOpenConnectDomain(domain)
if canonicalDomain != "" {
fqdn := mDNS.Fqdn(canonicalDomain)
_, loaded := routeIndex[fqdn]
if !loaded {
routeIndex[fqdn] = len(routes)
routes = append(routes, openConnectDNSRoute{domain: fqdn, resolvers: defaultResolvers})
}
}
}
searchDomains := make([]string, 0, len(configuration.SearchDomains))
searchDomainSet := make(map[string]bool)
for _, domain := range configuration.SearchDomains {
canonicalDomain := canonicalOpenConnectDomain(domain)
if canonicalDomain != "" {
fqdn := mDNS.Fqdn(canonicalDomain)
if !searchDomainSet[fqdn] {
searchDomainSet[fqdn] = true
searchDomains = append(searchDomains, fqdn)
}
}
}
if !t.acceptDefaultResolvers || !configuration.TunnelAllDNS && (len(configuration.SplitDNS) > 0 || len(configuration.SplitDNSRules) > 0) {
defaultResolvers = nil
}
t.access.Lock()
if t.closed {
t.access.Unlock()
for _, resolver := range resolverByAddress {
_ = resolver.Close()
}
return
}
oldResolvers := t.collectResolversLocked()
t.routes = routes
t.searchDomains = searchDomains
t.defaultResolvers = defaultResolvers
activeResolvers := t.collectResolversLocked()
t.access.Unlock()
for _, resolver := range oldResolvers {
_ = resolver.Close()
}
activeResolverSet := make(map[adapter.DNSTransport]bool, len(activeResolvers))
for _, resolver := range activeResolvers {
activeResolverSet[resolver] = true
}
for _, resolver := range resolverByAddress {
if !activeResolverSet[resolver] {
_ = resolver.Close()
}
}
if len(resolverByAddress) > 0 {
t.logger.Info("updated ", len(routes), " DNS routes and ", len(resolverByAddress), " resolvers")
} else {
t.logger.Info("cleared DNS configuration")
}
}
func (t *DNSTransport) Reset() {
t.access.RLock()
resolvers := t.collectResolversLocked()
t.access.RUnlock()
for _, resolver := range resolvers {
resolver.Reset()
}
}
func (t *DNSTransport) Close() error {
if t.endpoint != nil {
t.endpoint.dnsTransportAccess.Lock()
if t.endpoint.dnsTransport == t {
t.endpoint.dnsTransport = nil
}
t.endpoint.dnsTransportAccess.Unlock()
}
t.access.Lock()
resolvers := t.collectResolversLocked()
t.closed = true
t.routes = nil
t.searchDomains = nil
t.defaultResolvers = nil
t.access.Unlock()
var closeErr error
for _, resolver := range resolvers {
closeErr = E.Errors(closeErr, resolver.Close())
}
return closeErr
}
func (t *DNSTransport) PreferredDomain(domain string) bool {
canonicalDomain := mDNS.Fqdn(canonicalOpenConnectDomain(domain))
t.access.RLock()
routes := t.routes
t.access.RUnlock()
for _, route := range routes {
if mDNS.IsSubDomain(route.domain, canonicalDomain) {
return true
}
}
return false
}
func (t *DNSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
done := make(chan struct{})
var response *mDNS.Msg
var err error
t.ExchangeAsync(ctx, message, func(callbackResponse *mDNS.Msg, callbackErr error) {
response = callbackResponse
err = callbackErr
close(done)
})
<-done
return response, err
}
func (t *DNSTransport) ExchangeAsync(ctx context.Context, message *mDNS.Msg, callback func(response *mDNS.Msg, err error)) {
if len(message.Question) != 1 {
callback(nil, os.ErrInvalid)
return
}
t.access.RLock()
searchDomains := append([]string(nil), t.searchDomains...)
t.access.RUnlock()
if t.acceptSearchDomain && len(searchDomains) > 0 && mDNS.CountLabel(message.Question[0].Name) == 1 {
t.exchangeWithSearchDomains(ctx, message, searchDomains, callback)
return
}
t.exchangeOnce(ctx, message, callback)
}
func (t *DNSTransport) exchangeWithSearchDomains(ctx context.Context, message *mDNS.Msg, searchDomains []string, callback func(response *mDNS.Msg, err error)) {
originalQuestion := message.Question[0]
singleLabel := strings.TrimSuffix(originalQuestion.Name, ".")
exchangers := make([]transport.AsyncExchanger, 0, len(searchDomains)+1)
for _, searchDomain := range searchDomains {
expandedName := singleLabel + "." + searchDomain
exchangers = append(exchangers, func(exchangeCtx context.Context, exchangeCallback func(response *mDNS.Msg, err error)) {
question := originalQuestion
question.Name = expandedName
rewritten := *message
rewritten.Question = []mDNS.Question{question}
t.exchangeOnce(exchangeCtx, &rewritten, func(response *mDNS.Msg, err error) {
if err == nil {
restoreOpenConnectDNSQuestion(response, expandedName, originalQuestion)
}
exchangeCallback(response, err)
})
})
}
exchangers = append(exchangers, func(exchangeCtx context.Context, exchangeCallback func(response *mDNS.Msg, err error)) {
t.exchangeOnce(exchangeCtx, message, exchangeCallback)
})
transport.ExchangeSequential(ctx, exchangers, func(response *mDNS.Msg, err error) bool {
return err == nil && response.Rcode != mDNS.RcodeNameError
}, callback)
}
func restoreOpenConnectDNSQuestion(response *mDNS.Msg, expandedName string, originalQuestion mDNS.Question) {
response.Question = []mDNS.Question{originalQuestion}
for _, record := range response.Answer {
if strings.EqualFold(record.Header().Name, expandedName) {
record.Header().Name = originalQuestion.Name
}
}
}
func (t *DNSTransport) exchangeOnce(ctx context.Context, message *mDNS.Msg, callback func(response *mDNS.Msg, err error)) {
question := message.Question[0]
t.access.RLock()
routes := t.routes
defaultResolvers := t.defaultResolvers
t.access.RUnlock()
var matchedResolvers []adapter.DNSTransport
matchedDomainLength := -1
for _, route := range routes {
if len(route.domain) > matchedDomainLength && mDNS.IsSubDomain(route.domain, question.Name) {
matchedDomainLength = len(route.domain)
matchedResolvers = route.resolvers
}
}
if matchedDomainLength != -1 {
if len(matchedResolvers) == 0 {
callback(nil, dns.RcodeNameError)
return
}
transport.ExchangeSequential(ctx, openConnectDNSExchangers(matchedResolvers, message), nil, callback)
return
}
if len(defaultResolvers) == 0 {
callback(nil, dns.RcodeNameError)
return
}
transport.ExchangeSequential(ctx, openConnectDNSExchangers(defaultResolvers, message), nil, callback)
}
func openConnectDNSExchangers(resolvers []adapter.DNSTransport, message *mDNS.Msg) []transport.AsyncExchanger {
return common.Map(resolvers, func(resolver adapter.DNSTransport) transport.AsyncExchanger {
return func(ctx context.Context, callback func(response *mDNS.Msg, err error)) {
resolver.ExchangeAsync(ctx, message, callback)
}
})
}
func (t *DNSTransport) collectResolversLocked() []adapter.DNSTransport {
resolverSet := make(map[adapter.DNSTransport]bool)
for _, route := range t.routes {
for _, resolver := range route.resolvers {
resolverSet[resolver] = true
}
}
for _, resolver := range t.defaultResolvers {
resolverSet[resolver] = true
}
resolvers := make([]adapter.DNSTransport, 0, len(resolverSet))
for resolver := range resolverSet {
resolvers = append(resolvers, resolver)
}
return resolvers
}
+92
View File
@@ -133,6 +133,55 @@ func configurationFromClientEvent(event openconnect.TunnelConfigurationEvent) op
Metric: route.Metric, Metric: route.Metric,
} }
}) })
if configuration.RemoteAddress.IsValid() {
remoteAddress := configuration.RemoteAddress.Unmap()
if remoteAddress.Is6() {
remoteAddress = remoteAddress.WithZone("")
}
remoteAddressExcluded := false
for _, route := range excludedRoutes {
if route.Prefix.Contains(remoteAddress) {
remoteAddressExcluded = true
break
}
}
if !remoteAddressExcluded {
excludedRoutes = append(excludedRoutes, openconnecttransport.Route{
Prefix: netip.PrefixFrom(remoteAddress, remoteAddress.BitLen()),
})
}
}
dnsAddresses := append([]netip.Addr(nil), configuration.DNS...)
for _, rule := range configuration.SplitDNSRules {
dnsAddresses = append(dnsAddresses, rule.Servers...)
}
for _, dnsAddress := range dnsAddresses {
if !dnsAddress.IsValid() {
continue
}
dnsAddressExcluded := false
for _, route := range excludedRoutes {
if route.Prefix.Contains(dnsAddress) {
dnsAddressExcluded = true
break
}
}
if dnsAddressExcluded {
continue
}
dnsAddressIncluded := false
for _, route := range routes {
if route.Prefix.Contains(dnsAddress) {
dnsAddressIncluded = true
break
}
}
if !dnsAddressIncluded {
routes = append(routes, openconnecttransport.Route{
Prefix: netip.PrefixFrom(dnsAddress, dnsAddress.BitLen()),
})
}
}
splitDNSRules := common.Map(configuration.SplitDNSRules, func(rule openconnect.TunnelSplitDNSRule) openconnecttransport.SplitDNSRule { splitDNSRules := common.Map(configuration.SplitDNSRules, func(rule openconnect.TunnelSplitDNSRule) openconnecttransport.SplitDNSRule {
return openconnecttransport.SplitDNSRule{ return openconnecttransport.SplitDNSRule{
Domains: rule.Domains, Domains: rule.Domains,
@@ -168,3 +217,46 @@ func buildIPSet(routes []openconnecttransport.Route, excludedRoutes []openconnec
} }
return builder.IPSet() return builder.IPSet()
} }
func buildPreferredDomains(configuration openconnecttransport.Configuration) map[string]bool {
preferredDomains := make(map[string]bool)
for _, domain := range configuration.SearchDomains {
canonicalDomain := canonicalOpenConnectDomain(domain)
if canonicalDomain != "" {
preferredDomains[canonicalDomain] = true
}
}
for _, domain := range configuration.SplitDNS {
canonicalDomain := canonicalOpenConnectDomain(domain)
if canonicalDomain != "" {
preferredDomains[canonicalDomain] = true
}
}
for _, rule := range configuration.SplitDNSRules {
for _, domain := range rule.Domains {
canonicalDomain := canonicalOpenConnectDomain(domain)
if canonicalDomain != "" {
preferredDomains[canonicalDomain] = true
}
}
}
return preferredDomains
}
func canonicalOpenConnectDomain(domain string) string {
return strings.ToLower(strings.Trim(strings.TrimSpace(domain), "."))
}
func openConnectDomainMatchesAny(domain string, suffixes map[string]bool) bool {
for domain != "" {
if suffixes[domain] {
return true
}
dotIndex := strings.IndexByte(domain, '.')
if dotIndex == -1 {
break
}
domain = domain[dotIndex+1:]
}
return false
}
+279 -68
View File
@@ -4,6 +4,8 @@ import (
"context" "context"
"net" "net"
"net/netip" "net/netip"
"slices"
"strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
@@ -49,6 +51,7 @@ type ClientEndpoint struct {
device ovpntransport.Device device ovpntransport.Device
stateAccess sync.Mutex stateAccess sync.Mutex
state atomic.Pointer[clientState] state atomic.Pointer[clientState]
dnsTransport *DNSTransport
deviceStarted bool deviceStarted bool
readLoopDone chan struct{} readLoopDone chan struct{}
statusAccess sync.Mutex statusAccess sync.Mutex
@@ -63,6 +66,8 @@ type clientState struct {
localAddresses []netip.Prefix localAddresses []netip.Prefix
routeSet *netipx.IPSet routeSet *netipx.IPSet
blockIPv6 bool blockIPv6 bool
configuration ovpntransport.Configuration
preferredDomains []string
tunnelInfo adapter.OpenVPNTunnelInfo tunnelInfo adapter.OpenVPNTunnelInfo
} }
@@ -149,8 +154,14 @@ func NewClientEndpoint(ctx context.Context, router adapter.Router, logger log.Co
} }
func (c *ClientEndpoint) buildClientOptions(options option.OpenVPNClientEndpointOptions) (ovpn.ClientOptions, error) { func (c *ClientEndpoint) buildClientOptions(options option.OpenVPNClientEndpointOptions) (ovpn.ClientOptions, error) {
if options.TLS == nil { mode := options.Mode
return ovpn.ClientOptions{}, E.New("missing `tls` options") if mode == "" {
mode = ovpn.ModeTLS
}
switch mode {
case ovpn.ModeTLS, ovpn.ModeStaticKey:
default:
return ovpn.ClientOptions{}, E.New("unsupported mode: ", mode, " (expected \"tls\" or \"static_key\")")
} }
if options.Server != "" && len(options.Servers) > 0 { if options.Server != "" && len(options.Servers) > 0 {
return ovpn.ClientOptions{}, E.New("`server` is conflict with `servers`") return ovpn.ClientOptions{}, E.New("`server` is conflict with `servers`")
@@ -158,6 +169,26 @@ func (c *ClientEndpoint) buildClientOptions(options option.OpenVPNClientEndpoint
if options.Server == "" && len(options.Servers) == 0 { if options.Server == "" && len(options.Servers) == 0 {
return ovpn.ClientOptions{}, E.New("missing `server` or `servers`") return ovpn.ClientOptions{}, E.New("missing `server` or `servers`")
} }
protocol, remotes := buildClientRemoteOptions(options)
tunnelOptions, err := buildClientTunnelOptions(options, mode == ovpn.ModeStaticKey)
if err != nil {
return ovpn.ClientOptions{}, err
}
if mode == ovpn.ModeStaticKey {
return c.buildStaticKeyClientOptions(options, protocol, remotes, tunnelOptions)
}
if options.TLS == nil {
return ovpn.ClientOptions{}, E.New("missing `tls` options")
}
if len(options.StaticKey) > 0 || options.StaticKeyPath != "" {
return ovpn.ClientOptions{}, E.New("`static_key` and `static_key_path` are only supported in `static_key` mode")
}
if options.KeyDirection != "" {
return ovpn.ClientOptions{}, E.New("`key_direction` is only supported in `static_key` mode; use `tls.control_wrap.direction` for `tls_auth`")
}
if options.Cipher != "" {
return ovpn.ClientOptions{}, E.New("`cipher` is only supported in `static_key` mode; use `data_ciphers` or `data_ciphers_fallback` in TLS mode")
}
certificateAuthority, err := materialSource("tls.certificate", options.TLS.Certificate, options.TLS.CertificatePath) certificateAuthority, err := materialSource("tls.certificate", options.TLS.Certificate, options.TLS.CertificatePath)
if err != nil { if err != nil {
return ovpn.ClientOptions{}, err return ovpn.ClientOptions{}, err
@@ -198,34 +229,9 @@ func (c *ClientEndpoint) buildClientOptions(options option.OpenVPNClientEndpoint
} }
controlCryptV2 = controlKey controlCryptV2 = controlKey
case "": case "":
return ovpn.ClientOptions{}, E.New("missing OpenVPN control wrap type") return ovpn.ClientOptions{}, E.New("missing control wrap type")
default: default:
return ovpn.ClientOptions{}, E.New("unknown OpenVPN control wrap type: ", controlWrap.Type) return ovpn.ClientOptions{}, E.New("unknown control wrap type: ", controlWrap.Type)
}
}
protocol := options.Network
if protocol == "" {
protocol = N.NetworkUDP
}
var remotes []ovpn.Remote
if options.Server != "" {
remotes = append(remotes, ovpn.Remote{
Host: options.Server,
Port: options.ServerPort,
Protocol: protocol,
})
} else {
remotes = make([]ovpn.Remote, 0, len(options.Servers))
for _, remoteOptions := range options.Servers {
remoteProtocol := remoteOptions.Network
if remoteProtocol == "" {
remoteProtocol = protocol
}
remotes = append(remotes, ovpn.Remote{
Host: remoteOptions.Server,
Port: remoteOptions.ServerPort,
Protocol: remoteProtocol,
})
} }
} }
pullFilters := common.Map(options.PullFilters, func(filterOptions option.OpenVPNPullFilterOptions) ovpn.PullFilter { pullFilters := common.Map(options.PullFilters, func(filterOptions option.OpenVPNPullFilterOptions) ovpn.PullFilter {
@@ -234,9 +240,6 @@ func (c *ClientEndpoint) buildClientOptions(options option.OpenVPNClientEndpoint
Text: filterOptions.Text, Text: filterOptions.Text,
} }
}) })
tunnelRoutes := common.Map(options.Routes, func(route netip.Prefix) ovpn.TunnelRoute {
return ovpn.TunnelRoute{Prefix: route}
})
remoteCertificateTLS := options.TLS.RemoteCertificateTLS remoteCertificateTLS := options.TLS.RemoteCertificateTLS
switch remoteCertificateTLS { switch remoteCertificateTLS {
case "", "server", "client", "none": case "", "server", "client", "none":
@@ -264,6 +267,7 @@ func (c *ClientEndpoint) buildClientOptions(options option.OpenVPNClientEndpoint
RemoteCertificateKU: options.TLS.RemoteCertificateKU, RemoteCertificateKU: options.TLS.RemoteCertificateKU,
RemoteCertificateEKU: options.TLS.RemoteCertificateEKU, RemoteCertificateEKU: options.TLS.RemoteCertificateEKU,
RemoteCertificateTLS: remoteCertificateTLS, RemoteCertificateTLS: remoteCertificateTLS,
NSCertificateType: options.TLS.NSCertificateType,
VersionMin: options.TLS.VersionMin, VersionMin: options.TLS.VersionMin,
VersionMax: options.TLS.VersionMax, VersionMax: options.TLS.VersionMax,
CertificateProfile: options.TLS.CertificateProfile, CertificateProfile: options.TLS.CertificateProfile,
@@ -278,7 +282,7 @@ func (c *ClientEndpoint) buildClientOptions(options option.OpenVPNClientEndpoint
} }
return ovpn.ClientOptions{ return ovpn.ClientOptions{
Context: c.loopContext, Context: c.loopContext,
Mode: ovpn.ModeTLS, Mode: mode,
Transport: ovpn.ClientTransportOptions{ Transport: ovpn.ClientTransportOptions{
Remotes: remotes, Remotes: remotes,
RemoteRandom: options.RemoteRandom, RemoteRandom: options.RemoteRandom,
@@ -286,19 +290,8 @@ func (c *ClientEndpoint) buildClientOptions(options option.OpenVPNClientEndpoint
ExplicitExitNotify: options.ExplicitExitNotify, ExplicitExitNotify: options.ExplicitExitNotify,
DialContextWithAddressIndex: c.transportDialContextWithAddressIndex, DialContextWithAddressIndex: c.transportDialContextWithAddressIndex,
}, },
DataChannel: ovpn.ClientDataChannelOptions{ DataChannel: buildClientDataChannelOptions(options),
MTU: options.MTU, TLS: clientTLSOptions,
MSSFix: options.MSSFix,
Fragment: options.Fragment,
Ciphers: options.DataCiphers,
FallbackCipher: options.DataCiphersFallback,
Auth: options.Auth,
Compression: options.Compression,
CompressionLZO: options.CompressionLZO,
AllowCompression: options.AllowCompression,
PacketHeadroom: ovpntransport.PacketHeadroom,
},
TLS: clientTLSOptions,
Authentication: ovpn.ClientAuthenticationOptions{ Authentication: ovpn.ClientAuthenticationOptions{
Username: options.Username, Username: options.Username,
Password: options.Password, Password: options.Password,
@@ -311,25 +304,176 @@ func (c *ClientEndpoint) buildClientOptions(options option.OpenVPNClientEndpoint
Filters: pullFilters, Filters: pullFilters,
RouteNoPull: options.RouteNoPull, RouteNoPull: options.RouteNoPull,
}, },
Tunnel: ovpn.ClientTunnelOptions{ Tunnel: tunnelOptions,
DevType: "tun", Timing: buildClientTimingOptions(options),
RedirectGateway: options.RedirectGateway,
RedirectGatewayFlags: options.RedirectGatewayFlags,
RouteMetric: options.RouteMetric,
RouteGateway: options.RouteGateway.Build(netip.Addr{}),
Routes: tunnelRoutes,
},
Timing: ovpn.ClientTimingOptions{
RenegotiationInterval: time.Duration(options.RenegotiateInterval),
PingInterval: time.Duration(options.PingInterval),
PingRestart: time.Duration(options.PingRestart),
},
KeyDirection: keyDirection, KeyDirection: keyDirection,
OnTunnelConfiguration: c.handleTunnelConfiguration, OnTunnelConfiguration: c.handleTunnelConfiguration,
Logger: c.logger, Logger: c.logger,
}, nil }, nil
} }
func (c *ClientEndpoint) buildStaticKeyClientOptions(options option.OpenVPNClientEndpointOptions, protocol string, remotes []ovpn.Remote, tunnelOptions ovpn.ClientTunnelOptions) (ovpn.ClientOptions, error) {
if options.TLS != nil {
return ovpn.ClientOptions{}, E.New("`tls` options are not supported in `static_key` mode")
}
if options.Username != "" || options.Password != "" || (options.AuthRetry != "" && options.AuthRetry != "none") || options.StaticChallenge != "" || options.StaticChallengeEcho {
return ovpn.ClientOptions{}, E.New("username/password authentication is not supported in `static_key` mode")
}
if options.RouteNoPull || len(options.PullFilters) > 0 {
return ovpn.ClientOptions{}, E.New("pull options are not supported in `static_key` mode")
}
if options.RenegotiateInterval != 0 || options.RenegotiateDisabled || options.RenegotiateBytes != 0 || options.RenegotiatePackets != 0 || options.TLSTimeout != 0 || options.HandshakeWindow != 0 {
return ovpn.ClientOptions{}, E.New("TLS timing and renegotiation options are not supported in `static_key` mode")
}
if len(options.DataCiphers) > 0 || options.DataCiphersFallback != "" {
return ovpn.ClientOptions{}, E.New("`data_ciphers` and `data_ciphers_fallback` are not supported in `static_key` mode; use `cipher`")
}
staticKey, err := requiredMaterialSource("static_key", options.StaticKey, options.StaticKeyPath)
if err != nil {
return ovpn.ClientOptions{}, err
}
keyDirection, err := keyDirectionValue(options.KeyDirection)
if err != nil {
return ovpn.ClientOptions{}, err
}
return ovpn.ClientOptions{
Context: c.loopContext,
Mode: ovpn.ModeStaticKey,
Transport: ovpn.ClientTransportOptions{
Remotes: remotes,
RemoteRandom: options.RemoteRandom,
Protocol: protocol,
ExplicitExitNotify: options.ExplicitExitNotify,
DialContextWithAddressIndex: c.transportDialContextWithAddressIndex,
},
DataChannel: buildClientDataChannelOptions(options),
Tunnel: tunnelOptions,
Timing: buildClientTimingOptions(options),
StaticKey: staticKey,
KeyDirection: keyDirection,
OnTunnelConfiguration: c.handleTunnelConfiguration,
Logger: c.logger,
}, nil
}
func buildClientRemoteOptions(options option.OpenVPNClientEndpointOptions) (string, []ovpn.Remote) {
protocol := options.Network
if protocol == "" {
protocol = N.NetworkUDP
}
if options.Server != "" {
return protocol, []ovpn.Remote{{
Host: options.Server,
Port: options.ServerPort,
Protocol: protocol,
}}
}
remotes := make([]ovpn.Remote, 0, len(options.Servers))
for _, remoteOptions := range options.Servers {
remoteProtocol := remoteOptions.Network
if remoteProtocol == "" {
remoteProtocol = protocol
}
remotes = append(remotes, ovpn.Remote{
Host: remoteOptions.Server,
Port: remoteOptions.ServerPort,
Protocol: remoteProtocol,
})
}
return protocol, remotes
}
func buildClientDataChannelOptions(options option.OpenVPNClientEndpointOptions) ovpn.ClientDataChannelOptions {
return ovpn.ClientDataChannelOptions{
MTU: options.MTU,
MSSFix: options.MSSFix,
MSSFixDisabled: options.MSSFixDisabled,
MSSFixMode: options.MSSFixMode,
Fragment: options.Fragment,
Cipher: options.Cipher,
Ciphers: options.DataCiphers,
FallbackCipher: options.DataCiphersFallback,
Auth: options.Auth,
Compression: options.Compression,
CompressionLZO: options.CompressionLZO,
AllowCompression: options.AllowCompression,
ReplayWindow: options.ReplayWindow,
ReplayWindowTime: time.Duration(options.ReplayWindowTime),
PacketHeadroom: ovpntransport.PacketHeadroom,
}
}
func buildClientTunnelOptions(options option.OpenVPNClientEndpointOptions, requirePeerAddress bool) (ovpn.ClientTunnelOptions, error) {
vpnGateway := netip.Addr(options.PeerAddress)
if vpnGateway.IsValid() && !vpnGateway.Is4() {
return ovpn.ClientTunnelOptions{}, E.New("`peer_address` must be an IPv4 address")
}
vpnGatewayIPv6 := netip.Addr(options.PeerAddressIPv6)
if vpnGatewayIPv6.IsValid() && !vpnGatewayIPv6.Is6() {
return ovpn.ClientTunnelOptions{}, E.New("`peer_address_ipv6` must be an IPv6 address")
}
var hasIPv4 bool
var hasIPv6 bool
for addressIndex, address := range options.Address {
if !address.IsValid() {
return ovpn.ClientTunnelOptions{}, E.New("`address[", addressIndex, "]` is invalid")
}
if address.Addr().Is4() {
hasIPv4 = true
} else {
hasIPv6 = true
}
}
if requirePeerAddress {
if len(options.Address) == 0 {
return ovpn.ClientTunnelOptions{}, E.New("missing `address` in `static_key` mode")
}
if hasIPv4 && !vpnGateway.IsValid() {
return ovpn.ClientTunnelOptions{}, E.New("missing `peer_address` for the IPv4 tunnel address in `static_key` mode")
}
if hasIPv6 && !vpnGatewayIPv6.IsValid() {
return ovpn.ClientTunnelOptions{}, E.New("missing `peer_address_ipv6` for the IPv6 tunnel address in `static_key` mode")
}
if vpnGateway.IsValid() && !hasIPv4 {
return ovpn.ClientTunnelOptions{}, E.New("`peer_address` requires an IPv4 tunnel `address` in `static_key` mode")
}
if vpnGatewayIPv6.IsValid() && !hasIPv6 {
return ovpn.ClientTunnelOptions{}, E.New("`peer_address_ipv6` requires an IPv6 tunnel `address` in `static_key` mode")
}
}
tunnelRoutes := common.Map(options.Routes, func(route netip.Prefix) ovpn.TunnelRoute {
return ovpn.TunnelRoute{Prefix: route}
})
return ovpn.ClientTunnelOptions{
DevType: "tun",
Topology: options.Topology,
RedirectGateway: options.RedirectGateway,
RedirectGatewayFlags: options.RedirectGatewayFlags,
RedirectPrivate: options.RedirectPrivate,
BlockIPv6: options.BlockIPv6,
RouteMetric: options.RouteMetric,
RouteGateway: options.RouteGateway.Build(netip.Addr{}),
Routes: tunnelRoutes,
LocalAddress: options.Address,
VPNGateway: vpnGateway,
VPNGatewayIPv6: vpnGatewayIPv6,
}, nil
}
func buildClientTimingOptions(options option.OpenVPNClientEndpointOptions) ovpn.ClientTimingOptions {
return ovpn.ClientTimingOptions{
RenegotiationInterval: time.Duration(options.RenegotiateInterval),
RenegotiationDisabled: options.RenegotiateDisabled,
RenegotiationBytes: options.RenegotiateBytes,
RenegotiationPackets: options.RenegotiatePackets,
PingInterval: time.Duration(options.PingInterval),
PingRestart: time.Duration(options.PingRestart),
PingRestartDisabled: options.PingRestartDisabled,
TLSTimeout: time.Duration(options.TLSTimeout),
HandWindow: time.Duration(options.HandshakeWindow),
}
}
func (c *ClientEndpoint) transportDialContextWithAddressIndex(ctx context.Context, network string, address string, addressIndex int) (net.Conn, error) { func (c *ClientEndpoint) transportDialContextWithAddressIndex(ctx context.Context, network string, address string, addressIndex int) (net.Conn, error) {
destination := M.ParseSocksaddr(address) destination := M.ParseSocksaddr(address)
if destination.IsDomain() { if destination.IsDomain() {
@@ -361,33 +505,51 @@ func (c *ClientEndpoint) transportDialContextWithAddressIndex(ctx context.Contex
} }
func (c *ClientEndpoint) handleTunnelConfiguration(event ovpn.TunnelConfigurationEvent) error { func (c *ClientEndpoint) handleTunnelConfiguration(event ovpn.TunnelConfigurationEvent) error {
configuration := configurationFromClientEvent(event, c.logger)
defer c.notifyStatusUpdated() defer c.notifyStatusUpdated()
c.stateAccess.Lock() c.stateAccess.Lock()
defer c.stateAccess.Unlock() configuration := configurationFromClientEvent(event, c.logger)
c.updateState(func(state *clientState) { c.updateState(func(state *clientState) {
state.tunnelConfigured = false state.tunnelConfigured = false
}) })
err := c.device.UpdateConfiguration(configuration) deviceConfiguration := ovpntransport.Configuration{
MTU: configuration.MTU,
Address: configuration.Address,
BlockIPv6: configuration.BlockIPv6,
}
err := c.device.UpdateConfiguration(deviceConfiguration)
if err != nil { if err != nil {
c.stateAccess.Unlock()
return E.Cause(err, "update device configuration") return E.Cause(err, "update device configuration")
} }
if !c.deviceStarted { if !c.deviceStarted {
err = c.device.Start() err = c.device.Start()
if err != nil { if err != nil {
c.stateAccess.Unlock()
return E.Cause(err, "start device") return E.Cause(err, "start device")
} }
c.deviceStarted = true c.deviceStarted = true
} }
routeSet, err := buildIPSet(configuration.Routes) routeSet, err := buildIPSet(configuration.Routes, configuration.ExcludedRoutes)
if err != nil { if err != nil {
c.stateAccess.Unlock()
return E.Cause(err, "build route set") return E.Cause(err, "build route set")
} }
preferredDomains := slices.Clone(configuration.DNSRoutes)
preferredDomains = append(preferredDomains, configuration.SearchDomains...)
if len(configuration.DNSServers) > 0 {
servers := slices.Clone(configuration.DNSServers)
slices.SortFunc(servers, func(left ovpntransport.DNSServer, right ovpntransport.DNSServer) int {
return left.Priority - right.Priority
})
preferredDomains = append(preferredDomains, servers[0].ResolveDomains...)
}
c.updateState(func(state *clientState) { c.updateState(func(state *clientState) {
state.tunnelConfigured = true state.tunnelConfigured = true
state.localAddresses = configuration.Address state.localAddresses = configuration.Address
state.routeSet = routeSet state.routeSet = routeSet
state.blockIPv6 = configuration.BlockIPv6 state.blockIPv6 = configuration.BlockIPv6
state.configuration = configuration
state.preferredDomains = preferredDomains
state.tunnelInfo.Cipher = event.Configuration.SelectedCipher state.tunnelInfo.Cipher = event.Configuration.SelectedCipher
state.tunnelInfo.IPv4 = event.Configuration.LocalIPv4 state.tunnelInfo.IPv4 = event.Configuration.LocalIPv4
state.tunnelInfo.IPv6 = event.Configuration.LocalIPv6 state.tunnelInfo.IPv6 = event.Configuration.LocalIPv6
@@ -397,6 +559,11 @@ func (c *ClientEndpoint) handleTunnelConfiguration(event ovpn.TunnelConfiguratio
state.tunnelInfo.ConnectedSince = time.Now() state.tunnelInfo.ConnectedSince = time.Now()
} }
}) })
dnsTransport := c.dnsTransport
c.stateAccess.Unlock()
if dnsTransport != nil {
dnsTransport.onReconfiguration(configuration)
}
return nil return nil
} }
@@ -406,6 +573,28 @@ func (c *ClientEndpoint) updateState(update func(state *clientState)) {
c.state.Store(&newState) c.state.Store(&newState)
} }
func (c *ClientEndpoint) installDNSTransport(dnsTransport *DNSTransport) error {
c.stateAccess.Lock()
defer c.stateAccess.Unlock()
if c.dnsTransport != nil && c.dnsTransport != dnsTransport && c.dnsTransport.Tag() != dnsTransport.Tag() {
return E.New("only one DNS server is allowed for an endpoint")
}
err := dnsTransport.updateResolvers(c.state.Load().configuration)
if err != nil {
return err
}
c.dnsTransport = dnsTransport
return nil
}
func (c *ClientEndpoint) uninstallDNSTransport(dnsTransport *DNSTransport) {
c.stateAccess.Lock()
if c.dnsTransport == dnsTransport {
c.dnsTransport = nil
}
c.stateAccess.Unlock()
}
func (c *ClientEndpoint) Start(stage adapter.StartStage) error { func (c *ClientEndpoint) Start(stage adapter.StartStage) error {
if stage != adapter.StartStatePostStart { if stage != adapter.StartStatePostStart {
return nil return nil
@@ -434,7 +623,7 @@ func (c *ClientEndpoint) readLoop() {
if E.IsClosedOrCanceled(err) || c.loopContext.Err() != nil { if E.IsClosedOrCanceled(err) || c.loopContext.Err() != nil {
return return
} }
c.logger.Error(E.Cause(err, "OpenVPN client terminated")) c.logger.Error(E.Cause(err, "client terminated"))
c.setTerminalError(err) c.setTerminalError(err)
return return
} }
@@ -509,7 +698,7 @@ func (c *ClientEndpoint) ready() bool {
func (c *ClientEndpoint) WritePackets(packets [][]byte) error { func (c *ClientEndpoint) WritePackets(packets [][]byte) error {
state := c.state.Load() state := c.state.Load()
if !state.started || !state.tunnelConfigured { if !state.started || !state.tunnelConfigured {
return E.New("OpenVPN client is not ready yet") return E.New("endpoint is not ready yet")
} }
if state.blockIPv6 { if state.blockIPv6 {
outboundPackets := packets[:0] outboundPackets := packets[:0]
@@ -529,7 +718,7 @@ func (c *ClientEndpoint) WritePackets(packets [][]byte) error {
} }
err := c.client.WriteDataPacketBuffers(packetBuffers) err := c.client.WriteDataPacketBuffers(packetBuffers)
if E.IsMulti(err, ovpn.ErrDataChannelNotReady) { if E.IsMulti(err, ovpn.ErrDataChannelNotReady) {
return E.New("OpenVPN client is not ready yet") return E.New("endpoint is not ready yet")
} }
return err return err
} }
@@ -577,7 +766,7 @@ func (c *ClientEndpoint) DialContext(ctx context.Context, network string, destin
c.logger.InfoContext(ctx, "outbound packet connection to ", destination) c.logger.InfoContext(ctx, "outbound packet connection to ", destination)
} }
if !c.ready() || !c.client.Ready() { if !c.ready() || !c.client.Ready() {
return nil, E.New("OpenVPN client is not ready yet") return nil, E.New("endpoint is not ready yet")
} }
if destination.IsDomain() { if destination.IsDomain() {
destinationAddresses, err := c.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{}) destinationAddresses, err := c.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{})
@@ -595,7 +784,7 @@ func (c *ClientEndpoint) DialContext(ctx context.Context, network string, destin
func (c *ClientEndpoint) ListenPacketWithDestination(ctx context.Context, destination M.Socksaddr) (net.PacketConn, netip.Addr, error) { func (c *ClientEndpoint) ListenPacketWithDestination(ctx context.Context, destination M.Socksaddr) (net.PacketConn, netip.Addr, error) {
c.logger.InfoContext(ctx, "outbound packet connection to ", destination) c.logger.InfoContext(ctx, "outbound packet connection to ", destination)
if !c.ready() || !c.client.Ready() { if !c.ready() || !c.client.Ready() {
return nil, netip.Addr{}, E.New("OpenVPN client is not ready yet") return nil, netip.Addr{}, E.New("endpoint is not ready yet")
} }
if destination.IsDomain() { if destination.IsDomain() {
destinationAddresses, err := c.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{}) destinationAddresses, err := c.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{})
@@ -626,6 +815,15 @@ func (c *ClientEndpoint) ListenPacket(ctx context.Context, destination M.Socksad
} }
func (c *ClientEndpoint) PreferredDomain(metadata *adapter.InboundContext, domain string) bool { func (c *ClientEndpoint) PreferredDomain(metadata *adapter.InboundContext, domain string) bool {
state := c.state.Load()
if !state.started || !state.tunnelConfigured || !c.client.Ready() {
return false
}
for _, preferredDomain := range state.preferredDomains {
if openVPNDomainMatches(preferredDomain, domain) {
return true
}
}
return false return false
} }
@@ -636,3 +834,16 @@ func (c *ClientEndpoint) PreferredAddress(metadata *adapter.InboundContext, addr
} }
return state.routeSet.Contains(address) return state.routeSet.Contains(address)
} }
func openVPNDomainMatches(suffix string, domain string) bool {
normalizedSuffix := strings.ToLower(strings.TrimSpace(suffix))
if normalizedSuffix == "." {
return true
}
normalizedSuffix = strings.TrimSuffix(normalizedSuffix, ".")
normalizedDomain := strings.TrimSuffix(strings.ToLower(strings.TrimSpace(domain)), ".")
if normalizedSuffix == "" {
return false
}
return normalizedDomain == normalizedSuffix || strings.HasSuffix(normalizedDomain, "."+normalizedSuffix)
}
+427
View File
@@ -0,0 +1,427 @@
package openvpn
import (
"context"
"net"
"net/http"
"net/netip"
"net/url"
"os"
"slices"
"strconv"
"strings"
"sync"
"github.com/sagernet/sing-box/adapter"
boxTLS "github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
boxDNS "github.com/sagernet/sing-box/dns"
dnsTransport "github.com/sagernet/sing-box/dns/transport"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
ovpntransport "github.com/sagernet/sing-box/transport/openvpn"
"github.com/sagernet/sing/common"
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"
"github.com/sagernet/sing/service"
mDNS "github.com/miekg/dns"
"golang.org/x/net/http2"
)
func RegisterDNSTransport(registry *boxDNS.TransportRegistry) {
boxDNS.RegisterTransport[option.OpenVPNDNSServerOptions](registry, C.DNSTypeOpenVPN, NewDNSTransport)
}
type DNSTransport struct {
boxDNS.TransportAdapter
ctx context.Context
logger logger.ContextLogger
endpointTag string
acceptDefaultResolvers bool
acceptSearchDomain bool
endpointManager adapter.EndpointManager
endpoint *ClientEndpoint
dialer N.Dialer
updateAccess sync.Mutex
access sync.RWMutex
closed bool
routes map[string][]adapter.DNSTransport
searchDomains []string
defaultResolvers []adapter.DNSTransport
}
func NewDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.OpenVPNDNSServerOptions) (adapter.DNSTransport, error) {
if options.Endpoint == "" {
return nil, E.New("missing endpoint tag")
}
return &DNSTransport{
TransportAdapter: boxDNS.NewTransportAdapter(C.DNSTypeOpenVPN, tag, nil),
ctx: ctx,
logger: logger,
endpointTag: options.Endpoint,
acceptDefaultResolvers: options.AcceptDefaultResolvers,
acceptSearchDomain: options.AcceptSearchDomain,
endpointManager: service.FromContext[adapter.EndpointManager](ctx),
}, nil
}
func (t *DNSTransport) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateInitialize {
return nil
}
rawEndpoint, loaded := t.endpointManager.Get(t.endpointTag)
if !loaded {
return E.New("endpoint not found: ", t.endpointTag)
}
endpoint, isOpenVPN := rawEndpoint.(*ClientEndpoint)
if !isOpenVPN {
return E.New("endpoint is not an OpenVPN client: ", t.endpointTag)
}
t.endpoint = endpoint
t.dialer = endpoint
err := endpoint.installDNSTransport(t)
if err != nil {
t.endpoint = nil
t.dialer = nil
return err
}
return nil
}
func (t *DNSTransport) onReconfiguration(configuration ovpntransport.Configuration) {
err := t.updateResolvers(configuration)
if err != nil && !E.IsClosed(err) {
t.logger.Error(E.Cause(err, "update DNS resolvers"))
}
}
func (t *DNSTransport) updateResolvers(configuration ovpntransport.Configuration) error {
t.updateAccess.Lock()
defer t.updateAccess.Unlock()
t.access.RLock()
closed := t.closed
t.access.RUnlock()
if closed {
return net.ErrClosed
}
routes := make(map[string][]adapter.DNSTransport)
searchDomains := normalizeOpenVPNDomains(configuration.SearchDomains)
var defaultResolvers []adapter.DNSTransport
var newResolvers []adapter.DNSTransport
servers := slices.Clone(configuration.DNSServers)
slices.SortFunc(servers, func(left ovpntransport.DNSServer, right ovpntransport.DNSServer) int {
return left.Priority - right.Priority
})
var selectedResolvers []adapter.DNSTransport
if len(servers) > 0 {
server := servers[0]
if server.DNSSEC == "yes" {
return t.failResolverUpdate(newResolvers, E.New("DNSSEC validation is required but is not supported"))
}
for _, address := range server.Addresses {
resolver, err := t.createResolver(server, address)
if err != nil {
return t.failResolverUpdate(newResolvers, err)
}
selectedResolvers = append(selectedResolvers, resolver)
newResolvers = append(newResolvers, resolver)
}
if len(selectedResolvers) == 0 {
return t.failResolverUpdate(newResolvers, E.New("DNS server ", server.Priority, " has no addresses"))
}
if len(server.ResolveDomains) == 0 {
defaultResolvers = slices.Clone(selectedResolvers)
} else {
for _, domain := range server.ResolveDomains {
normalizedDomain := normalizeOpenVPNDomain(domain)
if normalizedDomain != "" {
routes[normalizedDomain] = slices.Clone(selectedResolvers)
}
}
}
} else {
for _, address := range configuration.DNS {
resolver := dnsTransport.NewUDPRaw(t.logger, t.TransportAdapter, t.dialer, M.SocksaddrFrom(address, 53))
selectedResolvers = append(selectedResolvers, resolver)
newResolvers = append(newResolvers, resolver)
}
if len(configuration.DNSRoutes) > 0 {
if len(selectedResolvers) == 0 {
return t.failResolverUpdate(newResolvers, E.New("DOMAIN-ROUTE requires traditional pushed DNS servers"))
}
for _, domain := range configuration.DNSRoutes {
normalizedDomain := normalizeOpenVPNDomain(domain)
if normalizedDomain != "" {
routes[normalizedDomain] = slices.Clone(selectedResolvers)
}
}
} else {
defaultResolvers = slices.Clone(selectedResolvers)
}
}
if len(searchDomains) > 0 && len(selectedResolvers) == 0 {
return t.failResolverUpdate(newResolvers, E.New("search domains require pushed DNS servers"))
}
for _, searchDomain := range searchDomains {
routes[searchDomain] = slices.Clone(selectedResolvers)
}
t.access.Lock()
oldResolvers := t.collectResolversLocked()
t.routes = routes
t.searchDomains = searchDomains
t.defaultResolvers = defaultResolvers
t.access.Unlock()
closeErr := closeDNSTransports(oldResolvers)
t.logger.Info("updated ", len(routes), " DNS routes, ", len(searchDomains), " search domains and ", len(defaultResolvers), " default resolvers")
return closeErr
}
func (t *DNSTransport) failResolverUpdate(newResolvers []adapter.DNSTransport, updateErr error) error {
newCloseErr := closeDNSTransports(newResolvers)
t.access.Lock()
oldResolvers := t.collectResolversLocked()
t.routes = nil
t.searchDomains = nil
t.defaultResolvers = nil
t.access.Unlock()
oldCloseErr := closeDNSTransports(oldResolvers)
return E.Errors(updateErr, newCloseErr, oldCloseErr)
}
func (t *DNSTransport) createResolver(server ovpntransport.DNSServer, address netip.AddrPort) (adapter.DNSTransport, error) {
transportType := strings.ToLower(server.Transport)
if transportType == "" {
transportType = "plain"
}
port := address.Port()
switch transportType {
case "plain":
if port == 0 {
port = 53
}
return dnsTransport.NewUDPRaw(t.logger, t.TransportAdapter, t.dialer, M.SocksaddrFrom(address.Addr(), port)), nil
case "dot", "doh":
default:
return nil, E.New("unsupported DNS transport: ", server.Transport)
}
serverName := server.SNI
if serverName == "" {
serverName = address.Addr().String()
}
if transportType == "dot" {
if port == 0 {
port = 853
}
tlsConfig, err := boxTLS.NewClient(t.ctx, t.logger, serverName, option.OutboundTLSOptions{
Enabled: true,
ServerName: serverName,
})
if err != nil {
return nil, err
}
return dnsTransport.NewTLSRaw(t.logger, t.TransportAdapter, t.dialer, M.SocksaddrFrom(address.Addr(), port), tlsConfig), nil
}
if port == 0 {
port = 443
}
tlsConfig, err := boxTLS.NewClient(t.ctx, t.logger, serverName, option.OutboundTLSOptions{
Enabled: true,
ServerName: serverName,
ALPN: []string{http2.NextProtoTLS, "http/1.1"},
})
if err != nil {
return nil, err
}
host := serverName
if port != 443 {
host = net.JoinHostPort(host, strconv.Itoa(int(port)))
} else if strings.Contains(host, ":") {
host = "[" + host + "]"
}
destination := &url.URL{Scheme: "https", Host: host, Path: "/dns-query"}
return dnsTransport.NewHTTPSRaw(t.TransportAdapter, t.logger, t.dialer, destination, http.Header{}, M.SocksaddrFrom(address.Addr(), port), tlsConfig), nil
}
func (t *DNSTransport) Reset() {
t.access.RLock()
resolvers := t.collectResolversLocked()
t.access.RUnlock()
for _, resolver := range resolvers {
resolver.Reset()
}
}
func (t *DNSTransport) Close() error {
if t.endpoint != nil {
t.endpoint.uninstallDNSTransport(t)
}
t.updateAccess.Lock()
t.access.Lock()
resolvers := t.collectResolversLocked()
t.closed = true
t.routes = nil
t.searchDomains = nil
t.defaultResolvers = nil
t.access.Unlock()
t.endpoint = nil
t.dialer = nil
t.updateAccess.Unlock()
return closeDNSTransports(resolvers)
}
func (t *DNSTransport) Raw() bool {
return true
}
func (t *DNSTransport) PreferredDomain(domain string) bool {
t.access.RLock()
defer t.access.RUnlock()
for route := range t.routes {
if openVPNDomainMatches(route, domain) {
return true
}
}
return false
}
func (t *DNSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
done := make(chan struct{})
var response *mDNS.Msg
var err error
t.ExchangeAsync(ctx, message, func(callbackResponse *mDNS.Msg, callbackErr error) {
response = callbackResponse
err = callbackErr
close(done)
})
<-done
return response, err
}
func (t *DNSTransport) ExchangeAsync(ctx context.Context, message *mDNS.Msg, callback func(response *mDNS.Msg, err error)) {
if len(message.Question) != 1 {
callback(nil, os.ErrInvalid)
return
}
t.access.RLock()
searchDomains := slices.Clone(t.searchDomains)
t.access.RUnlock()
if t.acceptSearchDomain && len(searchDomains) > 0 && mDNS.CountLabel(message.Question[0].Name) == 1 {
t.exchangeWithSearchDomains(ctx, message, searchDomains, callback)
return
}
t.exchangeOnce(ctx, message, t.acceptDefaultResolvers, callback)
}
func (t *DNSTransport) exchangeWithSearchDomains(ctx context.Context, message *mDNS.Msg, searchDomains []string, callback func(response *mDNS.Msg, err error)) {
originalQuestion := message.Question[0]
singleLabel := strings.TrimSuffix(originalQuestion.Name, ".")
exchangers := make([]dnsTransport.AsyncExchanger, 0, len(searchDomains)+1)
for _, searchDomain := range searchDomains {
expandedName := singleLabel + "." + searchDomain
exchangers = append(exchangers, func(exchangeCtx context.Context, exchangeCallback func(response *mDNS.Msg, err error)) {
question := originalQuestion
question.Name = expandedName
rewritten := *message
rewritten.Question = []mDNS.Question{question}
t.exchangeOnce(exchangeCtx, &rewritten, false, func(response *mDNS.Msg, err error) {
if err == nil {
restoreOpenVPNOriginalQuestion(response, expandedName, originalQuestion)
}
exchangeCallback(response, err)
})
})
}
exchangers = append(exchangers, func(exchangeCtx context.Context, exchangeCallback func(response *mDNS.Msg, err error)) {
t.exchangeOnce(exchangeCtx, message, t.acceptDefaultResolvers, exchangeCallback)
})
dnsTransport.ExchangeSequential(ctx, exchangers, func(response *mDNS.Msg, err error) bool {
return err == nil && response.Rcode != mDNS.RcodeNameError
}, callback)
}
func (t *DNSTransport) exchangeOnce(ctx context.Context, message *mDNS.Msg, allowDefaultResolvers bool, callback func(response *mDNS.Msg, err error)) {
question := message.Question[0]
t.access.RLock()
var matchedResolvers []adapter.DNSTransport
matchedLength := -1
for route, resolvers := range t.routes {
if openVPNDomainMatches(route, question.Name) && len(route) > matchedLength {
matchedLength = len(route)
matchedResolvers = resolvers
}
}
defaultResolvers := slices.Clone(t.defaultResolvers)
t.access.RUnlock()
if len(matchedResolvers) > 0 {
dnsTransport.ExchangeSequential(ctx, openVPNResolverExchangers(matchedResolvers, message), nil, callback)
return
}
if allowDefaultResolvers && len(defaultResolvers) > 0 {
dnsTransport.ExchangeSequential(ctx, openVPNResolverExchangers(defaultResolvers, message), nil, callback)
return
}
callback(nil, boxDNS.RcodeNameError)
}
func openVPNResolverExchangers(resolvers []adapter.DNSTransport, message *mDNS.Msg) []dnsTransport.AsyncExchanger {
return common.Map(resolvers, func(resolver adapter.DNSTransport) dnsTransport.AsyncExchanger {
return func(ctx context.Context, callback func(response *mDNS.Msg, err error)) {
resolver.ExchangeAsync(ctx, message, callback)
}
})
}
func (t *DNSTransport) collectResolversLocked() []adapter.DNSTransport {
var resolvers []adapter.DNSTransport
for _, routeResolvers := range t.routes {
resolvers = append(resolvers, routeResolvers...)
}
resolvers = append(resolvers, t.defaultResolvers...)
return common.Uniq(resolvers)
}
func closeDNSTransports(resolvers []adapter.DNSTransport) error {
var err error
for _, resolver := range common.Uniq(resolvers) {
err = E.Append(err, resolver.Close(), func(closeErr error) error {
return E.Cause(closeErr, "close DNS resolver")
})
}
return err
}
func normalizeOpenVPNDomain(domain string) string {
normalized := strings.TrimSpace(strings.ToLower(domain))
if normalized == "." {
return normalized
}
normalized = strings.TrimSuffix(normalized, ".")
if normalized == "" {
return ""
}
return normalized + "."
}
func normalizeOpenVPNDomains(domains []string) []string {
normalized := make([]string, 0, len(domains))
for _, domain := range domains {
normalizedDomain := normalizeOpenVPNDomain(domain)
if normalizedDomain != "" && normalizedDomain != "." && !slices.Contains(normalized, normalizedDomain) {
normalized = append(normalized, normalizedDomain)
}
}
return normalized
}
func restoreOpenVPNOriginalQuestion(response *mDNS.Msg, expandedName string, originalQuestion mDNS.Question) {
response.Question = []mDNS.Question{originalQuestion}
for _, resourceRecord := range response.Answer {
if strings.EqualFold(resourceRecord.Header().Name, expandedName) {
resourceRecord.Header().Name = originalQuestion.Name
}
}
}
+115 -25
View File
@@ -17,6 +17,7 @@ import (
ovpn "github.com/sagernet/sing-openvpn" ovpn "github.com/sagernet/sing-openvpn"
"github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun"
"github.com/sagernet/sing-tun/gtcpip/header" "github.com/sagernet/sing-tun/gtcpip/header"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/auth" "github.com/sagernet/sing/common/auth"
"github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
@@ -118,7 +119,7 @@ func keyDirectionValue(direction string) (int, error) {
case "client": case "client":
return 1, nil return 1, nil
default: default:
return 0, E.New("unsupported OpenVPN key direction: ", direction, " (expected \"server\" or \"client\")") return 0, E.New("unsupported key direction: ", direction, " (expected \"server\" or \"client\")")
} }
} }
@@ -187,21 +188,43 @@ func configurationFromClientEvent(event ovpn.TunnelConfigurationEvent, logger lo
hasInet6DefaultRoute = true hasInet6DefaultRoute = true
} }
} }
var excludedRoutes []ovpntransport.Route
for _, route := range configuration.ExcludedIPv4Routes {
excludedRoutes = append(excludedRoutes, ovpntransport.Route{Prefix: route.Prefix, Gateway: route.Gateway, Metric: route.Metric})
}
for _, route := range configuration.ExcludedIPv6Routes {
excludedRoutes = append(excludedRoutes, ovpntransport.Route{Prefix: route.Prefix, Gateway: route.Gateway, Metric: route.Metric})
}
if configuration.RedirectGateway { if configuration.RedirectGateway {
if !hasOpenVPNFlag(configuration.RedirectGatewayFlags, "!ipv4") && !hasInet4DefaultRoute { if !hasOpenVPNFlag(configuration.RedirectGatewayFlags, "!ipv4") && !hasInet4DefaultRoute {
routes = append(routes, ovpntransport.Route{ if hasOpenVPNFlag(configuration.RedirectGatewayFlags, "def1") {
Prefix: inet4DefaultRoute, for _, prefix := range []netip.Prefix{
Gateway: configuration.VPNGateway, netip.PrefixFrom(netip.IPv4Unspecified(), 1),
Metric: configuration.RouteMetric, netip.MustParsePrefix("128.0.0.0/1"),
}) } {
if !openVPNRoutesContainPrefix(routes, prefix) {
routes = append(routes, ovpntransport.Route{Prefix: prefix, Gateway: configuration.VPNGateway, Metric: configuration.RouteMetric})
}
}
} else {
routes = append(routes, ovpntransport.Route{
Prefix: inet4DefaultRoute,
Gateway: configuration.VPNGateway,
Metric: configuration.RouteMetric,
})
}
} }
if hasOpenVPNFlag(configuration.RedirectGatewayFlags, "ipv6") && !hasInet6DefaultRoute { if hasOpenVPNFlag(configuration.RedirectGatewayFlags, "ipv6") && !hasInet6DefaultRoute {
routes = append(routes, ovpntransport.Route{ for _, prefix := range []netip.Prefix{
Prefix: inet6DefaultRoute, netip.MustParsePrefix("::/3"),
Gateway: configuration.VPNGatewayIPv6, netip.MustParsePrefix("2000::/4"),
Metric: configuration.RouteMetric, netip.MustParsePrefix("3000::/4"),
}) netip.MustParsePrefix("fc00::/7"),
hasInet6DefaultRoute = true } {
if !openVPNRoutesContainPrefix(routes, prefix) {
routes = append(routes, ovpntransport.Route{Prefix: prefix, Gateway: configuration.VPNGatewayIPv6, Metric: configuration.RouteMetric})
}
}
} }
} }
if configuration.BlockIPv6 && !hasInet6DefaultRoute { if configuration.BlockIPv6 && !hasInet6DefaultRoute {
@@ -211,47 +234,114 @@ func configurationFromClientEvent(event ovpn.TunnelConfigurationEvent, logger lo
Metric: configuration.RouteMetric, Metric: configuration.RouteMetric,
}) })
} }
var dnsAddresses []netip.Addr
if len(configuration.DNSServers) > 0 {
servers := slices.Clone(configuration.DNSServers)
slices.SortFunc(servers, func(left ovpn.TunnelDNSServer, right ovpn.TunnelDNSServer) int {
return left.Priority - right.Priority
})
for _, address := range servers[0].Addresses {
if address.Addr().IsValid() && !slices.Contains(dnsAddresses, address.Addr()) {
dnsAddresses = append(dnsAddresses, address.Addr())
}
}
} else {
dnsAddresses = slices.Clone(configuration.DNS)
}
for _, dnsAddress := range dnsAddresses {
if openVPNRoutesContainAddress(routes, dnsAddress) || openVPNRoutesContainAddress(excludedRoutes, dnsAddress) {
continue
}
gateway := configuration.VPNGateway
if dnsAddress.Is6() {
gateway = configuration.VPNGatewayIPv6
}
routes = append(routes, ovpntransport.Route{
Prefix: netip.PrefixFrom(dnsAddress, dnsAddress.BitLen()),
Gateway: gateway,
Metric: configuration.RouteMetric,
})
}
var ignoredOptions []string var ignoredOptions []string
var notApplicableOptions []string
for _, flag := range configuration.RedirectGatewayFlags { for _, flag := range configuration.RedirectGatewayFlags {
switch strings.ToLower(flag) { switch strings.ToLower(flag) {
case "!ipv4", "ipv6": case "!ipv4", "ipv6", "def1", "local", "autolocal":
case "bypass-dhcp", "bypass-dns":
notApplicableOptions = append(notApplicableOptions, "redirect-gateway "+flag)
default: default:
if flag != "" { if flag != "" {
ignoredOptions = append(ignoredOptions, "redirect-gateway "+flag) ignoredOptions = append(ignoredOptions, "redirect-gateway "+flag)
} }
} }
} }
if configuration.RedirectPrivate {
ignoredOptions = append(ignoredOptions, "redirect-private")
}
if configuration.BlockOutsideDNS { if configuration.BlockOutsideDNS {
ignoredOptions = append(ignoredOptions, "block-outside-dns") ignoredOptions = append(ignoredOptions, "block-outside-dns")
} }
for _, dhcpOption := range configuration.DHCPOptions { for _, dhcpOption := range configuration.DHCPOptions {
fields := strings.Fields(dhcpOption) fields := strings.Fields(dhcpOption)
if len(fields) == 0 || strings.EqualFold(fields[0], "DNS") || strings.EqualFold(fields[0], "DNS6") { if len(fields) == 0 || slices.ContainsFunc([]string{"DNS", "DNS6", "DOMAIN", "ADAPTER_DOMAIN_SUFFIX", "DOMAIN-SEARCH", "DOMAIN-ROUTE"}, func(optionName string) bool {
return strings.EqualFold(fields[0], optionName)
}) {
continue continue
} }
ignoredOptions = append(ignoredOptions, "dhcp-option "+strings.TrimSpace(dhcpOption)) ignoredOptions = append(ignoredOptions, "dhcp-option "+strings.TrimSpace(dhcpOption))
} }
if len(ignoredOptions) > 0 && logger != nil { if len(ignoredOptions) > 0 && logger != nil {
logger.Debug("ignored pushed OpenVPN options: ", strings.Join(ignoredOptions, ", ")) logger.Debug("ignored pushed options: ", strings.Join(ignoredOptions, ", "))
}
if len(notApplicableOptions) > 0 && logger != nil {
logger.Debug("pushed options are not applicable: ", strings.Join(notApplicableOptions, ", "))
} }
return ovpntransport.Configuration{ return ovpntransport.Configuration{
MTU: mtu, MTU: mtu,
Address: addresses, Address: addresses,
Routes: routes, Routes: routes,
DNS: configuration.DNS, ExcludedRoutes: excludedRoutes,
Topology: configuration.Topology, DNS: configuration.DNS,
BlockIPv6: configuration.BlockIPv6, DNSServers: common.Map(configuration.DNSServers, func(server ovpn.TunnelDNSServer) ovpntransport.DNSServer {
return ovpntransport.DNSServer{
Priority: server.Priority,
Addresses: slices.Clone(server.Addresses),
ResolveDomains: slices.Clone(server.ResolveDomains),
DNSSEC: server.DNSSEC,
Transport: server.Transport,
SNI: server.SNI,
}
}),
SearchDomains: slices.Clone(configuration.SearchDomains),
DNSRoutes: slices.Clone(configuration.DNSRoutes),
Topology: configuration.Topology,
BlockIPv6: configuration.BlockIPv6,
} }
} }
func buildIPSet(routes []ovpntransport.Route) (*netipx.IPSet, error) { func openVPNRoutesContainAddress(routes []ovpntransport.Route, address netip.Addr) bool {
for _, route := range routes {
if route.Prefix.Contains(address) {
return true
}
}
return false
}
func openVPNRoutesContainPrefix(routes []ovpntransport.Route, prefix netip.Prefix) bool {
for _, route := range routes {
if route.Prefix == prefix {
return true
}
}
return false
}
func buildIPSet(routes []ovpntransport.Route, excludedRoutes []ovpntransport.Route) (*netipx.IPSet, error) {
var builder netipx.IPSetBuilder var builder netipx.IPSetBuilder
for _, route := range routes { for _, route := range routes {
builder.AddPrefix(route.Prefix) builder.AddPrefix(route.Prefix)
} }
for _, route := range excludedRoutes {
builder.RemovePrefix(route.Prefix)
}
return builder.IPSet() return builder.IPSet()
} }
+211 -32
View File
@@ -5,6 +5,7 @@ import (
"net" "net"
"net/netip" "net/netip"
"slices" "slices"
"strconv"
"sync/atomic" "sync/atomic"
"time" "time"
@@ -90,18 +91,16 @@ func NewServerEndpoint(ctx context.Context, router adapter.Router, logger log.Co
return nil, err return nil, err
} }
serverOptions.Context = loopContext serverOptions.Context = loopContext
serverOptions.Authentication.Authenticator = authenticatorFromUsers(options.Users) if serverOptions.Mode == ovpn.ModeTLS {
serverOptions.Authentication.DuplicateCN = options.DuplicateCN serverOptions.Authentication.Authenticator = authenticatorFromUsers(options.Users)
serverOptions.Authentication.DuplicateCN = options.DuplicateCN
}
serverOptions.Logger = logger serverOptions.Logger = logger
serverEndpoint.serverOptions = serverOptions serverEndpoint.serverOptions = serverOptions
udpTimeout := C.UDPTimeout udpTimeout := C.UDPTimeout
if options.UDPTimeout != 0 { if options.UDPTimeout != 0 {
udpTimeout = time.Duration(options.UDPTimeout) udpTimeout = time.Duration(options.UDPTimeout)
} }
deviceRoutes := make([]ovpntransport.Route, 0, len(options.Address))
for _, prefix := range options.Address {
deviceRoutes = append(deviceRoutes, ovpntransport.Route{Prefix: prefix.Masked()})
}
device, err := ovpntransport.NewDevice(ovpntransport.DeviceOptions{ device, err := ovpntransport.NewDevice(ovpntransport.DeviceOptions{
Context: ctx, Context: ctx,
Logger: logger, Logger: logger,
@@ -118,7 +117,6 @@ func NewServerEndpoint(ctx context.Context, router adapter.Router, logger log.Co
Configuration: ovpntransport.Configuration{ Configuration: ovpntransport.Configuration{
MTU: options.MTU, MTU: options.MTU,
Address: options.Address, Address: options.Address,
Routes: deviceRoutes,
Topology: options.Topology, Topology: options.Topology,
}, },
}) })
@@ -134,15 +132,18 @@ func NewServerEndpoint(ctx context.Context, router adapter.Router, logger log.Co
func validateServerAddresses(addresses []netip.Prefix) error { func validateServerAddresses(addresses []netip.Prefix) error {
var hasIPv4 bool var hasIPv4 bool
var hasIPv6 bool var hasIPv6 bool
for _, prefix := range addresses { for addressIndex, prefix := range addresses {
if !prefix.IsValid() {
return E.New("server address[", addressIndex, "] is invalid")
}
if prefix.Addr().Is4() { if prefix.Addr().Is4() {
if hasIPv4 { if hasIPv4 {
return E.New("multiple IPv4 OpenVPN server address pools are not supported") return E.New("multiple IPv4 server address pools are not supported")
} }
hasIPv4 = true hasIPv4 = true
} else { } else {
if hasIPv6 { if hasIPv6 {
return E.New("multiple IPv6 OpenVPN server address pools are not supported") return E.New("multiple IPv6 server address pools are not supported")
} }
hasIPv6 = true hasIPv6 = true
} }
@@ -155,7 +156,7 @@ func validateServerTopology(topology string) error {
case "", "subnet", "p2p", "net30": case "", "subnet", "p2p", "net30":
return nil return nil
default: default:
return E.New("invalid OpenVPN topology ", topology, ", allowed values: subnet, p2p, net30") return E.New("invalid topology ", topology, ", allowed values: subnet, p2p, net30")
} }
} }
@@ -258,11 +259,17 @@ func (s *ServerEndpoint) Start(stage adapter.StartStage) error {
} }
func buildServerOptions(options option.OpenVPNServerEndpointOptions) (ovpn.ServerOptions, error) { func buildServerOptions(options option.OpenVPNServerEndpointOptions) (ovpn.ServerOptions, error) {
if len(options.Address) == 0 { mode := options.Mode
return ovpn.ServerOptions{}, E.New("missing OpenVPN server address") if mode == "" {
mode = ovpn.ModeTLS
} }
if options.TLS == nil { switch mode {
return ovpn.ServerOptions{}, E.New("missing `tls` options") case ovpn.ModeTLS, ovpn.ModeStaticKey:
default:
return ovpn.ServerOptions{}, E.New("unsupported mode: ", mode, " (expected \"tls\" or \"static_key\")")
}
if len(options.Address) == 0 {
return ovpn.ServerOptions{}, E.New("missing server address")
} }
err := validateServerAddresses(options.Address) err := validateServerAddresses(options.Address)
if err != nil { if err != nil {
@@ -279,7 +286,16 @@ func buildServerOptions(options option.OpenVPNServerEndpointOptions) (ovpn.Serve
switch protocol { switch protocol {
case N.NetworkTCP, N.NetworkUDP: case N.NetworkTCP, N.NetworkUDP:
default: default:
return ovpn.ServerOptions{}, E.New("unsupported OpenVPN network: ", protocol) return ovpn.ServerOptions{}, E.New("unsupported network: ", protocol)
}
if mode == ovpn.ModeStaticKey {
return buildStaticKeyServerOptions(options, protocol)
}
if options.TLS == nil {
return ovpn.ServerOptions{}, E.New("missing `tls` options")
}
if len(options.StaticKey) > 0 || options.StaticKeyPath != "" || options.KeyDirection != "" || options.Cipher != "" || options.Remote != "" || options.RemotePort != 0 || netip.Addr(options.PeerAddress).IsValid() || netip.Addr(options.PeerAddressIPv6).IsValid() {
return ovpn.ServerOptions{}, E.New("static-key server options require `mode: static_key`")
} }
tlsOptions, keyDirection, err := buildServerTLSOptions(*options.TLS) tlsOptions, keyDirection, err := buildServerTLSOptions(*options.TLS)
if err != nil { if err != nil {
@@ -295,29 +311,136 @@ func buildServerOptions(options option.OpenVPNServerEndpointOptions) (ovpn.Serve
MaxClients: options.MaxClients, MaxClients: options.MaxClients,
}, },
DataChannel: ovpn.ServerDataChannelOptions{ DataChannel: ovpn.ServerDataChannelOptions{
MTU: options.MTU, MTU: options.MTU,
Ciphers: []string(options.DataCiphers), MSSFix: options.MSSFix,
FallbackCipher: options.DataCiphersFallback, MSSFixDisabled: options.MSSFixDisabled,
Auth: options.Auth, MSSFixMode: options.MSSFixMode,
PacketHeadroom: ovpntransport.PacketHeadroom, Ciphers: []string(options.DataCiphers),
FallbackCipher: options.DataCiphersFallback,
Auth: options.Auth,
ReplayWindow: options.ReplayWindow,
ReplayWindowTime: time.Duration(options.ReplayWindowTime),
PacketHeadroom: ovpntransport.PacketHeadroom,
}, },
TLS: tlsOptions, TLS: tlsOptions,
Timing: ovpn.ServerTimingOptions{ Timing: ovpn.ServerTimingOptions{
RenegotiationInterval: time.Duration(options.RenegotiateInterval), RenegotiationInterval: time.Duration(options.RenegotiateInterval),
RenegotiationDisabled: options.RenegotiateDisabled,
RenegotiationBytes: options.RenegotiateBytes,
RenegotiationPackets: options.RenegotiatePackets,
HandWindow: time.Duration(options.HandshakeWindow), HandWindow: time.Duration(options.HandshakeWindow),
PingInterval: time.Duration(options.PingInterval), PingInterval: time.Duration(options.PingInterval),
PingRestart: time.Duration(options.PingRestart), PingRestart: time.Duration(options.PingRestart),
}, },
} }
applyServerPushOptions(&serverOptions, options) err = applyServerPushOptions(&serverOptions, options)
if err != nil {
return ovpn.ServerOptions{}, err
}
return serverOptions, nil return serverOptions, nil
} }
func buildStaticKeyServerOptions(options option.OpenVPNServerEndpointOptions, protocol string) (ovpn.ServerOptions, error) {
if options.TLS != nil {
return ovpn.ServerOptions{}, E.New("`tls` options are not supported in `static_key` mode")
}
if len(options.Users) > 0 || options.DuplicateCN {
return ovpn.ServerOptions{}, E.New("user authentication is not supported in `static_key` mode")
}
if options.Push != nil {
return ovpn.ServerOptions{}, E.New("push options are not supported in `static_key` mode")
}
if options.RenegotiateInterval != 0 || options.RenegotiateDisabled || options.RenegotiateBytes != 0 || options.RenegotiatePackets != 0 || options.HandshakeWindow != 0 {
return ovpn.ServerOptions{}, E.New("TLS timing and renegotiation options are not supported in `static_key` mode")
}
if len(options.DataCiphers) > 0 || options.DataCiphersFallback != "" {
return ovpn.ServerOptions{}, E.New("`data_ciphers` and `data_ciphers_fallback` are not supported in `static_key` mode; use `cipher`")
}
staticKey, err := requiredMaterialSource("static_key", options.StaticKey, options.StaticKeyPath)
if err != nil {
return ovpn.ServerOptions{}, err
}
keyDirection, err := keyDirectionValue(options.KeyDirection)
if err != nil {
return ovpn.ServerOptions{}, err
}
vpnGateway := netip.Addr(options.PeerAddress)
if vpnGateway.IsValid() && !vpnGateway.Is4() {
return ovpn.ServerOptions{}, E.New("`peer_address` must be an IPv4 address")
}
vpnGatewayIPv6 := netip.Addr(options.PeerAddressIPv6)
if vpnGatewayIPv6.IsValid() && !vpnGatewayIPv6.Is6() {
return ovpn.ServerOptions{}, E.New("`peer_address_ipv6` must be an IPv6 address")
}
var hasIPv4 bool
var hasIPv6 bool
for _, address := range options.Address {
hasIPv4 = hasIPv4 || address.Addr().Is4()
hasIPv6 = hasIPv6 || address.Addr().Is6()
}
if hasIPv4 && !vpnGateway.IsValid() {
return ovpn.ServerOptions{}, E.New("missing `peer_address` for the IPv4 static-key tunnel")
}
if hasIPv6 && !vpnGatewayIPv6.IsValid() {
return ovpn.ServerOptions{}, E.New("missing `peer_address_ipv6` for the IPv6 static-key tunnel")
}
if vpnGateway.IsValid() && !hasIPv4 {
return ovpn.ServerOptions{}, E.New("`peer_address` requires an IPv4 tunnel `address` in `static_key` mode")
}
if vpnGatewayIPv6.IsValid() && !hasIPv6 {
return ovpn.ServerOptions{}, E.New("`peer_address_ipv6` requires an IPv6 tunnel `address` in `static_key` mode")
}
remoteAddress := ""
if protocol == N.NetworkUDP {
if options.Remote == "" || options.RemotePort == 0 {
return ovpn.ServerOptions{}, E.New("`remote` and `remote_port` are required for a UDP static-key server")
}
remoteAddress = net.JoinHostPort(options.Remote, strconv.Itoa(int(options.RemotePort)))
} else if options.Remote != "" || options.RemotePort != 0 {
return ovpn.ServerOptions{}, E.New("`remote` and `remote_port` are only used by a UDP static-key server")
}
topology := options.Topology
if topology == "" {
topology = "p2p"
}
return ovpn.ServerOptions{
Mode: ovpn.ModeStaticKey,
StaticKey: staticKey,
KeyDirection: keyDirection,
Transport: ovpn.ServerTransportOptions{
Protocol: protocol,
RemoteAddress: remoteAddress,
},
Resources: ovpn.ServerResourceOptions{MaxClients: options.MaxClients},
DataChannel: ovpn.ServerDataChannelOptions{
MTU: options.MTU,
MSSFix: options.MSSFix,
MSSFixDisabled: options.MSSFixDisabled,
MSSFixMode: options.MSSFixMode,
Cipher: options.Cipher,
Auth: options.Auth,
ReplayWindow: options.ReplayWindow,
ReplayWindowTime: time.Duration(options.ReplayWindowTime),
PacketHeadroom: ovpntransport.PacketHeadroom,
},
Timing: ovpn.ServerTimingOptions{
PingInterval: time.Duration(options.PingInterval),
PingRestart: time.Duration(options.PingRestart),
},
Tunnel: ovpn.ServerTunnelOptions{
Topology: topology,
LocalAddress: slices.Clone(options.Address),
VPNGateway: vpnGateway,
VPNGatewayIPv6: vpnGatewayIPv6,
},
}, nil
}
func buildServerTLSOptions(options option.OpenVPNInboundTLSOptions) (ovpn.ServerTLSOptions, int, error) { func buildServerTLSOptions(options option.OpenVPNInboundTLSOptions) (ovpn.ServerTLSOptions, int, error) {
switch options.VerifyClientCertificate { switch options.VerifyClientCertificate {
case "", "require", "optional", "none": case "", "require", "optional", "none":
default: default:
return ovpn.ServerTLSOptions{}, 0, E.New("invalid OpenVPN client certificate policy ", options.VerifyClientCertificate, ", allowed values: require, optional, none") return ovpn.ServerTLSOptions{}, 0, E.New("invalid client certificate policy ", options.VerifyClientCertificate, ", allowed values: require, optional, none")
} }
certificate, err := requiredMaterialSource("tls.certificate", options.Certificate, options.CertificatePath) certificate, err := requiredMaterialSource("tls.certificate", options.Certificate, options.CertificatePath)
if err != nil { if err != nil {
@@ -327,16 +450,46 @@ func buildServerTLSOptions(options option.OpenVPNInboundTLSOptions) (ovpn.Server
if err != nil { if err != nil {
return ovpn.ServerTLSOptions{}, 0, err return ovpn.ServerTLSOptions{}, 0, err
} }
certificateAuthority, err := requiredMaterialSource("tls.client_certificate", options.ClientCertificate, options.ClientCertificatePath) certificateAuthority, err := materialSource("tls.client_certificate", options.ClientCertificate, options.ClientCertificatePath)
if err != nil { if err != nil {
return ovpn.ServerTLSOptions{}, 0, err return ovpn.ServerTLSOptions{}, 0, err
} }
remoteCertificateTLS := options.RemoteCertificateTLS
switch remoteCertificateTLS {
case "", "server", "client", "none":
default:
return ovpn.ServerTLSOptions{}, 0, E.New("invalid `tls.remote_certificate_tls`: ", remoteCertificateTLS)
}
if options.RemoteCertificateEKU != "" && remoteCertificateTLS != "" {
return ovpn.ServerTLSOptions{}, 0, E.New("`tls.remote_certificate_eku` is conflict with `tls.remote_certificate_tls`")
}
if remoteCertificateTLS == "" && options.RemoteCertificateEKU == "" {
remoteCertificateTLS = "client"
} else if remoteCertificateTLS == "none" {
remoteCertificateTLS = ""
}
clientNameType := options.ClientNameType
if options.ClientName != "" && clientNameType == "" {
clientNameType = "name"
}
tlsOptions := ovpn.ServerTLSOptions{ tlsOptions := ovpn.ServerTLSOptions{
CertificateAuthority: certificateAuthority, CertificateAuthority: certificateAuthority,
Certificate: certificate, Certificate: certificate,
Key: key, Key: key,
VerifyClientCertificate: options.VerifyClientCertificate, VerifyClientCertificate: options.VerifyClientCertificate,
VerifyX509Name: options.ClientName,
VerifyX509Type: clientNameType,
PeerFingerprint: options.PeerFingerprint,
CRLVerify: options.CRLPath,
RemoteCertificateKU: options.RemoteCertificateKU,
RemoteCertificateEKU: options.RemoteCertificateEKU,
RemoteCertificateTLS: remoteCertificateTLS,
NSCertificateType: options.NSCertificateType,
CertificateProfile: options.CertificateProfile, CertificateProfile: options.CertificateProfile,
VersionMin: options.VersionMin,
VersionMax: options.VersionMax,
Cipher: options.Cipher,
Groups: options.Groups,
} }
keyDirection := -1 keyDirection := -1
controlWrap := options.ControlWrap controlWrap := options.ControlWrap
@@ -369,15 +522,15 @@ func buildServerTLSOptions(options option.OpenVPNInboundTLSOptions) (ovpn.Server
tlsOptions.CryptV2ForceCookie = controlWrap.ForceCookie tlsOptions.CryptV2ForceCookie = controlWrap.ForceCookie
} }
case "": case "":
return ovpn.ServerTLSOptions{}, 0, E.New("missing OpenVPN control wrap type") return ovpn.ServerTLSOptions{}, 0, E.New("missing control wrap type")
default: default:
return ovpn.ServerTLSOptions{}, 0, E.New("unknown OpenVPN control wrap type: ", controlWrap.Type) return ovpn.ServerTLSOptions{}, 0, E.New("unknown control wrap type: ", controlWrap.Type)
} }
} }
return tlsOptions, keyDirection, nil return tlsOptions, keyDirection, nil
} }
func applyServerPushOptions(serverOptions *ovpn.ServerOptions, options option.OpenVPNServerEndpointOptions) { func applyServerPushOptions(serverOptions *ovpn.ServerOptions, options option.OpenVPNServerEndpointOptions) error {
topology := options.Topology topology := options.Topology
if topology == "" { if topology == "" {
topology = "subnet" topology = "subnet"
@@ -399,10 +552,35 @@ func applyServerPushOptions(serverOptions *ovpn.ServerOptions, options option.Op
LocalAddress: localAddresses, LocalAddress: localAddresses,
} }
if options.Push == nil { if options.Push == nil {
return return nil
} }
serverOptions.Push.Routes = slices.Clone(options.Push.Routes) serverOptions.Push.Routes = slices.Clone(options.Push.Routes)
serverOptions.Push.DNS = slices.Clone(options.Push.DNS) serverOptions.Push.DNS = slices.Clone(options.Push.DNS)
serverOptions.Push.SearchDomains = slices.Clone(options.Push.SearchDomains)
serverOptions.Push.DHCPOptions = slices.Clone(options.Push.DHCPOptions)
for serverIndex, server := range options.Push.DNSServers {
addresses := make([]netip.AddrPort, 0, len(server.Addresses))
for addressIndex, addressValue := range server.Addresses {
address, err := netip.ParseAddr(addressValue)
if err == nil {
addresses = append(addresses, netip.AddrPortFrom(address, 0))
continue
}
addressPort, addressPortErr := netip.ParseAddrPort(addressValue)
if addressPortErr != nil || addressPort.Port() == 0 {
return E.New("invalid push.dns_servers[", serverIndex, "].addresses[", addressIndex, "]: ", addressValue)
}
addresses = append(addresses, addressPort)
}
serverOptions.Push.DNSServers = append(serverOptions.Push.DNSServers, ovpn.TunnelDNSServer{
Priority: server.Priority,
Addresses: addresses,
ResolveDomains: slices.Clone(server.ResolveDomains),
DNSSEC: server.DNSSEC,
Transport: server.Transport,
SNI: server.SNI,
})
}
serverOptions.Push.BlockOutsideDNS = options.Push.BlockOutsideDNS serverOptions.Push.BlockOutsideDNS = options.Push.BlockOutsideDNS
serverOptions.Push.PingInterval = time.Duration(options.Push.PingInterval) serverOptions.Push.PingInterval = time.Duration(options.Push.PingInterval)
serverOptions.Push.PingRestart = time.Duration(options.Push.PingRestart) serverOptions.Push.PingRestart = time.Duration(options.Push.PingRestart)
@@ -414,6 +592,7 @@ func applyServerPushOptions(serverOptions *ovpn.ServerOptions, options option.Op
serverOptions.Push.RedirectGatewayFlags = []string{"def1"} serverOptions.Push.RedirectGatewayFlags = []string{"def1"}
} }
} }
return nil
} }
func (s *ServerEndpoint) readLoop() { func (s *ServerEndpoint) readLoop() {
@@ -424,7 +603,7 @@ func (s *ServerEndpoint) readLoop() {
if E.IsClosedOrCanceled(err) || s.loopContext.Err() != nil { if E.IsClosedOrCanceled(err) || s.loopContext.Err() != nil {
return return
} }
s.logger.Error(E.Cause(err, "OpenVPN server terminated")) s.logger.Error(E.Cause(err, "server terminated"))
return return
} }
packetBuffers := make([]*buf.Buffer, len(serverPacketBuffers)) packetBuffers := make([]*buf.Buffer, len(serverPacketBuffers))
@@ -491,7 +670,7 @@ func (s *ServerEndpoint) NewDNSPacket(payload []byte, source M.Socksaddr, destin
func (s *ServerEndpoint) WritePackets(packets [][]byte) error { func (s *ServerEndpoint) WritePackets(packets [][]byte) error {
if !s.started.Load() { if !s.started.Load() {
return E.New("OpenVPN server is not ready yet") return E.New("endpoint is not ready yet")
} }
packetBuffers := make([]*buf.Buffer, len(packets)) packetBuffers := make([]*buf.Buffer, len(packets))
for i, packet := range packets { for i, packet := range packets {
@@ -547,7 +726,7 @@ func (s *ServerEndpoint) DialContext(ctx context.Context, network string, destin
s.logger.InfoContext(ctx, "outbound packet connection to ", destination) s.logger.InfoContext(ctx, "outbound packet connection to ", destination)
} }
if !s.started.Load() { if !s.started.Load() {
return nil, E.New("OpenVPN server is not ready yet") return nil, E.New("endpoint is not ready yet")
} }
if destination.IsDomain() { if destination.IsDomain() {
destinationAddresses, err := s.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{}) destinationAddresses, err := s.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{})
@@ -565,7 +744,7 @@ func (s *ServerEndpoint) DialContext(ctx context.Context, network string, destin
func (s *ServerEndpoint) ListenPacketWithDestination(ctx context.Context, destination M.Socksaddr) (net.PacketConn, netip.Addr, error) { func (s *ServerEndpoint) ListenPacketWithDestination(ctx context.Context, destination M.Socksaddr) (net.PacketConn, netip.Addr, error) {
s.logger.InfoContext(ctx, "outbound packet connection to ", destination) s.logger.InfoContext(ctx, "outbound packet connection to ", destination)
if !s.started.Load() { if !s.started.Load() {
return nil, netip.Addr{}, E.New("OpenVPN server is not ready yet") return nil, netip.Addr{}, E.New("endpoint is not ready yet")
} }
if destination.IsDomain() { if destination.IsDomain() {
destinationAddresses, err := s.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{}) destinationAddresses, err := s.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{})
+1 -1
View File
@@ -1 +1 @@
-X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0 -X runtime.godebugDefault=multipathtcp=0,tlssha1=1,tlsunsafeekm=1 -checklinkname=0
+219
View File
@@ -19,6 +19,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"slices"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@@ -29,6 +30,9 @@ import (
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
ovpn "github.com/sagernet/sing-openvpn"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing-tun/gtcpip/header"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/auth" "github.com/sagernet/sing/common/auth"
"github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/bufio"
@@ -91,6 +95,21 @@ type openVPNSelfCase struct {
remoteCertificateTLS string remoteCertificateTLS string
} }
type openVPNPacketReturn struct {
packets chan []byte
}
func (r *openVPNPacketReturn) ReturnHeadroom() int {
return 0
}
func (r *openVPNPacketReturn) ReturnPackets(packets [][]byte) [][]byte {
for _, packet := range packets {
r.packets <- slices.Clone(packet)
}
return nil
}
func TestOpenVPNSelfToSelf(t *testing.T) { func TestOpenVPNSelfToSelf(t *testing.T) {
testCases := []openVPNSelfCase{ testCases := []openVPNSelfCase{
{ {
@@ -121,6 +140,185 @@ func TestOpenVPNSelfToSelf(t *testing.T) {
} }
} }
func TestOpenVPNStaticKeyClientDataPath(t *testing.T) {
const (
clientTunnelAddress = "10.91.0.2"
peerTunnelAddress = "10.91.0.1"
)
listener, err := net.Listen(N.NetworkTCP, "127.0.0.1:0")
require.NoError(t, err)
t.Cleanup(func() { _ = listener.Close() })
openVPNPort := uint16(listener.Addr().(*net.TCPAddr).Port)
staticKey := createOpenVPNStaticKey(t)
staticKeyPath := writeOpenVPNStaticKeyFile(t, staticKey)
peerContext, cancelPeer := context.WithCancel(context.Background())
peerClient, err := ovpn.NewClient(ovpn.ClientOptions{
Context: peerContext,
Mode: ovpn.ModeStaticKey,
Transport: ovpn.ClientTransportOptions{
Remotes: []ovpn.Remote{{
Host: "127.0.0.1",
Port: openVPNPort,
Protocol: N.NetworkTCP,
}},
Protocol: N.NetworkTCP,
DialContext: func(ctx context.Context, network string, address string) (net.Conn, error) {
return listener.Accept()
},
},
DataChannel: ovpn.ClientDataChannelOptions{
MTU: 1500,
Cipher: "AES-256-CBC",
Auth: "SHA256",
},
Tunnel: ovpn.ClientTunnelOptions{
DevType: "tun",
Topology: "p2p",
LocalAddress: []netip.Prefix{netip.MustParsePrefix(peerTunnelAddress + "/30")},
VPNGateway: netip.MustParseAddr(clientTunnelAddress),
},
StaticKey: ovpn.Material{Content: []byte(staticKey)},
KeyDirection: 0,
})
require.NoError(t, err)
t.Cleanup(func() {
cancelPeer()
_ = listener.Close()
_ = peerClient.Close()
})
err = peerClient.Start()
require.NoError(t, err)
clientOptions := option.OpenVPNClientEndpointOptions{
ServerOptions: option.ServerOptions{
Server: "127.0.0.1",
ServerPort: openVPNPort,
},
Mode: ovpn.ModeStaticKey,
Network: N.NetworkTCP,
Address: []netip.Prefix{netip.MustParsePrefix(clientTunnelAddress + "/30")},
PeerAddress: badoption.Addr(netip.MustParseAddr(peerTunnelAddress)),
Topology: "p2p",
StaticKeyPath: staticKeyPath,
KeyDirection: "client",
Cipher: "AES-256-CBC",
Auth: "SHA256",
MSSFixDisabled: true,
PingRestartDisabled: true,
}
proxyPort := reserveOpenVPNTCPPort(t)
clientInstance := startInstance(t, openVPNClientInstanceOptions(clientOptions, proxyPort))
clientEndpoint := requireOpenVPNEndpoint(t, clientInstance, "openvpn-client")
connectedStatus := waitForOpenVPNStatus(t, clientEndpoint, 30*time.Second, func(status adapter.OpenVPNStatus) bool {
require.NotEqual(t, adapter.OpenVPNStateError, status.State, status.Error)
return status.State == adapter.OpenVPNStateConnected
})
require.Equal(t, []netip.Prefix{netip.MustParsePrefix(clientTunnelAddress + "/30")}, connectedStatus.TunnelInfo.IPv4)
port, supported := clientEndpoint.(tun.Port)
require.True(t, supported)
returnPath := &openVPNPacketReturn{packets: make(chan []byte, 1)}
err = port.AttachReturn(returnPath)
require.NoError(t, err)
t.Cleanup(func() { _ = port.DetachReturn(returnPath) })
outboundPacket := newOpenVPNDataPathUDPPacket(
netip.MustParseAddrPort(clientTunnelAddress+":12000"),
netip.MustParseAddrPort(peerTunnelAddress+":13000"),
[]byte("sing-box static-key outbound"),
)
err = port.WritePackets([][]byte{outboundPacket})
require.NoError(t, err)
readContext, cancelRead := context.WithTimeout(context.Background(), 10*time.Second)
peerPacket, err := peerClient.ReadDataPacket(readContext)
cancelRead()
require.NoError(t, err)
require.Equal(t, outboundPacket, peerPacket)
inboundPacket := newOpenVPNDataPathUDPPacket(
netip.MustParseAddrPort(peerTunnelAddress+":13000"),
netip.MustParseAddrPort(clientTunnelAddress+":12000"),
[]byte("sing-box static-key inbound"),
)
err = peerClient.WriteDataPacket(inboundPacket)
require.NoError(t, err)
select {
case returnedPacket := <-returnPath.packets:
require.Equal(t, inboundPacket, returnedPacket)
case <-time.After(10 * time.Second):
t.Fatal("timed out waiting for static-key inbound packet")
}
}
func TestOpenVPNStaticKeySelfToSelf(t *testing.T) {
for _, protocol := range []string{N.NetworkTCP, N.NetworkUDP} {
t.Run(protocol, func(t *testing.T) {
runOpenVPNStaticKeySelfToSelf(t, protocol)
})
}
}
func runOpenVPNStaticKeySelfToSelf(t *testing.T, protocol string) {
t.Helper()
const (
serverTunnelAddress = "10.92.0.1"
clientTunnelAddress = "10.92.0.2"
)
openVPNPort := reserveOpenVPNProtocolPort(t, protocol)
var clientOpenVPNPort uint16
var serverRemote string
if protocol == N.NetworkUDP {
clientOpenVPNPort = reserveOpenVPNUDPPort(t)
serverRemote = "127.0.0.1"
}
proxyPort := reserveOpenVPNTCPPort(t)
echoPort := reserveOpenVPNEchoPort(t)
readinessPort := reserveOpenVPNEchoPort(t)
staticKeyPath := writeOpenVPNStaticKeyFile(t, createOpenVPNStaticKey(t))
serverOptions := option.OpenVPNServerEndpointOptions{
ListenOptions: option.ListenOptions{
Listen: common.Ptr(badoption.Addr(netip.MustParseAddr("127.0.0.1"))),
ListenPort: openVPNPort,
},
Mode: ovpn.ModeStaticKey,
Network: protocol,
Remote: serverRemote,
RemotePort: clientOpenVPNPort,
MaxClients: 1,
Address: []netip.Prefix{netip.MustParsePrefix(serverTunnelAddress + "/30")},
PeerAddress: badoption.Addr(netip.MustParseAddr(clientTunnelAddress)),
Topology: "p2p",
StaticKeyPath: staticKeyPath,
KeyDirection: "server",
Cipher: "AES-256-CBC",
Auth: "SHA256",
MSSFixDisabled: true,
}
clientOptions := option.OpenVPNClientEndpointOptions{
ServerOptions: option.ServerOptions{
Server: "127.0.0.1",
ServerPort: openVPNPort,
},
Mode: ovpn.ModeStaticKey,
Network: protocol,
Address: []netip.Prefix{netip.MustParsePrefix(clientTunnelAddress + "/30")},
PeerAddress: badoption.Addr(netip.MustParseAddr(serverTunnelAddress)),
Topology: "p2p",
StaticKeyPath: staticKeyPath,
KeyDirection: "client",
Cipher: "AES-256-CBC",
Auth: "SHA256",
MSSFixDisabled: true,
PingRestartDisabled: true,
}
clientOptions.UDPBindPort = clientOpenVPNPort
startInstance(t, openVPNServerInstanceOptions(serverOptions))
startInstance(t, openVPNClientInstanceOptions(clientOptions, proxyPort))
waitForOpenVPNClientReady(t, proxyPort, readinessPort, serverTunnelAddress)
testSuitOpenVPN(t, proxyPort, echoPort, serverTunnelAddress)
}
func TestOpenVPNDockerInterop(t *testing.T) { func TestOpenVPNDockerInterop(t *testing.T) {
t.Run("official_server_to_sing_box_client", func(t *testing.T) { t.Run("official_server_to_sing_box_client", func(t *testing.T) {
testOpenVPNDockerOfficialServerToSingBoxClient(t) testOpenVPNDockerOfficialServerToSingBoxClient(t)
@@ -1777,6 +1975,27 @@ func createOpenVPNStaticKey(t *testing.T) string {
return strings.Join(lines, "\n") return strings.Join(lines, "\n")
} }
func newOpenVPNDataPathUDPPacket(source netip.AddrPort, destination netip.AddrPort, payload []byte) []byte {
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)
return packet
}
func reserveOpenVPNProtocolPort(t *testing.T, protocol string) uint16 { func reserveOpenVPNProtocolPort(t *testing.T, protocol string) uint16 {
t.Helper() t.Helper()
if protocol == N.NetworkTCP { if protocol == N.NetworkTCP {
+1 -1
View File
@@ -104,7 +104,7 @@ func (d *baseDevice) SetPacketWriter(writer PacketWriter) {
func (d *baseDevice) writeOutbound(packetBuffers []*buf.Buffer) error { func (d *baseDevice) writeOutbound(packetBuffers []*buf.Buffer) error {
if d.packetWriter == nil { if d.packetWriter == nil {
buf.ReleaseMulti(packetBuffers) buf.ReleaseMulti(packetBuffers)
return E.New("missing OpenConnect packet writer") return E.New("missing packet writer")
} }
return d.packetWriter(packetBuffers) return d.packetWriter(packetBuffers)
} }
+2 -2
View File
@@ -5,9 +5,9 @@ package openconnect
import E "github.com/sagernet/sing/common/exceptions" import E "github.com/sagernet/sing/common/exceptions"
func newStackDevice(options DeviceOptions) (Device, error) { func newStackDevice(options DeviceOptions) (Device, error) {
return nil, E.New("OpenConnect system:false requires the with_gvisor build tag") return nil, E.New("system:false requires the with_gvisor build tag")
} }
func newSystemStackDevice(options DeviceOptions) (Device, error) { func newSystemStackDevice(options DeviceOptions) (Device, error) {
return nil, E.New("OpenConnect system stack requires the with_gvisor build tag") return nil, E.New("system stack requires the with_gvisor build tag")
} }
+17 -29
View File
@@ -4,7 +4,6 @@ import (
"context" "context"
"net" "net"
"net/netip" "net/netip"
"runtime"
"slices" "slices"
"sync" "sync"
"syscall" "syscall"
@@ -14,7 +13,6 @@ import (
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun"
"github.com/sagernet/sing-tun/gtcpip/header" "github.com/sagernet/sing-tun/gtcpip/header"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
@@ -96,30 +94,21 @@ func (d *systemDevice) buildTunOptions() tun.Options {
d.inet4Address = inet4Address d.inet4Address = inet4Address
d.inet6Address = inet6Address d.inet6Address = inet6Address
inet4Addresses, inet6Addresses := splitPrefixes(d.options.Configuration.Addresses) inet4Addresses, inet6Addresses := splitPrefixes(d.options.Configuration.Addresses)
inet4Routes, inet6Routes := splitPrefixes(common.Map(d.options.Configuration.Routes, func(route Route) netip.Prefix { return route.Prefix }))
inet4ExcludedRoutes, inet6ExcludedRoutes := splitPrefixes(common.Map(d.options.Configuration.ExcludedRoutes, func(route Route) netip.Prefix { return route.Prefix }))
networkManager := service.FromContext[adapter.NetworkManager](d.options.Context) networkManager := service.FromContext[adapter.NetworkManager](d.options.Context)
tunOptions := tun.Options{ tunOptions := tun.Options{
Name: d.options.Name, Name: d.options.Name,
Inet4Address: inet4Addresses, Inet4Address: inet4Addresses,
Inet6Address: inet6Addresses, Inet6Address: inet6Addresses,
MTU: d.options.MTU, MTU: d.options.MTU,
GSO: true, GSO: true,
InterfaceScope: true, InterfaceScope: true,
DNSAddress: d.options.Configuration.DNS, DNSMode: tun.DNSModeDisabled,
Inet4RouteAddress: inet4Routes, InterfaceMonitor: nil,
Inet6RouteAddress: inet6Routes, InterfaceFinder: nil,
Inet4RouteExcludeAddress: inet4ExcludedRoutes, Logger: d.options.Logger,
Inet6RouteExcludeAddress: inet6ExcludedRoutes, IPRoute2TableIndex: tun.DefaultIPRoute2TableIndex,
InterfaceMonitor: nil, IPRoute2RuleIndex: tun.DefaultIPRoute2RuleIndex,
InterfaceFinder: nil, EXP_DisableDNSHijack: true,
Logger: d.options.Logger,
IPRoute2TableIndex: tun.DefaultIPRoute2TableIndex,
IPRoute2RuleIndex: tun.DefaultIPRoute2RuleIndex,
EXP_DisableDNSHijack: true,
}
if runtime.GOOS == "darwin" {
tunOptions.AutoRoute = true
} }
if networkManager != nil { if networkManager != nil {
tunOptions.InterfaceMonitor = networkManager.InterfaceMonitor() tunOptions.InterfaceMonitor = networkManager.InterfaceMonitor()
@@ -252,13 +241,12 @@ func (d *systemDevice) UpdateConfiguration(configuration Configuration) error {
return nil return nil
} }
if !slices.Equal(previousConfiguration.Addresses, configuration.Addresses) || if !slices.Equal(previousConfiguration.Addresses, configuration.Addresses) ||
previousMTU != updatedMTU || previousMTU != updatedMTU {
!slices.Equal(previousConfiguration.DNS, configuration.DNS) {
d.device.Close() d.device.Close()
d.device = nil d.device = nil
return d.startLocked() return d.startLocked()
} }
return d.device.UpdateRouteOptions(d.buildTunOptions()) return nil
} }
func (d *systemDevice) WriteInboundBuffers(packetBuffers []*buf.Buffer) error { func (d *systemDevice) WriteInboundBuffers(packetBuffers []*buf.Buffer) error {
@@ -270,7 +258,7 @@ func (d *systemDevice) writeBuffers(packetBuffers []*buf.Buffer) error {
tunInterface := d.device tunInterface := d.device
d.stateAccess.RUnlock() d.stateAccess.RUnlock()
if tunInterface == nil { if tunInterface == nil {
return E.New("OpenConnect system device is not ready") return E.New("system device is not ready")
} }
linuxTUN, isLinuxTUN := tunInterface.(tun.LinuxTUN) linuxTUN, isLinuxTUN := tunInterface.(tun.LinuxTUN)
if isLinuxTUN { if isLinuxTUN {
@@ -313,7 +301,7 @@ func (d *systemDevice) writePacket(packet []byte) error {
tunInterface := d.device tunInterface := d.device
d.stateAccess.RUnlock() d.stateAccess.RUnlock()
if tunInterface == nil { if tunInterface == nil {
return E.New("OpenConnect system device is not ready") return E.New("system device is not ready")
} }
if tun.PacketOffset == 0 { if tun.PacketOffset == 0 {
_, err := tunInterface.Write(packet) _, err := tunInterface.Write(packet)
+21 -37
View File
@@ -3,7 +3,6 @@ package openvpn
import ( import (
"context" "context"
"net/netip" "net/netip"
"slices"
"sync/atomic" "sync/atomic"
"time" "time"
@@ -54,13 +53,17 @@ type DeviceOptions struct {
} }
type Configuration struct { type Configuration struct {
MTU uint32 MTU uint32
Address []netip.Prefix Address []netip.Prefix
Routes []Route Routes []Route
DNS []netip.Addr ExcludedRoutes []Route
Topology string DNS []netip.Addr
Interface string DNSServers []DNSServer
BlockIPv6 bool SearchDomains []string
DNSRoutes []string
Topology string
Interface string
BlockIPv6 bool
} }
type Route struct { type Route struct {
@@ -69,6 +72,15 @@ type Route struct {
Metric int 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 NewDevice(options DeviceOptions) (Device, error) {
if !options.System { if !options.System {
return newStackDevice(options) return newStackDevice(options)
@@ -91,7 +103,7 @@ func (d *baseDevice) SetPacketWriter(writer PacketWriter) {
func (d *baseDevice) writeOutbound(packetBuffers []*buf.Buffer) error { func (d *baseDevice) writeOutbound(packetBuffers []*buf.Buffer) error {
if d.packetWriter == nil { if d.packetWriter == nil {
buf.ReleaseMulti(packetBuffers) buf.ReleaseMulti(packetBuffers)
return E.New("missing OpenVPN packet writer") return E.New("missing packet writer")
} }
return d.packetWriter(packetBuffers) return d.packetWriter(packetBuffers)
} }
@@ -192,34 +204,6 @@ func splitPrefixes(prefixes []netip.Prefix) ([]netip.Prefix, []netip.Prefix) {
return inet4Prefixes, inet6Prefixes return inet4Prefixes, inet6Prefixes
} }
func splitRoutes(routes []Route) ([]netip.Prefix, []netip.Prefix) {
var inet4Prefixes []netip.Prefix
var inet6Prefixes []netip.Prefix
for _, route := range routes {
if route.Prefix.Addr().Is4() {
inet4Prefixes = append(inet4Prefixes, route.Prefix)
} else {
inet6Prefixes = append(inet6Prefixes, route.Prefix)
}
}
return inet4Prefixes, inet6Prefixes
}
func routesWithBlockIPv6(configuration Configuration) []Route {
routes := configuration.Routes
if !configuration.BlockIPv6 {
return routes
}
inet6DefaultRoute := netip.PrefixFrom(netip.IPv6Unspecified(), 0)
for _, route := range routes {
if route.Prefix == inet6DefaultRoute {
return routes
}
}
routes = append(slices.Clone(routes), Route{Prefix: inet6DefaultRoute})
return routes
}
func hasRouteOptions(routes []Route) bool { func hasRouteOptions(routes []Route) bool {
for _, route := range routes { for _, route := range routes {
if route.Gateway.IsValid() || route.Metric != 0 { if route.Gateway.IsValid() || route.Metric != 0 {
+3 -3
View File
@@ -102,7 +102,7 @@ func (d *stackDevice) UpdateConfiguration(configuration Configuration) error {
d.stateAccess.Lock() d.stateAccess.Lock()
defer d.stateAccess.Unlock() defer d.stateAccess.Unlock()
if d.logRouteOptions && hasRouteOptions(configuration.Routes) { if d.logRouteOptions && hasRouteOptions(configuration.Routes) {
d.options.Logger.Debug("OpenVPN route gateway and metric options are not representable by the gVisor stack device; routes are installed by prefix") d.options.Logger.Debug("route gateway and metric options are not representable by the gVisor stack device; routes are installed by prefix")
d.logRouteOptions = false d.logRouteOptions = false
} }
if configuration.MTU != 0 { if configuration.MTU != 0 {
@@ -184,7 +184,7 @@ func (d *stackDevice) writeBuffers(packetBuffers []*buf.Buffer) error {
func (d *stackDevice) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { func (d *stackDevice) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
if destination.IsIPv6() && d.blockIPv6Enabled() { if destination.IsIPv6() && d.blockIPv6Enabled() {
return nil, E.New("IPv6 blocked by pushed OpenVPN block-ipv6") return nil, E.New("IPv6 blocked by pushed block-ipv6")
} }
inet4Address, inet6Address := d.PortAddresses() inet4Address, inet6Address := d.PortAddresses()
address := tcpip.FullAddress{ address := tcpip.FullAddress{
@@ -221,7 +221,7 @@ func (d *stackDevice) DialContext(ctx context.Context, network string, destinati
func (d *stackDevice) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { func (d *stackDevice) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
if destination.IsIPv6() && d.blockIPv6Enabled() { if destination.IsIPv6() && d.blockIPv6Enabled() {
return nil, E.New("IPv6 blocked by pushed OpenVPN block-ipv6") return nil, E.New("IPv6 blocked by pushed block-ipv6")
} }
inet4Address, inet6Address := d.PortAddresses() inet4Address, inet6Address := d.PortAddresses()
bind := tcpip.FullAddress{ bind := tcpip.FullAddress{
+2 -2
View File
@@ -5,9 +5,9 @@ package openvpn
import E "github.com/sagernet/sing/common/exceptions" import E "github.com/sagernet/sing/common/exceptions"
func newStackDevice(options DeviceOptions) (Device, error) { func newStackDevice(options DeviceOptions) (Device, error) {
return nil, E.New("OpenVPN system:false requires the with_gvisor build tag") return nil, E.New("system:false requires the with_gvisor build tag")
} }
func newSystemStackDevice(options DeviceOptions) (Device, error) { func newSystemStackDevice(options DeviceOptions) (Device, error) {
return nil, E.New("OpenVPN system stack requires the with_gvisor build tag") return nil, E.New("system stack requires the with_gvisor build tag")
} }
+16 -79
View File
@@ -4,7 +4,6 @@ import (
"context" "context"
"net" "net"
"net/netip" "net/netip"
"runtime"
"slices" "slices"
"sync" "sync"
"syscall" "syscall"
@@ -30,14 +29,13 @@ const (
type systemDevice struct { type systemDevice struct {
baseDevice baseDevice
stateAccess sync.RWMutex stateAccess sync.RWMutex
options DeviceOptions options DeviceOptions
dialer N.Dialer dialer N.Dialer
device tun.Tun device tun.Tun
inet4Address netip.Addr inet4Address netip.Addr
inet6Address netip.Addr inet6Address netip.Addr
logRouteOptions bool closed bool
closed bool
} }
func newSystemDevice(options DeviceOptions) (*systemDevice, error) { func newSystemDevice(options DeviceOptions) (*systemDevice, error) {
@@ -55,11 +53,10 @@ func newSystemDevice(options DeviceOptions) (*systemDevice, error) {
} }
inet4Address, inet6Address := firstAddresses(options.Configuration.Address) inet4Address, inet6Address := firstAddresses(options.Configuration.Address)
return &systemDevice{ return &systemDevice{
options: options, options: options,
dialer: interfaceDialer, dialer: interfaceDialer,
inet4Address: inet4Address, inet4Address: inet4Address,
inet6Address: inet6Address, inet6Address: inet6Address,
logRouteOptions: true,
}, nil }, nil
} }
@@ -97,13 +94,6 @@ func (d *systemDevice) buildTunOptions() tun.Options {
d.inet4Address = inet4Address d.inet4Address = inet4Address
d.inet6Address = inet6Address d.inet6Address = inet6Address
inet4Addresses, inet6Addresses := splitPrefixes(d.options.Configuration.Address) inet4Addresses, inet6Addresses := splitPrefixes(d.options.Configuration.Address)
if d.options.Configuration.BlockIPv6 && len(inet6Addresses) == 0 {
inet6Addresses = append(inet6Addresses, netip.MustParsePrefix("fddd:1194:1194:1194::2/64"))
}
routes := routesWithBlockIPv6(d.options.Configuration)
inet4Routes, inet6Routes := splitRoutes(routes)
inet4Gateway, _ := systemRouteGateway(routes, true)
inet6Gateway, _ := systemRouteGateway(routes, false)
networkManager := service.FromContext[adapter.NetworkManager](d.options.Context) networkManager := service.FromContext[adapter.NetworkManager](d.options.Context)
tunOptions := tun.Options{ tunOptions := tun.Options{
Name: d.options.Name, Name: d.options.Name,
@@ -112,11 +102,7 @@ func (d *systemDevice) buildTunOptions() tun.Options {
MTU: d.options.MTU, MTU: d.options.MTU,
GSO: true, GSO: true,
InterfaceScope: true, InterfaceScope: true,
DNSAddress: d.options.Configuration.DNS, DNSMode: tun.DNSModeDisabled,
Inet4Gateway: inet4Gateway,
Inet6Gateway: inet6Gateway,
Inet4RouteAddress: inet4Routes,
Inet6RouteAddress: inet6Routes,
InterfaceMonitor: nil, InterfaceMonitor: nil,
InterfaceFinder: nil, InterfaceFinder: nil,
Logger: d.options.Logger, Logger: d.options.Logger,
@@ -124,9 +110,6 @@ func (d *systemDevice) buildTunOptions() tun.Options {
IPRoute2RuleIndex: tun.DefaultIPRoute2RuleIndex, IPRoute2RuleIndex: tun.DefaultIPRoute2RuleIndex,
EXP_DisableDNSHijack: true, EXP_DisableDNSHijack: true,
} }
if runtime.GOOS == "darwin" {
tunOptions.AutoRoute = true
}
if networkManager != nil { if networkManager != nil {
tunOptions.InterfaceMonitor = networkManager.InterfaceMonitor() tunOptions.InterfaceMonitor = networkManager.InterfaceMonitor()
tunOptions.InterfaceFinder = networkManager.InterfaceFinder() tunOptions.InterfaceFinder = networkManager.InterfaceFinder()
@@ -134,43 +117,6 @@ func (d *systemDevice) buildTunOptions() tun.Options {
return tunOptions return tunOptions
} }
func systemRouteGateway(routes []Route, ipv4 bool) (netip.Addr, bool) {
var gateway netip.Addr
var hasGateway bool
var hasMissingGateway bool
var gatewayUnrepresentable bool
var metricUnrepresentable bool
for _, route := range routes {
if route.Prefix.Addr().Is4() != ipv4 {
continue
}
if route.Metric != 0 {
metricUnrepresentable = true
}
if !route.Gateway.IsValid() {
hasMissingGateway = true
continue
}
if route.Gateway.Is4() != ipv4 {
gatewayUnrepresentable = true
continue
}
if !hasGateway {
gateway = route.Gateway
hasGateway = true
} else if gateway != route.Gateway {
gatewayUnrepresentable = true
}
}
if hasGateway && hasMissingGateway {
gatewayUnrepresentable = true
}
if gatewayUnrepresentable {
gateway = netip.Addr{}
}
return gateway, gatewayUnrepresentable || metricUnrepresentable
}
func (d *systemDevice) readLoop(tunInterface tun.Tun, mtu int) { func (d *systemDevice) readLoop(tunInterface tun.Tun, mtu int) {
linuxTUN, isLinuxTUN := tunInterface.(tun.LinuxTUN) linuxTUN, isLinuxTUN := tunInterface.(tun.LinuxTUN)
if isLinuxTUN && linuxTUN.BatchSize() > 1 { if isLinuxTUN && linuxTUN.BatchSize() > 1 {
@@ -295,13 +241,6 @@ func (d *systemDevice) readLoopDarwin(tunInterface tun.DarwinTUN) {
func (d *systemDevice) UpdateConfiguration(configuration Configuration) error { func (d *systemDevice) UpdateConfiguration(configuration Configuration) error {
d.stateAccess.Lock() d.stateAccess.Lock()
defer d.stateAccess.Unlock() defer d.stateAccess.Unlock()
routes := routesWithBlockIPv6(configuration)
_, hasUnrepresentableInet4RouteOptions := systemRouteGateway(routes, true)
_, hasUnrepresentableInet6RouteOptions := systemRouteGateway(routes, false)
if d.logRouteOptions && (hasUnrepresentableInet4RouteOptions || hasUnrepresentableInet6RouteOptions) {
d.options.Logger.Debug("some OpenVPN route gateway or metric options are not representable by the system device; routes are installed by prefix")
d.logRouteOptions = false
}
previousConfiguration := d.options.Configuration previousConfiguration := d.options.Configuration
previousMTU := d.options.MTU previousMTU := d.options.MTU
updatedMTU := d.options.MTU updatedMTU := d.options.MTU
@@ -317,14 +256,12 @@ func (d *systemDevice) UpdateConfiguration(configuration Configuration) error {
return nil return nil
} }
if !slices.Equal(previousConfiguration.Address, configuration.Address) || if !slices.Equal(previousConfiguration.Address, configuration.Address) ||
previousMTU != updatedMTU || previousMTU != updatedMTU {
!slices.Equal(previousConfiguration.DNS, configuration.DNS) ||
previousConfiguration.BlockIPv6 != configuration.BlockIPv6 {
d.device.Close() d.device.Close()
d.device = nil d.device = nil
return d.startLocked() return d.startLocked()
} }
return d.device.UpdateRouteOptions(d.buildTunOptions()) return nil
} }
func (d *systemDevice) blockIPv6Enabled() bool { func (d *systemDevice) blockIPv6Enabled() bool {
@@ -342,7 +279,7 @@ func (d *systemDevice) writeBuffers(packetBuffers []*buf.Buffer) error {
tunInterface := d.device tunInterface := d.device
d.stateAccess.RUnlock() d.stateAccess.RUnlock()
if tunInterface == nil { if tunInterface == nil {
return E.New("OpenVPN system device is not ready") return E.New("system device is not ready")
} }
linuxTUN, isLinuxTUN := tunInterface.(tun.LinuxTUN) linuxTUN, isLinuxTUN := tunInterface.(tun.LinuxTUN)
if isLinuxTUN { if isLinuxTUN {
@@ -385,7 +322,7 @@ func (d *systemDevice) writePacket(packet []byte) error {
tunInterface := d.device tunInterface := d.device
d.stateAccess.RUnlock() d.stateAccess.RUnlock()
if tunInterface == nil { if tunInterface == nil {
return E.New("OpenVPN system device is not ready") return E.New("system device is not ready")
} }
if tun.PacketOffset == 0 { if tun.PacketOffset == 0 {
_, err := tunInterface.Write(packet) _, err := tunInterface.Write(packet)