From 2ff294c4f42ca1270ec5f28acd5603ceec9b4bb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 21 Jul 2026 09:38:21 +0800 Subject: [PATCH] Improve OpenVPN & OpenConnect interoperability --- cmd/internal/build_shared/flags.go | 2 +- common/dialer/default.go | 21 +- constant/dns.go | 28 +- docs/configuration/dns/rule.md | 15 +- docs/configuration/dns/rule.zh.md | 15 +- docs/configuration/dns/server/index.md | 2 + docs/configuration/dns/server/index.zh.md | 2 + docs/configuration/dns/server/openconnect.md | 97 ++++ .../dns/server/openconnect.zh.md | 97 ++++ docs/configuration/dns/server/openvpn.md | 82 ++++ docs/configuration/dns/server/openvpn.zh.md | 82 ++++ docs/configuration/endpoint/openconnect.md | 245 +++++++++- docs/configuration/endpoint/openconnect.zh.md | 245 +++++++++- docs/configuration/endpoint/openvpn-client.md | 218 ++++++++- .../endpoint/openvpn-client.zh.md | 195 +++++++- docs/configuration/endpoint/openvpn-server.md | 252 ++++++++++- .../endpoint/openvpn-server.zh.md | 236 +++++++++- docs/installation/build-from-source.md | 3 +- docs/installation/build-from-source.zh.md | 3 +- experimental/libbox/ffi.json | 8 +- go.mod | 7 +- go.sum | 76 +++- include/openconnect.go | 5 + include/openconnect_stub.go | 7 + include/openvpn.go | 5 + include/openvpn_stub.go | 7 + include/registry.go | 2 + mkdocs.yml | 2 + option/openconnect.go | 91 ++-- option/openvpn.go | 69 +++ option/outbound.go | 46 +- protocol/openconnect/client.go | 132 ++++-- protocol/openconnect/dns_transport.go | 373 +++++++++++++++ protocol/openconnect/endpoint.go | 92 ++++ protocol/openvpn/client.go | 347 +++++++++++--- protocol/openvpn/dns_transport.go | 427 ++++++++++++++++++ protocol/openvpn/endpoint.go | 140 +++++- protocol/openvpn/server.go | 243 ++++++++-- release/LDFLAGS | 2 +- test/openvpn_test.go | 219 +++++++++ transport/openconnect/device.go | 2 +- transport/openconnect/device_stack_stub.go | 4 +- transport/openconnect/device_system.go | 46 +- transport/openvpn/device.go | 58 +-- transport/openvpn/device_stack.go | 6 +- transport/openvpn/device_stack_stub.go | 4 +- transport/openvpn/device_system.go | 95 +--- 47 files changed, 3915 insertions(+), 440 deletions(-) create mode 100644 docs/configuration/dns/server/openconnect.md create mode 100644 docs/configuration/dns/server/openconnect.zh.md create mode 100644 docs/configuration/dns/server/openvpn.md create mode 100644 docs/configuration/dns/server/openvpn.zh.md create mode 100644 protocol/openconnect/dns_transport.go create mode 100644 protocol/openvpn/dns_transport.go diff --git a/cmd/internal/build_shared/flags.go b/cmd/internal/build_shared/flags.go index 88b501b3d..19552691a 100644 --- a/cmd/internal/build_shared/flags.go +++ b/cmd/internal/build_shared/flags.go @@ -5,7 +5,7 @@ import "strings" func LinkerFlags(version string, debug bool) string { flags := []string{ "-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", } if !debug { diff --git a/common/dialer/default.go b/common/dialer/default.go index 1c8ad71bd..3e7b69571 100644 --- a/common/dialer/default.go +++ b/common/dialer/default.go @@ -155,6 +155,13 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial if options.DisableTCPKeepAlive { dialer.KeepAlive = -1 dialer.KeepAliveConfig.Enable = false + } else if options.TCPKeepAliveSystemDefaults { + dialer.KeepAliveConfig = net.KeepAliveConfig{ + Enable: true, + Idle: -1, + Interval: -1, + Count: -1, + } } else { keepIdle := time.Duration(options.TCPKeepAlive) if keepIdle == 0 { @@ -188,8 +195,11 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial if options.Inet4BindAddress != nil { bindAddr := options.Inet4BindAddress.Build(netip.IPv4Unspecified()) dialer4.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()} - udpDialer4.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()} - udpAddr4 = M.SocksaddrFrom(bindAddr, 0).String() + udpDialer4.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice(), Port: int(options.UDPBindPort)} + 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 ( dialer6 = dialer @@ -199,8 +209,11 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial if options.Inet6BindAddress != nil { bindAddr := options.Inet6BindAddress.Build(netip.IPv6Unspecified()) dialer6.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()} - udpDialer6.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()} - udpAddr6 = M.SocksaddrFrom(bindAddr, 0).String() + udpDialer6.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice(), Port: int(options.UDPBindPort)} + 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 { dialer4.SetMultipathTCP(true) diff --git a/constant/dns.go b/constant/dns.go index d39ed82c8..d02688064 100644 --- a/constant/dns.go +++ b/constant/dns.go @@ -15,19 +15,21 @@ const ( ) const ( - DNSTypeLegacy = "legacy" - DNSTypeUDP = "udp" - DNSTypeTCP = "tcp" - DNSTypeTLS = "tls" - DNSTypeHTTPS = "https" - DNSTypeQUIC = "quic" - DNSTypeHTTP3 = "h3" - DNSTypeLocal = "local" - DNSTypeHosts = "hosts" - DNSTypeFakeIP = "fakeip" - DNSTypeDHCP = "dhcp" - DNSTypeMDNS = "mdns" - DNSTypeTailscale = "tailscale" + DNSTypeLegacy = "legacy" + DNSTypeUDP = "udp" + DNSTypeTCP = "tcp" + DNSTypeTLS = "tls" + DNSTypeHTTPS = "https" + DNSTypeQUIC = "quic" + DNSTypeHTTP3 = "h3" + DNSTypeLocal = "local" + DNSTypeHosts = "hosts" + DNSTypeFakeIP = "fakeip" + DNSTypeDHCP = "dhcp" + DNSTypeMDNS = "mdns" + DNSTypeTailscale = "tailscale" + DNSTypeOpenConnect = "openconnect" + DNSTypeOpenVPN = "openvpn" ) const ( diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 1fc4eafaf..6e136c6cb 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -507,13 +507,14 @@ Match source device hostname from DHCP leases. Match specified DNS servers' preferred domains. -| Type | Match | -|-------------|------------------------------------------------------------------------------| -| `hosts` | Match predefined entries and entries in hosts files | -| `local` | Match hosts entries, neighbor-resolved hosts, and mDNS local domains | -| `mdns` | Match mDNS local domains (`*.local.` and IPv4/IPv6 link-local reverse zones) | -| `tailscale` | Match MagicDNS hosts and DNS route suffixes | -| `resolved` | Match split DNS and search domains from systemd-resolved links | +| Type | Match | +|---------------|------------------------------------------------------------------------------| +| `hosts` | Match predefined entries and entries in hosts files | +| `local` | Match hosts entries, neighbor-resolved hosts, and mDNS local domains | +| `mdns` | Match mDNS local domains (`*.local.` and IPv4/IPv6 link-local reverse zones) | +| `tailscale` | Match MagicDNS hosts and DNS route suffixes | +| `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 diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index c56e7f5bf..ad56ca41c 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -499,13 +499,14 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`. 匹配指定 DNS 服务器的首选域名。 -| 类型 | 匹配 | -|-------------|-------------------------------------------------------------| -| `hosts` | 匹配预定义条目和 hosts 文件中的条目 | -| `local` | 匹配 hosts 中的条目、邻居解析得到的主机名以及 mDNS 本地域名 | -| `mdns` | 匹配 mDNS 本地域名(`*.local.` 以及 IPv4/IPv6 链路本地反向区域) | -| `tailscale` | 匹配 MagicDNS 主机和 DNS 路由后缀 | -| `resolved` | 匹配 systemd-resolved 链路中的分流域名和搜索域 | +| 类型 | 匹配 | +|---------------|-------------------------------------------------------------| +| `hosts` | 匹配预定义条目和 hosts 文件中的条目 | +| `local` | 匹配 hosts 中的条目、邻居解析得到的主机名以及 mDNS 本地域名 | +| `mdns` | 匹配 mDNS 本地域名(`*.local.` 以及 IPv4/IPv6 链路本地反向区域) | +| `tailscale` | 匹配 MagicDNS 主机和 DNS 路由后缀 | +| `openconnect` | 匹配 VPN 服务器推送的分流 DNS 和搜索域 | +| `resolved` | 匹配 systemd-resolved 链路中的分流域名和搜索域 | #### wifi_ssid diff --git a/docs/configuration/dns/server/index.md b/docs/configuration/dns/server/index.md index bcb058617..436627c4b 100644 --- a/docs/configuration/dns/server/index.md +++ b/docs/configuration/dns/server/index.md @@ -46,6 +46,8 @@ The type of the DNS server. | `mdns` | [mDNS](./mdns/) | | `fakeip` | [Fake IP](./fakeip/) | | `tailscale` | [Tailscale](./tailscale/) | +| `openconnect` | [OpenConnect](./openconnect/) | +| `openvpn` | [OpenVPN](./openvpn/) | | `resolved` | [Resolved](./resolved/) | #### tag diff --git a/docs/configuration/dns/server/index.zh.md b/docs/configuration/dns/server/index.zh.md index 54dd97e7f..70707fb12 100644 --- a/docs/configuration/dns/server/index.zh.md +++ b/docs/configuration/dns/server/index.zh.md @@ -46,6 +46,8 @@ DNS 服务器的类型。 | `mdns` | [mDNS](./mdns/) | | `fakeip` | [Fake IP](./fakeip/) | | `tailscale` | [Tailscale](./tailscale/) | +| `openconnect` | [OpenConnect](./openconnect/) | +| `openvpn` | [OpenVPN](./openvpn/) | | `resolved` | [Resolved](./resolved/) | #### tag diff --git a/docs/configuration/dns/server/openconnect.md b/docs/configuration/dns/server/openconnect.md new file mode 100644 index 000000000..0ba053be3 --- /dev/null +++ b/docs/configuration/dns/server/openconnect.md @@ -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 + } + ] + } + } + ``` diff --git a/docs/configuration/dns/server/openconnect.zh.md b/docs/configuration/dns/server/openconnect.zh.md new file mode 100644 index 000000000..af8128514 --- /dev/null +++ b/docs/configuration/dns/server/openconnect.zh.md @@ -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 + } + ] + } + } + ``` diff --git a/docs/configuration/dns/server/openvpn.md b/docs/configuration/dns/server/openvpn.md new file mode 100644 index 000000000..d11a45730 --- /dev/null +++ b/docs/configuration/dns/server/openvpn.md @@ -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" + } +} +``` diff --git a/docs/configuration/dns/server/openvpn.zh.md b/docs/configuration/dns/server/openvpn.zh.md new file mode 100644 index 000000000..29d1d6684 --- /dev/null +++ b/docs/configuration/dns/server/openvpn.zh.md @@ -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" + } +} +``` diff --git a/docs/configuration/endpoint/openconnect.md b/docs/configuration/endpoint/openconnect.md index 56377cdd5..2f6e4bf1d 100644 --- a/docs/configuration/endpoint/openconnect.md +++ b/docs/configuration/endpoint/openconnect.md @@ -21,9 +21,11 @@ "username": "", "password": "", "auth_group": "", + "cookie": "", "token": { "mode": "", "secret": "", + "secret_path": "", "pin": "", "password": "", "device_id": "", @@ -31,6 +33,13 @@ }, "reported_os": "", "user_agent": "", + "version": "", + "local_hostname": "", + "mobile": { + "platform_version": "", + "device_type": "", + "device_unique_id": "" + }, "csd": { "wrapper_path": "" }, @@ -50,8 +59,28 @@ ] }, "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, "tls": { + "insecure": false, + "server_name": "", + "peer_fingerprint": [], + "system_trust_disabled": false, "certificate_authority": [], "certificate_authority_path": "", "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. +### 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 -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 ==Required== -Software token mode, one of: +Token mode, one of: - `totp`: Time-based One-Time Password. - `hotp`: HMAC-based One-Time Password. - `stoken`: RSA SecurID software token. +- `oidc`: OIDC access token used for HTTP Bearer authentication. ### token.secret -==Required== - 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 `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 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`. -`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 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 @@ -263,16 +346,158 @@ Conflict with `tncc.certificates.certificate`. 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 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 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:`: SHA-1 SPKI fingerprint. +- `sha256:`: SHA-256 SPKI fingerprint. +- `pin-sha256:`: 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 Additional trusted CA certificate content in PEM format. @@ -392,3 +617,7 @@ See [Dial Fields](/configuration/shared/dial/) for details. ## Interactive authentication 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. diff --git a/docs/configuration/endpoint/openconnect.zh.md b/docs/configuration/endpoint/openconnect.zh.md index a0abc3ea2..f4dfa353e 100644 --- a/docs/configuration/endpoint/openconnect.zh.md +++ b/docs/configuration/endpoint/openconnect.zh.md @@ -21,9 +21,11 @@ "username": "", "password": "", "auth_group": "", + "cookie": "", "token": { "mode": "", "secret": "", + "secret_path": "", "pin": "", "password": "", "device_id": "", @@ -31,6 +33,13 @@ }, "reported_os": "", "user_agent": "", + "version": "", + "local_hostname": "", + "mobile": { + "platform_version": "", + "device_type": "", + "device_unique_id": "" + }, "csd": { "wrapper_path": "" }, @@ -50,8 +59,28 @@ ] }, "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, "tls": { + "insecure": false, + "server_name": "", + "peer_fingerprint": [], + "system_trust_disabled": false, "certificate_authority": [], "certificate_authority_path": "", "client_certificate": [], @@ -125,30 +154,56 @@ OpenConnect 协议 flavor,可选值为 `anyconnect`、`gp`、`fortinet`、`f5` 认证组,用于在所选 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 字段或进行 HTTP Bearer 认证的 token 配置。 + +必须设置 `token.secret` 或 `token.secret_path` 之一。 ### token.mode ==必填== -软件 token 模式,可选值为: +Token 模式,可选值为: - `totp`:基于时间的一次性密码。 - `hotp`:基于 HMAC 的一次性密码。 - `stoken`:RSA SecurID 软件 token。 +- `oidc`:用于 HTTP Bearer 认证的 OIDC access token。 ### token.secret -==必填== - 软件 token 密钥。 对于 `totp` 和 `hotp`,可以是 Base32 密钥、带 `base32:` 前缀的密钥或类型匹配的 `otpauth://` URI。 对于 `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 `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` 默认使用 `linux-64`。`gp` 和 `pulse` 默认根据系统平台选择值。 +默认值根据系统平台选择:Windows 使用 `win`,macOS 使用 `mac-intel`,Android 使用 `android`,iOS 使用 `apple-ios`,其他 64 位或 32 位系统使用 `linux-64` 或 `linux`。 ### 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 @@ -263,16 +346,158 @@ PEM 格式的 TNCC 机器证书路径。 禁用 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 -允许旧版 VPN 服务器所需的已弃用 TLS 和 DTLS 版本及密码套件。 +启用旧版 VPN 服务器所需的弱 TLS 和 DTLS 密码套件及 TLS 1.0 兼容性。 -默认禁用。此选项不会禁用服务器证书验证。 +默认禁用;未启用时会拒绝低于 TLS 1.2 的版本。此选项不会禁用服务器证书验证。 ### 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:`:SHA-1 SPKI 指纹。 +- `sha256:`:SHA-256 SPKI 指纹。 +- `pin-sha256:`:Base64 编码的 SHA-256 SPKI pin。 + +每种格式的编码指纹均可缩写为至少四个字符的前缀。配置后,对端证书必须匹配其中一个指纹;匹配的指纹可以授权未通过其他方式信任的证书。 + +### tls.system_trust_disabled + +禁用系统 CA 证书池。 + +启用时,使用 `tls.certificate_authority` 或 `tls.peer_fingerprint` 建立信任。 + ### tls.certificate_authority PEM 格式的附加受信任 CA 证书内容。 @@ -392,3 +617,7 @@ MCA 证书和私钥必须同时设置或同时为空。 ## 交互式认证 在 sing-box dashboard 或任意 sing-box 图形客户端的 `工具` > `端点` 中认证和管理 endpoint。 + +## DNS + +推送的 DNS 设置不会安装到操作系统中。配置 [OpenConnect DNS 服务器](/zh/configuration/dns/server/openconnect/) 以通过 sing-box 使用这些设置。 diff --git a/docs/configuration/endpoint/openvpn-client.md b/docs/configuration/endpoint/openvpn-client.md index 86d2a54e2..d35a35f77 100644 --- a/docs/configuration/endpoint/openvpn-client.md +++ b/docs/configuration/endpoint/openvpn-client.md @@ -9,6 +9,7 @@ "type": "openvpn-client", "tag": "ovpn-client", + "mode": "tls", "server": "127.0.0.1", "server_port": 1194, "servers": [ @@ -20,11 +21,18 @@ ], "remote_random": false, "network": "udp", + "address": [], + "peer_address": "", + "peer_address_ipv6": "", + "topology": "", "username": "", "password": "", "auth_retry": "none", "static_challenge": "", "static_challenge_echo": false, + "static_key": [], + "static_key_path": "", + "key_direction": "", "tls": { "server_name": "", "server_name_type": "name", @@ -40,6 +48,7 @@ "remote_certificate_eku": "", "remote_certificate_tls": "", "certificate_profile": "", + "ns_certificate_type": "", "version_min": "1.2", "version_max": "", "cipher": "", @@ -51,11 +60,16 @@ "direction": "" } }, + "cipher": "", "data_ciphers": [], "data_ciphers_fallback": "", "auth": "", "mss_fix": 0, + "mss_fix_disabled": false, + "mss_fix_mode": "", "fragment": 0, + "replay_window": 0, + "replay_window_time": "", "compression": "", "compression_lzo": "", "allow_compression": "no", @@ -71,9 +85,17 @@ "route_metric": 0, "redirect_gateway": false, "redirect_gateway_flags": [], + "redirect_private": false, + "block_ipv6": false, "ping_interval": "", "ping_restart": "", + "ping_restart_disabled": false, "renegotiate_interval": "", + "renegotiate_disabled": false, + "renegotiate_bytes": 0, + "renegotiate_packets": 0, + "tls_timeout": "", + "handshake_window": "", "explicit_exit_notify": 0, "system": false, "name": "", @@ -91,6 +113,17 @@ ## 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 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`. +### 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 for OpenVPN username/password authentication. +Only available in TLS mode. + ### password 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. +### 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 -==Required== +Required in TLS mode. OpenVPN control channel TLS configuration. @@ -283,6 +366,19 @@ Certificate profile, one of `insecure`, `legacy`, `preferred`, or `suiteb`. `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 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. +### 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 Allowed OpenVPN data channel ciphers. +Only available in TLS mode. + `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 channel cipher for peers that do not support cipher negotiation. Disabled by default. +Only available in TLS mode. + ### auth OpenVPN data channel authentication digest. `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 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, 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 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. +### 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 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`. -`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`. @@ -433,36 +580,66 @@ For example, `route ` matches pushed IPv4 route options without matching `route- ### 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. +They do not install operating-system routes. Select the endpoint through +sing-box route rules or its preferred-route behavior. + ### route_gateway IPv4 gateway for routes through the OpenVPN endpoint. 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 Default metric for routes through the OpenVPN endpoint. The platform default is used when `0`. +The value is retained for OpenVPN configuration compatibility and does not +install a system route. + ### 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. +This does not install an operating-system default route. + ### 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. +### 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 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 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 OpenVPN TLS renegotiation interval. 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 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. +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. ### name diff --git a/docs/configuration/endpoint/openvpn-client.zh.md b/docs/configuration/endpoint/openvpn-client.zh.md index db1c07120..1ee072dea 100644 --- a/docs/configuration/endpoint/openvpn-client.zh.md +++ b/docs/configuration/endpoint/openvpn-client.zh.md @@ -9,6 +9,7 @@ "type": "openvpn-client", "tag": "ovpn-client", + "mode": "tls", "server": "127.0.0.1", "server_port": 1194, "servers": [ @@ -20,11 +21,18 @@ ], "remote_random": false, "network": "udp", + "address": [], + "peer_address": "", + "peer_address_ipv6": "", + "topology": "", "username": "", "password": "", "auth_retry": "none", "static_challenge": "", "static_challenge_echo": false, + "static_key": [], + "static_key_path": "", + "key_direction": "", "tls": { "server_name": "", "server_name_type": "name", @@ -40,6 +48,7 @@ "remote_certificate_eku": "", "remote_certificate_tls": "", "certificate_profile": "", + "ns_certificate_type": "", "version_min": "1.2", "version_max": "", "cipher": "", @@ -51,11 +60,16 @@ "direction": "" } }, + "cipher": "", "data_ciphers": [], "data_ciphers_fallback": "", "auth": "", "mss_fix": 0, + "mss_fix_disabled": false, + "mss_fix_mode": "", "fragment": 0, + "replay_window": 0, + "replay_window_time": "", "compression": "", "compression_lzo": "", "allow_compression": "no", @@ -71,9 +85,17 @@ "route_metric": 0, "redirect_gateway": false, "redirect_gateway_flags": [], + "redirect_private": false, + "block_ipv6": false, "ping_interval": "", "ping_restart": "", + "ping_restart_disabled": false, "renegotiate_interval": "", + "renegotiate_disabled": false, + "renegotiate_bytes": 0, + "renegotiate_packets": 0, + "tls_timeout": "", + "handshake_window": "", "explicit_exit_notify": 0, "system": false, "name": "", @@ -91,6 +113,16 @@ ## 字段 +### mode + +OpenVPN 会话模式,可选值为 `tls` 或 `static_key`。 + +默认使用 `tls`。 + +`static_key` 是已弃用的 OpenVPN 模式,不使用 TLS 控制通道且不提供前向保密。 +为兼容无法修改的企业 VPN 服务器,此模式仍作为显式兼容选项保留。该模式不使用 +`tls`、用户名/密码认证、拉取选项或 TLS 重协商选项。 + ### server OpenVPN 服务器地址。 @@ -147,10 +179,36 @@ OpenVPN 服务器端口。 该值应用于 `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 OpenVPN 用户名/密码认证的用户名。 +仅在 TLS 模式下可用。 + ### password 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 模式下必填。 OpenVPN 控制通道 TLS 配置。 @@ -283,6 +363,16 @@ OpenVPN 控制通道 TLS 配置。 默认使用 `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 版本,可选值为 `1.0`、`1.1`、`1.2` 或 `1.3`。 @@ -341,24 +431,43 @@ OpenVPN 控制通道封装。 仅当 `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 允许的 OpenVPN 数据通道 cipher。 +仅在 TLS 模式下可用。 + 默认使用 `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 用于不支持 cipher 协商的对端的数据通道 cipher。 默认禁用。 +仅在 TLS 模式下可用。 + ### auth OpenVPN 数据通道认证摘要。 默认使用 `SHA1`,仅应用于非 AEAD 数据 cipher 和 `tls_auth`。 +为兼容既有服务器,显式配置时仍可使用 `MD5` 和 `RIPEMD160` 等旧摘要。 + ### mss_fix OpenVPN UDP packet 的最大大小,用于限制通过隧道发送的 TCP 连接 MSS。 @@ -367,6 +476,20 @@ OpenVPN UDP packet 的最大大小,用于限制通过隧道发送的 TCP 连 为空时使用上游 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 用于 OpenVPN 数据通道 fragmentation 的最大 OpenVPN UDP packet 大小。 @@ -375,6 +498,18 @@ OpenVPN UDP packet 的最大大小,用于限制通过隧道发送的 TCP 连 与 TCP 传输冲突。 +### replay_window + +UDP 数据通道重放窗口大小。默认使用 `64`,最大值为 `65536`。 + +TCP 始终要求数据包 ID 严格连续。 + +### replay_window_time + +UDP 数据通道重放窗口时长。默认使用 `15s`,最大值为 `10m`。 + +该值必须使用整秒。 + ### compression OpenVPN `compress` framing 模式,可选值为 `none`、`no`、`lz4`、`lz4-v2`、`stub`、`stub-v2`、`disabled` 或 `off`。 @@ -395,7 +530,7 @@ Compression 可能削弱流量机密性。仅在服务器要求时启用。 服务器推送的 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 冲突。 @@ -432,36 +567,56 @@ Filter action,可选值为 `accept`、`ignore` 或 `reject`。 ### routes -通过 OpenVPN endpoint 路由的 IPv4 和 IPv6 route prefix。 +sing-box 路由优先选择此 OpenVPN endpoint 的 IPv4 和 IPv6 前缀。 这些 route 会与从服务器接受的 route 一起使用。 +它们不会安装操作系统路由。请通过 sing-box 路由规则或 endpoint 的首选路由行为选择此 endpoint。 + ### route_gateway 通过 OpenVPN endpoint 路由的 IPv4 gateway。 为空时使用从服务器接收的 VPN gateway。 +该值仅为兼容 OpenVPN 配置而保留;endpoint 的路由偏好只按前缀判断,不会安装系统 gateway 路由。 + ### route_metric 通过 OpenVPN endpoint 路由的默认 metric。 设为 `0` 时使用平台默认值。 +该值仅为兼容 OpenVPN 配置而保留,不会安装系统路由。 + ### redirect_gateway -通过 OpenVPN endpoint 路由所有 IPv4 流量。 +在 sing-box 路由中对所有 IPv4 目的地优先选择 OpenVPN endpoint。 默认禁用。 +这不会安装操作系统默认路由。 + ### redirect_gateway_flags 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 客户端未向服务器发送任何 packet 时,发送 data channel ping 的间隔。 @@ -482,12 +637,40 @@ OpenVPN `redirect-gateway` flag。 为空时,启用了 pull 的 UDP 连接会使用 `120s`,直到服务器推送其他值。TCP 不使用默认接收超时。 +### ping_restart_disabled + +禁用初始 `120s` UDP 拉取超时和本地配置的 ping 重启超时。 + +与 `ping_restart` 冲突。 + ### renegotiate_interval OpenVPN TLS 重新协商间隔。 为空时使用 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 关闭 UDP 连接时发送的 OpenVPN exit notification 数量。 @@ -500,6 +683,8 @@ Notification 之间间隔一秒。设为 `0` 时禁用。 需要权限,且不能与现有系统接口冲突。 +endpoint 会配置接口地址和 MTU,但不会安装操作系统路由或 DNS 设置。 + 禁用时,sing-box 使用内部网络栈。 ### name diff --git a/docs/configuration/endpoint/openvpn-server.md b/docs/configuration/endpoint/openvpn-server.md index 0cb029f05..37802ba81 100644 --- a/docs/configuration/endpoint/openvpn-server.md +++ b/docs/configuration/endpoint/openvpn-server.md @@ -14,9 +14,14 @@ "system": false, "name": "", "mtu": 1500, + "mode": "tls", "network": "udp", + "remote": "", + "remote_port": 0, "max_clients": 1024, "address": [], + "peer_address": "", + "peer_address_ipv6": "", "topology": "subnet", "duplicate_cn": false, "users": [ @@ -25,6 +30,9 @@ "password": "" } ], + "static_key": [], + "static_key_path": "", + "key_direction": "", "tls": { "certificate": [], "certificate_path": "", @@ -33,7 +41,19 @@ "client_certificate": [], "client_certificate_path": "", "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": "", + "ns_certificate_type": "", + "version_min": "1.2", + "version_max": "", + "cipher": "", + "groups": "", "control_wrap": { "type": "tls_crypt", "key": [], @@ -42,12 +62,21 @@ "force_cookie": false } }, + "cipher": "", "data_ciphers": [], "data_ciphers_fallback": "", "auth": "", + "mss_fix": 0, + "mss_fix_disabled": false, + "mss_fix_mode": "", + "replay_window": 0, + "replay_window_time": "", "push": { "routes": [], "dns": [], + "dns_servers": [], + "search_domains": [], + "dhcp_options": [], "redirect_gateway": false, "redirect_gateway_flags": [], "block_outside_dns": false, @@ -57,6 +86,9 @@ "ping_interval": "", "ping_restart": "", "renegotiate_interval": "", + "renegotiate_disabled": false, + "renegotiate_bytes": 0, + "renegotiate_packets": 0, "handshake_window": "1m", ... // UDP NAT Fields @@ -79,6 +111,9 @@ Use system interface. 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. ### name @@ -93,6 +128,16 @@ OpenVPN interface MTU. `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 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, 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 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. +`static_key` mode supports one peer, so this value must be `0` or `1`. + ### address ==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. +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 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 @@ -135,12 +210,16 @@ When disabled, a newly authenticated session replaces the existing session with Disabled by default. +Only available in TLS mode. + ### 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`. +Only available in TLS mode. + ### users.username Username. @@ -149,9 +228,34 @@ Username. 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 -==Required== +Required in TLS mode. OpenVPN control channel TLS configuration. @@ -191,7 +295,7 @@ Conflict with `tls.key`. 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`. @@ -199,7 +303,7 @@ Conflict with `tls.client_certificate_path`. 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`. @@ -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. +### 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 Certificate profile, one of `insecure`, `legacy`, `preferred`, or `suiteb`. `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 OpenVPN control channel wrapping. @@ -272,12 +435,30 @@ clients without cookie support are accepted using the upstream `allow-noncookie` 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 Allowed OpenVPN data channel ciphers. `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 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. +Only available in TLS mode. + ### auth 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`. +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 Options pushed to clients. @@ -306,6 +512,22 @@ IPv4 and IPv6 prefixes can be mixed. 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` 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. +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 Maximum time allowed for the initial TLS handshake and each TLS renegotiation. `1m` is used by default. +Only available in TLS mode. + ## UDP NAT Fields These fields configure UDP sessions for traffic through the OpenVPN interface. diff --git a/docs/configuration/endpoint/openvpn-server.zh.md b/docs/configuration/endpoint/openvpn-server.zh.md index b862908b4..2ebdbce14 100644 --- a/docs/configuration/endpoint/openvpn-server.zh.md +++ b/docs/configuration/endpoint/openvpn-server.zh.md @@ -14,9 +14,14 @@ "system": false, "name": "", "mtu": 1500, + "mode": "tls", "network": "udp", + "remote": "", + "remote_port": 0, "max_clients": 1024, "address": [], + "peer_address": "", + "peer_address_ipv6": "", "topology": "subnet", "duplicate_cn": false, "users": [ @@ -25,6 +30,9 @@ "password": "" } ], + "static_key": [], + "static_key_path": "", + "key_direction": "", "tls": { "certificate": [], "certificate_path": "", @@ -33,7 +41,19 @@ "client_certificate": [], "client_certificate_path": "", "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": "", + "ns_certificate_type": "", + "version_min": "1.2", + "version_max": "", + "cipher": "", + "groups": "", "control_wrap": { "type": "tls_crypt", "key": [], @@ -42,12 +62,21 @@ "force_cookie": false } }, + "cipher": "", "data_ciphers": [], "data_ciphers_fallback": "", "auth": "", + "mss_fix": 0, + "mss_fix_disabled": false, + "mss_fix_mode": "", + "replay_window": 0, + "replay_window_time": "", "push": { "routes": [], "dns": [], + "dns_servers": [], + "search_domains": [], + "dhcp_options": [], "redirect_gateway": false, "redirect_gateway_flags": [], "block_outside_dns": false, @@ -57,6 +86,9 @@ "ping_interval": "", "ping_restart": "", "renegotiate_interval": "", + "renegotiate_disabled": false, + "renegotiate_bytes": 0, + "renegotiate_packets": 0, "handshake_window": "1m", ... // UDP NAT 字段 @@ -79,6 +111,8 @@ 需要特权且不能与已有系统接口冲突。 +endpoint 会配置接口地址和 MTU,但不会安装操作系统路由或 DNS 设置。 + 如果禁用,sing-box 将使用内部网络栈。 ### name @@ -93,6 +127,14 @@ OpenVPN 接口 MTU。 默认使用 `1500`。 +### mode + +OpenVPN 会话模式,`tls` 或 `static_key` 之一。 + +默认使用 `tls`。 + +`static_key` 在没有 TLS 控制信道和前向保密的情况下服务一个对端,仅作为不可变部署的显式兼容选项保留。该模式不使用 `tls`、`users`、推送选项或 TLS 重协商选项。 + ### network OpenVPN 传输网络,`udp` 或 `tcp` 之一。 @@ -103,12 +145,26 @@ OpenVPN 传输网络,`udp` 或 `tcp` 之一。 需要配置两个端点并使用互不重叠的 `address` 子网, 与上游 OpenVPN 需要两个服务进程一致。 +### remote + +UDP `static_key` 服务器的固定远端地址。 + +在 UDP `static_key` 模式下与 `remote_port` 一起必填。TCP 服务器从监听套接字接受单个对端,不使用此字段。 + +### remote_port + +UDP `static_key` 服务器的固定远端端口。 + +在 UDP `static_key` 模式下与 `remote` 一起必填。 + ### max_clients 已建立与握手中的 TLS 客户端会话的最大数量。 默认使用 `1024`。该值必须小于 OpenVPN peer-id 空间的大小 `16777216`。 +`static_key` 模式仅支持一个对端,因此此值必须为 `0` 或 `1`。 + ### address ==必填== @@ -121,11 +177,25 @@ OpenVPN 服务器地址前缀列表。 第一个 IPv4 和 IPv6 前缀地址用作端点的本地地址。 +在 `static_key` 模式下,这些地址是本地隧道前缀,而不是地址池。 + +### peer_address + +IPv4 隧道对端地址。 + +在 `static_key` 模式下配置 IPv4 `address` 时必填。 + +### peer_address_ipv6 + +IPv6 隧道对端地址。 + +在 `static_key` 模式下配置 IPv6 `address` 时必填。 + ### topology 推送给客户端的 OpenVPN topology,`subnet`、`p2p` 或 `net30` 之一。 -默认使用 `subnet`。 +TLS 模式默认使用 `subnet`,`static_key` 模式默认使用 `p2p`。 ### duplicate_cn @@ -135,12 +205,16 @@ OpenVPN 服务器地址前缀列表。 默认禁用。 +仅在 TLS 模式下可用。 + ### users OpenVPN 用户名/密码用户列表。 如果设置,客户端除了通过 `tls.verify_client_certificate` 配置的证书策略外,还必须通过用户名/密码认证。 +仅在 TLS 模式下可用。 + ### 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 模式下必填。 OpenVPN 控制信道 TLS 配置。 @@ -191,7 +289,7 @@ TLS 服务器私钥路径。 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` 冲突。 @@ -199,7 +297,7 @@ 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` 冲突。 @@ -215,12 +313,68 @@ OpenVPN 客户端证书策略,`require`、`optional` 或 `none` 之一。 该字段不替代 `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 证书 profile,可选值为 `insecure`、`legacy`、`preferred` 或 `suiteb`。 默认使用 `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 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 允许的 OpenVPN 数据信道加密方式。 默认使用 `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 用于不支持加密方式协商的遗留客户端的 OpenVPN 数据信道加密方式。 @@ -285,12 +451,36 @@ OpenVPN `tls-auth` 密钥方向,`server` 或 `client` 之一。 默认禁用。 +仅在 TLS 模式下可用。 + ### auth OpenVPN 数据信道认证摘要。 默认使用 `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 推送给客户端的选项。 @@ -305,6 +495,22 @@ IPv4 和 IPv6 前缀可以混用。 推送给客户端的 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 向客户端推送 `redirect-gateway`,根据 `push.redirect_gateway_flags` 通过 VPN 路由客户端流量。 @@ -371,12 +577,34 @@ OpenVPN TLS 重协商间隔。 为空时使用 OpenVPN 默认值 `1h`。 +仅在 TLS 模式下可用。 + +### renegotiate_disabled + +禁用基于时间的 TLS 重协商,包括默认间隔。 + +仅在 TLS 模式下可用。 + +### renegotiate_bytes + +传输指定字节数后重新协商数据通道密钥。`0` 使用与密码算法相关的 OpenVPN 默认值。 + +仅在 TLS 模式下可用。 + +### renegotiate_packets + +传输指定数据包数后重新协商数据通道密钥。`0` 使用与密码算法相关的 OpenVPN 默认值。 + +仅在 TLS 模式下可用。 + ### handshake_window 初始 TLS 握手和每次 TLS 重协商允许使用的最长时间。 默认使用 `1m`。 +仅在 TLS 模式下可用。 + ## UDP NAT 字段 这些字段配置通过 OpenVPN 接口的流量的 UDP 会话。 diff --git a/docs/installation/build-from-source.md b/docs/installation/build-from-source.md index 48b53b178..ea799863a 100644 --- a/docs/installation/build-from-source.md +++ b/docs/installation/build-from-source.md @@ -69,11 +69,10 @@ It is not recommended to change the default build tag list unless you really kno ## :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 | |-------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `-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. | ## :material-package-variant: For Downstream Packagers diff --git a/docs/installation/build-from-source.zh.md b/docs/installation/build-from-source.zh.md index 4ffebdc65..cfd8ac76c 100644 --- a/docs/installation/build-from-source.zh.md +++ b/docs/installation/build-from-source.zh.md @@ -73,11 +73,10 @@ go build -tags "tag_a tag_b" ./cmd/sing-box ## :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` 构建标记一起使用。 | ## :material-package-variant: 下游打包者 diff --git a/experimental/libbox/ffi.json b/experimental/libbox/ffi.json index 81fae27da..90d9bd59f 100644 --- a/experimental/libbox/ffi.json +++ b/experimental/libbox/ffi.json @@ -44,7 +44,7 @@ "ts_omit_synology", "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 } }, @@ -73,7 +73,7 @@ "ts_omit_synology", "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 } }, @@ -105,7 +105,7 @@ "ts_omit_synology", "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 }, "overrides": [ @@ -146,7 +146,7 @@ "ts_omit_synology", "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 } } diff --git a/go.mod b/go.mod index 7007ddfa1..39621d1eb 100644 --- a/go.mod +++ b/go.mod @@ -46,8 +46,8 @@ require ( github.com/sagernet/sing v0.8.12-0.20260717153536-4f1ed45a99a5 github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 github.com/sagernet/sing-mux v0.3.5 - github.com/sagernet/sing-openconnect v0.0.0-20260720032640-bf28b6a6f10e - github.com/sagernet/sing-openvpn v0.0.0-20260720132803-a5e407d00242 + github.com/sagernet/sing-openconnect v0.0.0-20260721013312-6c25fa7e089a + github.com/sagernet/sing-openvpn v0.0.0-20260721005523-64b754d1c277 github.com/sagernet/sing-quic v0.6.4-0.20260720125339-63fa6a7ebc31 github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks2 v0.2.1 @@ -82,6 +82,7 @@ require ( require ( filippo.io/edwards25519 v1.1.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/akutz/memconn v0.1.0 // indirect github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect @@ -95,6 +96,7 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // 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/florianl/go-nfqueue/v2 v2.0.2 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect @@ -177,6 +179,7 @@ require ( github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // 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/x448/float16 v0.8.4 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect diff --git a/go.sum b/go.sum index 3ad3a153f..0c4e1b70e 100644 --- a/go.sum +++ b/go.sum @@ -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/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/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM= 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/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A= 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/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= 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/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/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/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk= 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/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= 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/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/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/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= 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/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.0.2 h1:FL5lQTeetgpCvac1TRwSfgaXUn0YSO7WzGvWNIp3JPE= github.com/florianl/go-nfqueue/v2 v2.0.2/go.mod h1:VA09+iPOT43OMoCKNfXHyzujQUty2xmzyCRkBOlmabc= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= @@ -92,14 +106,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/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/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/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/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 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/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/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.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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/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/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/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= 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-mux v0.3.5 h1:RHnhVEc+SFqkrK4xMygYjDwwLhzp2Bj3lztSukONfhI= github.com/sagernet/sing-mux v0.3.5/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= -github.com/sagernet/sing-openconnect v0.0.0-20260720032640-bf28b6a6f10e h1:z0QFO/Bj3ikw4qDM7iwlEzSRdt/04Mh1s1pPeI/LRs0= -github.com/sagernet/sing-openconnect v0.0.0-20260720032640-bf28b6a6f10e/go.mod h1:EIzh5HtImfQJxPKXFwS9lyMnmMy4aCQCx7ntQ4u41Gs= -github.com/sagernet/sing-openvpn v0.0.0-20260720132803-a5e407d00242 h1:0ZvKyBmBIlAuZ9G+zAWdGh6SrmXIXN7NWENHDrptxtI= -github.com/sagernet/sing-openvpn v0.0.0-20260720132803-a5e407d00242/go.mod h1:mK4GzZyUIhG751Mt1MSvSqLGYOR7DFJd8q5QfgFjE2Y= +github.com/sagernet/sing-openconnect v0.0.0-20260721013312-6c25fa7e089a h1:DafepSfytV5uShjQctZ0Cnw+Q9cOBIMT88p8jU2DSck= +github.com/sagernet/sing-openconnect v0.0.0-20260721013312-6c25fa7e089a/go.mod h1:4AKZLVcvY3r54UaK2Gbnm7aN8pOwdLz+y4EP0QFZ5Eg= +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-20260721005523-64b754d1c277/go.mod h1:PWX7WygD8jpwfqfaGNySXpJYTn0SOwjBI1BKHHC2+Bw= github.com/sagernet/sing-quic v0.6.4-0.20260720125339-63fa6a7ebc31 h1:FuiTs45PaA1/f2Kj6E2QlctFBrJUUFB8QAHnV5im9c8= github.com/sagernet/sing-quic v0.6.4-0.20260720125339-63fa6a7ebc31/go.mod h1:9k+dzGsWMttUGldBzq3dU792YHXzW6NgfbOGltnXq+0= 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/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s= 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/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= 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= 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-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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 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.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +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/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= 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/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/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= @@ -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.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +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-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-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 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.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +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/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-20210220032951-036812b2e83c/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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +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-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-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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -486,6 +534,10 @@ golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 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-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 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= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= 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/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/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/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 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/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= diff --git a/include/openconnect.go b/include/openconnect.go index af48a8ea6..259340c9d 100644 --- a/include/openconnect.go +++ b/include/openconnect.go @@ -4,9 +4,14 @@ package include import ( "github.com/sagernet/sing-box/adapter/endpoint" + "github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/protocol/openconnect" ) func registerOpenConnectEndpoint(registry *endpoint.Registry) { openconnect.RegisterEndpoint(registry) } + +func registerOpenConnectDNSTransport(registry *dns.TransportRegistry) { + openconnect.RegisterDNSTransport(registry) +} diff --git a/include/openconnect_stub.go b/include/openconnect_stub.go index 93b9fa481..360ed8d9b 100644 --- a/include/openconnect_stub.go +++ b/include/openconnect_stub.go @@ -8,6 +8,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/endpoint" C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" 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`) }) } + +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`) + }) +} diff --git a/include/openvpn.go b/include/openvpn.go index ba3b0d086..58d03c709 100644 --- a/include/openvpn.go +++ b/include/openvpn.go @@ -4,9 +4,14 @@ package include import ( "github.com/sagernet/sing-box/adapter/endpoint" + "github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/protocol/openvpn" ) func registerOpenVPNEndpoints(registry *endpoint.Registry) { openvpn.RegisterEndpoint(registry) } + +func registerOpenVPNDNSTransport(registry *dns.TransportRegistry) { + openvpn.RegisterDNSTransport(registry) +} diff --git a/include/openvpn_stub.go b/include/openvpn_stub.go index 559577a89..a1e24c8e7 100644 --- a/include/openvpn_stub.go +++ b/include/openvpn_stub.go @@ -8,6 +8,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/endpoint" C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" 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`) }) } + +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`) + }) +} diff --git a/include/registry.go b/include/registry.go index 49e6b1a3e..9bb9220c1 100644 --- a/include/registry.go +++ b/include/registry.go @@ -134,6 +134,8 @@ func DNSTransportRegistry() *dns.TransportRegistry { registerQUICTransports(registry) registerDHCPTransport(registry) registerTailscaleTransport(registry) + registerOpenConnectDNSTransport(registry) + registerOpenVPNDNSTransport(registry) return registry } diff --git a/mkdocs.yml b/mkdocs.yml index 31ba90b7d..f8cb94669 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -99,6 +99,8 @@ nav: - mDNS: configuration/dns/server/mdns.md - FakeIP: configuration/dns/server/fakeip.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 - DNS Rule: configuration/dns/rule.md - DNS Rule Action: configuration/dns/rule_action.md diff --git a/option/openconnect.go b/option/openconnect.go index 28cef8823..f2450871b 100644 --- a/option/openconnect.go +++ b/option/openconnect.go @@ -4,36 +4,63 @@ import "github.com/sagernet/sing/common/json/badoption" type OpenConnectEndpointOptions struct { DialerOptions - System bool `json:"system,omitempty"` - Name string `json:"name,omitempty"` - UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"` - UDPMapping UDPNATBehavior `json:"udp_mapping,omitempty"` - UDPFiltering UDPNATBehavior `json:"udp_filtering,omitempty"` - UDPNATMax uint32 `json:"udp_nat_max,omitempty"` - Server string `json:"server"` - Flavor string `json:"flavor,omitempty"` - Username string `json:"username,omitempty"` - Password string `json:"password,omitempty"` - AuthGroup string `json:"auth_group,omitempty"` - Token *OpenConnectTokenOptions `json:"token,omitempty"` - ReportedOS string `json:"reported_os,omitempty"` - UserAgent string `json:"user_agent,omitempty"` - CSD *OpenConnectCSDOptions `json:"csd,omitempty"` - HIP *OpenConnectHIPOptions `json:"hip,omitempty"` - TNCC *OpenConnectTNCCOptions `json:"tncc,omitempty"` - NoUDP bool `json:"no_udp,omitempty"` - AllowInsecureCrypto bool `json:"allow_insecure_crypto,omitempty"` - TLS OpenConnectTLSOptions `json:"tls,omitempty"` - FormEntries []OpenConnectFormEntryOptions `json:"form_entries,omitempty"` + System bool `json:"system,omitempty"` + Name string `json:"name,omitempty"` + UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"` + UDPMapping UDPNATBehavior `json:"udp_mapping,omitempty"` + UDPFiltering UDPNATBehavior `json:"udp_filtering,omitempty"` + UDPNATMax uint32 `json:"udp_nat_max,omitempty"` + Server string `json:"server"` + Flavor string `json:"flavor,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + AuthGroup string `json:"auth_group,omitempty"` + Cookie string `json:"cookie,omitempty"` + Token *OpenConnectTokenOptions `json:"token,omitempty"` + ReportedOS string `json:"reported_os,omitempty"` + UserAgent string `json:"user_agent,omitempty"` + Version string `json:"version,omitempty"` + LocalHostname string `json:"local_hostname,omitempty"` + Mobile *OpenConnectMobileOptions `json:"mobile,omitempty"` + CSD *OpenConnectCSDOptions `json:"csd,omitempty"` + HIP *OpenConnectHIPOptions `json:"hip,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 { - Mode string `json:"mode,omitempty"` - Secret string `json:"secret,omitempty"` - PIN string `json:"pin,omitempty"` - Password string `json:"password,omitempty"` - DeviceID string `json:"device_id,omitempty"` - Counter uint64 `json:"counter,omitempty"` + Mode string `json:"mode,omitempty"` + Secret string `json:"secret,omitempty"` + SecretPath string `json:"secret_path,omitempty"` + PIN string `json:"pin,omitempty"` + Password string `json:"password,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 { @@ -58,6 +85,10 @@ type OpenConnectTNCCCertificateOptions 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"` CertificateAuthorityPath string `json:"certificate_authority_path,omitempty"` ClientCertificate badoption.Listable[string] `json:"client_certificate,omitempty"` @@ -79,3 +110,9 @@ type OpenConnectFormEntryOptions struct { Value string `json:"value,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"` +} diff --git a/option/openvpn.go b/option/openvpn.go index 7ad68006c..642287283 100644 --- a/option/openvpn.go +++ b/option/openvpn.go @@ -20,20 +20,33 @@ type OpenVPNClientEndpointOptions struct { DialerOptions ServerOptions OpenVPNEndpointOptions + Mode string `json:"mode,omitempty"` Network string `json:"network,omitempty"` Servers []OpenVPNRemoteOptions `json:"servers,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"` Password string `json:"password,omitempty"` AuthRetry string `json:"auth_retry,omitempty"` StaticChallenge string `json:"static_challenge,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"` + Cipher string `json:"cipher,omitempty"` DataCiphers badoption.Listable[string] `json:"data_ciphers,omitempty"` DataCiphersFallback string `json:"data_ciphers_fallback,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"` Fragment uint32 `json:"fragment,omitempty"` + ReplayWindow uint32 `json:"replay_window,omitempty"` + ReplayWindowTime badoption.Duration `json:"replay_window_time,omitempty"` Compression string `json:"compression,omitempty"` CompressionLZO string `json:"compression_lzo,omitempty"` AllowCompression string `json:"allow_compression,omitempty"` @@ -44,9 +57,17 @@ type OpenVPNClientEndpointOptions struct { RouteMetric int `json:"route_metric,omitempty"` RedirectGateway bool `json:"redirect_gateway,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"` PingRestart badoption.Duration `json:"ping_restart,omitempty"` + PingRestartDisabled bool `json:"ping_restart_disabled,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"` UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"` } @@ -54,20 +75,37 @@ type OpenVPNClientEndpointOptions struct { type OpenVPNServerEndpointOptions struct { ListenOptions OpenVPNEndpointOptions + Mode string `json:"mode,omitempty"` Network string `json:"network,omitempty"` + Remote string `json:"remote,omitempty"` + RemotePort uint16 `json:"remote_port,omitempty"` MaxClients int `json:"max_clients,omitempty"` 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"` DuplicateCN bool `json:"duplicate_cn,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"` + Cipher string `json:"cipher,omitempty"` DataCiphers badoption.Listable[string] `json:"data_ciphers,omitempty"` DataCiphersFallback string `json:"data_ciphers_fallback,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"` PingInterval badoption.Duration `json:"ping_interval,omitempty"` PingRestart badoption.Duration `json:"ping_restart,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"` } @@ -96,6 +134,7 @@ type OpenVPNOutboundTLSOptions struct { RemoteCertificateEKU string `json:"remote_certificate_eku,omitempty"` RemoteCertificateTLS string `json:"remote_certificate_tls,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"` @@ -111,7 +150,19 @@ type OpenVPNInboundTLSOptions struct { ClientCertificate badoption.Listable[string] `json:"client_certificate,omitempty"` ClientCertificatePath string `json:"client_certificate_path,omitempty"` VerifyClientCertificate string `json:"verify_client_certificate,omitempty"` + 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"` + 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"` } @@ -133,9 +184,27 @@ type OpenVPNInboundControlWrapOptions struct { type OpenVPNPushOptions struct { Routes badoption.Listable[netip.Prefix] `json:"routes,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"` RedirectGatewayFlags badoption.Listable[string] `json:"redirect_gateway_flags,omitempty"` BlockOutsideDNS bool `json:"block_outside_dns,omitempty"` PingInterval badoption.Duration `json:"ping_interval,omitempty"` PingRestart badoption.Duration `json:"ping_restart,omitempty"` } + +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"` +} diff --git a/option/outbound.go b/option/outbound.go index aa32db814..f357bd449 100644 --- a/option/outbound.go +++ b/option/outbound.go @@ -65,28 +65,30 @@ type DialerOptionsWrapper interface { } type DialerOptions struct { - Detour string `json:"detour,omitempty"` - BindInterface string `json:"bind_interface,omitempty"` - Inet4BindAddress *badoption.Addr `json:"inet4_bind_address,omitempty"` - Inet6BindAddress *badoption.Addr `json:"inet6_bind_address,omitempty"` - BindAddressNoPort bool `json:"bind_address_no_port,omitempty"` - ProtectPath string `json:"protect_path,omitempty"` - RoutingMark FwMark `json:"routing_mark,omitempty"` - ReuseAddr bool `json:"reuse_addr,omitempty"` - NetNs string `json:"netns,omitempty"` - ConnectTimeout badoption.Duration `json:"connect_timeout,omitempty"` - TCPFastOpen bool `json:"tcp_fast_open,omitempty"` - TCPMultiPath bool `json:"tcp_multi_path,omitempty"` - DisableTCPKeepAlive bool `json:"disable_tcp_keep_alive,omitempty"` - TCPKeepAlive badoption.Duration `json:"tcp_keep_alive,omitempty"` - TCPKeepAliveInterval badoption.Duration `json:"tcp_keep_alive_interval,omitempty"` - UDPFragment *bool `json:"udp_fragment,omitempty"` - UDPFragmentDefault bool `json:"-"` - DomainResolver *DomainResolveOptions `json:"domain_resolver,omitempty"` - NetworkStrategy *NetworkStrategy `json:"network_strategy,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"` + Detour string `json:"detour,omitempty"` + BindInterface string `json:"bind_interface,omitempty"` + Inet4BindAddress *badoption.Addr `json:"inet4_bind_address,omitempty"` + Inet6BindAddress *badoption.Addr `json:"inet6_bind_address,omitempty"` + BindAddressNoPort bool `json:"bind_address_no_port,omitempty"` + ProtectPath string `json:"protect_path,omitempty"` + RoutingMark FwMark `json:"routing_mark,omitempty"` + ReuseAddr bool `json:"reuse_addr,omitempty"` + NetNs string `json:"netns,omitempty"` + ConnectTimeout badoption.Duration `json:"connect_timeout,omitempty"` + TCPFastOpen bool `json:"tcp_fast_open,omitempty"` + TCPMultiPath bool `json:"tcp_multi_path,omitempty"` + DisableTCPKeepAlive bool `json:"disable_tcp_keep_alive,omitempty"` + TCPKeepAlive badoption.Duration `json:"tcp_keep_alive,omitempty"` + TCPKeepAliveInterval badoption.Duration `json:"tcp_keep_alive_interval,omitempty"` + TCPKeepAliveSystemDefaults bool `json:"-"` + UDPBindPort uint16 `json:"-"` + UDPFragment *bool `json:"udp_fragment,omitempty"` + UDPFragmentDefault bool `json:"-"` + DomainResolver *DomainResolveOptions `json:"domain_resolver,omitempty"` + NetworkStrategy *NetworkStrategy `json:"network_strategy,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 DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"` diff --git a/protocol/openconnect/client.go b/protocol/openconnect/client.go index 6622adf63..715066a8b 100644 --- a/protocol/openconnect/client.go +++ b/protocol/openconnect/client.go @@ -2,6 +2,7 @@ package openconnect import ( "context" + "crypto/tls" "net" "net/netip" "net/url" @@ -50,6 +51,8 @@ type Endpoint struct { flavor string stateAccess sync.Mutex state atomic.Pointer[clientState] + dnsTransportAccess sync.Mutex + dnsTransport *DNSTransport deviceStarted bool readLoopDone chan struct{} statusAccess sync.Mutex @@ -65,10 +68,22 @@ type clientState struct { tunnelConfigured bool localAddresses []netip.Prefix routeSet *netipx.IPSet + preferredDomains map[string]bool + configuration openconnecttransport.Configuration tunnelInfo adapter.OpenConnectTunnelInfo } 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) openConnectEndpoint := &Endpoint{ endpointBase: endpointBase{ @@ -98,7 +113,7 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL } serverURL, err := url.Parse(server) if err != nil { - return nil, E.Cause(err, "parse OpenConnect server") + return nil, E.Cause(err, "parse server") } serverPort := serverURL.Port() 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) { + 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) if err != nil { return openconnect.ClientOptions{}, err @@ -185,12 +204,13 @@ func (e *Endpoint) buildClientOptions(options option.OpenConnectEndpointOptions, var tokenOptions *openconnect.TokenOptions if options.Token != nil { tokenOptions = &openconnect.TokenOptions{ - Mode: options.Token.Mode, - Secret: options.Token.Secret, - PIN: options.Token.PIN, - Password: options.Token.Password, - DeviceID: options.Token.DeviceID, - Counter: options.Token.Counter, + Mode: options.Token.Mode, + Secret: options.Token.Secret, + SecretPath: options.Token.SecretPath, + PIN: options.Token.PIN, + Password: options.Token.Password, + DeviceID: options.Token.DeviceID, + Counter: options.Token.Counter, } if tokenOptions.Mode == openconnect.TokenModeHOTP { e.hotpCounter.Store(tokenOptions.Counter) @@ -201,6 +221,14 @@ func (e *Endpoint) buildClientOptions(options option.OpenConnectEndpointOptions, } } 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 { csdOptions = &openconnect.CSDOptions{WrapperPath: options.CSD.WrapperPath} } @@ -236,21 +264,44 @@ func (e *Endpoint) buildClientOptions(options option.OpenConnectEndpointOptions, } }) return openconnect.ClientOptions{ - Context: e.loopContext, - Server: options.Server, - Flavor: options.Flavor, - Username: options.Username, - Password: options.Password, - AuthGroup: options.AuthGroup, - Token: tokenOptions, - ReportedOS: options.ReportedOS, - UserAgent: options.UserAgent, - CSD: csdOptions, - HIP: hipOptions, - TNCC: tnccOptions, - NoUDP: options.NoUDP, - AllowInsecureCrypto: options.AllowInsecureCrypto, + Context: e.loopContext, + Server: options.Server, + Flavor: options.Flavor, + Username: options.Username, + Password: options.Password, + AuthGroup: options.AuthGroup, + Cookie: options.Cookie, + Token: tokenOptions, + ReportedOS: options.ReportedOS, + UserAgent: options.UserAgent, + Version: options.Version, + LocalHostname: options.LocalHostname, + Mobile: mobileOptions, + 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{ + Config: tlsConfig, + ServerName: options.TLS.ServerName, + PeerFingerprints: options.TLS.PeerFingerprint, + SystemTrustDisabled: options.TLS.SystemTrustDisabled, CertificateAuthority: certificateAuthority, Certificate: clientCertificate, Key: clientKey, @@ -274,7 +325,14 @@ func (e *Endpoint) handleTunnelConfiguration(event openconnect.TunnelConfigurati e.updateState(func(state *clientState) { 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 { return E.Cause(err, "update device configuration") } @@ -285,10 +343,7 @@ func (e *Endpoint) handleTunnelConfiguration(event openconnect.TunnelConfigurati } e.deviceStarted = true } - routeSet, err := buildIPSet(configuration.Routes, configuration.ExcludedRoutes) - if err != nil { - return E.Cause(err, "build route set") - } + preferredDomains := buildPreferredDomains(configuration) var ipv4Addresses []netip.Prefix var ipv6Addresses []netip.Prefix for _, address := range configuration.Addresses { @@ -308,6 +363,8 @@ func (e *Endpoint) handleTunnelConfiguration(event openconnect.TunnelConfigurati state.tunnelConfigured = true state.localAddresses = configuration.Addresses state.routeSet = routeSet + state.preferredDomains = preferredDomains + state.configuration = configuration state.tunnelInfo = adapter.OpenConnectTunnelInfo{ Server: e.server, Flavor: e.flavor, @@ -319,6 +376,12 @@ func (e *Endpoint) handleTunnelConfiguration(event openconnect.TunnelConfigurati ConnectedSince: connectedSince, } }) + e.dnsTransportAccess.Lock() + dnsTransport := e.dnsTransport + e.dnsTransportAccess.Unlock() + if dnsTransport != nil { + dnsTransport.updateConfiguration(configuration) + } return nil } @@ -358,7 +421,7 @@ func (e *Endpoint) readLoop() { if E.IsClosedOrCanceled(err) || e.loopContext.Err() != nil { return } - e.logger.Error(E.Cause(err, "OpenConnect client terminated")) + e.logger.Error(E.Cause(err, "client terminated")) e.setTerminalError(err) return } @@ -436,11 +499,11 @@ func (e *Endpoint) ready() bool { func (e *Endpoint) WritePackets(packets [][]byte) error { 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) 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 } @@ -473,7 +536,7 @@ func (e *Endpoint) DialContext(ctx context.Context, network string, destination e.logger.InfoContext(ctx, "outbound packet connection to ", destination) } 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() { 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) { e.logger.InfoContext(ctx, "outbound packet connection to ", destination) 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() { 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 { - 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 { diff --git a/protocol/openconnect/dns_transport.go b/protocol/openconnect/dns_transport.go new file mode 100644 index 000000000..eb97ff0ba --- /dev/null +++ b/protocol/openconnect/dns_transport.go @@ -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 +} diff --git a/protocol/openconnect/endpoint.go b/protocol/openconnect/endpoint.go index 9cf4021e0..219993652 100644 --- a/protocol/openconnect/endpoint.go +++ b/protocol/openconnect/endpoint.go @@ -133,6 +133,55 @@ func configurationFromClientEvent(event openconnect.TunnelConfigurationEvent) op 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 { return openconnecttransport.SplitDNSRule{ Domains: rule.Domains, @@ -168,3 +217,46 @@ func buildIPSet(routes []openconnecttransport.Route, excludedRoutes []openconnec } 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 +} diff --git a/protocol/openvpn/client.go b/protocol/openvpn/client.go index 9756ccaa2..69d44258c 100644 --- a/protocol/openvpn/client.go +++ b/protocol/openvpn/client.go @@ -4,6 +4,8 @@ import ( "context" "net" "net/netip" + "slices" + "strings" "sync" "sync/atomic" "time" @@ -49,6 +51,7 @@ type ClientEndpoint struct { device ovpntransport.Device stateAccess sync.Mutex state atomic.Pointer[clientState] + dnsTransport *DNSTransport deviceStarted bool readLoopDone chan struct{} statusAccess sync.Mutex @@ -63,6 +66,8 @@ type clientState struct { localAddresses []netip.Prefix routeSet *netipx.IPSet blockIPv6 bool + configuration ovpntransport.Configuration + preferredDomains []string 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) { - if options.TLS == nil { - return ovpn.ClientOptions{}, E.New("missing `tls` options") + mode := options.Mode + 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 { 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 { 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) if err != nil { return ovpn.ClientOptions{}, err @@ -198,34 +229,9 @@ func (c *ClientEndpoint) buildClientOptions(options option.OpenVPNClientEndpoint } controlCryptV2 = controlKey case "": - return ovpn.ClientOptions{}, E.New("missing OpenVPN control wrap type") + return ovpn.ClientOptions{}, E.New("missing control wrap type") default: - return ovpn.ClientOptions{}, E.New("unknown OpenVPN 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, - }) + return ovpn.ClientOptions{}, E.New("unknown control wrap type: ", controlWrap.Type) } } 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, } }) - tunnelRoutes := common.Map(options.Routes, func(route netip.Prefix) ovpn.TunnelRoute { - return ovpn.TunnelRoute{Prefix: route} - }) remoteCertificateTLS := options.TLS.RemoteCertificateTLS switch remoteCertificateTLS { case "", "server", "client", "none": @@ -264,6 +267,7 @@ func (c *ClientEndpoint) buildClientOptions(options option.OpenVPNClientEndpoint RemoteCertificateKU: options.TLS.RemoteCertificateKU, RemoteCertificateEKU: options.TLS.RemoteCertificateEKU, RemoteCertificateTLS: remoteCertificateTLS, + NSCertificateType: options.TLS.NSCertificateType, VersionMin: options.TLS.VersionMin, VersionMax: options.TLS.VersionMax, CertificateProfile: options.TLS.CertificateProfile, @@ -278,7 +282,7 @@ func (c *ClientEndpoint) buildClientOptions(options option.OpenVPNClientEndpoint } return ovpn.ClientOptions{ Context: c.loopContext, - Mode: ovpn.ModeTLS, + Mode: mode, Transport: ovpn.ClientTransportOptions{ Remotes: remotes, RemoteRandom: options.RemoteRandom, @@ -286,19 +290,8 @@ func (c *ClientEndpoint) buildClientOptions(options option.OpenVPNClientEndpoint ExplicitExitNotify: options.ExplicitExitNotify, DialContextWithAddressIndex: c.transportDialContextWithAddressIndex, }, - DataChannel: ovpn.ClientDataChannelOptions{ - MTU: options.MTU, - 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, + DataChannel: buildClientDataChannelOptions(options), + TLS: clientTLSOptions, Authentication: ovpn.ClientAuthenticationOptions{ Username: options.Username, Password: options.Password, @@ -311,25 +304,176 @@ func (c *ClientEndpoint) buildClientOptions(options option.OpenVPNClientEndpoint Filters: pullFilters, RouteNoPull: options.RouteNoPull, }, - Tunnel: ovpn.ClientTunnelOptions{ - DevType: "tun", - 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), - }, + Tunnel: tunnelOptions, + Timing: buildClientTimingOptions(options), KeyDirection: keyDirection, OnTunnelConfiguration: c.handleTunnelConfiguration, Logger: c.logger, }, 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) { destination := M.ParseSocksaddr(address) if destination.IsDomain() { @@ -361,33 +505,51 @@ func (c *ClientEndpoint) transportDialContextWithAddressIndex(ctx context.Contex } func (c *ClientEndpoint) handleTunnelConfiguration(event ovpn.TunnelConfigurationEvent) error { - configuration := configurationFromClientEvent(event, c.logger) defer c.notifyStatusUpdated() c.stateAccess.Lock() - defer c.stateAccess.Unlock() + configuration := configurationFromClientEvent(event, c.logger) c.updateState(func(state *clientState) { 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 { + c.stateAccess.Unlock() return E.Cause(err, "update device configuration") } if !c.deviceStarted { err = c.device.Start() if err != nil { + c.stateAccess.Unlock() return E.Cause(err, "start device") } c.deviceStarted = true } - routeSet, err := buildIPSet(configuration.Routes) + routeSet, err := buildIPSet(configuration.Routes, configuration.ExcludedRoutes) if err != nil { + c.stateAccess.Unlock() 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) { state.tunnelConfigured = true state.localAddresses = configuration.Address state.routeSet = routeSet state.blockIPv6 = configuration.BlockIPv6 + state.configuration = configuration + state.preferredDomains = preferredDomains state.tunnelInfo.Cipher = event.Configuration.SelectedCipher state.tunnelInfo.IPv4 = event.Configuration.LocalIPv4 state.tunnelInfo.IPv6 = event.Configuration.LocalIPv6 @@ -397,6 +559,11 @@ func (c *ClientEndpoint) handleTunnelConfiguration(event ovpn.TunnelConfiguratio state.tunnelInfo.ConnectedSince = time.Now() } }) + dnsTransport := c.dnsTransport + c.stateAccess.Unlock() + if dnsTransport != nil { + dnsTransport.onReconfiguration(configuration) + } return nil } @@ -406,6 +573,28 @@ func (c *ClientEndpoint) updateState(update func(state *clientState)) { 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 { if stage != adapter.StartStatePostStart { return nil @@ -434,7 +623,7 @@ func (c *ClientEndpoint) readLoop() { if E.IsClosedOrCanceled(err) || c.loopContext.Err() != nil { return } - c.logger.Error(E.Cause(err, "OpenVPN client terminated")) + c.logger.Error(E.Cause(err, "client terminated")) c.setTerminalError(err) return } @@ -509,7 +698,7 @@ func (c *ClientEndpoint) ready() bool { func (c *ClientEndpoint) WritePackets(packets [][]byte) error { state := c.state.Load() 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 { outboundPackets := packets[:0] @@ -529,7 +718,7 @@ func (c *ClientEndpoint) WritePackets(packets [][]byte) error { } err := c.client.WriteDataPacketBuffers(packetBuffers) 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 } @@ -577,7 +766,7 @@ func (c *ClientEndpoint) DialContext(ctx context.Context, network string, destin c.logger.InfoContext(ctx, "outbound packet connection to ", destination) } 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() { 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) { c.logger.InfoContext(ctx, "outbound packet connection to ", destination) 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() { 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 { + 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 } @@ -636,3 +834,16 @@ func (c *ClientEndpoint) PreferredAddress(metadata *adapter.InboundContext, addr } 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) +} diff --git a/protocol/openvpn/dns_transport.go b/protocol/openvpn/dns_transport.go new file mode 100644 index 000000000..188da2af5 --- /dev/null +++ b/protocol/openvpn/dns_transport.go @@ -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 + } + } +} diff --git a/protocol/openvpn/endpoint.go b/protocol/openvpn/endpoint.go index 954d43db5..0aa7ff67a 100644 --- a/protocol/openvpn/endpoint.go +++ b/protocol/openvpn/endpoint.go @@ -17,6 +17,7 @@ import ( 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/auth" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" @@ -118,7 +119,7 @@ func keyDirectionValue(direction string) (int, error) { case "client": return 1, nil 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 } } + 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 !hasOpenVPNFlag(configuration.RedirectGatewayFlags, "!ipv4") && !hasInet4DefaultRoute { - routes = append(routes, ovpntransport.Route{ - Prefix: inet4DefaultRoute, - Gateway: configuration.VPNGateway, - Metric: configuration.RouteMetric, - }) + if hasOpenVPNFlag(configuration.RedirectGatewayFlags, "def1") { + for _, prefix := range []netip.Prefix{ + netip.PrefixFrom(netip.IPv4Unspecified(), 1), + 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 { - routes = append(routes, ovpntransport.Route{ - Prefix: inet6DefaultRoute, - Gateway: configuration.VPNGatewayIPv6, - Metric: configuration.RouteMetric, - }) - hasInet6DefaultRoute = true + for _, prefix := range []netip.Prefix{ + netip.MustParsePrefix("::/3"), + netip.MustParsePrefix("2000::/4"), + netip.MustParsePrefix("3000::/4"), + netip.MustParsePrefix("fc00::/7"), + } { + if !openVPNRoutesContainPrefix(routes, prefix) { + routes = append(routes, ovpntransport.Route{Prefix: prefix, Gateway: configuration.VPNGatewayIPv6, Metric: configuration.RouteMetric}) + } + } } } if configuration.BlockIPv6 && !hasInet6DefaultRoute { @@ -211,47 +234,114 @@ func configurationFromClientEvent(event ovpn.TunnelConfigurationEvent, logger lo 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 notApplicableOptions []string for _, flag := range configuration.RedirectGatewayFlags { 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: if flag != "" { ignoredOptions = append(ignoredOptions, "redirect-gateway "+flag) } } } - if configuration.RedirectPrivate { - ignoredOptions = append(ignoredOptions, "redirect-private") - } if configuration.BlockOutsideDNS { ignoredOptions = append(ignoredOptions, "block-outside-dns") } for _, dhcpOption := range configuration.DHCPOptions { 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 } ignoredOptions = append(ignoredOptions, "dhcp-option "+strings.TrimSpace(dhcpOption)) } 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{ - MTU: mtu, - Address: addresses, - Routes: routes, - DNS: configuration.DNS, - Topology: configuration.Topology, - BlockIPv6: configuration.BlockIPv6, + MTU: mtu, + Address: addresses, + Routes: routes, + ExcludedRoutes: excludedRoutes, + DNS: configuration.DNS, + 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 for _, route := range routes { builder.AddPrefix(route.Prefix) } + for _, route := range excludedRoutes { + builder.RemovePrefix(route.Prefix) + } return builder.IPSet() } diff --git a/protocol/openvpn/server.go b/protocol/openvpn/server.go index 1492f0da0..ae5542ec5 100644 --- a/protocol/openvpn/server.go +++ b/protocol/openvpn/server.go @@ -5,6 +5,7 @@ import ( "net" "net/netip" "slices" + "strconv" "sync/atomic" "time" @@ -90,18 +91,16 @@ func NewServerEndpoint(ctx context.Context, router adapter.Router, logger log.Co return nil, err } serverOptions.Context = loopContext - serverOptions.Authentication.Authenticator = authenticatorFromUsers(options.Users) - serverOptions.Authentication.DuplicateCN = options.DuplicateCN + if serverOptions.Mode == ovpn.ModeTLS { + serverOptions.Authentication.Authenticator = authenticatorFromUsers(options.Users) + serverOptions.Authentication.DuplicateCN = options.DuplicateCN + } serverOptions.Logger = logger serverEndpoint.serverOptions = serverOptions udpTimeout := C.UDPTimeout if options.UDPTimeout != 0 { 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{ Context: ctx, Logger: logger, @@ -118,7 +117,6 @@ func NewServerEndpoint(ctx context.Context, router adapter.Router, logger log.Co Configuration: ovpntransport.Configuration{ MTU: options.MTU, Address: options.Address, - Routes: deviceRoutes, Topology: options.Topology, }, }) @@ -134,15 +132,18 @@ func NewServerEndpoint(ctx context.Context, router adapter.Router, logger log.Co func validateServerAddresses(addresses []netip.Prefix) error { var hasIPv4 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 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 } else { 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 } @@ -155,7 +156,7 @@ func validateServerTopology(topology string) error { case "", "subnet", "p2p", "net30": return nil 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) { - if len(options.Address) == 0 { - return ovpn.ServerOptions{}, E.New("missing OpenVPN server address") + mode := options.Mode + if mode == "" { + mode = ovpn.ModeTLS } - if options.TLS == nil { - return ovpn.ServerOptions{}, E.New("missing `tls` options") + switch mode { + 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) if err != nil { @@ -279,7 +286,16 @@ func buildServerOptions(options option.OpenVPNServerEndpointOptions) (ovpn.Serve switch protocol { case N.NetworkTCP, N.NetworkUDP: 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) if err != nil { @@ -295,29 +311,136 @@ func buildServerOptions(options option.OpenVPNServerEndpointOptions) (ovpn.Serve MaxClients: options.MaxClients, }, DataChannel: ovpn.ServerDataChannelOptions{ - MTU: options.MTU, - Ciphers: []string(options.DataCiphers), - FallbackCipher: options.DataCiphersFallback, - Auth: options.Auth, - PacketHeadroom: ovpntransport.PacketHeadroom, + MTU: options.MTU, + MSSFix: options.MSSFix, + MSSFixDisabled: options.MSSFixDisabled, + MSSFixMode: options.MSSFixMode, + Ciphers: []string(options.DataCiphers), + FallbackCipher: options.DataCiphersFallback, + Auth: options.Auth, + ReplayWindow: options.ReplayWindow, + ReplayWindowTime: time.Duration(options.ReplayWindowTime), + PacketHeadroom: ovpntransport.PacketHeadroom, }, TLS: tlsOptions, Timing: ovpn.ServerTimingOptions{ RenegotiationInterval: time.Duration(options.RenegotiateInterval), + RenegotiationDisabled: options.RenegotiateDisabled, + RenegotiationBytes: options.RenegotiateBytes, + RenegotiationPackets: options.RenegotiatePackets, HandWindow: time.Duration(options.HandshakeWindow), PingInterval: time.Duration(options.PingInterval), PingRestart: time.Duration(options.PingRestart), }, } - applyServerPushOptions(&serverOptions, options) + err = applyServerPushOptions(&serverOptions, options) + if err != nil { + return ovpn.ServerOptions{}, err + } 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) { switch options.VerifyClientCertificate { case "", "require", "optional", "none": 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) if err != nil { @@ -327,16 +450,46 @@ func buildServerTLSOptions(options option.OpenVPNInboundTLSOptions) (ovpn.Server if err != nil { 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 { 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{ CertificateAuthority: certificateAuthority, Certificate: certificate, Key: key, 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, + VersionMin: options.VersionMin, + VersionMax: options.VersionMax, + Cipher: options.Cipher, + Groups: options.Groups, } keyDirection := -1 controlWrap := options.ControlWrap @@ -369,15 +522,15 @@ func buildServerTLSOptions(options option.OpenVPNInboundTLSOptions) (ovpn.Server tlsOptions.CryptV2ForceCookie = controlWrap.ForceCookie } case "": - return ovpn.ServerTLSOptions{}, 0, E.New("missing OpenVPN control wrap type") + return ovpn.ServerTLSOptions{}, 0, E.New("missing control wrap type") 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 } -func applyServerPushOptions(serverOptions *ovpn.ServerOptions, options option.OpenVPNServerEndpointOptions) { +func applyServerPushOptions(serverOptions *ovpn.ServerOptions, options option.OpenVPNServerEndpointOptions) error { topology := options.Topology if topology == "" { topology = "subnet" @@ -399,10 +552,35 @@ func applyServerPushOptions(serverOptions *ovpn.ServerOptions, options option.Op LocalAddress: localAddresses, } if options.Push == nil { - return + return nil } serverOptions.Push.Routes = slices.Clone(options.Push.Routes) 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.PingInterval = time.Duration(options.Push.PingInterval) 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"} } } + return nil } func (s *ServerEndpoint) readLoop() { @@ -424,7 +603,7 @@ func (s *ServerEndpoint) readLoop() { if E.IsClosedOrCanceled(err) || s.loopContext.Err() != nil { return } - s.logger.Error(E.Cause(err, "OpenVPN server terminated")) + s.logger.Error(E.Cause(err, "server terminated")) return } 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 { 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)) 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) } 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() { 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) { s.logger.InfoContext(ctx, "outbound packet connection to ", destination) 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() { destinationAddresses, err := s.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{}) diff --git a/release/LDFLAGS b/release/LDFLAGS index 8f613f971..5aa5c957a 100644 --- a/release/LDFLAGS +++ b/release/LDFLAGS @@ -1 +1 @@ --X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0 \ No newline at end of file +-X runtime.godebugDefault=multipathtcp=0,tlssha1=1,tlsunsafeekm=1 -checklinkname=0 diff --git a/test/openvpn_test.go b/test/openvpn_test.go index d67d6fe34..80f27e58e 100644 --- a/test/openvpn_test.go +++ b/test/openvpn_test.go @@ -19,6 +19,7 @@ import ( "os" "path/filepath" "runtime" + "slices" "strconv" "strings" "sync" @@ -29,6 +30,9 @@ import ( "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "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/auth" "github.com/sagernet/sing/common/bufio" @@ -91,6 +95,21 @@ type openVPNSelfCase struct { 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) { 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) { t.Run("official_server_to_sing_box_client", func(t *testing.T) { testOpenVPNDockerOfficialServerToSingBoxClient(t) @@ -1777,6 +1975,27 @@ func createOpenVPNStaticKey(t *testing.T) string { 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 { t.Helper() if protocol == N.NetworkTCP { diff --git a/transport/openconnect/device.go b/transport/openconnect/device.go index 2edcd5e18..7a2b18fe6 100644 --- a/transport/openconnect/device.go +++ b/transport/openconnect/device.go @@ -104,7 +104,7 @@ func (d *baseDevice) SetPacketWriter(writer PacketWriter) { func (d *baseDevice) writeOutbound(packetBuffers []*buf.Buffer) error { if d.packetWriter == nil { buf.ReleaseMulti(packetBuffers) - return E.New("missing OpenConnect packet writer") + return E.New("missing packet writer") } return d.packetWriter(packetBuffers) } diff --git a/transport/openconnect/device_stack_stub.go b/transport/openconnect/device_stack_stub.go index 8df454dd7..1a96524c3 100644 --- a/transport/openconnect/device_stack_stub.go +++ b/transport/openconnect/device_stack_stub.go @@ -5,9 +5,9 @@ package openconnect import E "github.com/sagernet/sing/common/exceptions" 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) { - 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") } diff --git a/transport/openconnect/device_system.go b/transport/openconnect/device_system.go index 2b73b773c..5af1e50db 100644 --- a/transport/openconnect/device_system.go +++ b/transport/openconnect/device_system.go @@ -4,7 +4,6 @@ import ( "context" "net" "net/netip" - "runtime" "slices" "sync" "syscall" @@ -14,7 +13,6 @@ import ( "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun/gtcpip/header" - "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" @@ -96,30 +94,21 @@ func (d *systemDevice) buildTunOptions() tun.Options { d.inet4Address = inet4Address d.inet6Address = inet6Address 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) tunOptions := tun.Options{ - Name: d.options.Name, - Inet4Address: inet4Addresses, - Inet6Address: inet6Addresses, - MTU: d.options.MTU, - GSO: true, - InterfaceScope: true, - DNSAddress: d.options.Configuration.DNS, - Inet4RouteAddress: inet4Routes, - Inet6RouteAddress: inet6Routes, - Inet4RouteExcludeAddress: inet4ExcludedRoutes, - Inet6RouteExcludeAddress: inet6ExcludedRoutes, - InterfaceMonitor: nil, - InterfaceFinder: nil, - Logger: d.options.Logger, - IPRoute2TableIndex: tun.DefaultIPRoute2TableIndex, - IPRoute2RuleIndex: tun.DefaultIPRoute2RuleIndex, - EXP_DisableDNSHijack: true, - } - if runtime.GOOS == "darwin" { - tunOptions.AutoRoute = true + Name: d.options.Name, + Inet4Address: inet4Addresses, + Inet6Address: inet6Addresses, + MTU: d.options.MTU, + GSO: true, + InterfaceScope: true, + DNSMode: tun.DNSModeDisabled, + InterfaceMonitor: nil, + InterfaceFinder: nil, + Logger: d.options.Logger, + IPRoute2TableIndex: tun.DefaultIPRoute2TableIndex, + IPRoute2RuleIndex: tun.DefaultIPRoute2RuleIndex, + EXP_DisableDNSHijack: true, } if networkManager != nil { tunOptions.InterfaceMonitor = networkManager.InterfaceMonitor() @@ -252,13 +241,12 @@ func (d *systemDevice) UpdateConfiguration(configuration Configuration) error { return nil } if !slices.Equal(previousConfiguration.Addresses, configuration.Addresses) || - previousMTU != updatedMTU || - !slices.Equal(previousConfiguration.DNS, configuration.DNS) { + previousMTU != updatedMTU { d.device.Close() d.device = nil return d.startLocked() } - return d.device.UpdateRouteOptions(d.buildTunOptions()) + return nil } func (d *systemDevice) WriteInboundBuffers(packetBuffers []*buf.Buffer) error { @@ -270,7 +258,7 @@ func (d *systemDevice) writeBuffers(packetBuffers []*buf.Buffer) error { tunInterface := d.device d.stateAccess.RUnlock() 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) if isLinuxTUN { @@ -313,7 +301,7 @@ func (d *systemDevice) writePacket(packet []byte) error { tunInterface := d.device d.stateAccess.RUnlock() if tunInterface == nil { - return E.New("OpenConnect system device is not ready") + return E.New("system device is not ready") } if tun.PacketOffset == 0 { _, err := tunInterface.Write(packet) diff --git a/transport/openvpn/device.go b/transport/openvpn/device.go index 1553fbe36..570d8fac6 100644 --- a/transport/openvpn/device.go +++ b/transport/openvpn/device.go @@ -3,7 +3,6 @@ package openvpn import ( "context" "net/netip" - "slices" "sync/atomic" "time" @@ -54,13 +53,17 @@ type DeviceOptions struct { } type Configuration struct { - MTU uint32 - Address []netip.Prefix - Routes []Route - DNS []netip.Addr - Topology string - Interface string - BlockIPv6 bool + MTU uint32 + Address []netip.Prefix + Routes []Route + ExcludedRoutes []Route + DNS []netip.Addr + DNSServers []DNSServer + SearchDomains []string + DNSRoutes []string + Topology string + Interface string + BlockIPv6 bool } type Route struct { @@ -69,6 +72,15 @@ type Route struct { Metric int } +type DNSServer struct { + Priority int + Addresses []netip.AddrPort + ResolveDomains []string + DNSSEC string + Transport string + SNI string +} + func NewDevice(options DeviceOptions) (Device, error) { if !options.System { return newStackDevice(options) @@ -91,7 +103,7 @@ func (d *baseDevice) SetPacketWriter(writer PacketWriter) { func (d *baseDevice) writeOutbound(packetBuffers []*buf.Buffer) error { if d.packetWriter == nil { buf.ReleaseMulti(packetBuffers) - return E.New("missing OpenVPN packet writer") + return E.New("missing packet writer") } return d.packetWriter(packetBuffers) } @@ -192,34 +204,6 @@ func splitPrefixes(prefixes []netip.Prefix) ([]netip.Prefix, []netip.Prefix) { 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 { for _, route := range routes { if route.Gateway.IsValid() || route.Metric != 0 { diff --git a/transport/openvpn/device_stack.go b/transport/openvpn/device_stack.go index 6dc8eab4d..9a0bd5edd 100644 --- a/transport/openvpn/device_stack.go +++ b/transport/openvpn/device_stack.go @@ -102,7 +102,7 @@ func (d *stackDevice) UpdateConfiguration(configuration Configuration) error { d.stateAccess.Lock() defer d.stateAccess.Unlock() 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 } 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) { 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() 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) { 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() bind := tcpip.FullAddress{ diff --git a/transport/openvpn/device_stack_stub.go b/transport/openvpn/device_stack_stub.go index fc438a7ca..a9aa08061 100644 --- a/transport/openvpn/device_stack_stub.go +++ b/transport/openvpn/device_stack_stub.go @@ -5,9 +5,9 @@ package openvpn import E "github.com/sagernet/sing/common/exceptions" 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) { - 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") } diff --git a/transport/openvpn/device_system.go b/transport/openvpn/device_system.go index 85c257cf4..3f2a58a40 100644 --- a/transport/openvpn/device_system.go +++ b/transport/openvpn/device_system.go @@ -4,7 +4,6 @@ import ( "context" "net" "net/netip" - "runtime" "slices" "sync" "syscall" @@ -30,14 +29,13 @@ const ( type systemDevice struct { baseDevice - stateAccess sync.RWMutex - options DeviceOptions - dialer N.Dialer - device tun.Tun - inet4Address netip.Addr - inet6Address netip.Addr - logRouteOptions bool - closed bool + stateAccess sync.RWMutex + options DeviceOptions + dialer N.Dialer + device tun.Tun + inet4Address netip.Addr + inet6Address netip.Addr + closed bool } func newSystemDevice(options DeviceOptions) (*systemDevice, error) { @@ -55,11 +53,10 @@ func newSystemDevice(options DeviceOptions) (*systemDevice, error) { } inet4Address, inet6Address := firstAddresses(options.Configuration.Address) return &systemDevice{ - options: options, - dialer: interfaceDialer, - inet4Address: inet4Address, - inet6Address: inet6Address, - logRouteOptions: true, + options: options, + dialer: interfaceDialer, + inet4Address: inet4Address, + inet6Address: inet6Address, }, nil } @@ -97,13 +94,6 @@ func (d *systemDevice) buildTunOptions() tun.Options { d.inet4Address = inet4Address d.inet6Address = inet6Address 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) tunOptions := tun.Options{ Name: d.options.Name, @@ -112,11 +102,7 @@ func (d *systemDevice) buildTunOptions() tun.Options { MTU: d.options.MTU, GSO: true, InterfaceScope: true, - DNSAddress: d.options.Configuration.DNS, - Inet4Gateway: inet4Gateway, - Inet6Gateway: inet6Gateway, - Inet4RouteAddress: inet4Routes, - Inet6RouteAddress: inet6Routes, + DNSMode: tun.DNSModeDisabled, InterfaceMonitor: nil, InterfaceFinder: nil, Logger: d.options.Logger, @@ -124,9 +110,6 @@ func (d *systemDevice) buildTunOptions() tun.Options { IPRoute2RuleIndex: tun.DefaultIPRoute2RuleIndex, EXP_DisableDNSHijack: true, } - if runtime.GOOS == "darwin" { - tunOptions.AutoRoute = true - } if networkManager != nil { tunOptions.InterfaceMonitor = networkManager.InterfaceMonitor() tunOptions.InterfaceFinder = networkManager.InterfaceFinder() @@ -134,43 +117,6 @@ func (d *systemDevice) buildTunOptions() tun.Options { 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) { linuxTUN, isLinuxTUN := tunInterface.(tun.LinuxTUN) if isLinuxTUN && linuxTUN.BatchSize() > 1 { @@ -295,13 +241,6 @@ func (d *systemDevice) readLoopDarwin(tunInterface tun.DarwinTUN) { func (d *systemDevice) UpdateConfiguration(configuration Configuration) error { d.stateAccess.Lock() 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 previousMTU := d.options.MTU updatedMTU := d.options.MTU @@ -317,14 +256,12 @@ func (d *systemDevice) UpdateConfiguration(configuration Configuration) error { return nil } if !slices.Equal(previousConfiguration.Address, configuration.Address) || - previousMTU != updatedMTU || - !slices.Equal(previousConfiguration.DNS, configuration.DNS) || - previousConfiguration.BlockIPv6 != configuration.BlockIPv6 { + previousMTU != updatedMTU { d.device.Close() d.device = nil return d.startLocked() } - return d.device.UpdateRouteOptions(d.buildTunOptions()) + return nil } func (d *systemDevice) blockIPv6Enabled() bool { @@ -342,7 +279,7 @@ func (d *systemDevice) writeBuffers(packetBuffers []*buf.Buffer) error { tunInterface := d.device d.stateAccess.RUnlock() 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) if isLinuxTUN { @@ -385,7 +322,7 @@ func (d *systemDevice) writePacket(packet []byte) error { tunInterface := d.device d.stateAccess.RUnlock() if tunInterface == nil { - return E.New("OpenVPN system device is not ready") + return E.New("system device is not ready") } if tun.PacketOffset == 0 { _, err := tunInterface.Write(packet)