From d3575cc8c2bf8b4a1d3db6a833d34e22a43fa17d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 3 Mar 2026 20:59:13 +0800 Subject: [PATCH 01/93] Add MAC and hostname rule items --- adapter/inbound.go | 3 + adapter/neighbor.go | 13 + adapter/router.go | 2 + docs/configuration/dns/rule.md | 31 ++ docs/configuration/dns/rule.zh.md | 31 ++ docs/configuration/inbound/tun.md | 30 ++ docs/configuration/inbound/tun.zh.md | 35 ++ docs/configuration/route/index.md | 31 ++ docs/configuration/route/index.zh.md | 31 ++ docs/configuration/route/rule.md | 31 ++ docs/configuration/route/rule.zh.md | 31 ++ go.mod | 8 +- go.sum | 8 +- option/route.go | 2 + option/rule.go | 2 + option/rule_dns.go | 2 + option/tun.go | 2 + protocol/tun/inbound.go | 18 + route/neighbor_resolver_linux.go | 596 +++++++++++++++++++++ route/neighbor_resolver_stub.go | 14 + route/route.go | 17 + route/router.go | 39 ++ route/rule/rule_default.go | 10 + route/rule/rule_dns.go | 10 + route/rule/rule_item_source_hostname.go | 42 ++ route/rule/rule_item_source_mac_address.go | 48 ++ route/rule_conds.go | 8 + 27 files changed, 1087 insertions(+), 8 deletions(-) create mode 100644 adapter/neighbor.go create mode 100644 route/neighbor_resolver_linux.go create mode 100644 route/neighbor_resolver_stub.go create mode 100644 route/rule/rule_item_source_hostname.go create mode 100644 route/rule/rule_item_source_mac_address.go diff --git a/adapter/inbound.go b/adapter/inbound.go index f047199e43..52af336e5b 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -2,6 +2,7 @@ package adapter import ( "context" + "net" "net/netip" "time" @@ -82,6 +83,8 @@ type InboundContext struct { SourceGeoIPCode string GeoIPCode string ProcessInfo *ConnectionOwner + SourceMACAddress net.HardwareAddr + SourceHostname string QueryType uint16 FakeIP bool diff --git a/adapter/neighbor.go b/adapter/neighbor.go new file mode 100644 index 0000000000..920398f674 --- /dev/null +++ b/adapter/neighbor.go @@ -0,0 +1,13 @@ +package adapter + +import ( + "net" + "net/netip" +) + +type NeighborResolver interface { + LookupMAC(address netip.Addr) (net.HardwareAddr, bool) + LookupHostname(address netip.Addr) (string, bool) + Start() error + Close() error +} diff --git a/adapter/router.go b/adapter/router.go index 3d5310c4ee..82e6881a60 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -26,6 +26,8 @@ type Router interface { RuleSet(tag string) (RuleSet, bool) Rules() []Rule NeedFindProcess() bool + NeedFindNeighbor() bool + NeighborResolver() NeighborResolver AppendTracker(tracker ConnectionTracker) ResetNetwork() } diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 4348674847..f8a7ac4c37 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -2,6 +2,11 @@ icon: material/alert-decagram --- +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [source_mac_address](#source_mac_address) + :material-plus: [source_hostname](#source_hostname) + !!! quote "Changes in sing-box 1.13.0" :material-plus: [interface_address](#interface_address) @@ -149,6 +154,12 @@ icon: material/alert-decagram "default_interface_address": [ "2000::/3" ], + "source_mac_address": [ + "00:11:22:33:44:55" + ], + "source_hostname": [ + "my-device" + ], "wifi_ssid": [ "My WIFI" ], @@ -408,6 +419,26 @@ Matches network interface (same values as `network_type`) address. Match default interface address. +#### source_mac_address + +!!! question "Since sing-box 1.14.0" + +!!! quote "" + + Only supported on Linux with `route.find_neighbor` enabled. + +Match source device MAC address. + +#### source_hostname + +!!! question "Since sing-box 1.14.0" + +!!! quote "" + + Only supported on Linux with `route.find_neighbor` enabled. + +Match source device hostname from DHCP leases. + #### wifi_ssid !!! quote "" diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index f35cfc7e3e..421fdfb5c1 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -2,6 +2,11 @@ icon: material/alert-decagram --- +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [source_mac_address](#source_mac_address) + :material-plus: [source_hostname](#source_hostname) + !!! quote "sing-box 1.13.0 中的更改" :material-plus: [interface_address](#interface_address) @@ -149,6 +154,12 @@ icon: material/alert-decagram "default_interface_address": [ "2000::/3" ], + "source_mac_address": [ + "00:11:22:33:44:55" + ], + "source_hostname": [ + "my-device" + ], "wifi_ssid": [ "My WIFI" ], @@ -407,6 +418,26 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`. 匹配默认接口地址。 +#### source_mac_address + +!!! question "自 sing-box 1.14.0 起" + +!!! quote "" + + 仅支持 Linux,且需要 `route.find_neighbor` 已启用。 + +匹配源设备 MAC 地址。 + +#### source_hostname + +!!! question "自 sing-box 1.14.0 起" + +!!! quote "" + + 仅支持 Linux,且需要 `route.find_neighbor` 已启用。 + +匹配源设备从 DHCP 租约获取的主机名。 + #### wifi_ssid !!! quote "" diff --git a/docs/configuration/inbound/tun.md b/docs/configuration/inbound/tun.md index 74d02dc933..5a2f58d3db 100644 --- a/docs/configuration/inbound/tun.md +++ b/docs/configuration/inbound/tun.md @@ -134,6 +134,12 @@ icon: material/new-box "exclude_package": [ "com.android.captiveportallogin" ], + "include_mac_address": [ + "00:11:22:33:44:55" + ], + "exclude_mac_address": [ + "66:77:88:99:aa:bb" + ], "platform": { "http_proxy": { "enabled": false, @@ -560,6 +566,30 @@ Limit android packages in route. Exclude android packages in route. +#### include_mac_address + +!!! question "Since sing-box 1.14.0" + +!!! quote "" + + Only supported on Linux with `auto_route` and `auto_redirect` enabled. + +Limit MAC addresses in route. Not limited by default. + +Conflict with `exclude_mac_address`. + +#### exclude_mac_address + +!!! question "Since sing-box 1.14.0" + +!!! quote "" + + Only supported on Linux with `auto_route` and `auto_redirect` enabled. + +Exclude MAC addresses in route. + +Conflict with `include_mac_address`. + #### platform Platform-specific settings, provided by client applications. diff --git a/docs/configuration/inbound/tun.zh.md b/docs/configuration/inbound/tun.zh.md index eaf5ff49c3..a41e5ae9ff 100644 --- a/docs/configuration/inbound/tun.zh.md +++ b/docs/configuration/inbound/tun.zh.md @@ -2,6 +2,11 @@ icon: material/new-box --- +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [include_mac_address](#include_mac_address) + :material-plus: [exclude_mac_address](#exclude_mac_address) + !!! quote "sing-box 1.13.3 中的更改" :material-alert: [strict_route](#strict_route) @@ -130,6 +135,12 @@ icon: material/new-box "exclude_package": [ "com.android.captiveportallogin" ], + "include_mac_address": [ + "00:11:22:33:44:55" + ], + "exclude_mac_address": [ + "66:77:88:99:aa:bb" + ], "platform": { "http_proxy": { "enabled": false, @@ -543,6 +554,30 @@ TCP/IP 栈。 排除路由的 Android 应用包名。 +#### include_mac_address + +!!! question "自 sing-box 1.14.0 起" + +!!! quote "" + + 仅支持 Linux,且需要 `auto_route` 和 `auto_redirect` 已启用。 + +限制被路由的 MAC 地址。默认不限制。 + +与 `exclude_mac_address` 冲突。 + +#### exclude_mac_address + +!!! question "自 sing-box 1.14.0 起" + +!!! quote "" + + 仅支持 Linux,且需要 `auto_route` 和 `auto_redirect` 已启用。 + +排除路由的 MAC 地址。 + +与 `include_mac_address` 冲突。 + #### platform 平台特定的设置,由客户端应用提供。 diff --git a/docs/configuration/route/index.md b/docs/configuration/route/index.md index 1fc9bfd231..01e405614e 100644 --- a/docs/configuration/route/index.md +++ b/docs/configuration/route/index.md @@ -4,6 +4,11 @@ icon: material/alert-decagram # Route +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [find_neighbor](#find_neighbor) + :material-plus: [dhcp_lease_files](#dhcp_lease_files) + !!! quote "Changes in sing-box 1.12.0" :material-plus: [default_domain_resolver](#default_domain_resolver) @@ -35,6 +40,8 @@ icon: material/alert-decagram "override_android_vpn": false, "default_interface": "", "default_mark": 0, + "find_neighbor": false, + "dhcp_lease_files": [], "default_domain_resolver": "", // or {} "default_network_strategy": "", "default_network_type": [], @@ -107,6 +114,30 @@ Set routing mark by default. Takes no effect if `outbound.routing_mark` is set. +#### find_neighbor + +!!! question "Since sing-box 1.14.0" + +!!! quote "" + + Only supported on Linux. + +Enable neighbor resolution for source MAC address and hostname lookup. + +Required for `source_mac_address` and `source_hostname` rule items. + +#### dhcp_lease_files + +!!! question "Since sing-box 1.14.0" + +!!! quote "" + + Only supported on Linux. + +Custom DHCP lease file paths for hostname and MAC address resolution. + +Automatically detected from common DHCP servers (dnsmasq, odhcpd, ISC dhcpd, Kea) if empty. + #### default_domain_resolver !!! question "Since sing-box 1.12.0" diff --git a/docs/configuration/route/index.zh.md b/docs/configuration/route/index.zh.md index 1a50d3e3b5..2c12a58eb3 100644 --- a/docs/configuration/route/index.zh.md +++ b/docs/configuration/route/index.zh.md @@ -4,6 +4,11 @@ icon: material/alert-decagram # 路由 +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [find_neighbor](#find_neighbor) + :material-plus: [dhcp_lease_files](#dhcp_lease_files) + !!! quote "sing-box 1.12.0 中的更改" :material-plus: [default_domain_resolver](#default_domain_resolver) @@ -37,6 +42,8 @@ icon: material/alert-decagram "override_android_vpn": false, "default_interface": "", "default_mark": 0, + "find_neighbor": false, + "dhcp_lease_files": [], "default_network_strategy": "", "default_fallback_delay": "" } @@ -106,6 +113,30 @@ icon: material/alert-decagram 如果设置了 `outbound.routing_mark` 设置,则不生效。 +#### find_neighbor + +!!! question "自 sing-box 1.14.0 起" + +!!! quote "" + + 仅支持 Linux。 + +启用邻居解析以查找源 MAC 地址和主机名。 + +`source_mac_address` 和 `source_hostname` 规则项需要此选项。 + +#### dhcp_lease_files + +!!! question "自 sing-box 1.14.0 起" + +!!! quote "" + + 仅支持 Linux。 + +用于主机名和 MAC 地址解析的自定义 DHCP 租约文件路径。 + +为空时自动从常见 DHCP 服务器(dnsmasq、odhcpd、ISC dhcpd、Kea)检测。 + #### default_domain_resolver !!! question "自 sing-box 1.12.0 起" diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md index 925187261c..16c100c1c0 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -2,6 +2,11 @@ icon: material/new-box --- +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [source_mac_address](#source_mac_address) + :material-plus: [source_hostname](#source_hostname) + !!! quote "Changes in sing-box 1.13.0" :material-plus: [interface_address](#interface_address) @@ -159,6 +164,12 @@ icon: material/new-box "tailscale", "wireguard" ], + "source_mac_address": [ + "00:11:22:33:44:55" + ], + "source_hostname": [ + "my-device" + ], "rule_set": [ "geoip-cn", "geosite-cn" @@ -449,6 +460,26 @@ Match specified outbounds' preferred routes. | `tailscale` | Match MagicDNS domains and peers' allowed IPs | | `wireguard` | Match peers's allowed IPs | +#### source_mac_address + +!!! question "Since sing-box 1.14.0" + +!!! quote "" + + Only supported on Linux with `route.find_neighbor` enabled. + +Match source device MAC address. + +#### source_hostname + +!!! question "Since sing-box 1.14.0" + +!!! quote "" + + Only supported on Linux with `route.find_neighbor` enabled. + +Match source device hostname from DHCP leases. + #### rule_set !!! question "Since sing-box 1.8.0" diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md index 53da4475f1..f21e6677b8 100644 --- a/docs/configuration/route/rule.zh.md +++ b/docs/configuration/route/rule.zh.md @@ -2,6 +2,11 @@ icon: material/new-box --- +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [source_mac_address](#source_mac_address) + :material-plus: [source_hostname](#source_hostname) + !!! quote "sing-box 1.13.0 中的更改" :material-plus: [interface_address](#interface_address) @@ -157,6 +162,12 @@ icon: material/new-box "tailscale", "wireguard" ], + "source_mac_address": [ + "00:11:22:33:44:55" + ], + "source_hostname": [ + "my-device" + ], "rule_set": [ "geoip-cn", "geosite-cn" @@ -447,6 +458,26 @@ icon: material/new-box | `tailscale` | 匹配 MagicDNS 域名和对端的 allowed IPs | | `wireguard` | 匹配对端的 allowed IPs | +#### source_mac_address + +!!! question "自 sing-box 1.14.0 起" + +!!! quote "" + + 仅支持 Linux,且需要 `route.find_neighbor` 已启用。 + +匹配源设备 MAC 地址。 + +#### source_hostname + +!!! question "自 sing-box 1.14.0 起" + +!!! quote "" + + 仅支持 Linux,且需要 `route.find_neighbor` 已启用。 + +匹配源设备从 DHCP 租约获取的主机名。 + #### rule_set !!! question "自 sing-box 1.8.0 起" diff --git a/go.mod b/go.mod index 2b7f943545..db27120bd5 100644 --- a/go.mod +++ b/go.mod @@ -14,11 +14,13 @@ require ( github.com/godbus/dbus/v5 v5.2.2 github.com/gofrs/uuid/v5 v5.4.0 github.com/insomniacslk/dhcp v0.0.0-20260220084031-5adc3eb26f91 + github.com/jsimonetti/rtnetlink v1.4.0 github.com/keybase/go-keychain v0.0.1 github.com/libdns/acmedns v0.5.0 github.com/libdns/alidns v1.0.6 github.com/libdns/cloudflare v0.2.2 github.com/logrusorgru/aurora v2.0.3+incompatible + github.com/mdlayher/netlink v1.9.0 github.com/metacubex/utls v1.8.4 github.com/mholt/acmez/v3 v3.1.6 github.com/miekg/dns v1.1.72 @@ -33,13 +35,13 @@ require ( github.com/sagernet/gomobile v0.1.12 github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 - github.com/sagernet/sing v0.8.9 + github.com/sagernet/sing v0.8.10-0.20260421111925-3e730f2301b4 github.com/sagernet/sing-mux v0.3.4 github.com/sagernet/sing-quic v0.6.1 github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 - github.com/sagernet/sing-tun v0.8.9 + github.com/sagernet/sing-tun v0.8.10-0.20260420012056-73f8bbda86a6 github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 github.com/sagernet/smux v1.5.50-sing-box-mod.1 github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7 @@ -92,11 +94,9 @@ require ( github.com/hashicorp/yamux v0.1.2 // indirect github.com/hdevalence/ed25519consensus v0.2.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jsimonetti/rtnetlink v1.4.0 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/libdns/libdns v1.1.1 // indirect - github.com/mdlayher/netlink v1.9.0 // indirect github.com/mdlayher/socket v0.5.1 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect diff --git a/go.sum b/go.sum index ccb4c9098d..3b1f4c2098 100644 --- a/go.sum +++ b/go.sum @@ -236,8 +236,8 @@ github.com/sagernet/nftables v0.3.0-mod.2 h1:ck2KMU02OxL1eDFgGaWYglMDpoOZ7OHzxje github.com/sagernet/nftables v0.3.0-mod.2/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4= -github.com/sagernet/sing v0.8.9 h1:iX8FyMrWNl/divVgTe7cLT9n36v6bfzfnCYlcM1cLaU= -github.com/sagernet/sing v0.8.9/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing v0.8.10-0.20260421111925-3e730f2301b4 h1:pqb1VEG6BXNTid2Llp/AT8ok7FZuCCis41glydWhgno= +github.com/sagernet/sing v0.8.10-0.20260421111925-3e730f2301b4/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s= github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= github.com/sagernet/sing-quic v0.6.1 h1:lx0tcm99wIA1RkyvILNzRSsMy1k7TTQYIhx71E/WBlw= @@ -248,8 +248,8 @@ github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnq github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA= -github.com/sagernet/sing-tun v0.8.9 h1:ixFKKUGdVcJl4wb0xbL36hobiw9l6DIH497EQf5ILpM= -github.com/sagernet/sing-tun v0.8.9/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ= +github.com/sagernet/sing-tun v0.8.10-0.20260420012056-73f8bbda86a6 h1:44lj7uQQES94KGjTEInxmj+b3C9aVfYT4yv5Jf/nL1s= +github.com/sagernet/sing-tun v0.8.10-0.20260420012056-73f8bbda86a6/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY= github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478= diff --git a/option/route.go b/option/route.go index f4b6539156..0c3e576d13 100644 --- a/option/route.go +++ b/option/route.go @@ -9,6 +9,8 @@ type RouteOptions struct { RuleSet []RuleSet `json:"rule_set,omitempty"` Final string `json:"final,omitempty"` FindProcess bool `json:"find_process,omitempty"` + FindNeighbor bool `json:"find_neighbor,omitempty"` + DHCPLeaseFiles badoption.Listable[string] `json:"dhcp_lease_files,omitempty"` AutoDetectInterface bool `json:"auto_detect_interface,omitempty"` OverrideAndroidVPN bool `json:"override_android_vpn,omitempty"` DefaultInterface string `json:"default_interface,omitempty"` diff --git a/option/rule.go b/option/rule.go index 3e7fd8771b..b792ccf4b2 100644 --- a/option/rule.go +++ b/option/rule.go @@ -103,6 +103,8 @@ type RawDefaultRule struct { InterfaceAddress *badjson.TypedMap[string, badoption.Listable[*badoption.Prefixable]] `json:"interface_address,omitempty"` NetworkInterfaceAddress *badjson.TypedMap[InterfaceType, badoption.Listable[*badoption.Prefixable]] `json:"network_interface_address,omitempty"` DefaultInterfaceAddress badoption.Listable[*badoption.Prefixable] `json:"default_interface_address,omitempty"` + SourceMACAddress badoption.Listable[string] `json:"source_mac_address,omitempty"` + SourceHostname badoption.Listable[string] `json:"source_hostname,omitempty"` PreferredBy badoption.Listable[string] `json:"preferred_by,omitempty"` RuleSet badoption.Listable[string] `json:"rule_set,omitempty"` RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"` diff --git a/option/rule_dns.go b/option/rule_dns.go index dbc1657898..880b96ac54 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -106,6 +106,8 @@ type RawDefaultDNSRule struct { InterfaceAddress *badjson.TypedMap[string, badoption.Listable[*badoption.Prefixable]] `json:"interface_address,omitempty"` NetworkInterfaceAddress *badjson.TypedMap[InterfaceType, badoption.Listable[*badoption.Prefixable]] `json:"network_interface_address,omitempty"` DefaultInterfaceAddress badoption.Listable[*badoption.Prefixable] `json:"default_interface_address,omitempty"` + SourceMACAddress badoption.Listable[string] `json:"source_mac_address,omitempty"` + SourceHostname badoption.Listable[string] `json:"source_hostname,omitempty"` RuleSet badoption.Listable[string] `json:"rule_set,omitempty"` RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"` RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"` diff --git a/option/tun.go b/option/tun.go index 72b6e456ba..fda028b69e 100644 --- a/option/tun.go +++ b/option/tun.go @@ -39,6 +39,8 @@ type TunInboundOptions struct { IncludeAndroidUser badoption.Listable[int] `json:"include_android_user,omitempty"` IncludePackage badoption.Listable[string] `json:"include_package,omitempty"` ExcludePackage badoption.Listable[string] `json:"exclude_package,omitempty"` + IncludeMACAddress badoption.Listable[string] `json:"include_mac_address,omitempty"` + ExcludeMACAddress badoption.Listable[string] `json:"exclude_mac_address,omitempty"` UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"` Stack string `json:"stack,omitempty"` Platform *TunPlatformOptions `json:"platform,omitempty"` diff --git a/protocol/tun/inbound.go b/protocol/tun/inbound.go index 6820831a5c..4b113f4a78 100644 --- a/protocol/tun/inbound.go +++ b/protocol/tun/inbound.go @@ -160,6 +160,22 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo if nfQueue == 0 { nfQueue = tun.DefaultAutoRedirectNFQueue } + var includeMACAddress []net.HardwareAddr + for i, macString := range options.IncludeMACAddress { + mac, macErr := net.ParseMAC(macString) + if macErr != nil { + return nil, E.Cause(macErr, "parse include_mac_address[", i, "]") + } + includeMACAddress = append(includeMACAddress, mac) + } + var excludeMACAddress []net.HardwareAddr + for i, macString := range options.ExcludeMACAddress { + mac, macErr := net.ParseMAC(macString) + if macErr != nil { + return nil, E.Cause(macErr, "parse exclude_mac_address[", i, "]") + } + excludeMACAddress = append(excludeMACAddress, mac) + } networkManager := service.FromContext[adapter.NetworkManager](ctx) multiPendingPackets := C.IsDarwin && ((options.Stack == "gvisor" && tunMTU < 32768) || (options.Stack != "gvisor" && options.MTU <= 9000)) inbound := &Inbound{ @@ -197,6 +213,8 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo IncludeAndroidUser: options.IncludeAndroidUser, IncludePackage: options.IncludePackage, ExcludePackage: options.ExcludePackage, + IncludeMACAddress: includeMACAddress, + ExcludeMACAddress: excludeMACAddress, InterfaceMonitor: networkManager.InterfaceMonitor(), EXP_MultiPendingPackets: multiPendingPackets, }, diff --git a/route/neighbor_resolver_linux.go b/route/neighbor_resolver_linux.go new file mode 100644 index 0000000000..40db5766ad --- /dev/null +++ b/route/neighbor_resolver_linux.go @@ -0,0 +1,596 @@ +//go:build linux + +package route + +import ( + "bufio" + "encoding/binary" + "encoding/hex" + "net" + "net/netip" + "os" + "slices" + "strconv" + "strings" + "sync" + "time" + + "github.com/sagernet/fswatch" + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + + "github.com/jsimonetti/rtnetlink" + "github.com/mdlayher/netlink" + "golang.org/x/sys/unix" +) + +var defaultLeaseFiles = []string{ + "/tmp/dhcp.leases", + "/var/lib/dhcp/dhcpd.leases", + "/var/lib/dhcpd/dhcpd.leases", + "/var/lib/kea/kea-leases4.csv", + "/var/lib/kea/kea-leases6.csv", +} + +type neighborResolver struct { + logger logger.ContextLogger + leaseFiles []string + access sync.RWMutex + neighborIPToMAC map[netip.Addr]net.HardwareAddr + leaseIPToMAC map[netip.Addr]net.HardwareAddr + ipToHostname map[netip.Addr]string + macToHostname map[string]string + watcher *fswatch.Watcher + done chan struct{} +} + +func newNeighborResolver(resolverLogger logger.ContextLogger, leaseFiles []string) (adapter.NeighborResolver, error) { + if len(leaseFiles) == 0 { + for _, path := range defaultLeaseFiles { + info, err := os.Stat(path) + if err == nil && info.Size() > 0 { + leaseFiles = append(leaseFiles, path) + } + } + } + return &neighborResolver{ + logger: resolverLogger, + leaseFiles: leaseFiles, + neighborIPToMAC: make(map[netip.Addr]net.HardwareAddr), + leaseIPToMAC: make(map[netip.Addr]net.HardwareAddr), + ipToHostname: make(map[netip.Addr]string), + macToHostname: make(map[string]string), + done: make(chan struct{}), + }, nil +} + +func (r *neighborResolver) Start() error { + err := r.loadNeighborTable() + if err != nil { + r.logger.Warn(E.Cause(err, "load neighbor table")) + } + r.reloadLeaseFiles() + go r.subscribeNeighborUpdates() + if len(r.leaseFiles) > 0 { + watcher, err := fswatch.NewWatcher(fswatch.Options{ + Path: r.leaseFiles, + Logger: r.logger, + Callback: func(_ string) { + r.reloadLeaseFiles() + }, + }) + if err != nil { + r.logger.Warn(E.Cause(err, "create lease file watcher")) + } else { + r.watcher = watcher + err = watcher.Start() + if err != nil { + r.logger.Warn(E.Cause(err, "start lease file watcher")) + } + } + } + return nil +} + +func (r *neighborResolver) Close() error { + close(r.done) + if r.watcher != nil { + return r.watcher.Close() + } + return nil +} + +func (r *neighborResolver) LookupMAC(address netip.Addr) (net.HardwareAddr, bool) { + r.access.RLock() + defer r.access.RUnlock() + mac, found := r.neighborIPToMAC[address] + if found { + return mac, true + } + mac, found = r.leaseIPToMAC[address] + if found { + return mac, true + } + mac, found = extractMACFromEUI64(address) + if found { + return mac, true + } + return nil, false +} + +func (r *neighborResolver) LookupHostname(address netip.Addr) (string, bool) { + r.access.RLock() + defer r.access.RUnlock() + hostname, found := r.ipToHostname[address] + if found { + return hostname, true + } + mac, macFound := r.neighborIPToMAC[address] + if !macFound { + mac, macFound = r.leaseIPToMAC[address] + } + if !macFound { + mac, macFound = extractMACFromEUI64(address) + } + if macFound { + hostname, found = r.macToHostname[mac.String()] + if found { + return hostname, true + } + } + return "", false +} + +func (r *neighborResolver) loadNeighborTable() error { + connection, err := rtnetlink.Dial(nil) + if err != nil { + return E.Cause(err, "dial rtnetlink") + } + defer connection.Close() + neighbors, err := connection.Neigh.List() + if err != nil { + return E.Cause(err, "list neighbors") + } + r.access.Lock() + defer r.access.Unlock() + for _, neigh := range neighbors { + if neigh.Attributes == nil { + continue + } + if neigh.Attributes.LLAddress == nil || len(neigh.Attributes.Address) == 0 { + continue + } + address, ok := netip.AddrFromSlice(neigh.Attributes.Address) + if !ok { + continue + } + r.neighborIPToMAC[address] = slices.Clone(neigh.Attributes.LLAddress) + } + return nil +} + +func (r *neighborResolver) subscribeNeighborUpdates() { + connection, err := netlink.Dial(unix.NETLINK_ROUTE, &netlink.Config{ + Groups: 1 << (unix.RTNLGRP_NEIGH - 1), + }) + if err != nil { + r.logger.Warn(E.Cause(err, "subscribe neighbor updates")) + return + } + defer connection.Close() + for { + select { + case <-r.done: + return + default: + } + err = connection.SetReadDeadline(time.Now().Add(3 * time.Second)) + if err != nil { + r.logger.Warn(E.Cause(err, "set netlink read deadline")) + return + } + messages, err := connection.Receive() + if err != nil { + if nerr, ok := err.(net.Error); ok && nerr.Timeout() { + continue + } + select { + case <-r.done: + return + default: + } + r.logger.Warn(E.Cause(err, "receive neighbor update")) + continue + } + for _, message := range messages { + switch message.Header.Type { + case unix.RTM_NEWNEIGH: + var neighMessage rtnetlink.NeighMessage + unmarshalErr := neighMessage.UnmarshalBinary(message.Data) + if unmarshalErr != nil { + continue + } + if neighMessage.Attributes == nil { + continue + } + if neighMessage.Attributes.LLAddress == nil || len(neighMessage.Attributes.Address) == 0 { + continue + } + address, ok := netip.AddrFromSlice(neighMessage.Attributes.Address) + if !ok { + continue + } + r.access.Lock() + r.neighborIPToMAC[address] = slices.Clone(neighMessage.Attributes.LLAddress) + r.access.Unlock() + case unix.RTM_DELNEIGH: + var neighMessage rtnetlink.NeighMessage + unmarshalErr := neighMessage.UnmarshalBinary(message.Data) + if unmarshalErr != nil { + continue + } + if neighMessage.Attributes == nil || len(neighMessage.Attributes.Address) == 0 { + continue + } + address, ok := netip.AddrFromSlice(neighMessage.Attributes.Address) + if !ok { + continue + } + r.access.Lock() + delete(r.neighborIPToMAC, address) + r.access.Unlock() + } + } + } +} + +func (r *neighborResolver) reloadLeaseFiles() { + leaseIPToMAC := make(map[netip.Addr]net.HardwareAddr) + ipToHostname := make(map[netip.Addr]string) + macToHostname := make(map[string]string) + for _, path := range r.leaseFiles { + r.parseLeaseFile(path, leaseIPToMAC, ipToHostname, macToHostname) + } + r.access.Lock() + r.leaseIPToMAC = leaseIPToMAC + r.ipToHostname = ipToHostname + r.macToHostname = macToHostname + r.access.Unlock() +} + +func (r *neighborResolver) parseLeaseFile(path string, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) { + file, err := os.Open(path) + if err != nil { + return + } + defer file.Close() + if strings.HasSuffix(path, "kea-leases4.csv") { + r.parseKeaCSV4(file, ipToMAC, ipToHostname, macToHostname) + return + } + if strings.HasSuffix(path, "kea-leases6.csv") { + r.parseKeaCSV6(file, ipToMAC, ipToHostname, macToHostname) + return + } + if strings.HasSuffix(path, "dhcpd.leases") { + r.parseISCDhcpd(file, ipToMAC, ipToHostname, macToHostname) + return + } + r.parseDnsmasqOdhcpd(file, ipToMAC, ipToHostname, macToHostname) +} + +func (r *neighborResolver) parseDnsmasqOdhcpd(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) { + now := time.Now().Unix() + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "duid ") { + continue + } + if strings.HasPrefix(line, "# ") { + r.parseOdhcpdLine(line[2:], ipToMAC, ipToHostname, macToHostname) + continue + } + fields := strings.Fields(line) + if len(fields) < 4 { + continue + } + expiry, err := strconv.ParseInt(fields[0], 10, 64) + if err != nil { + continue + } + if expiry != 0 && expiry < now { + continue + } + if strings.Contains(fields[1], ":") { + mac, macErr := net.ParseMAC(fields[1]) + if macErr != nil { + continue + } + address, addrOK := netip.AddrFromSlice(net.ParseIP(fields[2])) + if !addrOK { + continue + } + address = address.Unmap() + ipToMAC[address] = mac + hostname := fields[3] + if hostname != "*" { + ipToHostname[address] = hostname + macToHostname[mac.String()] = hostname + } + } else { + var mac net.HardwareAddr + if len(fields) >= 5 { + duid, duidErr := parseDUID(fields[4]) + if duidErr == nil { + mac, _ = extractMACFromDUID(duid) + } + } + address, addrOK := netip.AddrFromSlice(net.ParseIP(fields[2])) + if !addrOK { + continue + } + address = address.Unmap() + if mac != nil { + ipToMAC[address] = mac + } + hostname := fields[3] + if hostname != "*" { + ipToHostname[address] = hostname + if mac != nil { + macToHostname[mac.String()] = hostname + } + } + } + } +} + +func (r *neighborResolver) parseOdhcpdLine(line string, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) { + fields := strings.Fields(line) + if len(fields) < 5 { + return + } + validTime, err := strconv.ParseInt(fields[4], 10, 64) + if err != nil { + return + } + if validTime == 0 { + return + } + if validTime > 0 && validTime < time.Now().Unix() { + return + } + hostname := fields[3] + if hostname == "-" || strings.HasPrefix(hostname, `broken\x20`) { + hostname = "" + } + if len(fields) >= 8 && fields[2] == "ipv4" { + mac, macErr := net.ParseMAC(fields[1]) + if macErr != nil { + return + } + addressField := fields[7] + slashIndex := strings.IndexByte(addressField, '/') + if slashIndex >= 0 { + addressField = addressField[:slashIndex] + } + address, addrOK := netip.AddrFromSlice(net.ParseIP(addressField)) + if !addrOK { + return + } + address = address.Unmap() + ipToMAC[address] = mac + if hostname != "" { + ipToHostname[address] = hostname + macToHostname[mac.String()] = hostname + } + return + } + var mac net.HardwareAddr + duidHex := fields[1] + duidBytes, hexErr := hex.DecodeString(duidHex) + if hexErr == nil { + mac, _ = extractMACFromDUID(duidBytes) + } + for i := 7; i < len(fields); i++ { + addressField := fields[i] + slashIndex := strings.IndexByte(addressField, '/') + if slashIndex >= 0 { + addressField = addressField[:slashIndex] + } + address, addrOK := netip.AddrFromSlice(net.ParseIP(addressField)) + if !addrOK { + continue + } + address = address.Unmap() + if mac != nil { + ipToMAC[address] = mac + } + if hostname != "" { + ipToHostname[address] = hostname + if mac != nil { + macToHostname[mac.String()] = hostname + } + } + } +} + +func (r *neighborResolver) parseISCDhcpd(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) { + scanner := bufio.NewScanner(file) + var currentIP netip.Addr + var currentMAC net.HardwareAddr + var currentHostname string + var currentActive bool + var inLease bool + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if strings.HasPrefix(line, "lease ") && strings.HasSuffix(line, "{") { + ipString := strings.TrimSuffix(strings.TrimPrefix(line, "lease "), " {") + parsed, addrOK := netip.AddrFromSlice(net.ParseIP(ipString)) + if addrOK { + currentIP = parsed.Unmap() + inLease = true + currentMAC = nil + currentHostname = "" + currentActive = false + } + continue + } + if line == "}" && inLease { + if currentActive && currentMAC != nil { + ipToMAC[currentIP] = currentMAC + if currentHostname != "" { + ipToHostname[currentIP] = currentHostname + macToHostname[currentMAC.String()] = currentHostname + } + } else { + delete(ipToMAC, currentIP) + delete(ipToHostname, currentIP) + } + inLease = false + continue + } + if !inLease { + continue + } + if strings.HasPrefix(line, "hardware ethernet ") { + macString := strings.TrimSuffix(strings.TrimPrefix(line, "hardware ethernet "), ";") + parsed, macErr := net.ParseMAC(macString) + if macErr == nil { + currentMAC = parsed + } + } else if strings.HasPrefix(line, "client-hostname ") { + hostname := strings.TrimSuffix(strings.TrimPrefix(line, "client-hostname "), ";") + hostname = strings.Trim(hostname, "\"") + if hostname != "" { + currentHostname = hostname + } + } else if strings.HasPrefix(line, "binding state ") { + state := strings.TrimSuffix(strings.TrimPrefix(line, "binding state "), ";") + currentActive = state == "active" + } + } +} + +func (r *neighborResolver) parseKeaCSV4(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) { + scanner := bufio.NewScanner(file) + firstLine := true + for scanner.Scan() { + if firstLine { + firstLine = false + continue + } + fields := strings.Split(scanner.Text(), ",") + if len(fields) < 10 { + continue + } + if fields[9] != "0" { + continue + } + address, addrOK := netip.AddrFromSlice(net.ParseIP(fields[0])) + if !addrOK { + continue + } + address = address.Unmap() + mac, macErr := net.ParseMAC(fields[1]) + if macErr != nil { + continue + } + ipToMAC[address] = mac + hostname := "" + if len(fields) > 8 { + hostname = fields[8] + } + if hostname != "" { + ipToHostname[address] = hostname + macToHostname[mac.String()] = hostname + } + } +} + +func (r *neighborResolver) parseKeaCSV6(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) { + scanner := bufio.NewScanner(file) + firstLine := true + for scanner.Scan() { + if firstLine { + firstLine = false + continue + } + fields := strings.Split(scanner.Text(), ",") + if len(fields) < 14 { + continue + } + if fields[13] != "0" { + continue + } + address, addrOK := netip.AddrFromSlice(net.ParseIP(fields[0])) + if !addrOK { + continue + } + address = address.Unmap() + var mac net.HardwareAddr + if fields[12] != "" { + mac, _ = net.ParseMAC(fields[12]) + } + if mac == nil { + duid, duidErr := hex.DecodeString(strings.ReplaceAll(fields[1], ":", "")) + if duidErr == nil { + mac, _ = extractMACFromDUID(duid) + } + } + hostname := "" + if len(fields) > 11 { + hostname = fields[11] + } + if mac != nil { + ipToMAC[address] = mac + } + if hostname != "" { + ipToHostname[address] = hostname + if mac != nil { + macToHostname[mac.String()] = hostname + } + } + } +} + +func extractMACFromDUID(duid []byte) (net.HardwareAddr, bool) { + if len(duid) < 4 { + return nil, false + } + duidType := binary.BigEndian.Uint16(duid[0:2]) + hwType := binary.BigEndian.Uint16(duid[2:4]) + if hwType != 1 { + return nil, false + } + switch duidType { + case 1: + if len(duid) < 14 { + return nil, false + } + return net.HardwareAddr(slices.Clone(duid[8:14])), true + case 3: + if len(duid) < 10 { + return nil, false + } + return net.HardwareAddr(slices.Clone(duid[4:10])), true + } + return nil, false +} + +func extractMACFromEUI64(address netip.Addr) (net.HardwareAddr, bool) { + if !address.Is6() { + return nil, false + } + b := address.As16() + if b[11] != 0xff || b[12] != 0xfe { + return nil, false + } + return net.HardwareAddr{b[8] ^ 0x02, b[9], b[10], b[13], b[14], b[15]}, true +} + +func parseDUID(s string) ([]byte, error) { + cleaned := strings.ReplaceAll(s, ":", "") + return hex.DecodeString(cleaned) +} diff --git a/route/neighbor_resolver_stub.go b/route/neighbor_resolver_stub.go new file mode 100644 index 0000000000..9288892a8d --- /dev/null +++ b/route/neighbor_resolver_stub.go @@ -0,0 +1,14 @@ +//go:build !linux + +package route + +import ( + "os" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common/logger" +) + +func newNeighborResolver(_ logger.ContextLogger, _ []string) (adapter.NeighborResolver, error) { + return nil, os.ErrInvalid +} diff --git a/route/route.go b/route/route.go index 7c24219e30..62a9e4af57 100644 --- a/route/route.go +++ b/route/route.go @@ -408,6 +408,23 @@ func (r *Router) matchRule( buffers []*buf.Buffer, packetBuffers []*N.PacketBuffer, fatalErr error, ) { r.searchProcessInfo(ctx, metadata) + if r.neighborResolver != nil && metadata.SourceMACAddress == nil && metadata.Source.Addr.IsValid() { + mac, macFound := r.neighborResolver.LookupMAC(metadata.Source.Addr) + if macFound { + metadata.SourceMACAddress = mac + } + hostname, hostnameFound := r.neighborResolver.LookupHostname(metadata.Source.Addr) + if hostnameFound { + metadata.SourceHostname = hostname + if macFound { + r.logger.InfoContext(ctx, "found neighbor: ", mac, ", hostname: ", hostname) + } else { + r.logger.InfoContext(ctx, "found neighbor hostname: ", hostname) + } + } else if macFound { + r.logger.InfoContext(ctx, "found neighbor: ", mac) + } + } if metadata.Destination.Addr.IsValid() && r.dnsTransport.FakeIP() != nil && r.dnsTransport.FakeIP().Store().Contains(metadata.Destination.Addr) { domain, loaded := r.dnsTransport.FakeIP().Store().Lookup(metadata.Destination.Addr) if !loaded { diff --git a/route/router.go b/route/router.go index bc19b5d38f..52eb9e4362 100644 --- a/route/router.go +++ b/route/router.go @@ -35,10 +35,13 @@ type Router struct { network adapter.NetworkManager rules []adapter.Rule needFindProcess bool + needFindNeighbor bool + leaseFiles []string ruleSets []adapter.RuleSet ruleSetMap map[string]adapter.RuleSet processSearcher process.Searcher processCache freelru.Cache[processCacheKey, processCacheEntry] + neighborResolver adapter.NeighborResolver pauseManager pause.Manager trackers []adapter.ConnectionTracker platformInterface adapter.PlatformInterface @@ -58,6 +61,8 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.Route rules: make([]adapter.Rule, 0, len(options.Rules)), ruleSetMap: make(map[string]adapter.RuleSet), needFindProcess: hasRule(options.Rules, isProcessRule) || hasDNSRule(dnsOptions.Rules, isProcessDNSRule) || options.FindProcess, + needFindNeighbor: hasRule(options.Rules, isNeighborRule) || hasDNSRule(dnsOptions.Rules, isNeighborDNSRule) || options.FindNeighbor, + leaseFiles: options.DHCPLeaseFiles, pauseManager: service.FromContext[pause.Manager](ctx), platformInterface: service.FromContext[adapter.PlatformInterface](ctx), } @@ -117,6 +122,7 @@ func (r *Router) Start(stage adapter.StartStage) error { } r.network.Initialize(r.ruleSets) needFindProcess := r.needFindProcess + needFindNeighbor := r.needFindNeighbor for _, ruleSet := range r.ruleSets { metadata := ruleSet.Metadata() if metadata.ContainsProcessRule { @@ -151,6 +157,24 @@ func (r *Router) Start(stage adapter.StartStage) error { processCache.SetLifetime(200 * time.Millisecond) r.processCache = processCache } + r.needFindNeighbor = needFindNeighbor + if needFindNeighbor { + monitor.Start("initialize neighbor resolver") + resolver, err := newNeighborResolver(r.logger, r.leaseFiles) + monitor.Finish() + if err != nil { + if err != os.ErrInvalid { + r.logger.Warn(E.Cause(err, "create neighbor resolver")) + } + } else { + err = resolver.Start() + if err != nil { + r.logger.Warn(E.Cause(err, "start neighbor resolver")) + } else { + r.neighborResolver = resolver + } + } + } case adapter.StartStatePostStart: for i, rule := range r.rules { monitor.Start("initialize rule[", i, "]") @@ -182,6 +206,13 @@ func (r *Router) Start(stage adapter.StartStage) error { func (r *Router) Close() error { monitor := taskmonitor.New(r.logger, C.StopTimeout) var err error + if r.neighborResolver != nil { + monitor.Start("close neighbor resolver") + err = E.Append(err, r.neighborResolver.Close(), func(closeErr error) error { + return E.Cause(closeErr, "close neighbor resolver") + }) + monitor.Finish() + } for i, rule := range r.rules { monitor.Start("close rule[", i, "]") err = E.Append(err, rule.Close(), func(err error) error { @@ -223,6 +254,14 @@ func (r *Router) NeedFindProcess() bool { return r.needFindProcess } +func (r *Router) NeedFindNeighbor() bool { + return r.needFindNeighbor +} + +func (r *Router) NeighborResolver() adapter.NeighborResolver { + return r.neighborResolver +} + func (r *Router) ResetNetwork() { r.network.ResetNetwork() r.dns.ResetNetwork() diff --git a/route/rule/rule_default.go b/route/rule/rule_default.go index b921c8b286..5ce1f87d4a 100644 --- a/route/rule/rule_default.go +++ b/route/rule/rule_default.go @@ -264,6 +264,16 @@ func NewDefaultRule(ctx context.Context, logger log.ContextLogger, options optio rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.SourceMACAddress) > 0 { + item := NewSourceMACAddressItem(options.SourceMACAddress) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourceHostname) > 0 { + item := NewSourceHostnameItem(options.SourceHostname) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } if len(options.PreferredBy) > 0 { item := NewPreferredByItem(ctx, options.PreferredBy) rule.items = append(rule.items, item) diff --git a/route/rule/rule_dns.go b/route/rule/rule_dns.go index 04f0f236b2..f33d6096ae 100644 --- a/route/rule/rule_dns.go +++ b/route/rule/rule_dns.go @@ -265,6 +265,16 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.SourceMACAddress) > 0 { + item := NewSourceMACAddressItem(options.SourceMACAddress) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.SourceHostname) > 0 { + item := NewSourceHostnameItem(options.SourceHostname) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } if len(options.RuleSet) > 0 { //nolint:staticcheck if options.Deprecated_RulesetIPCIDRMatchSource { diff --git a/route/rule/rule_item_source_hostname.go b/route/rule/rule_item_source_hostname.go new file mode 100644 index 0000000000..0df11c8c8a --- /dev/null +++ b/route/rule/rule_item_source_hostname.go @@ -0,0 +1,42 @@ +package rule + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" +) + +var _ RuleItem = (*SourceHostnameItem)(nil) + +type SourceHostnameItem struct { + hostnames []string + hostnameMap map[string]bool +} + +func NewSourceHostnameItem(hostnameList []string) *SourceHostnameItem { + rule := &SourceHostnameItem{ + hostnames: hostnameList, + hostnameMap: make(map[string]bool), + } + for _, hostname := range hostnameList { + rule.hostnameMap[hostname] = true + } + return rule +} + +func (r *SourceHostnameItem) Match(metadata *adapter.InboundContext) bool { + if metadata.SourceHostname == "" { + return false + } + return r.hostnameMap[metadata.SourceHostname] +} + +func (r *SourceHostnameItem) String() string { + var description string + if len(r.hostnames) == 1 { + description = "source_hostname=" + r.hostnames[0] + } else { + description = "source_hostname=[" + strings.Join(r.hostnames, " ") + "]" + } + return description +} diff --git a/route/rule/rule_item_source_mac_address.go b/route/rule/rule_item_source_mac_address.go new file mode 100644 index 0000000000..feeadb1dbf --- /dev/null +++ b/route/rule/rule_item_source_mac_address.go @@ -0,0 +1,48 @@ +package rule + +import ( + "net" + "strings" + + "github.com/sagernet/sing-box/adapter" +) + +var _ RuleItem = (*SourceMACAddressItem)(nil) + +type SourceMACAddressItem struct { + addresses []string + addressMap map[string]bool +} + +func NewSourceMACAddressItem(addressList []string) *SourceMACAddressItem { + rule := &SourceMACAddressItem{ + addresses: addressList, + addressMap: make(map[string]bool), + } + for _, address := range addressList { + parsed, err := net.ParseMAC(address) + if err == nil { + rule.addressMap[parsed.String()] = true + } else { + rule.addressMap[address] = true + } + } + return rule +} + +func (r *SourceMACAddressItem) Match(metadata *adapter.InboundContext) bool { + if metadata.SourceMACAddress == nil { + return false + } + return r.addressMap[metadata.SourceMACAddress.String()] +} + +func (r *SourceMACAddressItem) String() string { + var description string + if len(r.addresses) == 1 { + description = "source_mac_address=" + r.addresses[0] + } else { + description = "source_mac_address=[" + strings.Join(r.addresses, " ") + "]" + } + return description +} diff --git a/route/rule_conds.go b/route/rule_conds.go index 55c4a058e2..22ce94fffd 100644 --- a/route/rule_conds.go +++ b/route/rule_conds.go @@ -45,6 +45,14 @@ func isProcessDNSRule(rule option.DefaultDNSRule) bool { return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.ProcessPathRegex) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0 } +func isNeighborRule(rule option.DefaultRule) bool { + return len(rule.SourceMACAddress) > 0 || len(rule.SourceHostname) > 0 +} + +func isNeighborDNSRule(rule option.DefaultDNSRule) bool { + return len(rule.SourceMACAddress) > 0 || len(rule.SourceHostname) > 0 +} + func isWIFIRule(rule option.DefaultRule) bool { return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0 } From c57e86427cbc059ceaea1ceba6ab7d186c2d0d7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 5 Mar 2026 00:15:37 +0800 Subject: [PATCH 02/93] Add Android support for MAC and hostname rule items --- adapter/neighbor.go | 10 ++ adapter/platform.go | 4 + experimental/libbox/config.go | 12 +++ experimental/libbox/neighbor.go | 135 +++++++++++++++++++++++++++ experimental/libbox/neighbor_stub.go | 24 +++++ experimental/libbox/platform.go | 6 ++ experimental/libbox/service.go | 37 ++++++++ route/neighbor_resolver_linux.go | 85 ++--------------- route/neighbor_resolver_parse.go | 50 ++++++++++ route/neighbor_resolver_platform.go | 84 +++++++++++++++++ route/neighbor_table_linux.go | 68 ++++++++++++++ route/router.go | 33 +++++-- 12 files changed, 462 insertions(+), 86 deletions(-) create mode 100644 experimental/libbox/neighbor.go create mode 100644 experimental/libbox/neighbor_stub.go create mode 100644 route/neighbor_resolver_parse.go create mode 100644 route/neighbor_resolver_platform.go create mode 100644 route/neighbor_table_linux.go diff --git a/adapter/neighbor.go b/adapter/neighbor.go index 920398f674..d917db5b7a 100644 --- a/adapter/neighbor.go +++ b/adapter/neighbor.go @@ -5,9 +5,19 @@ import ( "net/netip" ) +type NeighborEntry struct { + Address netip.Addr + MACAddress net.HardwareAddr + Hostname string +} + type NeighborResolver interface { LookupMAC(address netip.Addr) (net.HardwareAddr, bool) LookupHostname(address netip.Addr) (string, bool) Start() error Close() error } + +type NeighborUpdateListener interface { + UpdateNeighborTable(entries []NeighborEntry) +} diff --git a/adapter/platform.go b/adapter/platform.go index df1f447149..e574b885a8 100644 --- a/adapter/platform.go +++ b/adapter/platform.go @@ -40,6 +40,10 @@ type PlatformInterface interface { SendNotification(notification *Notification) error MyInterfaceAddress() []netip.Addr + + UsePlatformNeighborResolver() bool + StartNeighborMonitor(listener NeighborUpdateListener) error + CloseNeighborMonitor(listener NeighborUpdateListener) error } type FindConnectionOwnerRequest struct { diff --git a/experimental/libbox/config.go b/experimental/libbox/config.go index b1676ab61b..9d0b977567 100644 --- a/experimental/libbox/config.go +++ b/experimental/libbox/config.go @@ -149,6 +149,18 @@ func (s *platformInterfaceStub) MyInterfaceAddress() []netip.Addr { return nil } +func (s *platformInterfaceStub) UsePlatformNeighborResolver() bool { + return false +} + +func (s *platformInterfaceStub) StartNeighborMonitor(listener adapter.NeighborUpdateListener) error { + return os.ErrInvalid +} + +func (s *platformInterfaceStub) CloseNeighborMonitor(listener adapter.NeighborUpdateListener) error { + return nil +} + func (s *platformInterfaceStub) UsePlatformLocalDNSTransport() bool { return false } diff --git a/experimental/libbox/neighbor.go b/experimental/libbox/neighbor.go new file mode 100644 index 0000000000..b2ded5f7a1 --- /dev/null +++ b/experimental/libbox/neighbor.go @@ -0,0 +1,135 @@ +//go:build linux + +package libbox + +import ( + "net" + "net/netip" + "slices" + "time" + + "github.com/sagernet/sing-box/route" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/mdlayher/netlink" + "golang.org/x/sys/unix" +) + +type NeighborEntry struct { + Address string + MACAddress string + Hostname string +} + +type NeighborEntryIterator interface { + Next() *NeighborEntry + HasNext() bool +} + +type NeighborSubscription struct { + done chan struct{} +} + +func SubscribeNeighborTable(listener NeighborUpdateListener) (*NeighborSubscription, error) { + entries, err := route.ReadNeighborEntries() + if err != nil { + return nil, E.Cause(err, "initial neighbor dump") + } + table := make(map[netip.Addr]net.HardwareAddr) + for _, entry := range entries { + table[entry.Address] = entry.MACAddress + } + listener.UpdateNeighborTable(tableToIterator(table)) + connection, err := netlink.Dial(unix.NETLINK_ROUTE, &netlink.Config{ + Groups: 1 << (unix.RTNLGRP_NEIGH - 1), + }) + if err != nil { + return nil, E.Cause(err, "subscribe neighbor updates") + } + subscription := &NeighborSubscription{ + done: make(chan struct{}), + } + go subscription.loop(listener, connection, table) + return subscription, nil +} + +func (s *NeighborSubscription) Close() { + close(s.done) +} + +func (s *NeighborSubscription) loop(listener NeighborUpdateListener, connection *netlink.Conn, table map[netip.Addr]net.HardwareAddr) { + defer connection.Close() + for { + select { + case <-s.done: + return + default: + } + err := connection.SetReadDeadline(time.Now().Add(3 * time.Second)) + if err != nil { + return + } + messages, err := connection.Receive() + if err != nil { + if nerr, ok := err.(net.Error); ok && nerr.Timeout() { + continue + } + select { + case <-s.done: + return + default: + } + continue + } + changed := false + for _, message := range messages { + address, mac, isDelete, ok := route.ParseNeighborMessage(message) + if !ok { + continue + } + if isDelete { + if _, exists := table[address]; exists { + delete(table, address) + changed = true + } + } else { + existing, exists := table[address] + if !exists || !slices.Equal(existing, mac) { + table[address] = mac + changed = true + } + } + } + if changed { + listener.UpdateNeighborTable(tableToIterator(table)) + } + } +} + +func tableToIterator(table map[netip.Addr]net.HardwareAddr) NeighborEntryIterator { + entries := make([]*NeighborEntry, 0, len(table)) + for address, mac := range table { + entries = append(entries, &NeighborEntry{ + Address: address.String(), + MACAddress: mac.String(), + }) + } + return &neighborEntryIterator{entries} +} + +type neighborEntryIterator struct { + entries []*NeighborEntry +} + +func (i *neighborEntryIterator) HasNext() bool { + return len(i.entries) > 0 +} + +func (i *neighborEntryIterator) Next() *NeighborEntry { + if len(i.entries) == 0 { + return nil + } + entry := i.entries[0] + i.entries = i.entries[1:] + return entry +} diff --git a/experimental/libbox/neighbor_stub.go b/experimental/libbox/neighbor_stub.go new file mode 100644 index 0000000000..95f6dc7d6f --- /dev/null +++ b/experimental/libbox/neighbor_stub.go @@ -0,0 +1,24 @@ +//go:build !linux + +package libbox + +import "os" + +type NeighborEntry struct { + Address string + MACAddress string + Hostname string +} + +type NeighborEntryIterator interface { + Next() *NeighborEntry + HasNext() bool +} + +type NeighborSubscription struct{} + +func SubscribeNeighborTable(listener NeighborUpdateListener) (*NeighborSubscription, error) { + return nil, os.ErrInvalid +} + +func (s *NeighborSubscription) Close() {} diff --git a/experimental/libbox/platform.go b/experimental/libbox/platform.go index 4db32a2226..759b14e88c 100644 --- a/experimental/libbox/platform.go +++ b/experimental/libbox/platform.go @@ -21,6 +21,12 @@ type PlatformInterface interface { SystemCertificates() StringIterator ClearDNSCache() SendNotification(notification *Notification) error + StartNeighborMonitor(listener NeighborUpdateListener) error + CloseNeighborMonitor(listener NeighborUpdateListener) error +} + +type NeighborUpdateListener interface { + UpdateNeighborTable(entries NeighborEntryIterator) } type ConnectionOwner struct { diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go index 37fd56c980..1c2a6b1324 100644 --- a/experimental/libbox/service.go +++ b/experimental/libbox/service.go @@ -234,6 +234,43 @@ func (w *platformInterfaceWrapper) SendNotification(notification *adapter.Notifi return w.iif.SendNotification((*Notification)(notification)) } +func (w *platformInterfaceWrapper) UsePlatformNeighborResolver() bool { + return true +} + +func (w *platformInterfaceWrapper) StartNeighborMonitor(listener adapter.NeighborUpdateListener) error { + return w.iif.StartNeighborMonitor(&neighborUpdateListenerWrapper{listener: listener}) +} + +func (w *platformInterfaceWrapper) CloseNeighborMonitor(listener adapter.NeighborUpdateListener) error { + return w.iif.CloseNeighborMonitor(nil) +} + +type neighborUpdateListenerWrapper struct { + listener adapter.NeighborUpdateListener +} + +func (w *neighborUpdateListenerWrapper) UpdateNeighborTable(entries NeighborEntryIterator) { + var result []adapter.NeighborEntry + for entries.HasNext() { + entry := entries.Next() + address, err := netip.ParseAddr(entry.Address) + if err != nil { + continue + } + macAddress, err := net.ParseMAC(entry.MACAddress) + if err != nil { + continue + } + result = append(result, adapter.NeighborEntry{ + Address: address, + MACAddress: macAddress, + Hostname: entry.Hostname, + }) + } + w.listener.UpdateNeighborTable(result) +} + func AvailablePort(startPort int32) (int32, error) { for port := int(startPort); ; port++ { if port > 65535 { diff --git a/route/neighbor_resolver_linux.go b/route/neighbor_resolver_linux.go index 40db5766ad..111cc6f040 100644 --- a/route/neighbor_resolver_linux.go +++ b/route/neighbor_resolver_linux.go @@ -4,7 +4,6 @@ package route import ( "bufio" - "encoding/binary" "encoding/hex" "net" "net/netip" @@ -204,43 +203,17 @@ func (r *neighborResolver) subscribeNeighborUpdates() { continue } for _, message := range messages { - switch message.Header.Type { - case unix.RTM_NEWNEIGH: - var neighMessage rtnetlink.NeighMessage - unmarshalErr := neighMessage.UnmarshalBinary(message.Data) - if unmarshalErr != nil { - continue - } - if neighMessage.Attributes == nil { - continue - } - if neighMessage.Attributes.LLAddress == nil || len(neighMessage.Attributes.Address) == 0 { - continue - } - address, ok := netip.AddrFromSlice(neighMessage.Attributes.Address) - if !ok { - continue - } - r.access.Lock() - r.neighborIPToMAC[address] = slices.Clone(neighMessage.Attributes.LLAddress) - r.access.Unlock() - case unix.RTM_DELNEIGH: - var neighMessage rtnetlink.NeighMessage - unmarshalErr := neighMessage.UnmarshalBinary(message.Data) - if unmarshalErr != nil { - continue - } - if neighMessage.Attributes == nil || len(neighMessage.Attributes.Address) == 0 { - continue - } - address, ok := netip.AddrFromSlice(neighMessage.Attributes.Address) - if !ok { - continue - } - r.access.Lock() + address, mac, isDelete, ok := ParseNeighborMessage(message) + if !ok { + continue + } + r.access.Lock() + if isDelete { delete(r.neighborIPToMAC, address) - r.access.Unlock() + } else { + r.neighborIPToMAC[address] = mac } + r.access.Unlock() } } } @@ -554,43 +527,3 @@ func (r *neighborResolver) parseKeaCSV6(file *os.File, ipToMAC map[netip.Addr]ne } } } - -func extractMACFromDUID(duid []byte) (net.HardwareAddr, bool) { - if len(duid) < 4 { - return nil, false - } - duidType := binary.BigEndian.Uint16(duid[0:2]) - hwType := binary.BigEndian.Uint16(duid[2:4]) - if hwType != 1 { - return nil, false - } - switch duidType { - case 1: - if len(duid) < 14 { - return nil, false - } - return net.HardwareAddr(slices.Clone(duid[8:14])), true - case 3: - if len(duid) < 10 { - return nil, false - } - return net.HardwareAddr(slices.Clone(duid[4:10])), true - } - return nil, false -} - -func extractMACFromEUI64(address netip.Addr) (net.HardwareAddr, bool) { - if !address.Is6() { - return nil, false - } - b := address.As16() - if b[11] != 0xff || b[12] != 0xfe { - return nil, false - } - return net.HardwareAddr{b[8] ^ 0x02, b[9], b[10], b[13], b[14], b[15]}, true -} - -func parseDUID(s string) ([]byte, error) { - cleaned := strings.ReplaceAll(s, ":", "") - return hex.DecodeString(cleaned) -} diff --git a/route/neighbor_resolver_parse.go b/route/neighbor_resolver_parse.go new file mode 100644 index 0000000000..1979b7eabc --- /dev/null +++ b/route/neighbor_resolver_parse.go @@ -0,0 +1,50 @@ +package route + +import ( + "encoding/binary" + "encoding/hex" + "net" + "net/netip" + "slices" + "strings" +) + +func extractMACFromDUID(duid []byte) (net.HardwareAddr, bool) { + if len(duid) < 4 { + return nil, false + } + duidType := binary.BigEndian.Uint16(duid[0:2]) + hwType := binary.BigEndian.Uint16(duid[2:4]) + if hwType != 1 { + return nil, false + } + switch duidType { + case 1: + if len(duid) < 14 { + return nil, false + } + return net.HardwareAddr(slices.Clone(duid[8:14])), true + case 3: + if len(duid) < 10 { + return nil, false + } + return net.HardwareAddr(slices.Clone(duid[4:10])), true + } + return nil, false +} + +func extractMACFromEUI64(address netip.Addr) (net.HardwareAddr, bool) { + if !address.Is6() { + return nil, false + } + b := address.As16() + if b[11] != 0xff || b[12] != 0xfe { + return nil, false + } + return net.HardwareAddr{b[8] ^ 0x02, b[9], b[10], b[13], b[14], b[15]}, true +} + +func parseDUID(s string) ([]byte, error) { + cleaned := strings.ReplaceAll(s, ":", "") + return hex.DecodeString(cleaned) +} diff --git a/route/neighbor_resolver_platform.go b/route/neighbor_resolver_platform.go new file mode 100644 index 0000000000..ddb9a99592 --- /dev/null +++ b/route/neighbor_resolver_platform.go @@ -0,0 +1,84 @@ +package route + +import ( + "net" + "net/netip" + "sync" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common/logger" +) + +type platformNeighborResolver struct { + logger logger.ContextLogger + platform adapter.PlatformInterface + access sync.RWMutex + ipToMAC map[netip.Addr]net.HardwareAddr + ipToHostname map[netip.Addr]string + macToHostname map[string]string +} + +func newPlatformNeighborResolver(resolverLogger logger.ContextLogger, platform adapter.PlatformInterface) adapter.NeighborResolver { + return &platformNeighborResolver{ + logger: resolverLogger, + platform: platform, + ipToMAC: make(map[netip.Addr]net.HardwareAddr), + ipToHostname: make(map[netip.Addr]string), + macToHostname: make(map[string]string), + } +} + +func (r *platformNeighborResolver) Start() error { + return r.platform.StartNeighborMonitor(r) +} + +func (r *platformNeighborResolver) Close() error { + return r.platform.CloseNeighborMonitor(r) +} + +func (r *platformNeighborResolver) LookupMAC(address netip.Addr) (net.HardwareAddr, bool) { + r.access.RLock() + defer r.access.RUnlock() + mac, found := r.ipToMAC[address] + if found { + return mac, true + } + return extractMACFromEUI64(address) +} + +func (r *platformNeighborResolver) LookupHostname(address netip.Addr) (string, bool) { + r.access.RLock() + defer r.access.RUnlock() + hostname, found := r.ipToHostname[address] + if found { + return hostname, true + } + mac, found := r.ipToMAC[address] + if !found { + mac, found = extractMACFromEUI64(address) + } + if !found { + return "", false + } + hostname, found = r.macToHostname[mac.String()] + return hostname, found +} + +func (r *platformNeighborResolver) UpdateNeighborTable(entries []adapter.NeighborEntry) { + ipToMAC := make(map[netip.Addr]net.HardwareAddr) + ipToHostname := make(map[netip.Addr]string) + macToHostname := make(map[string]string) + for _, entry := range entries { + ipToMAC[entry.Address] = entry.MACAddress + if entry.Hostname != "" { + ipToHostname[entry.Address] = entry.Hostname + macToHostname[entry.MACAddress.String()] = entry.Hostname + } + } + r.access.Lock() + r.ipToMAC = ipToMAC + r.ipToHostname = ipToHostname + r.macToHostname = macToHostname + r.access.Unlock() + r.logger.Info("updated neighbor table: ", len(entries), " entries") +} diff --git a/route/neighbor_table_linux.go b/route/neighbor_table_linux.go new file mode 100644 index 0000000000..61a214fd3a --- /dev/null +++ b/route/neighbor_table_linux.go @@ -0,0 +1,68 @@ +//go:build linux + +package route + +import ( + "net" + "net/netip" + "slices" + + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/jsimonetti/rtnetlink" + "github.com/mdlayher/netlink" + "golang.org/x/sys/unix" +) + +func ReadNeighborEntries() ([]adapter.NeighborEntry, error) { + connection, err := rtnetlink.Dial(nil) + if err != nil { + return nil, E.Cause(err, "dial rtnetlink") + } + defer connection.Close() + neighbors, err := connection.Neigh.List() + if err != nil { + return nil, E.Cause(err, "list neighbors") + } + var entries []adapter.NeighborEntry + for _, neighbor := range neighbors { + if neighbor.Attributes == nil { + continue + } + if neighbor.Attributes.LLAddress == nil || len(neighbor.Attributes.Address) == 0 { + continue + } + address, ok := netip.AddrFromSlice(neighbor.Attributes.Address) + if !ok { + continue + } + entries = append(entries, adapter.NeighborEntry{ + Address: address, + MACAddress: slices.Clone(neighbor.Attributes.LLAddress), + }) + } + return entries, nil +} + +func ParseNeighborMessage(message netlink.Message) (address netip.Addr, macAddress net.HardwareAddr, isDelete bool, ok bool) { + var neighMessage rtnetlink.NeighMessage + err := neighMessage.UnmarshalBinary(message.Data) + if err != nil { + return + } + if neighMessage.Attributes == nil || len(neighMessage.Attributes.Address) == 0 { + return + } + address, ok = netip.AddrFromSlice(neighMessage.Attributes.Address) + if !ok { + return + } + isDelete = message.Header.Type == unix.RTM_DELNEIGH + if !isDelete && neighMessage.Attributes.LLAddress == nil { + ok = false + return + } + macAddress = slices.Clone(neighMessage.Attributes.LLAddress) + return +} diff --git a/route/router.go b/route/router.go index 52eb9e4362..c6677d20f9 100644 --- a/route/router.go +++ b/route/router.go @@ -159,21 +159,34 @@ func (r *Router) Start(stage adapter.StartStage) error { } r.needFindNeighbor = needFindNeighbor if needFindNeighbor { - monitor.Start("initialize neighbor resolver") - resolver, err := newNeighborResolver(r.logger, r.leaseFiles) - monitor.Finish() - if err != nil { - if err != os.ErrInvalid { - r.logger.Warn(E.Cause(err, "create neighbor resolver")) - } - } else { - err = resolver.Start() + if r.platformInterface != nil && r.platformInterface.UsePlatformNeighborResolver() { + monitor.Start("initialize neighbor resolver") + resolver := newPlatformNeighborResolver(r.logger, r.platformInterface) + err := resolver.Start() + monitor.Finish() if err != nil { - r.logger.Warn(E.Cause(err, "start neighbor resolver")) + r.logger.Error(E.Cause(err, "start neighbor resolver")) } else { r.neighborResolver = resolver } } + if r.neighborResolver == nil { + monitor.Start("initialize neighbor resolver") + resolver, err := newNeighborResolver(r.logger, r.leaseFiles) + monitor.Finish() + if err != nil { + if err != os.ErrInvalid { + r.logger.Error(E.Cause(err, "create neighbor resolver")) + } + } else { + err = resolver.Start() + if err != nil { + r.logger.Error(E.Cause(err, "start neighbor resolver")) + } else { + r.neighborResolver = resolver + } + } + } } case adapter.StartStatePostStart: for i, rule := range r.rules { From 1c02d7e8b085c882013dd4f9c97bfaa07acc51fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 6 Mar 2026 08:47:37 +0800 Subject: [PATCH 03/93] Add macOS support for MAC and hostname rule items --- experimental/libbox/neighbor.go | 86 +----- experimental/libbox/neighbor_darwin.go | 123 ++++++++ experimental/libbox/neighbor_linux.go | 88 ++++++ experimental/libbox/neighbor_stub.go | 19 +- experimental/libbox/platform.go | 1 + experimental/libbox/service.go | 6 +- route/neighbor_resolver_darwin.go | 239 +++++++++++++++ route/neighbor_resolver_lease.go | 386 +++++++++++++++++++++++++ route/neighbor_resolver_linux.go | 313 +------------------- route/neighbor_resolver_stub.go | 2 +- route/neighbor_table_darwin.go | 104 +++++++ route/router.go | 3 +- 12 files changed, 956 insertions(+), 414 deletions(-) create mode 100644 experimental/libbox/neighbor_darwin.go create mode 100644 experimental/libbox/neighbor_linux.go create mode 100644 route/neighbor_resolver_darwin.go create mode 100644 route/neighbor_resolver_lease.go create mode 100644 route/neighbor_table_darwin.go diff --git a/experimental/libbox/neighbor.go b/experimental/libbox/neighbor.go index b2ded5f7a1..e38aa8023f 100644 --- a/experimental/libbox/neighbor.go +++ b/experimental/libbox/neighbor.go @@ -1,23 +1,13 @@ -//go:build linux - package libbox import ( "net" "net/netip" - "slices" - "time" - - "github.com/sagernet/sing-box/route" - E "github.com/sagernet/sing/common/exceptions" - - "github.com/mdlayher/netlink" - "golang.org/x/sys/unix" ) type NeighborEntry struct { Address string - MACAddress string + MacAddress string Hostname string } @@ -30,88 +20,16 @@ type NeighborSubscription struct { done chan struct{} } -func SubscribeNeighborTable(listener NeighborUpdateListener) (*NeighborSubscription, error) { - entries, err := route.ReadNeighborEntries() - if err != nil { - return nil, E.Cause(err, "initial neighbor dump") - } - table := make(map[netip.Addr]net.HardwareAddr) - for _, entry := range entries { - table[entry.Address] = entry.MACAddress - } - listener.UpdateNeighborTable(tableToIterator(table)) - connection, err := netlink.Dial(unix.NETLINK_ROUTE, &netlink.Config{ - Groups: 1 << (unix.RTNLGRP_NEIGH - 1), - }) - if err != nil { - return nil, E.Cause(err, "subscribe neighbor updates") - } - subscription := &NeighborSubscription{ - done: make(chan struct{}), - } - go subscription.loop(listener, connection, table) - return subscription, nil -} - func (s *NeighborSubscription) Close() { close(s.done) } -func (s *NeighborSubscription) loop(listener NeighborUpdateListener, connection *netlink.Conn, table map[netip.Addr]net.HardwareAddr) { - defer connection.Close() - for { - select { - case <-s.done: - return - default: - } - err := connection.SetReadDeadline(time.Now().Add(3 * time.Second)) - if err != nil { - return - } - messages, err := connection.Receive() - if err != nil { - if nerr, ok := err.(net.Error); ok && nerr.Timeout() { - continue - } - select { - case <-s.done: - return - default: - } - continue - } - changed := false - for _, message := range messages { - address, mac, isDelete, ok := route.ParseNeighborMessage(message) - if !ok { - continue - } - if isDelete { - if _, exists := table[address]; exists { - delete(table, address) - changed = true - } - } else { - existing, exists := table[address] - if !exists || !slices.Equal(existing, mac) { - table[address] = mac - changed = true - } - } - } - if changed { - listener.UpdateNeighborTable(tableToIterator(table)) - } - } -} - func tableToIterator(table map[netip.Addr]net.HardwareAddr) NeighborEntryIterator { entries := make([]*NeighborEntry, 0, len(table)) for address, mac := range table { entries = append(entries, &NeighborEntry{ Address: address.String(), - MACAddress: mac.String(), + MacAddress: mac.String(), }) } return &neighborEntryIterator{entries} diff --git a/experimental/libbox/neighbor_darwin.go b/experimental/libbox/neighbor_darwin.go new file mode 100644 index 0000000000..d7484a69b4 --- /dev/null +++ b/experimental/libbox/neighbor_darwin.go @@ -0,0 +1,123 @@ +//go:build darwin + +package libbox + +import ( + "net" + "net/netip" + "os" + "slices" + "time" + + "github.com/sagernet/sing-box/route" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + + xroute "golang.org/x/net/route" + "golang.org/x/sys/unix" +) + +func SubscribeNeighborTable(listener NeighborUpdateListener) (*NeighborSubscription, error) { + entries, err := route.ReadNeighborEntries() + if err != nil { + return nil, E.Cause(err, "initial neighbor dump") + } + table := make(map[netip.Addr]net.HardwareAddr) + for _, entry := range entries { + table[entry.Address] = entry.MACAddress + } + listener.UpdateNeighborTable(tableToIterator(table)) + routeSocket, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0) + if err != nil { + return nil, E.Cause(err, "open route socket") + } + err = unix.SetNonblock(routeSocket, true) + if err != nil { + unix.Close(routeSocket) + return nil, E.Cause(err, "set route socket nonblock") + } + subscription := &NeighborSubscription{ + done: make(chan struct{}), + } + go subscription.loop(listener, routeSocket, table) + return subscription, nil +} + +func (s *NeighborSubscription) loop(listener NeighborUpdateListener, routeSocket int, table map[netip.Addr]net.HardwareAddr) { + routeSocketFile := os.NewFile(uintptr(routeSocket), "route") + defer routeSocketFile.Close() + buffer := buf.NewPacket() + defer buffer.Release() + for { + select { + case <-s.done: + return + default: + } + tv := unix.NsecToTimeval(int64(3 * time.Second)) + _ = unix.SetsockoptTimeval(routeSocket, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &tv) + n, err := routeSocketFile.Read(buffer.FreeBytes()) + if err != nil { + if nerr, ok := err.(net.Error); ok && nerr.Timeout() { + continue + } + select { + case <-s.done: + return + default: + } + continue + } + messages, err := xroute.ParseRIB(xroute.RIBTypeRoute, buffer.FreeBytes()[:n]) + if err != nil { + continue + } + changed := false + for _, message := range messages { + routeMessage, isRouteMessage := message.(*xroute.RouteMessage) + if !isRouteMessage { + continue + } + if routeMessage.Flags&unix.RTF_LLINFO == 0 { + continue + } + address, mac, isDelete, ok := route.ParseRouteNeighborMessage(routeMessage) + if !ok { + continue + } + if isDelete { + if _, exists := table[address]; exists { + delete(table, address) + changed = true + } + } else { + existing, exists := table[address] + if !exists || !slices.Equal(existing, mac) { + table[address] = mac + changed = true + } + } + } + if changed { + listener.UpdateNeighborTable(tableToIterator(table)) + } + } +} + +func ReadBootpdLeases() NeighborEntryIterator { + leaseIPToMAC, ipToHostname, macToHostname := route.ReloadLeaseFiles([]string{"/var/db/dhcpd_leases"}) + entries := make([]*NeighborEntry, 0, len(leaseIPToMAC)) + for address, mac := range leaseIPToMAC { + entry := &NeighborEntry{ + Address: address.String(), + MacAddress: mac.String(), + } + hostname, found := ipToHostname[address] + if !found { + hostname = macToHostname[mac.String()] + } + entry.Hostname = hostname + entries = append(entries, entry) + } + return &neighborEntryIterator{entries} +} diff --git a/experimental/libbox/neighbor_linux.go b/experimental/libbox/neighbor_linux.go new file mode 100644 index 0000000000..ae10bdd2ee --- /dev/null +++ b/experimental/libbox/neighbor_linux.go @@ -0,0 +1,88 @@ +//go:build linux + +package libbox + +import ( + "net" + "net/netip" + "slices" + "time" + + "github.com/sagernet/sing-box/route" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/mdlayher/netlink" + "golang.org/x/sys/unix" +) + +func SubscribeNeighborTable(listener NeighborUpdateListener) (*NeighborSubscription, error) { + entries, err := route.ReadNeighborEntries() + if err != nil { + return nil, E.Cause(err, "initial neighbor dump") + } + table := make(map[netip.Addr]net.HardwareAddr) + for _, entry := range entries { + table[entry.Address] = entry.MACAddress + } + listener.UpdateNeighborTable(tableToIterator(table)) + connection, err := netlink.Dial(unix.NETLINK_ROUTE, &netlink.Config{ + Groups: 1 << (unix.RTNLGRP_NEIGH - 1), + }) + if err != nil { + return nil, E.Cause(err, "subscribe neighbor updates") + } + subscription := &NeighborSubscription{ + done: make(chan struct{}), + } + go subscription.loop(listener, connection, table) + return subscription, nil +} + +func (s *NeighborSubscription) loop(listener NeighborUpdateListener, connection *netlink.Conn, table map[netip.Addr]net.HardwareAddr) { + defer connection.Close() + for { + select { + case <-s.done: + return + default: + } + err := connection.SetReadDeadline(time.Now().Add(3 * time.Second)) + if err != nil { + return + } + messages, err := connection.Receive() + if err != nil { + if nerr, ok := err.(net.Error); ok && nerr.Timeout() { + continue + } + select { + case <-s.done: + return + default: + } + continue + } + changed := false + for _, message := range messages { + address, mac, isDelete, ok := route.ParseNeighborMessage(message) + if !ok { + continue + } + if isDelete { + if _, exists := table[address]; exists { + delete(table, address) + changed = true + } + } else { + existing, exists := table[address] + if !exists || !slices.Equal(existing, mac) { + table[address] = mac + changed = true + } + } + } + if changed { + listener.UpdateNeighborTable(tableToIterator(table)) + } + } +} diff --git a/experimental/libbox/neighbor_stub.go b/experimental/libbox/neighbor_stub.go index 95f6dc7d6f..d465bc7bb0 100644 --- a/experimental/libbox/neighbor_stub.go +++ b/experimental/libbox/neighbor_stub.go @@ -1,24 +1,9 @@ -//go:build !linux +//go:build !linux && !darwin package libbox import "os" -type NeighborEntry struct { - Address string - MACAddress string - Hostname string -} - -type NeighborEntryIterator interface { - Next() *NeighborEntry - HasNext() bool -} - -type NeighborSubscription struct{} - -func SubscribeNeighborTable(listener NeighborUpdateListener) (*NeighborSubscription, error) { +func SubscribeNeighborTable(_ NeighborUpdateListener) (*NeighborSubscription, error) { return nil, os.ErrInvalid } - -func (s *NeighborSubscription) Close() {} diff --git a/experimental/libbox/platform.go b/experimental/libbox/platform.go index 759b14e88c..e65d08184b 100644 --- a/experimental/libbox/platform.go +++ b/experimental/libbox/platform.go @@ -23,6 +23,7 @@ type PlatformInterface interface { SendNotification(notification *Notification) error StartNeighborMonitor(listener NeighborUpdateListener) error CloseNeighborMonitor(listener NeighborUpdateListener) error + RegisterMyInterface(name string) } type NeighborUpdateListener interface { diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go index 1c2a6b1324..0aaa51a556 100644 --- a/experimental/libbox/service.go +++ b/experimental/libbox/service.go @@ -80,6 +80,7 @@ func (w *platformInterfaceWrapper) OpenInterface(options *tun.Options, platformO options.FileDescriptor = dupFd w.myTunName = options.Name w.myTunAddress = myTunAddress(options) + w.iif.RegisterMyInterface(options.Name) return tun.New(*options) } @@ -254,11 +255,14 @@ func (w *neighborUpdateListenerWrapper) UpdateNeighborTable(entries NeighborEntr var result []adapter.NeighborEntry for entries.HasNext() { entry := entries.Next() + if entry == nil { + continue + } address, err := netip.ParseAddr(entry.Address) if err != nil { continue } - macAddress, err := net.ParseMAC(entry.MACAddress) + macAddress, err := net.ParseMAC(entry.MacAddress) if err != nil { continue } diff --git a/route/neighbor_resolver_darwin.go b/route/neighbor_resolver_darwin.go new file mode 100644 index 0000000000..a8884ae628 --- /dev/null +++ b/route/neighbor_resolver_darwin.go @@ -0,0 +1,239 @@ +//go:build darwin + +package route + +import ( + "net" + "net/netip" + "os" + "sync" + "time" + + "github.com/sagernet/fswatch" + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + + "golang.org/x/net/route" + "golang.org/x/sys/unix" +) + +var defaultLeaseFiles = []string{ + "/var/db/dhcpd_leases", + "/tmp/dhcp.leases", +} + +type neighborResolver struct { + logger logger.ContextLogger + leaseFiles []string + access sync.RWMutex + neighborIPToMAC map[netip.Addr]net.HardwareAddr + leaseIPToMAC map[netip.Addr]net.HardwareAddr + ipToHostname map[netip.Addr]string + macToHostname map[string]string + watcher *fswatch.Watcher + done chan struct{} +} + +func newNeighborResolver(resolverLogger logger.ContextLogger, leaseFiles []string) (adapter.NeighborResolver, error) { + if len(leaseFiles) == 0 { + for _, path := range defaultLeaseFiles { + info, err := os.Stat(path) + if err == nil && info.Size() > 0 { + leaseFiles = append(leaseFiles, path) + } + } + } + return &neighborResolver{ + logger: resolverLogger, + leaseFiles: leaseFiles, + neighborIPToMAC: make(map[netip.Addr]net.HardwareAddr), + leaseIPToMAC: make(map[netip.Addr]net.HardwareAddr), + ipToHostname: make(map[netip.Addr]string), + macToHostname: make(map[string]string), + done: make(chan struct{}), + }, nil +} + +func (r *neighborResolver) Start() error { + err := r.loadNeighborTable() + if err != nil { + r.logger.Warn(E.Cause(err, "load neighbor table")) + } + r.doReloadLeaseFiles() + go r.subscribeNeighborUpdates() + if len(r.leaseFiles) > 0 { + watcher, err := fswatch.NewWatcher(fswatch.Options{ + Path: r.leaseFiles, + Logger: r.logger, + Callback: func(_ string) { + r.doReloadLeaseFiles() + }, + }) + if err != nil { + r.logger.Warn(E.Cause(err, "create lease file watcher")) + } else { + r.watcher = watcher + err = watcher.Start() + if err != nil { + r.logger.Warn(E.Cause(err, "start lease file watcher")) + } + } + } + return nil +} + +func (r *neighborResolver) Close() error { + close(r.done) + if r.watcher != nil { + return r.watcher.Close() + } + return nil +} + +func (r *neighborResolver) LookupMAC(address netip.Addr) (net.HardwareAddr, bool) { + r.access.RLock() + defer r.access.RUnlock() + mac, found := r.neighborIPToMAC[address] + if found { + return mac, true + } + mac, found = r.leaseIPToMAC[address] + if found { + return mac, true + } + mac, found = extractMACFromEUI64(address) + if found { + return mac, true + } + return nil, false +} + +func (r *neighborResolver) LookupHostname(address netip.Addr) (string, bool) { + r.access.RLock() + defer r.access.RUnlock() + hostname, found := r.ipToHostname[address] + if found { + return hostname, true + } + mac, macFound := r.neighborIPToMAC[address] + if !macFound { + mac, macFound = r.leaseIPToMAC[address] + } + if !macFound { + mac, macFound = extractMACFromEUI64(address) + } + if macFound { + hostname, found = r.macToHostname[mac.String()] + if found { + return hostname, true + } + } + return "", false +} + +func (r *neighborResolver) loadNeighborTable() error { + entries, err := ReadNeighborEntries() + if err != nil { + return err + } + r.access.Lock() + defer r.access.Unlock() + for _, entry := range entries { + r.neighborIPToMAC[entry.Address] = entry.MACAddress + } + return nil +} + +func (r *neighborResolver) subscribeNeighborUpdates() { + routeSocket, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0) + if err != nil { + r.logger.Warn(E.Cause(err, "subscribe neighbor updates")) + return + } + err = unix.SetNonblock(routeSocket, true) + if err != nil { + unix.Close(routeSocket) + r.logger.Warn(E.Cause(err, "set route socket nonblock")) + return + } + routeSocketFile := os.NewFile(uintptr(routeSocket), "route") + defer routeSocketFile.Close() + buffer := buf.NewPacket() + defer buffer.Release() + for { + select { + case <-r.done: + return + default: + } + err = setReadDeadline(routeSocketFile, 3*time.Second) + if err != nil { + r.logger.Warn(E.Cause(err, "set route socket read deadline")) + return + } + n, err := routeSocketFile.Read(buffer.FreeBytes()) + if err != nil { + if nerr, ok := err.(net.Error); ok && nerr.Timeout() { + continue + } + select { + case <-r.done: + return + default: + } + r.logger.Warn(E.Cause(err, "receive neighbor update")) + continue + } + messages, err := route.ParseRIB(route.RIBTypeRoute, buffer.FreeBytes()[:n]) + if err != nil { + continue + } + for _, message := range messages { + routeMessage, isRouteMessage := message.(*route.RouteMessage) + if !isRouteMessage { + continue + } + if routeMessage.Flags&unix.RTF_LLINFO == 0 { + continue + } + address, mac, isDelete, ok := ParseRouteNeighborMessage(routeMessage) + if !ok { + continue + } + r.access.Lock() + if isDelete { + delete(r.neighborIPToMAC, address) + } else { + r.neighborIPToMAC[address] = mac + } + r.access.Unlock() + } + } +} + +func (r *neighborResolver) doReloadLeaseFiles() { + leaseIPToMAC, ipToHostname, macToHostname := ReloadLeaseFiles(r.leaseFiles) + r.access.Lock() + r.leaseIPToMAC = leaseIPToMAC + r.ipToHostname = ipToHostname + r.macToHostname = macToHostname + r.access.Unlock() +} + +func setReadDeadline(file *os.File, timeout time.Duration) error { + rawConn, err := file.SyscallConn() + if err != nil { + return err + } + var controlErr error + err = rawConn.Control(func(fd uintptr) { + tv := unix.NsecToTimeval(int64(timeout)) + controlErr = unix.SetsockoptTimeval(int(fd), unix.SOL_SOCKET, unix.SO_RCVTIMEO, &tv) + }) + if err != nil { + return err + } + return controlErr +} diff --git a/route/neighbor_resolver_lease.go b/route/neighbor_resolver_lease.go new file mode 100644 index 0000000000..e3f9c0b464 --- /dev/null +++ b/route/neighbor_resolver_lease.go @@ -0,0 +1,386 @@ +package route + +import ( + "bufio" + "encoding/hex" + "net" + "net/netip" + "os" + "strconv" + "strings" + "time" +) + +func parseLeaseFile(path string, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) { + file, err := os.Open(path) + if err != nil { + return + } + defer file.Close() + if strings.HasSuffix(path, "dhcpd_leases") { + parseBootpdLeases(file, ipToMAC, ipToHostname, macToHostname) + return + } + if strings.HasSuffix(path, "kea-leases4.csv") { + parseKeaCSV4(file, ipToMAC, ipToHostname, macToHostname) + return + } + if strings.HasSuffix(path, "kea-leases6.csv") { + parseKeaCSV6(file, ipToMAC, ipToHostname, macToHostname) + return + } + if strings.HasSuffix(path, "dhcpd.leases") { + parseISCDhcpd(file, ipToMAC, ipToHostname, macToHostname) + return + } + parseDnsmasqOdhcpd(file, ipToMAC, ipToHostname, macToHostname) +} + +func ReloadLeaseFiles(leaseFiles []string) (leaseIPToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) { + leaseIPToMAC = make(map[netip.Addr]net.HardwareAddr) + ipToHostname = make(map[netip.Addr]string) + macToHostname = make(map[string]string) + for _, path := range leaseFiles { + parseLeaseFile(path, leaseIPToMAC, ipToHostname, macToHostname) + } + return +} + +func parseDnsmasqOdhcpd(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) { + now := time.Now().Unix() + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "duid ") { + continue + } + if strings.HasPrefix(line, "# ") { + parseOdhcpdLine(line[2:], ipToMAC, ipToHostname, macToHostname) + continue + } + fields := strings.Fields(line) + if len(fields) < 4 { + continue + } + expiry, err := strconv.ParseInt(fields[0], 10, 64) + if err != nil { + continue + } + if expiry != 0 && expiry < now { + continue + } + if strings.Contains(fields[1], ":") { + mac, macErr := net.ParseMAC(fields[1]) + if macErr != nil { + continue + } + address, addrOK := netip.AddrFromSlice(net.ParseIP(fields[2])) + if !addrOK { + continue + } + address = address.Unmap() + ipToMAC[address] = mac + hostname := fields[3] + if hostname != "*" { + ipToHostname[address] = hostname + macToHostname[mac.String()] = hostname + } + } else { + var mac net.HardwareAddr + if len(fields) >= 5 { + duid, duidErr := parseDUID(fields[4]) + if duidErr == nil { + mac, _ = extractMACFromDUID(duid) + } + } + address, addrOK := netip.AddrFromSlice(net.ParseIP(fields[2])) + if !addrOK { + continue + } + address = address.Unmap() + if mac != nil { + ipToMAC[address] = mac + } + hostname := fields[3] + if hostname != "*" { + ipToHostname[address] = hostname + if mac != nil { + macToHostname[mac.String()] = hostname + } + } + } + } +} + +func parseOdhcpdLine(line string, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) { + fields := strings.Fields(line) + if len(fields) < 5 { + return + } + validTime, err := strconv.ParseInt(fields[4], 10, 64) + if err != nil { + return + } + if validTime == 0 { + return + } + if validTime > 0 && validTime < time.Now().Unix() { + return + } + hostname := fields[3] + if hostname == "-" || strings.HasPrefix(hostname, `broken\x20`) { + hostname = "" + } + if len(fields) >= 8 && fields[2] == "ipv4" { + mac, macErr := net.ParseMAC(fields[1]) + if macErr != nil { + return + } + addressField := fields[7] + slashIndex := strings.IndexByte(addressField, '/') + if slashIndex >= 0 { + addressField = addressField[:slashIndex] + } + address, addrOK := netip.AddrFromSlice(net.ParseIP(addressField)) + if !addrOK { + return + } + address = address.Unmap() + ipToMAC[address] = mac + if hostname != "" { + ipToHostname[address] = hostname + macToHostname[mac.String()] = hostname + } + return + } + var mac net.HardwareAddr + duidHex := fields[1] + duidBytes, hexErr := hex.DecodeString(duidHex) + if hexErr == nil { + mac, _ = extractMACFromDUID(duidBytes) + } + for i := 7; i < len(fields); i++ { + addressField := fields[i] + slashIndex := strings.IndexByte(addressField, '/') + if slashIndex >= 0 { + addressField = addressField[:slashIndex] + } + address, addrOK := netip.AddrFromSlice(net.ParseIP(addressField)) + if !addrOK { + continue + } + address = address.Unmap() + if mac != nil { + ipToMAC[address] = mac + } + if hostname != "" { + ipToHostname[address] = hostname + if mac != nil { + macToHostname[mac.String()] = hostname + } + } + } +} + +func parseISCDhcpd(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) { + scanner := bufio.NewScanner(file) + var currentIP netip.Addr + var currentMAC net.HardwareAddr + var currentHostname string + var currentActive bool + var inLease bool + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if strings.HasPrefix(line, "lease ") && strings.HasSuffix(line, "{") { + ipString := strings.TrimSuffix(strings.TrimPrefix(line, "lease "), " {") + parsed, addrOK := netip.AddrFromSlice(net.ParseIP(ipString)) + if addrOK { + currentIP = parsed.Unmap() + inLease = true + currentMAC = nil + currentHostname = "" + currentActive = false + } + continue + } + if line == "}" && inLease { + if currentActive && currentMAC != nil { + ipToMAC[currentIP] = currentMAC + if currentHostname != "" { + ipToHostname[currentIP] = currentHostname + macToHostname[currentMAC.String()] = currentHostname + } + } else { + delete(ipToMAC, currentIP) + delete(ipToHostname, currentIP) + } + inLease = false + continue + } + if !inLease { + continue + } + if strings.HasPrefix(line, "hardware ethernet ") { + macString := strings.TrimSuffix(strings.TrimPrefix(line, "hardware ethernet "), ";") + parsed, macErr := net.ParseMAC(macString) + if macErr == nil { + currentMAC = parsed + } + } else if strings.HasPrefix(line, "client-hostname ") { + hostname := strings.TrimSuffix(strings.TrimPrefix(line, "client-hostname "), ";") + hostname = strings.Trim(hostname, "\"") + if hostname != "" { + currentHostname = hostname + } + } else if strings.HasPrefix(line, "binding state ") { + state := strings.TrimSuffix(strings.TrimPrefix(line, "binding state "), ";") + currentActive = state == "active" + } + } +} + +func parseKeaCSV4(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) { + scanner := bufio.NewScanner(file) + firstLine := true + for scanner.Scan() { + if firstLine { + firstLine = false + continue + } + fields := strings.Split(scanner.Text(), ",") + if len(fields) < 10 { + continue + } + if fields[9] != "0" { + continue + } + address, addrOK := netip.AddrFromSlice(net.ParseIP(fields[0])) + if !addrOK { + continue + } + address = address.Unmap() + mac, macErr := net.ParseMAC(fields[1]) + if macErr != nil { + continue + } + ipToMAC[address] = mac + hostname := "" + if len(fields) > 8 { + hostname = fields[8] + } + if hostname != "" { + ipToHostname[address] = hostname + macToHostname[mac.String()] = hostname + } + } +} + +func parseKeaCSV6(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) { + scanner := bufio.NewScanner(file) + firstLine := true + for scanner.Scan() { + if firstLine { + firstLine = false + continue + } + fields := strings.Split(scanner.Text(), ",") + if len(fields) < 14 { + continue + } + if fields[13] != "0" { + continue + } + address, addrOK := netip.AddrFromSlice(net.ParseIP(fields[0])) + if !addrOK { + continue + } + address = address.Unmap() + var mac net.HardwareAddr + if fields[12] != "" { + mac, _ = net.ParseMAC(fields[12]) + } + if mac == nil { + duid, duidErr := hex.DecodeString(strings.ReplaceAll(fields[1], ":", "")) + if duidErr == nil { + mac, _ = extractMACFromDUID(duid) + } + } + hostname := "" + if len(fields) > 11 { + hostname = fields[11] + } + if mac != nil { + ipToMAC[address] = mac + } + if hostname != "" { + ipToHostname[address] = hostname + if mac != nil { + macToHostname[mac.String()] = hostname + } + } + } +} + +func parseBootpdLeases(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) { + now := time.Now().Unix() + scanner := bufio.NewScanner(file) + var currentName string + var currentIP netip.Addr + var currentMAC net.HardwareAddr + var currentLease int64 + var inBlock bool + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "{" { + inBlock = true + currentName = "" + currentIP = netip.Addr{} + currentMAC = nil + currentLease = 0 + continue + } + if line == "}" && inBlock { + if currentMAC != nil && currentIP.IsValid() { + if currentLease == 0 || currentLease >= now { + ipToMAC[currentIP] = currentMAC + if currentName != "" { + ipToHostname[currentIP] = currentName + macToHostname[currentMAC.String()] = currentName + } + } + } + inBlock = false + continue + } + if !inBlock { + continue + } + key, value, found := strings.Cut(line, "=") + if !found { + continue + } + switch key { + case "name": + currentName = value + case "ip_address": + parsed, addrOK := netip.AddrFromSlice(net.ParseIP(value)) + if addrOK { + currentIP = parsed.Unmap() + } + case "hw_address": + typeAndMAC, hasSep := strings.CutPrefix(value, "1,") + if hasSep { + mac, macErr := net.ParseMAC(typeAndMAC) + if macErr == nil { + currentMAC = mac + } + } + case "lease": + leaseHex := strings.TrimPrefix(value, "0x") + parsed, parseErr := strconv.ParseInt(leaseHex, 16, 64) + if parseErr == nil { + currentLease = parsed + } + } + } +} diff --git a/route/neighbor_resolver_linux.go b/route/neighbor_resolver_linux.go index 111cc6f040..b7991b4c89 100644 --- a/route/neighbor_resolver_linux.go +++ b/route/neighbor_resolver_linux.go @@ -3,14 +3,10 @@ package route import ( - "bufio" - "encoding/hex" "net" "net/netip" "os" "slices" - "strconv" - "strings" "sync" "time" @@ -69,14 +65,14 @@ func (r *neighborResolver) Start() error { if err != nil { r.logger.Warn(E.Cause(err, "load neighbor table")) } - r.reloadLeaseFiles() + r.doReloadLeaseFiles() go r.subscribeNeighborUpdates() if len(r.leaseFiles) > 0 { watcher, err := fswatch.NewWatcher(fswatch.Options{ Path: r.leaseFiles, Logger: r.logger, Callback: func(_ string) { - r.reloadLeaseFiles() + r.doReloadLeaseFiles() }, }) if err != nil { @@ -218,312 +214,11 @@ func (r *neighborResolver) subscribeNeighborUpdates() { } } -func (r *neighborResolver) reloadLeaseFiles() { - leaseIPToMAC := make(map[netip.Addr]net.HardwareAddr) - ipToHostname := make(map[netip.Addr]string) - macToHostname := make(map[string]string) - for _, path := range r.leaseFiles { - r.parseLeaseFile(path, leaseIPToMAC, ipToHostname, macToHostname) - } +func (r *neighborResolver) doReloadLeaseFiles() { + leaseIPToMAC, ipToHostname, macToHostname := ReloadLeaseFiles(r.leaseFiles) r.access.Lock() r.leaseIPToMAC = leaseIPToMAC r.ipToHostname = ipToHostname r.macToHostname = macToHostname r.access.Unlock() } - -func (r *neighborResolver) parseLeaseFile(path string, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) { - file, err := os.Open(path) - if err != nil { - return - } - defer file.Close() - if strings.HasSuffix(path, "kea-leases4.csv") { - r.parseKeaCSV4(file, ipToMAC, ipToHostname, macToHostname) - return - } - if strings.HasSuffix(path, "kea-leases6.csv") { - r.parseKeaCSV6(file, ipToMAC, ipToHostname, macToHostname) - return - } - if strings.HasSuffix(path, "dhcpd.leases") { - r.parseISCDhcpd(file, ipToMAC, ipToHostname, macToHostname) - return - } - r.parseDnsmasqOdhcpd(file, ipToMAC, ipToHostname, macToHostname) -} - -func (r *neighborResolver) parseDnsmasqOdhcpd(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) { - now := time.Now().Unix() - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := scanner.Text() - if strings.HasPrefix(line, "duid ") { - continue - } - if strings.HasPrefix(line, "# ") { - r.parseOdhcpdLine(line[2:], ipToMAC, ipToHostname, macToHostname) - continue - } - fields := strings.Fields(line) - if len(fields) < 4 { - continue - } - expiry, err := strconv.ParseInt(fields[0], 10, 64) - if err != nil { - continue - } - if expiry != 0 && expiry < now { - continue - } - if strings.Contains(fields[1], ":") { - mac, macErr := net.ParseMAC(fields[1]) - if macErr != nil { - continue - } - address, addrOK := netip.AddrFromSlice(net.ParseIP(fields[2])) - if !addrOK { - continue - } - address = address.Unmap() - ipToMAC[address] = mac - hostname := fields[3] - if hostname != "*" { - ipToHostname[address] = hostname - macToHostname[mac.String()] = hostname - } - } else { - var mac net.HardwareAddr - if len(fields) >= 5 { - duid, duidErr := parseDUID(fields[4]) - if duidErr == nil { - mac, _ = extractMACFromDUID(duid) - } - } - address, addrOK := netip.AddrFromSlice(net.ParseIP(fields[2])) - if !addrOK { - continue - } - address = address.Unmap() - if mac != nil { - ipToMAC[address] = mac - } - hostname := fields[3] - if hostname != "*" { - ipToHostname[address] = hostname - if mac != nil { - macToHostname[mac.String()] = hostname - } - } - } - } -} - -func (r *neighborResolver) parseOdhcpdLine(line string, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) { - fields := strings.Fields(line) - if len(fields) < 5 { - return - } - validTime, err := strconv.ParseInt(fields[4], 10, 64) - if err != nil { - return - } - if validTime == 0 { - return - } - if validTime > 0 && validTime < time.Now().Unix() { - return - } - hostname := fields[3] - if hostname == "-" || strings.HasPrefix(hostname, `broken\x20`) { - hostname = "" - } - if len(fields) >= 8 && fields[2] == "ipv4" { - mac, macErr := net.ParseMAC(fields[1]) - if macErr != nil { - return - } - addressField := fields[7] - slashIndex := strings.IndexByte(addressField, '/') - if slashIndex >= 0 { - addressField = addressField[:slashIndex] - } - address, addrOK := netip.AddrFromSlice(net.ParseIP(addressField)) - if !addrOK { - return - } - address = address.Unmap() - ipToMAC[address] = mac - if hostname != "" { - ipToHostname[address] = hostname - macToHostname[mac.String()] = hostname - } - return - } - var mac net.HardwareAddr - duidHex := fields[1] - duidBytes, hexErr := hex.DecodeString(duidHex) - if hexErr == nil { - mac, _ = extractMACFromDUID(duidBytes) - } - for i := 7; i < len(fields); i++ { - addressField := fields[i] - slashIndex := strings.IndexByte(addressField, '/') - if slashIndex >= 0 { - addressField = addressField[:slashIndex] - } - address, addrOK := netip.AddrFromSlice(net.ParseIP(addressField)) - if !addrOK { - continue - } - address = address.Unmap() - if mac != nil { - ipToMAC[address] = mac - } - if hostname != "" { - ipToHostname[address] = hostname - if mac != nil { - macToHostname[mac.String()] = hostname - } - } - } -} - -func (r *neighborResolver) parseISCDhcpd(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) { - scanner := bufio.NewScanner(file) - var currentIP netip.Addr - var currentMAC net.HardwareAddr - var currentHostname string - var currentActive bool - var inLease bool - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if strings.HasPrefix(line, "lease ") && strings.HasSuffix(line, "{") { - ipString := strings.TrimSuffix(strings.TrimPrefix(line, "lease "), " {") - parsed, addrOK := netip.AddrFromSlice(net.ParseIP(ipString)) - if addrOK { - currentIP = parsed.Unmap() - inLease = true - currentMAC = nil - currentHostname = "" - currentActive = false - } - continue - } - if line == "}" && inLease { - if currentActive && currentMAC != nil { - ipToMAC[currentIP] = currentMAC - if currentHostname != "" { - ipToHostname[currentIP] = currentHostname - macToHostname[currentMAC.String()] = currentHostname - } - } else { - delete(ipToMAC, currentIP) - delete(ipToHostname, currentIP) - } - inLease = false - continue - } - if !inLease { - continue - } - if strings.HasPrefix(line, "hardware ethernet ") { - macString := strings.TrimSuffix(strings.TrimPrefix(line, "hardware ethernet "), ";") - parsed, macErr := net.ParseMAC(macString) - if macErr == nil { - currentMAC = parsed - } - } else if strings.HasPrefix(line, "client-hostname ") { - hostname := strings.TrimSuffix(strings.TrimPrefix(line, "client-hostname "), ";") - hostname = strings.Trim(hostname, "\"") - if hostname != "" { - currentHostname = hostname - } - } else if strings.HasPrefix(line, "binding state ") { - state := strings.TrimSuffix(strings.TrimPrefix(line, "binding state "), ";") - currentActive = state == "active" - } - } -} - -func (r *neighborResolver) parseKeaCSV4(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) { - scanner := bufio.NewScanner(file) - firstLine := true - for scanner.Scan() { - if firstLine { - firstLine = false - continue - } - fields := strings.Split(scanner.Text(), ",") - if len(fields) < 10 { - continue - } - if fields[9] != "0" { - continue - } - address, addrOK := netip.AddrFromSlice(net.ParseIP(fields[0])) - if !addrOK { - continue - } - address = address.Unmap() - mac, macErr := net.ParseMAC(fields[1]) - if macErr != nil { - continue - } - ipToMAC[address] = mac - hostname := "" - if len(fields) > 8 { - hostname = fields[8] - } - if hostname != "" { - ipToHostname[address] = hostname - macToHostname[mac.String()] = hostname - } - } -} - -func (r *neighborResolver) parseKeaCSV6(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) { - scanner := bufio.NewScanner(file) - firstLine := true - for scanner.Scan() { - if firstLine { - firstLine = false - continue - } - fields := strings.Split(scanner.Text(), ",") - if len(fields) < 14 { - continue - } - if fields[13] != "0" { - continue - } - address, addrOK := netip.AddrFromSlice(net.ParseIP(fields[0])) - if !addrOK { - continue - } - address = address.Unmap() - var mac net.HardwareAddr - if fields[12] != "" { - mac, _ = net.ParseMAC(fields[12]) - } - if mac == nil { - duid, duidErr := hex.DecodeString(strings.ReplaceAll(fields[1], ":", "")) - if duidErr == nil { - mac, _ = extractMACFromDUID(duid) - } - } - hostname := "" - if len(fields) > 11 { - hostname = fields[11] - } - if mac != nil { - ipToMAC[address] = mac - } - if hostname != "" { - ipToHostname[address] = hostname - if mac != nil { - macToHostname[mac.String()] = hostname - } - } - } -} diff --git a/route/neighbor_resolver_stub.go b/route/neighbor_resolver_stub.go index 9288892a8d..177a1fccbc 100644 --- a/route/neighbor_resolver_stub.go +++ b/route/neighbor_resolver_stub.go @@ -1,4 +1,4 @@ -//go:build !linux +//go:build !linux && !darwin package route diff --git a/route/neighbor_table_darwin.go b/route/neighbor_table_darwin.go new file mode 100644 index 0000000000..8ca2d0f0b7 --- /dev/null +++ b/route/neighbor_table_darwin.go @@ -0,0 +1,104 @@ +//go:build darwin + +package route + +import ( + "net" + "net/netip" + "syscall" + + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" + + "golang.org/x/net/route" + "golang.org/x/sys/unix" +) + +func ReadNeighborEntries() ([]adapter.NeighborEntry, error) { + var entries []adapter.NeighborEntry + ipv4Entries, err := readNeighborEntriesAF(syscall.AF_INET) + if err != nil { + return nil, E.Cause(err, "read IPv4 neighbors") + } + entries = append(entries, ipv4Entries...) + ipv6Entries, err := readNeighborEntriesAF(syscall.AF_INET6) + if err != nil { + return nil, E.Cause(err, "read IPv6 neighbors") + } + entries = append(entries, ipv6Entries...) + return entries, nil +} + +func readNeighborEntriesAF(addressFamily int) ([]adapter.NeighborEntry, error) { + rib, err := route.FetchRIB(addressFamily, route.RIBType(syscall.NET_RT_FLAGS), syscall.RTF_LLINFO) + if err != nil { + return nil, err + } + messages, err := route.ParseRIB(route.RIBType(syscall.NET_RT_FLAGS), rib) + if err != nil { + return nil, err + } + var entries []adapter.NeighborEntry + for _, message := range messages { + routeMessage, isRouteMessage := message.(*route.RouteMessage) + if !isRouteMessage { + continue + } + address, macAddress, ok := parseRouteNeighborEntry(routeMessage) + if !ok { + continue + } + entries = append(entries, adapter.NeighborEntry{ + Address: address, + MACAddress: macAddress, + }) + } + return entries, nil +} + +func parseRouteNeighborEntry(message *route.RouteMessage) (address netip.Addr, macAddress net.HardwareAddr, ok bool) { + if len(message.Addrs) <= unix.RTAX_GATEWAY { + return + } + gateway, isLinkAddr := message.Addrs[unix.RTAX_GATEWAY].(*route.LinkAddr) + if !isLinkAddr || len(gateway.Addr) < 6 { + return + } + switch destination := message.Addrs[unix.RTAX_DST].(type) { + case *route.Inet4Addr: + address = netip.AddrFrom4(destination.IP) + case *route.Inet6Addr: + address = netip.AddrFrom16(destination.IP) + default: + return + } + macAddress = net.HardwareAddr(make([]byte, len(gateway.Addr))) + copy(macAddress, gateway.Addr) + ok = true + return +} + +func ParseRouteNeighborMessage(message *route.RouteMessage) (address netip.Addr, macAddress net.HardwareAddr, isDelete bool, ok bool) { + isDelete = message.Type == unix.RTM_DELETE + if len(message.Addrs) <= unix.RTAX_GATEWAY { + return + } + switch destination := message.Addrs[unix.RTAX_DST].(type) { + case *route.Inet4Addr: + address = netip.AddrFrom4(destination.IP) + case *route.Inet6Addr: + address = netip.AddrFrom16(destination.IP) + default: + return + } + if !isDelete { + gateway, isLinkAddr := message.Addrs[unix.RTAX_GATEWAY].(*route.LinkAddr) + if !isLinkAddr || len(gateway.Addr) < 6 { + return + } + macAddress = net.HardwareAddr(make([]byte, len(gateway.Addr))) + copy(macAddress, gateway.Addr) + } + ok = true + return +} diff --git a/route/router.go b/route/router.go index c6677d20f9..2815d5095b 100644 --- a/route/router.go +++ b/route/router.go @@ -169,8 +169,7 @@ func (r *Router) Start(stage adapter.StartStage) error { } else { r.neighborResolver = resolver } - } - if r.neighborResolver == nil { + } else { monitor.Start("initialize neighbor resolver") resolver, err := newNeighborResolver(r.logger, r.leaseFiles) monitor.Finish() From 4f6d0ffafc6ad0c1eae1224f8f233b9b9ad91c52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 6 Mar 2026 21:43:21 +0800 Subject: [PATCH 04/93] documentation: Update descriptions for neighbor rules --- docs/configuration/dns/rule.md | 4 +- docs/configuration/dns/rule.zh.md | 4 +- docs/configuration/route/index.md | 17 ++++++-- docs/configuration/route/index.zh.md | 17 ++++++-- docs/configuration/route/rule.md | 4 +- docs/configuration/route/rule.zh.md | 4 +- docs/configuration/shared/neighbor.md | 49 ++++++++++++++++++++++++ docs/configuration/shared/neighbor.zh.md | 49 ++++++++++++++++++++++++ mkdocs.yml | 1 + 9 files changed, 133 insertions(+), 16 deletions(-) create mode 100644 docs/configuration/shared/neighbor.md create mode 100644 docs/configuration/shared/neighbor.zh.md diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index f8a7ac4c37..0b3e56da69 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -425,7 +425,7 @@ Match default interface address. !!! quote "" - Only supported on Linux with `route.find_neighbor` enabled. + Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup. Match source device MAC address. @@ -435,7 +435,7 @@ Match source device MAC address. !!! quote "" - Only supported on Linux with `route.find_neighbor` enabled. + Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup. Match source device hostname from DHCP leases. diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index 421fdfb5c1..82f85648f0 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -424,7 +424,7 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`. !!! quote "" - 仅支持 Linux,且需要 `route.find_neighbor` 已启用。 + 仅支持 Linux、macOS,或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。 匹配源设备 MAC 地址。 @@ -434,7 +434,7 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`. !!! quote "" - 仅支持 Linux,且需要 `route.find_neighbor` 已启用。 + 仅支持 Linux、macOS,或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。 匹配源设备从 DHCP 租约获取的主机名。 diff --git a/docs/configuration/route/index.md b/docs/configuration/route/index.md index 01e405614e..40104b619e 100644 --- a/docs/configuration/route/index.md +++ b/docs/configuration/route/index.md @@ -40,6 +40,7 @@ icon: material/alert-decagram "override_android_vpn": false, "default_interface": "", "default_mark": 0, + "find_process": false, "find_neighbor": false, "dhcp_lease_files": [], "default_domain_resolver": "", // or {} @@ -114,17 +115,25 @@ Set routing mark by default. Takes no effect if `outbound.routing_mark` is set. +#### find_process + +!!! quote "" + + Only supported on Linux, Windows, and macOS. + +Enable process search for logging when no `process_name`, `process_path`, `package_name`, `user` or `user_id` rules exist. + #### find_neighbor !!! question "Since sing-box 1.14.0" !!! quote "" - Only supported on Linux. + Only supported on Linux and macOS. -Enable neighbor resolution for source MAC address and hostname lookup. +Enable neighbor resolution for logging when no `source_mac_address` or `source_hostname` rules exist. -Required for `source_mac_address` and `source_hostname` rule items. +See [Neighbor Resolution](/configuration/shared/neighbor/) for setup. #### dhcp_lease_files @@ -132,7 +141,7 @@ Required for `source_mac_address` and `source_hostname` rule items. !!! quote "" - Only supported on Linux. + Only supported on Linux and macOS. Custom DHCP lease file paths for hostname and MAC address resolution. diff --git a/docs/configuration/route/index.zh.md b/docs/configuration/route/index.zh.md index 2c12a58eb3..4977b084e2 100644 --- a/docs/configuration/route/index.zh.md +++ b/docs/configuration/route/index.zh.md @@ -42,6 +42,7 @@ icon: material/alert-decagram "override_android_vpn": false, "default_interface": "", "default_mark": 0, + "find_process": false, "find_neighbor": false, "dhcp_lease_files": [], "default_network_strategy": "", @@ -113,17 +114,25 @@ icon: material/alert-decagram 如果设置了 `outbound.routing_mark` 设置,则不生效。 +#### find_process + +!!! quote "" + + 仅支持 Linux、Windows 和 macOS。 + +在没有 `process_name`、`process_path`、`package_name`、`user` 或 `user_id` 规则时启用进程搜索以输出日志。 + #### find_neighbor !!! question "自 sing-box 1.14.0 起" !!! quote "" - 仅支持 Linux。 + 仅支持 Linux 和 macOS。 -启用邻居解析以查找源 MAC 地址和主机名。 +在没有 `source_mac_address` 或 `source_hostname` 规则时启用邻居解析以输出日志。 -`source_mac_address` 和 `source_hostname` 规则项需要此选项。 +参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。 #### dhcp_lease_files @@ -131,7 +140,7 @@ icon: material/alert-decagram !!! quote "" - 仅支持 Linux。 + 仅支持 Linux 和 macOS。 用于主机名和 MAC 地址解析的自定义 DHCP 租约文件路径。 diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md index 16c100c1c0..37e651c924 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -466,7 +466,7 @@ Match specified outbounds' preferred routes. !!! quote "" - Only supported on Linux with `route.find_neighbor` enabled. + Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup. Match source device MAC address. @@ -476,7 +476,7 @@ Match source device MAC address. !!! quote "" - Only supported on Linux with `route.find_neighbor` enabled. + Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup. Match source device hostname from DHCP leases. diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md index f21e6677b8..181a57398d 100644 --- a/docs/configuration/route/rule.zh.md +++ b/docs/configuration/route/rule.zh.md @@ -464,7 +464,7 @@ icon: material/new-box !!! quote "" - 仅支持 Linux,且需要 `route.find_neighbor` 已启用。 + 仅支持 Linux、macOS,或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。 匹配源设备 MAC 地址。 @@ -474,7 +474,7 @@ icon: material/new-box !!! quote "" - 仅支持 Linux,且需要 `route.find_neighbor` 已启用。 + 仅支持 Linux、macOS,或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。 匹配源设备从 DHCP 租约获取的主机名。 diff --git a/docs/configuration/shared/neighbor.md b/docs/configuration/shared/neighbor.md new file mode 100644 index 0000000000..c67d995ebe --- /dev/null +++ b/docs/configuration/shared/neighbor.md @@ -0,0 +1,49 @@ +--- +icon: material/lan +--- + +# Neighbor Resolution + +Match LAN devices by MAC address and hostname using +[`source_mac_address`](/configuration/route/rule/#source_mac_address) and +[`source_hostname`](/configuration/route/rule/#source_hostname) rule items. + +Neighbor resolution is automatically enabled when these rule items exist. +Use [`route.find_neighbor`](/configuration/route/#find_neighbor) to force enable it for logging without rules. + +## Linux + +Works natively. No special setup required. + +Hostname resolution requires DHCP lease files, +automatically detected from common DHCP servers (dnsmasq, odhcpd, ISC dhcpd, Kea). +Custom paths can be set via [`route.dhcp_lease_files`](/configuration/route/#dhcp_lease_files). + +## Android + +!!! quote "" + + Only supported in graphical clients. + +Requires Android 11 or above and ROOT. + +Must use [VPNHotspot](https://github.com/Mygod/VPNHotspot) to share the VPN connection. +ROM built-in features like "Use VPN for connected devices" can share VPN +but cannot provide MAC address or hostname information. + +Set **IP Masquerade Mode** to **None** in VPNHotspot settings. + +Only route/DNS rules are supported. TUN include/exclude routes are not supported. + +### Hostname Visibility + +Hostname is only visible in sing-box if it is visible in VPNHotspot. +For Apple devices, change **Private Wi-Fi Address** from **Rotating** to **Fixed** in the Wi-Fi settings +of the connected network. Non-Apple devices are always visible. + +## macOS + +Requires the standalone version (macOS system extension). +The App Store version can share the VPN as a hotspot but does not support MAC address or hostname reading. + +See [VPN Hotspot](/manual/misc/vpn-hotspot/#macos) for Internet Sharing setup. diff --git a/docs/configuration/shared/neighbor.zh.md b/docs/configuration/shared/neighbor.zh.md new file mode 100644 index 0000000000..96297fcb57 --- /dev/null +++ b/docs/configuration/shared/neighbor.zh.md @@ -0,0 +1,49 @@ +--- +icon: material/lan +--- + +# 邻居解析 + +通过 +[`source_mac_address`](/configuration/route/rule/#source_mac_address) 和 +[`source_hostname`](/configuration/route/rule/#source_hostname) 规则项匹配局域网设备的 MAC 地址和主机名。 + +当这些规则项存在时,邻居解析自动启用。 +使用 [`route.find_neighbor`](/configuration/route/#find_neighbor) 可在没有规则时强制启用以输出日志。 + +## Linux + +原生支持,无需特殊设置。 + +主机名解析需要 DHCP 租约文件, +自动从常见 DHCP 服务器(dnsmasq、odhcpd、ISC dhcpd、Kea)检测。 +可通过 [`route.dhcp_lease_files`](/configuration/route/#dhcp_lease_files) 设置自定义路径。 + +## Android + +!!! quote "" + + 仅在图形客户端中支持。 + +需要 Android 11 或以上版本和 ROOT。 + +必须使用 [VPNHotspot](https://github.com/Mygod/VPNHotspot) 共享 VPN 连接。 +ROM 自带的「通过 VPN 共享连接」等功能可以共享 VPN, +但无法提供 MAC 地址或主机名信息。 + +在 VPNHotspot 设置中将 **IP 遮掩模式** 设为 **无**。 + +仅支持路由/DNS 规则。不支持 TUN 的 include/exclude 路由。 + +### 设备可见性 + +MAC 地址和主机名仅在 VPNHotspot 中可见时 sing-box 才能读取。 +对于 Apple 设备,需要在所连接网络的 Wi-Fi 设置中将**私有无线局域网地址**从**轮替**改为**固定**。 +非 Apple 设备始终可见。 + +## macOS + +需要独立版本(macOS 系统扩展)。 +App Store 版本可以共享 VPN 热点但不支持 MAC 地址或主机名读取。 + +参阅 [VPN 热点](/manual/misc/vpn-hotspot/#macos) 了解互联网共享设置。 diff --git a/mkdocs.yml b/mkdocs.yml index e295926610..5f95842a5d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -129,6 +129,7 @@ nav: - UDP over TCP: configuration/shared/udp-over-tcp.md - TCP Brutal: configuration/shared/tcp-brutal.md - Wi-Fi State: configuration/shared/wifi-state.md + - Neighbor Resolution: configuration/shared/neighbor.md - Endpoint: - configuration/endpoint/index.md - WireGuard: configuration/endpoint/wireguard.md From 83fa58f60a0b00ba36680ad2cb39792bb616857f Mon Sep 17 00:00:00 2001 From: nekohasekai Date: Mon, 23 Mar 2026 20:04:36 +0800 Subject: [PATCH 05/93] Refactor ACME support to certificate provider --- adapter/certificate/adapter.go | 21 + adapter/certificate/manager.go | 158 +++++ adapter/certificate/registry.go | 72 ++ adapter/certificate_provider.go | 38 ++ box.go | 123 ++-- common/tls/acme.go | 37 +- common/tls/acme_logger.go | 41 ++ common/tls/reality_server.go | 4 + common/tls/std_server.go | 179 ++++- constant/proxy.go | 62 +- .../tls/acme_contstant.go => constant/tls.go | 2 +- docs/configuration/inbound/tun.md | 2 +- docs/configuration/index.md | 5 +- docs/configuration/index.zh.md | 5 +- .../shared/certificate-provider/acme.md | 150 +++++ .../shared/certificate-provider/acme.zh.md | 145 ++++ .../cloudflare-origin-ca.md | 82 +++ .../cloudflare-origin-ca.zh.md | 82 +++ .../shared/certificate-provider/index.md | 32 + .../shared/certificate-provider/index.zh.md | 32 + .../shared/certificate-provider/tailscale.md | 27 + .../certificate-provider/tailscale.zh.md | 27 + docs/configuration/shared/dns01_challenge.md | 53 ++ .../shared/dns01_challenge.zh.md | 53 ++ docs/configuration/shared/tls.md | 31 +- docs/configuration/shared/tls.zh.md | 29 +- docs/deprecated.md | 30 +- docs/deprecated.zh.md | 32 +- docs/migration.md | 77 +++ docs/migration.zh.md | 77 +++ experimental/deprecated/constants.go | 10 + experimental/libbox/config.go | 2 +- include/acme.go | 12 + include/acme_stub.go | 20 + include/registry.go | 14 +- include/tailscale.go | 5 + include/tailscale_stub.go | 7 + mkdocs.yml | 7 + option/acme.go | 106 +++ option/certificate_provider.go | 100 +++ option/options.go | 44 +- option/origin_ca.go | 76 +++ option/tailscale.go | 4 + option/tls.go | 10 +- protocol/tailscale/certificate_provider.go | 98 +++ service/acme/service.go | 411 ++++++++++++ service/acme/stub.go | 3 + service/origin_ca/service.go | 618 ++++++++++++++++++ 48 files changed, 3083 insertions(+), 172 deletions(-) create mode 100644 adapter/certificate/adapter.go create mode 100644 adapter/certificate/manager.go create mode 100644 adapter/certificate/registry.go create mode 100644 adapter/certificate_provider.go create mode 100644 common/tls/acme_logger.go rename common/tls/acme_contstant.go => constant/tls.go (69%) create mode 100644 docs/configuration/shared/certificate-provider/acme.md create mode 100644 docs/configuration/shared/certificate-provider/acme.zh.md create mode 100644 docs/configuration/shared/certificate-provider/cloudflare-origin-ca.md create mode 100644 docs/configuration/shared/certificate-provider/cloudflare-origin-ca.zh.md create mode 100644 docs/configuration/shared/certificate-provider/index.md create mode 100644 docs/configuration/shared/certificate-provider/index.zh.md create mode 100644 docs/configuration/shared/certificate-provider/tailscale.md create mode 100644 docs/configuration/shared/certificate-provider/tailscale.zh.md create mode 100644 include/acme.go create mode 100644 include/acme_stub.go create mode 100644 option/acme.go create mode 100644 option/certificate_provider.go create mode 100644 option/origin_ca.go create mode 100644 protocol/tailscale/certificate_provider.go create mode 100644 service/acme/service.go create mode 100644 service/acme/stub.go create mode 100644 service/origin_ca/service.go diff --git a/adapter/certificate/adapter.go b/adapter/certificate/adapter.go new file mode 100644 index 0000000000..802020c1e4 --- /dev/null +++ b/adapter/certificate/adapter.go @@ -0,0 +1,21 @@ +package certificate + +type Adapter struct { + providerType string + providerTag string +} + +func NewAdapter(providerType string, providerTag string) Adapter { + return Adapter{ + providerType: providerType, + providerTag: providerTag, + } +} + +func (a *Adapter) Type() string { + return a.providerType +} + +func (a *Adapter) Tag() string { + return a.providerTag +} diff --git a/adapter/certificate/manager.go b/adapter/certificate/manager.go new file mode 100644 index 0000000000..e4b9b535bb --- /dev/null +++ b/adapter/certificate/manager.go @@ -0,0 +1,158 @@ +package certificate + +import ( + "context" + "os" + "sync" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/taskmonitor" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" +) + +var _ adapter.CertificateProviderManager = (*Manager)(nil) + +type Manager struct { + logger log.ContextLogger + registry adapter.CertificateProviderRegistry + access sync.Mutex + started bool + stage adapter.StartStage + providers []adapter.CertificateProviderService + providerByTag map[string]adapter.CertificateProviderService +} + +func NewManager(logger log.ContextLogger, registry adapter.CertificateProviderRegistry) *Manager { + return &Manager{ + logger: logger, + registry: registry, + providerByTag: make(map[string]adapter.CertificateProviderService), + } +} + +func (m *Manager) Start(stage adapter.StartStage) error { + m.access.Lock() + if m.started && m.stage >= stage { + panic("already started") + } + m.started = true + m.stage = stage + providers := m.providers + m.access.Unlock() + for _, provider := range providers { + name := "certificate-provider/" + provider.Type() + "[" + provider.Tag() + "]" + m.logger.Trace(stage, " ", name) + startTime := time.Now() + err := adapter.LegacyStart(provider, stage) + if err != nil { + return E.Cause(err, stage, " ", name) + } + m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + } + return nil +} + +func (m *Manager) Close() error { + m.access.Lock() + defer m.access.Unlock() + if !m.started { + return nil + } + m.started = false + providers := m.providers + m.providers = nil + monitor := taskmonitor.New(m.logger, C.StopTimeout) + var err error + for _, provider := range providers { + name := "certificate-provider/" + provider.Type() + "[" + provider.Tag() + "]" + m.logger.Trace("close ", name) + startTime := time.Now() + monitor.Start("close ", name) + err = E.Append(err, provider.Close(), func(err error) error { + return E.Cause(err, "close ", name) + }) + monitor.Finish() + m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + } + return err +} + +func (m *Manager) CertificateProviders() []adapter.CertificateProviderService { + m.access.Lock() + defer m.access.Unlock() + return m.providers +} + +func (m *Manager) Get(tag string) (adapter.CertificateProviderService, bool) { + m.access.Lock() + provider, found := m.providerByTag[tag] + m.access.Unlock() + return provider, found +} + +func (m *Manager) Remove(tag string) error { + m.access.Lock() + provider, found := m.providerByTag[tag] + if !found { + m.access.Unlock() + return os.ErrInvalid + } + delete(m.providerByTag, tag) + index := common.Index(m.providers, func(it adapter.CertificateProviderService) bool { + return it == provider + }) + if index == -1 { + panic("invalid certificate provider index") + } + m.providers = append(m.providers[:index], m.providers[index+1:]...) + started := m.started + m.access.Unlock() + if started { + return provider.Close() + } + return nil +} + +func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) error { + provider, err := m.registry.Create(ctx, logger, tag, providerType, options) + if err != nil { + return err + } + m.access.Lock() + defer m.access.Unlock() + if m.started { + name := "certificate-provider/" + provider.Type() + "[" + provider.Tag() + "]" + for _, stage := range adapter.ListStartStages { + m.logger.Trace(stage, " ", name) + startTime := time.Now() + err = adapter.LegacyStart(provider, stage) + if err != nil { + return E.Cause(err, stage, " ", name) + } + m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + } + } + if existsProvider, loaded := m.providerByTag[tag]; loaded { + if m.started { + err = existsProvider.Close() + if err != nil { + return E.Cause(err, "close certificate-provider/", existsProvider.Type(), "[", existsProvider.Tag(), "]") + } + } + existsIndex := common.Index(m.providers, func(it adapter.CertificateProviderService) bool { + return it == existsProvider + }) + if existsIndex == -1 { + panic("invalid certificate provider index") + } + m.providers = append(m.providers[:existsIndex], m.providers[existsIndex+1:]...) + } + m.providers = append(m.providers, provider) + m.providerByTag[tag] = provider + return nil +} diff --git a/adapter/certificate/registry.go b/adapter/certificate/registry.go new file mode 100644 index 0000000000..5a080f2ccc --- /dev/null +++ b/adapter/certificate/registry.go @@ -0,0 +1,72 @@ +package certificate + +import ( + "context" + "sync" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" +) + +type ConstructorFunc[T any] func(ctx context.Context, logger log.ContextLogger, tag string, options T) (adapter.CertificateProviderService, error) + +func Register[Options any](registry *Registry, providerType string, constructor ConstructorFunc[Options]) { + registry.register(providerType, func() any { + return new(Options) + }, func(ctx context.Context, logger log.ContextLogger, tag string, rawOptions any) (adapter.CertificateProviderService, error) { + var options *Options + if rawOptions != nil { + options = rawOptions.(*Options) + } + return constructor(ctx, logger, tag, common.PtrValueOrDefault(options)) + }) +} + +var _ adapter.CertificateProviderRegistry = (*Registry)(nil) + +type ( + optionsConstructorFunc func() any + constructorFunc func(ctx context.Context, logger log.ContextLogger, tag string, options any) (adapter.CertificateProviderService, error) +) + +type Registry struct { + access sync.Mutex + optionsType map[string]optionsConstructorFunc + constructor map[string]constructorFunc +} + +func NewRegistry() *Registry { + return &Registry{ + optionsType: make(map[string]optionsConstructorFunc), + constructor: make(map[string]constructorFunc), + } +} + +func (m *Registry) CreateOptions(providerType string) (any, bool) { + m.access.Lock() + defer m.access.Unlock() + optionsConstructor, loaded := m.optionsType[providerType] + if !loaded { + return nil, false + } + return optionsConstructor(), true +} + +func (m *Registry) Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) (adapter.CertificateProviderService, error) { + m.access.Lock() + defer m.access.Unlock() + constructor, loaded := m.constructor[providerType] + if !loaded { + return nil, E.New("certificate provider type not found: " + providerType) + } + return constructor(ctx, logger, tag, options) +} + +func (m *Registry) register(providerType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) { + m.access.Lock() + defer m.access.Unlock() + m.optionsType[providerType] = optionsConstructor + m.constructor[providerType] = constructor +} diff --git a/adapter/certificate_provider.go b/adapter/certificate_provider.go new file mode 100644 index 0000000000..70bdeb8838 --- /dev/null +++ b/adapter/certificate_provider.go @@ -0,0 +1,38 @@ +package adapter + +import ( + "context" + "crypto/tls" + + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" +) + +type CertificateProvider interface { + GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) +} + +type ACMECertificateProvider interface { + CertificateProvider + GetACMENextProtos() []string +} + +type CertificateProviderService interface { + Lifecycle + Type() string + Tag() string + CertificateProvider +} + +type CertificateProviderRegistry interface { + option.CertificateProviderOptionsRegistry + Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) (CertificateProviderService, error) +} + +type CertificateProviderManager interface { + Lifecycle + CertificateProviders() []CertificateProviderService + Get(tag string) (CertificateProviderService, bool) + Remove(tag string) error + Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) error +} diff --git a/box.go b/box.go index c88a9bc9a8..67feadc2d4 100644 --- a/box.go +++ b/box.go @@ -9,6 +9,7 @@ import ( "time" "github.com/sagernet/sing-box/adapter" + boxCertificate "github.com/sagernet/sing-box/adapter/certificate" "github.com/sagernet/sing-box/adapter/endpoint" "github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/adapter/outbound" @@ -36,20 +37,21 @@ import ( var _ adapter.SimpleLifecycle = (*Box)(nil) type Box struct { - createdAt time.Time - logFactory log.Factory - logger log.ContextLogger - network *route.NetworkManager - endpoint *endpoint.Manager - inbound *inbound.Manager - outbound *outbound.Manager - service *boxService.Manager - dnsTransport *dns.TransportManager - dnsRouter *dns.Router - connection *route.ConnectionManager - router *route.Router - internalService []adapter.LifecycleService - done chan struct{} + createdAt time.Time + logFactory log.Factory + logger log.ContextLogger + network *route.NetworkManager + endpoint *endpoint.Manager + inbound *inbound.Manager + outbound *outbound.Manager + service *boxService.Manager + certificateProvider *boxCertificate.Manager + dnsTransport *dns.TransportManager + dnsRouter *dns.Router + connection *route.ConnectionManager + router *route.Router + internalService []adapter.LifecycleService + done chan struct{} } type Options struct { @@ -65,6 +67,7 @@ func Context( endpointRegistry adapter.EndpointRegistry, dnsTransportRegistry adapter.DNSTransportRegistry, serviceRegistry adapter.ServiceRegistry, + certificateProviderRegistry adapter.CertificateProviderRegistry, ) context.Context { if service.FromContext[option.InboundOptionsRegistry](ctx) == nil || service.FromContext[adapter.InboundRegistry](ctx) == nil { @@ -89,6 +92,10 @@ func Context( ctx = service.ContextWith[option.ServiceOptionsRegistry](ctx, serviceRegistry) ctx = service.ContextWith[adapter.ServiceRegistry](ctx, serviceRegistry) } + if service.FromContext[adapter.CertificateProviderRegistry](ctx) == nil { + ctx = service.ContextWith[option.CertificateProviderOptionsRegistry](ctx, certificateProviderRegistry) + ctx = service.ContextWith[adapter.CertificateProviderRegistry](ctx, certificateProviderRegistry) + } return ctx } @@ -105,6 +112,7 @@ func New(options Options) (*Box, error) { outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx) dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx) serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx) + certificateProviderRegistry := service.FromContext[adapter.CertificateProviderRegistry](ctx) if endpointRegistry == nil { return nil, E.New("missing endpoint registry in context") @@ -121,6 +129,9 @@ func New(options Options) (*Box, error) { if serviceRegistry == nil { return nil, E.New("missing service registry in context") } + if certificateProviderRegistry == nil { + return nil, E.New("missing certificate provider registry in context") + } ctx = pause.WithDefaultManager(ctx) experimentalOptions := common.PtrValueOrDefault(options.Experimental) @@ -178,11 +189,13 @@ func New(options Options) (*Box, error) { outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final) dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final) serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry) + certificateProviderManager := boxCertificate.NewManager(logFactory.NewLogger("certificate-provider"), certificateProviderRegistry) service.MustRegister[adapter.EndpointManager](ctx, endpointManager) service.MustRegister[adapter.InboundManager](ctx, inboundManager) service.MustRegister[adapter.OutboundManager](ctx, outboundManager) service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager) service.MustRegister[adapter.ServiceManager](ctx, serviceManager) + service.MustRegister[adapter.CertificateProviderManager](ctx, certificateProviderManager) dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions) service.MustRegister[adapter.DNSRouter](ctx, dnsRouter) networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions) @@ -271,6 +284,24 @@ func New(options Options) (*Box, error) { return nil, E.Cause(err, "initialize inbound[", i, "]") } } + for i, serviceOptions := range options.Services { + var tag string + if serviceOptions.Tag != "" { + tag = serviceOptions.Tag + } else { + tag = F.ToString(i) + } + err = serviceManager.Create( + ctx, + logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")), + tag, + serviceOptions.Type, + serviceOptions.Options, + ) + if err != nil { + return nil, E.Cause(err, "initialize service[", i, "]") + } + } for i, outboundOptions := range options.Outbounds { var tag string if outboundOptions.Tag != "" { @@ -297,22 +328,22 @@ func New(options Options) (*Box, error) { return nil, E.Cause(err, "initialize outbound[", i, "]") } } - for i, serviceOptions := range options.Services { + for i, certificateProviderOptions := range options.CertificateProviders { var tag string - if serviceOptions.Tag != "" { - tag = serviceOptions.Tag + if certificateProviderOptions.Tag != "" { + tag = certificateProviderOptions.Tag } else { tag = F.ToString(i) } - err = serviceManager.Create( + err = certificateProviderManager.Create( ctx, - logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")), + logFactory.NewLogger(F.ToString("certificate-provider/", certificateProviderOptions.Type, "[", tag, "]")), tag, - serviceOptions.Type, - serviceOptions.Options, + certificateProviderOptions.Type, + certificateProviderOptions.Options, ) if err != nil { - return nil, E.Cause(err, "initialize service[", i, "]") + return nil, E.Cause(err, "initialize certificate provider[", i, "]") } } outboundManager.Initialize(func() (adapter.Outbound, error) { @@ -383,20 +414,21 @@ func New(options Options) (*Box, error) { internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service")) } return &Box{ - network: networkManager, - endpoint: endpointManager, - inbound: inboundManager, - outbound: outboundManager, - dnsTransport: dnsTransportManager, - service: serviceManager, - dnsRouter: dnsRouter, - connection: connectionManager, - router: router, - createdAt: createdAt, - logFactory: logFactory, - logger: logFactory.Logger(), - internalService: internalServices, - done: make(chan struct{}), + network: networkManager, + endpoint: endpointManager, + inbound: inboundManager, + outbound: outboundManager, + dnsTransport: dnsTransportManager, + service: serviceManager, + certificateProvider: certificateProviderManager, + dnsRouter: dnsRouter, + connection: connectionManager, + router: router, + createdAt: createdAt, + logFactory: logFactory, + logger: logFactory.Logger(), + internalService: internalServices, + done: make(chan struct{}), }, nil } @@ -450,7 +482,7 @@ func (s *Box) preStart() error { if err != nil { return err } - err = adapter.Start(s.logger, adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service) + err = adapter.Start(s.logger, adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service, s.certificateProvider) if err != nil { return err } @@ -470,11 +502,19 @@ func (s *Box) start() error { if err != nil { return err } - err = adapter.Start(s.logger, adapter.StartStateStart, s.inbound, s.endpoint, s.service) + err = adapter.Start(s.logger, adapter.StartStateStart, s.endpoint) if err != nil { return err } - err = adapter.Start(s.logger, adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.service) + err = adapter.Start(s.logger, adapter.StartStateStart, s.certificateProvider) + if err != nil { + return err + } + err = adapter.Start(s.logger, adapter.StartStateStart, s.inbound, s.service) + if err != nil { + return err + } + err = adapter.Start(s.logger, adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.endpoint, s.certificateProvider, s.inbound, s.service) if err != nil { return err } @@ -482,7 +522,7 @@ func (s *Box) start() error { if err != nil { return err } - err = adapter.Start(s.logger, adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service) + err = adapter.Start(s.logger, adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.endpoint, s.certificateProvider, s.inbound, s.service) if err != nil { return err } @@ -506,8 +546,9 @@ func (s *Box) Close() error { service adapter.Lifecycle }{ {"service", s.service}, - {"endpoint", s.endpoint}, {"inbound", s.inbound}, + {"certificate-provider", s.certificateProvider}, + {"endpoint", s.endpoint}, {"outbound", s.outbound}, {"router", s.router}, {"connection", s.connection}, diff --git a/common/tls/acme.go b/common/tls/acme.go index c96e002c8a..d576fc6b1e 100644 --- a/common/tls/acme.go +++ b/common/tls/acme.go @@ -38,37 +38,6 @@ func (w *acmeWrapper) Close() error { return nil } -type acmeLogWriter struct { - logger logger.Logger -} - -func (w *acmeLogWriter) Write(p []byte) (n int, err error) { - logLine := strings.ReplaceAll(string(p), " ", ": ") - switch { - case strings.HasPrefix(logLine, "error: "): - w.logger.Error(logLine[7:]) - case strings.HasPrefix(logLine, "warn: "): - w.logger.Warn(logLine[6:]) - case strings.HasPrefix(logLine, "info: "): - w.logger.Info(logLine[6:]) - case strings.HasPrefix(logLine, "debug: "): - w.logger.Debug(logLine[7:]) - default: - w.logger.Debug(logLine) - } - return len(p), nil -} - -func (w *acmeLogWriter) Sync() error { - return nil -} - -func encoderConfig() zapcore.EncoderConfig { - config := zap.NewProductionEncoderConfig() - config.TimeKey = zapcore.OmitKey - return config -} - func startACME(ctx context.Context, logger logger.Logger, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) { var acmeServer string switch options.Provider { @@ -91,8 +60,8 @@ func startACME(ctx context.Context, logger logger.Logger, options option.Inbound storage = certmagic.Default.Storage } zapLogger := zap.New(zapcore.NewCore( - zapcore.NewConsoleEncoder(encoderConfig()), - &acmeLogWriter{logger: logger}, + zapcore.NewConsoleEncoder(ACMEEncoderConfig()), + &ACMELogWriter{Logger: logger}, zap.DebugLevel, )) config := &certmagic.Config{ @@ -158,7 +127,7 @@ func startACME(ctx context.Context, logger logger.Logger, options option.Inbound } else { tlsConfig = &tls.Config{ GetCertificate: config.GetCertificate, - NextProtos: []string{ACMETLS1Protocol}, + NextProtos: []string{C.ACMETLS1Protocol}, } } return tlsConfig, &acmeWrapper{ctx: ctx, cfg: config, cache: cache, domain: options.Domain}, nil diff --git a/common/tls/acme_logger.go b/common/tls/acme_logger.go new file mode 100644 index 0000000000..cb3a1e3ce3 --- /dev/null +++ b/common/tls/acme_logger.go @@ -0,0 +1,41 @@ +package tls + +import ( + "strings" + + "github.com/sagernet/sing/common/logger" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type ACMELogWriter struct { + Logger logger.Logger +} + +func (w *ACMELogWriter) Write(p []byte) (n int, err error) { + logLine := strings.ReplaceAll(string(p), " ", ": ") + switch { + case strings.HasPrefix(logLine, "error: "): + w.Logger.Error(logLine[7:]) + case strings.HasPrefix(logLine, "warn: "): + w.Logger.Warn(logLine[6:]) + case strings.HasPrefix(logLine, "info: "): + w.Logger.Info(logLine[6:]) + case strings.HasPrefix(logLine, "debug: "): + w.Logger.Debug(logLine[7:]) + default: + w.Logger.Debug(logLine) + } + return len(p), nil +} + +func (w *ACMELogWriter) Sync() error { + return nil +} + +func ACMEEncoderConfig() zapcore.EncoderConfig { + config := zap.NewProductionEncoderConfig() + config.TimeKey = zapcore.OmitKey + return config +} diff --git a/common/tls/reality_server.go b/common/tls/reality_server.go index 5fc684756b..c2e70733a3 100644 --- a/common/tls/reality_server.go +++ b/common/tls/reality_server.go @@ -32,6 +32,10 @@ type RealityServerConfig struct { func NewRealityServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) { var tlsConfig utls.RealityConfig + if options.CertificateProvider != nil { + return nil, E.New("certificate_provider is unavailable in reality") + } + //nolint:staticcheck if options.ACME != nil && len(options.ACME.Domain) > 0 { return nil, E.New("acme is unavailable in reality") } diff --git a/common/tls/std_server.go b/common/tls/std_server.go index 760c4b3a7f..86584cd482 100644 --- a/common/tls/std_server.go +++ b/common/tls/std_server.go @@ -13,19 +13,87 @@ import ( "github.com/sagernet/fswatch" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/experimental/deprecated" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/ntp" + "github.com/sagernet/sing/service" ) var errInsecureUnused = E.New("tls: insecure unused") +type managedCertificateProvider interface { + adapter.CertificateProvider + adapter.SimpleLifecycle +} + +type sharedCertificateProvider struct { + tag string + manager adapter.CertificateProviderManager + provider adapter.CertificateProviderService +} + +func (p *sharedCertificateProvider) Start() error { + provider, found := p.manager.Get(p.tag) + if !found { + return E.New("certificate provider not found: ", p.tag) + } + p.provider = provider + return nil +} + +func (p *sharedCertificateProvider) Close() error { + return nil +} + +func (p *sharedCertificateProvider) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { + return p.provider.GetCertificate(hello) +} + +func (p *sharedCertificateProvider) GetACMENextProtos() []string { + return getACMENextProtos(p.provider) +} + +type inlineCertificateProvider struct { + provider adapter.CertificateProviderService +} + +func (p *inlineCertificateProvider) Start() error { + for _, stage := range adapter.ListStartStages { + err := adapter.LegacyStart(p.provider, stage) + if err != nil { + return err + } + } + return nil +} + +func (p *inlineCertificateProvider) Close() error { + return p.provider.Close() +} + +func (p *inlineCertificateProvider) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { + return p.provider.GetCertificate(hello) +} + +func (p *inlineCertificateProvider) GetACMENextProtos() []string { + return getACMENextProtos(p.provider) +} + +func getACMENextProtos(provider adapter.CertificateProvider) []string { + if acmeProvider, isACME := provider.(adapter.ACMECertificateProvider); isACME { + return acmeProvider.GetACMENextProtos() + } + return nil +} + type STDServerConfig struct { access sync.RWMutex config *tls.Config logger log.Logger + certificateProvider managedCertificateProvider acmeService adapter.SimpleLifecycle certificate []byte key []byte @@ -53,18 +121,17 @@ func (c *STDServerConfig) SetServerName(serverName string) { func (c *STDServerConfig) NextProtos() []string { c.access.RLock() defer c.access.RUnlock() - if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol { + if c.hasACMEALPN() && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == C.ACMETLS1Protocol { return c.config.NextProtos[1:] - } else { - return c.config.NextProtos } + return c.config.NextProtos } func (c *STDServerConfig) SetNextProtos(nextProto []string) { c.access.Lock() defer c.access.Unlock() config := c.config.Clone() - if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol { + if c.hasACMEALPN() && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == C.ACMETLS1Protocol { config.NextProtos = append(c.config.NextProtos[:1], nextProto...) } else { config.NextProtos = nextProto @@ -72,6 +139,18 @@ func (c *STDServerConfig) SetNextProtos(nextProto []string) { c.config = config } +func (c *STDServerConfig) hasACMEALPN() bool { + if c.acmeService != nil { + return true + } + if c.certificateProvider != nil { + if acmeProvider, isACME := c.certificateProvider.(adapter.ACMECertificateProvider); isACME { + return len(acmeProvider.GetACMENextProtos()) > 0 + } + } + return false +} + func (c *STDServerConfig) STDConfig() (*STDConfig, error) { return c.config, nil } @@ -91,15 +170,39 @@ func (c *STDServerConfig) Clone() Config { } func (c *STDServerConfig) Start() error { + if c.certificateProvider != nil { + err := c.certificateProvider.Start() + if err != nil { + return err + } + if acmeProvider, isACME := c.certificateProvider.(adapter.ACMECertificateProvider); isACME { + nextProtos := acmeProvider.GetACMENextProtos() + if len(nextProtos) > 0 { + c.access.Lock() + config := c.config.Clone() + mergedNextProtos := append([]string{}, nextProtos...) + for _, nextProto := range config.NextProtos { + if !common.Contains(mergedNextProtos, nextProto) { + mergedNextProtos = append(mergedNextProtos, nextProto) + } + } + config.NextProtos = mergedNextProtos + c.config = config + c.access.Unlock() + } + } + } if c.acmeService != nil { - return c.acmeService.Start() - } else { - err := c.startWatcher() + err := c.acmeService.Start() if err != nil { - c.logger.Warn("create fsnotify watcher: ", err) + return err } - return nil } + err := c.startWatcher() + if err != nil { + c.logger.Warn("create fsnotify watcher: ", err) + } + return nil } func (c *STDServerConfig) startWatcher() error { @@ -203,23 +306,34 @@ func (c *STDServerConfig) certificateUpdated(path string) error { } func (c *STDServerConfig) Close() error { - if c.acmeService != nil { - return c.acmeService.Close() - } - if c.watcher != nil { - return c.watcher.Close() - } - return nil + return common.Close(c.certificateProvider, c.acmeService, c.watcher) } func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) { if !options.Enabled { return nil, nil } + //nolint:staticcheck + if options.CertificateProvider != nil && options.ACME != nil { + return nil, E.New("certificate_provider and acme are mutually exclusive") + } var tlsConfig *tls.Config + var certificateProvider managedCertificateProvider var acmeService adapter.SimpleLifecycle var err error - if options.ACME != nil && len(options.ACME.Domain) > 0 { + if options.CertificateProvider != nil { + certificateProvider, err = newCertificateProvider(ctx, logger, options.CertificateProvider) + if err != nil { + return nil, err + } + tlsConfig = &tls.Config{ + GetCertificate: certificateProvider.GetCertificate, + } + if options.Insecure { + return nil, errInsecureUnused + } + } else if options.ACME != nil && len(options.ACME.Domain) > 0 { //nolint:staticcheck + deprecated.Report(ctx, deprecated.OptionInlineACME) //nolint:staticcheck tlsConfig, acmeService, err = startACME(ctx, logger, common.PtrValueOrDefault(options.ACME)) if err != nil { @@ -272,7 +386,7 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option. certificate []byte key []byte ) - if acmeService == nil { + if certificateProvider == nil && acmeService == nil { if len(options.Certificate) > 0 { certificate = []byte(strings.Join(options.Certificate, "\n")) } else if options.CertificatePath != "" { @@ -360,6 +474,7 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option. serverConfig := &STDServerConfig{ config: tlsConfig, logger: logger, + certificateProvider: certificateProvider, acmeService: acmeService, certificate: certificate, key: key, @@ -369,8 +484,8 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option. echKeyPath: echKeyPath, } serverConfig.config.GetConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) { - serverConfig.access.Lock() - defer serverConfig.access.Unlock() + serverConfig.access.RLock() + defer serverConfig.access.RUnlock() return serverConfig.config, nil } var config ServerConfig = serverConfig @@ -387,3 +502,27 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option. } return config, nil } + +func newCertificateProvider(ctx context.Context, logger log.ContextLogger, options *option.CertificateProviderOptions) (managedCertificateProvider, error) { + if options.IsShared() { + manager := service.FromContext[adapter.CertificateProviderManager](ctx) + if manager == nil { + return nil, E.New("missing certificate provider manager in context") + } + return &sharedCertificateProvider{ + tag: options.Tag, + manager: manager, + }, nil + } + registry := service.FromContext[adapter.CertificateProviderRegistry](ctx) + if registry == nil { + return nil, E.New("missing certificate provider registry in context") + } + provider, err := registry.Create(ctx, logger, "", options.Type, options.Options) + if err != nil { + return nil, E.Cause(err, "create inline certificate provider") + } + return &inlineCertificateProvider{ + provider: provider, + }, nil +} diff --git a/constant/proxy.go b/constant/proxy.go index 278a46c2f6..add66c95e5 100644 --- a/constant/proxy.go +++ b/constant/proxy.go @@ -1,36 +1,38 @@ package constant const ( - TypeTun = "tun" - TypeRedirect = "redirect" - TypeTProxy = "tproxy" - TypeDirect = "direct" - TypeBlock = "block" - TypeDNS = "dns" - TypeSOCKS = "socks" - TypeHTTP = "http" - TypeMixed = "mixed" - TypeShadowsocks = "shadowsocks" - TypeVMess = "vmess" - TypeTrojan = "trojan" - TypeNaive = "naive" - TypeWireGuard = "wireguard" - TypeHysteria = "hysteria" - TypeTor = "tor" - TypeSSH = "ssh" - TypeShadowTLS = "shadowtls" - TypeAnyTLS = "anytls" - TypeShadowsocksR = "shadowsocksr" - TypeVLESS = "vless" - TypeTUIC = "tuic" - TypeHysteria2 = "hysteria2" - TypeTailscale = "tailscale" - TypeDERP = "derp" - TypeResolved = "resolved" - TypeSSMAPI = "ssm-api" - TypeCCM = "ccm" - TypeOCM = "ocm" - TypeOOMKiller = "oom-killer" + TypeTun = "tun" + TypeRedirect = "redirect" + TypeTProxy = "tproxy" + TypeDirect = "direct" + TypeBlock = "block" + TypeDNS = "dns" + TypeSOCKS = "socks" + TypeHTTP = "http" + TypeMixed = "mixed" + TypeShadowsocks = "shadowsocks" + TypeVMess = "vmess" + TypeTrojan = "trojan" + TypeNaive = "naive" + TypeWireGuard = "wireguard" + TypeHysteria = "hysteria" + TypeTor = "tor" + TypeSSH = "ssh" + TypeShadowTLS = "shadowtls" + TypeAnyTLS = "anytls" + TypeShadowsocksR = "shadowsocksr" + TypeVLESS = "vless" + TypeTUIC = "tuic" + TypeHysteria2 = "hysteria2" + TypeTailscale = "tailscale" + TypeDERP = "derp" + TypeResolved = "resolved" + TypeSSMAPI = "ssm-api" + TypeCCM = "ccm" + TypeOCM = "ocm" + TypeOOMKiller = "oom-killer" + TypeACME = "acme" + TypeCloudflareOriginCA = "cloudflare-origin-ca" ) const ( diff --git a/common/tls/acme_contstant.go b/constant/tls.go similarity index 69% rename from common/tls/acme_contstant.go rename to constant/tls.go index c5cd2ff164..2d4f64bc3a 100644 --- a/common/tls/acme_contstant.go +++ b/constant/tls.go @@ -1,3 +1,3 @@ -package tls +package constant const ACMETLS1Protocol = "acme-tls/1" diff --git a/docs/configuration/inbound/tun.md b/docs/configuration/inbound/tun.md index 5a2f58d3db..6dae06e18a 100644 --- a/docs/configuration/inbound/tun.md +++ b/docs/configuration/inbound/tun.md @@ -4,7 +4,7 @@ icon: material/new-box !!! quote "Changes in sing-box 1.14.0" - :material-plus: [include_mac_address](#include_mac_address) + :material-plus: [include_mac_address](#include_mac_address) :material-plus: [exclude_mac_address](#exclude_mac_address) !!! quote "Changes in sing-box 1.13.3" diff --git a/docs/configuration/index.md b/docs/configuration/index.md index 1f6eec1375..81cb8f3863 100644 --- a/docs/configuration/index.md +++ b/docs/configuration/index.md @@ -1,7 +1,6 @@ # Introduction sing-box uses JSON for configuration files. - ### Structure ```json @@ -10,6 +9,7 @@ sing-box uses JSON for configuration files. "dns": {}, "ntp": {}, "certificate": {}, + "certificate_providers": [], "endpoints": [], "inbounds": [], "outbounds": [], @@ -27,6 +27,7 @@ sing-box uses JSON for configuration files. | `dns` | [DNS](./dns/) | | `ntp` | [NTP](./ntp/) | | `certificate` | [Certificate](./certificate/) | +| `certificate_providers` | [Certificate Provider](./shared/certificate-provider/) | | `endpoints` | [Endpoint](./endpoint/) | | `inbounds` | [Inbound](./inbound/) | | `outbounds` | [Outbound](./outbound/) | @@ -50,4 +51,4 @@ sing-box format -w -c config.json -D config_directory ```bash sing-box merge output.json -c config.json -D config_directory -``` \ No newline at end of file +``` diff --git a/docs/configuration/index.zh.md b/docs/configuration/index.zh.md index 3bdc352187..350db5d4c4 100644 --- a/docs/configuration/index.zh.md +++ b/docs/configuration/index.zh.md @@ -1,7 +1,6 @@ # 引言 sing-box 使用 JSON 作为配置文件格式。 - ### 结构 ```json @@ -10,6 +9,7 @@ sing-box 使用 JSON 作为配置文件格式。 "dns": {}, "ntp": {}, "certificate": {}, + "certificate_providers": [], "endpoints": [], "inbounds": [], "outbounds": [], @@ -27,6 +27,7 @@ sing-box 使用 JSON 作为配置文件格式。 | `dns` | [DNS](./dns/) | | `ntp` | [NTP](./ntp/) | | `certificate` | [证书](./certificate/) | +| `certificate_providers` | [证书提供者](./shared/certificate-provider/) | | `endpoints` | [端点](./endpoint/) | | `inbounds` | [入站](./inbound/) | | `outbounds` | [出站](./outbound/) | @@ -50,4 +51,4 @@ sing-box format -w -c config.json -D config_directory ```bash sing-box merge output.json -c config.json -D config_directory -``` \ No newline at end of file +``` diff --git a/docs/configuration/shared/certificate-provider/acme.md b/docs/configuration/shared/certificate-provider/acme.md new file mode 100644 index 0000000000..440ed1568d --- /dev/null +++ b/docs/configuration/shared/certificate-provider/acme.md @@ -0,0 +1,150 @@ +--- +icon: material/new-box +--- + +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [account_key](#account_key) + :material-plus: [key_type](#key_type) + :material-plus: [detour](#detour) + +# ACME + +!!! quote "" + + `with_acme` build tag required. + +### Structure + +```json +{ + "type": "acme", + "tag": "", + + "domain": [], + "data_directory": "", + "default_server_name": "", + "email": "", + "provider": "", + "account_key": "", + "disable_http_challenge": false, + "disable_tls_alpn_challenge": false, + "alternative_http_port": 0, + "alternative_tls_port": 0, + "external_account": { + "key_id": "", + "mac_key": "" + }, + "dns01_challenge": {}, + "key_type": "", + "detour": "" +} +``` + +### Fields + +#### domain + +==Required== + +List of domains. + +#### data_directory + +The directory to store ACME data. + +`$XDG_DATA_HOME/certmagic|$HOME/.local/share/certmagic` will be used if empty. + +#### default_server_name + +Server name to use when choosing a certificate if the ClientHello's ServerName field is empty. + +#### email + +The email address to use when creating or selecting an existing ACME server account. + +#### provider + +The ACME CA provider to use. + +| Value | Provider | +|-------------------------|---------------| +| `letsencrypt (default)` | Let's Encrypt | +| `zerossl` | ZeroSSL | +| `https://...` | Custom | + +When `provider` is `zerossl`, sing-box will automatically request ZeroSSL EAB credentials if `email` is set and +`external_account` is empty. + +When `provider` is `zerossl`, at least one of `external_account`, `email`, or `account_key` is required. + +#### account_key + +!!! question "Since sing-box 1.14.0" + +The PEM-encoded private key of an existing ACME account. + +#### disable_http_challenge + +Disable all HTTP challenges. + +#### disable_tls_alpn_challenge + +Disable all TLS-ALPN challenges + +#### alternative_http_port + +The alternate port to use for the ACME HTTP challenge; if non-empty, this port will be used instead of 80 to spin up a +listener for the HTTP challenge. + +#### alternative_tls_port + +The alternate port to use for the ACME TLS-ALPN challenge; the system must forward 443 to this port for challenge to +succeed. + +#### external_account + +EAB (External Account Binding) contains information necessary to bind or map an ACME account to some other account known +by the CA. + +External account bindings are used to associate an ACME account with an existing account in a non-ACME system, such as +a CA customer database. + +To enable ACME account binding, the CA operating the ACME server needs to provide the ACME client with a MAC key and a +key identifier, using some mechanism outside of ACME. §7.3.4 + +#### external_account.key_id + +The key identifier. + +#### external_account.mac_key + +The MAC key. + +#### dns01_challenge + +ACME DNS01 challenge field. If configured, other challenge methods will be disabled. + +See [DNS01 Challenge Fields](/configuration/shared/dns01_challenge/) for details. + +#### key_type + +!!! question "Since sing-box 1.14.0" + +The private key type to generate for new certificates. + +| Value | Type | +|------------|---------| +| `ed25519` | Ed25519 | +| `p256` | P-256 | +| `p384` | P-384 | +| `rsa2048` | RSA | +| `rsa4096` | RSA | + +#### detour + +!!! question "Since sing-box 1.14.0" + +The tag of the upstream outbound. + +All provider HTTP requests will use this outbound. diff --git a/docs/configuration/shared/certificate-provider/acme.zh.md b/docs/configuration/shared/certificate-provider/acme.zh.md new file mode 100644 index 0000000000..d95930a550 --- /dev/null +++ b/docs/configuration/shared/certificate-provider/acme.zh.md @@ -0,0 +1,145 @@ +--- +icon: material/new-box +--- + +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [account_key](#account_key) + :material-plus: [key_type](#key_type) + :material-plus: [detour](#detour) + +# ACME + +!!! quote "" + + 需要 `with_acme` 构建标签。 + +### 结构 + +```json +{ + "type": "acme", + "tag": "", + + "domain": [], + "data_directory": "", + "default_server_name": "", + "email": "", + "provider": "", + "account_key": "", + "disable_http_challenge": false, + "disable_tls_alpn_challenge": false, + "alternative_http_port": 0, + "alternative_tls_port": 0, + "external_account": { + "key_id": "", + "mac_key": "" + }, + "dns01_challenge": {}, + "key_type": "", + "detour": "" +} +``` + +### 字段 + +#### domain + +==必填== + +域名列表。 + +#### data_directory + +ACME 数据存储目录。 + +如果为空则使用 `$XDG_DATA_HOME/certmagic|$HOME/.local/share/certmagic`。 + +#### default_server_name + +如果 ClientHello 的 ServerName 字段为空,则选择证书时要使用的服务器名称。 + +#### email + +创建或选择现有 ACME 服务器帐户时使用的电子邮件地址。 + +#### provider + +要使用的 ACME CA 提供商。 + +| 值 | 提供商 | +|--------------------|---------------| +| `letsencrypt (默认)` | Let's Encrypt | +| `zerossl` | ZeroSSL | +| `https://...` | 自定义 | + +当 `provider` 为 `zerossl` 时,如果设置了 `email` 且未设置 `external_account`, +sing-box 会自动向 ZeroSSL 请求 EAB 凭据。 + +当 `provider` 为 `zerossl` 时,必须至少设置 `external_account`、`email` 或 `account_key` 之一。 + +#### account_key + +!!! question "自 sing-box 1.14.0 起" + +现有 ACME 帐户的 PEM 编码私钥。 + +#### disable_http_challenge + +禁用所有 HTTP 质询。 + +#### disable_tls_alpn_challenge + +禁用所有 TLS-ALPN 质询。 + +#### alternative_http_port + +用于 ACME HTTP 质询的备用端口;如果非空,将使用此端口而不是 80 来启动 HTTP 质询的侦听器。 + +#### alternative_tls_port + +用于 ACME TLS-ALPN 质询的备用端口; 系统必须将 443 转发到此端口以使质询成功。 + +#### external_account + +EAB(外部帐户绑定)包含将 ACME 帐户绑定或映射到 CA 已知的其他帐户所需的信息。 + +外部帐户绑定用于将 ACME 帐户与非 ACME 系统中的现有帐户相关联,例如 CA 客户数据库。 + +为了启用 ACME 帐户绑定,运行 ACME 服务器的 CA 需要使用 ACME 之外的某种机制向 ACME 客户端提供 MAC 密钥和密钥标识符。§7.3.4 + +#### external_account.key_id + +密钥标识符。 + +#### external_account.mac_key + +MAC 密钥。 + +#### dns01_challenge + +ACME DNS01 质询字段。如果配置,将禁用其他质询方法。 + +参阅 [DNS01 质询字段](/zh/configuration/shared/dns01_challenge/)。 + +#### key_type + +!!! question "自 sing-box 1.14.0 起" + +为新证书生成的私钥类型。 + +| 值 | 类型 | +|-----------|----------| +| `ed25519` | Ed25519 | +| `p256` | P-256 | +| `p384` | P-384 | +| `rsa2048` | RSA | +| `rsa4096` | RSA | + +#### detour + +!!! question "自 sing-box 1.14.0 起" + +上游出站的标签。 + +所有提供者 HTTP 请求将使用此出站。 diff --git a/docs/configuration/shared/certificate-provider/cloudflare-origin-ca.md b/docs/configuration/shared/certificate-provider/cloudflare-origin-ca.md new file mode 100644 index 0000000000..cfd2da4fe1 --- /dev/null +++ b/docs/configuration/shared/certificate-provider/cloudflare-origin-ca.md @@ -0,0 +1,82 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.14.0" + +# Cloudflare Origin CA + +### Structure + +```json +{ + "type": "cloudflare-origin-ca", + "tag": "", + + "domain": [], + "data_directory": "", + "api_token": "", + "origin_ca_key": "", + "request_type": "", + "requested_validity": 0, + "detour": "" +} +``` + +### Fields + +#### domain + +==Required== + +List of domain names or wildcard domain names to include in the certificate. + +#### data_directory + +Root directory used to store the issued certificate, private key, and metadata. + +If empty, sing-box uses the same default data directory as the ACME certificate provider: +`$XDG_DATA_HOME/certmagic` or `$HOME/.local/share/certmagic`. + +#### api_token + +Cloudflare API token used to create the certificate. + +Get or create one in [Cloudflare Dashboard > My Profile > API Tokens](https://dash.cloudflare.com/profile/api-tokens). + +Requires the `Zone / SSL and Certificates / Edit` permission. + +Conflict with `origin_ca_key`. + +#### origin_ca_key + +Cloudflare Origin CA Key. + +Get it in [Cloudflare Dashboard > My Profile > API Tokens > API Keys > Origin CA Key](https://dash.cloudflare.com/profile/api-tokens). + +Conflict with `api_token`. + +#### request_type + +The signature type to request from Cloudflare. + +| Value | Type | +|----------------------|-------------| +| `origin-rsa` | RSA | +| `origin-ecc` | ECDSA P-256 | + +`origin-rsa` is used if empty. + +#### requested_validity + +The requested certificate validity in days. + +Available values: `7`, `30`, `90`, `365`, `730`, `1095`, `5475`. + +`5475` days (15 years) is used if empty. + +#### detour + +The tag of the upstream outbound. + +All provider HTTP requests will use this outbound. diff --git a/docs/configuration/shared/certificate-provider/cloudflare-origin-ca.zh.md b/docs/configuration/shared/certificate-provider/cloudflare-origin-ca.zh.md new file mode 100644 index 0000000000..85036268df --- /dev/null +++ b/docs/configuration/shared/certificate-provider/cloudflare-origin-ca.zh.md @@ -0,0 +1,82 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.14.0 起" + +# Cloudflare Origin CA + +### 结构 + +```json +{ + "type": "cloudflare-origin-ca", + "tag": "", + + "domain": [], + "data_directory": "", + "api_token": "", + "origin_ca_key": "", + "request_type": "", + "requested_validity": 0, + "detour": "" +} +``` + +### 字段 + +#### domain + +==必填== + +要写入证书的域名或通配符域名列表。 + +#### data_directory + +保存签发证书、私钥和元数据的根目录。 + +如果为空,sing-box 会使用与 ACME 证书提供者相同的默认数据目录: +`$XDG_DATA_HOME/certmagic` 或 `$HOME/.local/share/certmagic`。 + +#### api_token + +用于创建证书的 Cloudflare API Token。 + +可在 [Cloudflare Dashboard > My Profile > API Tokens](https://dash.cloudflare.com/profile/api-tokens) 获取或创建。 + +需要 `Zone / SSL and Certificates / Edit` 权限。 + +与 `origin_ca_key` 冲突。 + +#### origin_ca_key + +Cloudflare Origin CA Key。 + +可在 [Cloudflare Dashboard > My Profile > API Tokens > API Keys > Origin CA Key](https://dash.cloudflare.com/profile/api-tokens) 获取。 + +与 `api_token` 冲突。 + +#### request_type + +向 Cloudflare 请求的签名类型。 + +| 值 | 类型 | +|----------------------|-------------| +| `origin-rsa` | RSA | +| `origin-ecc` | ECDSA P-256 | + +如果为空,使用 `origin-rsa`。 + +#### requested_validity + +请求的证书有效期,单位为天。 + +可用值:`7`、`30`、`90`、`365`、`730`、`1095`、`5475`。 + +如果为空,使用 `5475` 天(15 年)。 + +#### detour + +上游出站的标签。 + +所有提供者 HTTP 请求将使用此出站。 diff --git a/docs/configuration/shared/certificate-provider/index.md b/docs/configuration/shared/certificate-provider/index.md new file mode 100644 index 0000000000..c493550aaa --- /dev/null +++ b/docs/configuration/shared/certificate-provider/index.md @@ -0,0 +1,32 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.14.0" + +# Certificate Provider + +### Structure + +```json +{ + "certificate_providers": [ + { + "type": "", + "tag": "" + } + ] +} +``` + +### Fields + +| Type | Format | +|--------|------------------| +| `acme` | [ACME](/configuration/shared/certificate-provider/acme) | +| `tailscale` | [Tailscale](/configuration/shared/certificate-provider/tailscale) | +| `cloudflare-origin-ca` | [Cloudflare Origin CA](/configuration/shared/certificate-provider/cloudflare-origin-ca) | + +#### tag + +The tag of the certificate provider. diff --git a/docs/configuration/shared/certificate-provider/index.zh.md b/docs/configuration/shared/certificate-provider/index.zh.md new file mode 100644 index 0000000000..2df4b36387 --- /dev/null +++ b/docs/configuration/shared/certificate-provider/index.zh.md @@ -0,0 +1,32 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.14.0 起" + +# 证书提供者 + +### 结构 + +```json +{ + "certificate_providers": [ + { + "type": "", + "tag": "" + } + ] +} +``` + +### 字段 + +| 类型 | 格式 | +|--------|------------------| +| `acme` | [ACME](/zh/configuration/shared/certificate-provider/acme) | +| `tailscale` | [Tailscale](/zh/configuration/shared/certificate-provider/tailscale) | +| `cloudflare-origin-ca` | [Cloudflare Origin CA](/zh/configuration/shared/certificate-provider/cloudflare-origin-ca) | + +#### tag + +证书提供者的标签。 diff --git a/docs/configuration/shared/certificate-provider/tailscale.md b/docs/configuration/shared/certificate-provider/tailscale.md new file mode 100644 index 0000000000..045f2c5ec5 --- /dev/null +++ b/docs/configuration/shared/certificate-provider/tailscale.md @@ -0,0 +1,27 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.14.0" + +# Tailscale + +### Structure + +```json +{ + "type": "tailscale", + "tag": "ts-cert", + "endpoint": "ts-ep" +} +``` + +### Fields + +#### endpoint + +==Required== + +The tag of the [Tailscale endpoint](/configuration/endpoint/tailscale/) to reuse. + +[MagicDNS and HTTPS](https://tailscale.com/kb/1153/enabling-https) must be enabled in the Tailscale admin console. diff --git a/docs/configuration/shared/certificate-provider/tailscale.zh.md b/docs/configuration/shared/certificate-provider/tailscale.zh.md new file mode 100644 index 0000000000..1987da5084 --- /dev/null +++ b/docs/configuration/shared/certificate-provider/tailscale.zh.md @@ -0,0 +1,27 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.14.0 起" + +# Tailscale + +### 结构 + +```json +{ + "type": "tailscale", + "tag": "ts-cert", + "endpoint": "ts-ep" +} +``` + +### 字段 + +#### endpoint + +==必填== + +要复用的 [Tailscale 端点](/zh/configuration/endpoint/tailscale/) 的标签。 + +必须在 Tailscale 管理控制台中启用 [MagicDNS 和 HTTPS](https://tailscale.com/kb/1153/enabling-https)。 diff --git a/docs/configuration/shared/dns01_challenge.md b/docs/configuration/shared/dns01_challenge.md index 8bdbfc97a7..0157cb4596 100644 --- a/docs/configuration/shared/dns01_challenge.md +++ b/docs/configuration/shared/dns01_challenge.md @@ -2,6 +2,14 @@ icon: material/new-box --- +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [ttl](#ttl) + :material-plus: [propagation_delay](#propagation_delay) + :material-plus: [propagation_timeout](#propagation_timeout) + :material-plus: [resolvers](#resolvers) + :material-plus: [override_domain](#override_domain) + !!! quote "Changes in sing-box 1.13.0" :material-plus: [alidns.security_token](#security_token) @@ -12,12 +20,57 @@ icon: material/new-box ```json { + "ttl": "", + "propagation_delay": "", + "propagation_timeout": "", + "resolvers": [], + "override_domain": "", "provider": "", ... // Provider Fields } ``` +### Fields + +#### ttl + +!!! question "Since sing-box 1.14.0" + +The TTL of the temporary TXT record used for the DNS challenge. + +#### propagation_delay + +!!! question "Since sing-box 1.14.0" + +How long to wait after creating the challenge record before starting propagation checks. + +#### propagation_timeout + +!!! question "Since sing-box 1.14.0" + +The maximum time to wait for the challenge record to propagate. + +Set to `-1` to disable propagation checks. + +#### resolvers + +!!! question "Since sing-box 1.14.0" + +Preferred DNS resolvers to use for DNS propagation checks. + +#### override_domain + +!!! question "Since sing-box 1.14.0" + +Override the domain name used for the DNS challenge record. + +Useful when `_acme-challenge` is delegated to a different zone. + +#### provider + +The DNS provider. See below for provider-specific fields. + ### Provider Fields #### Alibaba Cloud DNS diff --git a/docs/configuration/shared/dns01_challenge.zh.md b/docs/configuration/shared/dns01_challenge.zh.md index e6919338cd..8c582bb544 100644 --- a/docs/configuration/shared/dns01_challenge.zh.md +++ b/docs/configuration/shared/dns01_challenge.zh.md @@ -2,6 +2,14 @@ icon: material/new-box --- +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [ttl](#ttl) + :material-plus: [propagation_delay](#propagation_delay) + :material-plus: [propagation_timeout](#propagation_timeout) + :material-plus: [resolvers](#resolvers) + :material-plus: [override_domain](#override_domain) + !!! quote "sing-box 1.13.0 中的更改" :material-plus: [alidns.security_token](#security_token) @@ -12,12 +20,57 @@ icon: material/new-box ```json { + "ttl": "", + "propagation_delay": "", + "propagation_timeout": "", + "resolvers": [], + "override_domain": "", "provider": "", ... // 提供商字段 } ``` +### 字段 + +#### ttl + +!!! question "自 sing-box 1.14.0 起" + +DNS 质询临时 TXT 记录的 TTL。 + +#### propagation_delay + +!!! question "自 sing-box 1.14.0 起" + +创建质询记录后,在开始传播检查前要等待的时间。 + +#### propagation_timeout + +!!! question "自 sing-box 1.14.0 起" + +等待质询记录传播完成的最长时间。 + +设为 `-1` 可禁用传播检查。 + +#### resolvers + +!!! question "自 sing-box 1.14.0 起" + +进行 DNS 传播检查时优先使用的 DNS 解析器。 + +#### override_domain + +!!! question "自 sing-box 1.14.0 起" + +覆盖 DNS 质询记录使用的域名。 + +适用于将 `_acme-challenge` 委托到其他 zone 的场景。 + +#### provider + +DNS 提供商。提供商专有字段见下文。 + ### 提供商字段 #### Alibaba Cloud DNS diff --git a/docs/configuration/shared/tls.md b/docs/configuration/shared/tls.md index 73ceffccef..518b2f9176 100644 --- a/docs/configuration/shared/tls.md +++ b/docs/configuration/shared/tls.md @@ -2,6 +2,11 @@ icon: material/new-box --- +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [certificate_provider](#certificate_provider) + :material-delete-clock: [acme](#acme-fields) + !!! quote "Changes in sing-box 1.13.0" :material-plus: [kernel_tx](#kernel_tx) @@ -49,6 +54,10 @@ icon: material/new-box "key_path": "", "kernel_tx": false, "kernel_rx": false, + "certificate_provider": "", + + // Deprecated + "acme": { "domain": [], "data_directory": "", @@ -408,6 +417,18 @@ Enable kernel TLS transmit support. Enable kernel TLS receive support. +#### certificate_provider + +!!! question "Since sing-box 1.14.0" + +==Server only== + +A string or an object. + +When string, the tag of a shared [Certificate Provider](/configuration/shared/certificate-provider/). + +When object, an inline certificate provider. See [Certificate Provider](/configuration/shared/certificate-provider/) for available types and fields. + ## Custom TLS support !!! info "QUIC support" @@ -469,7 +490,7 @@ The ECH key and configuration can be generated by `sing-box generate ech-keypair !!! failure "Deprecated in sing-box 1.12.0" - ECH support has been migrated to use stdlib in sing-box 1.12.0, which does not come with support for PQ signature schemes, so `pq_signature_schemes_enabled` has been deprecated and no longer works. + `pq_signature_schemes_enabled` is deprecated in sing-box 1.12.0 and removed in sing-box 1.13.0. Enable support for post-quantum peer certificate signature schemes. @@ -477,7 +498,7 @@ Enable support for post-quantum peer certificate signature schemes. !!! failure "Deprecated in sing-box 1.12.0" - `dynamic_record_sizing_disabled` has nothing to do with ECH, was added by mistake, has been deprecated and no longer works. + `dynamic_record_sizing_disabled` is deprecated in sing-box 1.12.0 and removed in sing-box 1.13.0. Disables adaptive sizing of TLS records. @@ -566,6 +587,10 @@ Fragment TLS handshake into multiple TLS records to bypass firewalls. ### ACME Fields +!!! failure "Deprecated in sing-box 1.14.0" + + Inline ACME options are deprecated in sing-box 1.14.0 and will be removed in sing-box 1.16.0, check [Migration](/migration/#migrate-inline-acme-to-certificate-provider). + #### domain List of domain. @@ -677,4 +702,4 @@ A hexadecimal string with zero to eight digits. The maximum time difference between the server and the client. -Check disabled if empty. \ No newline at end of file +Check disabled if empty. diff --git a/docs/configuration/shared/tls.zh.md b/docs/configuration/shared/tls.zh.md index 0b47189bc6..56b90d33f1 100644 --- a/docs/configuration/shared/tls.zh.md +++ b/docs/configuration/shared/tls.zh.md @@ -2,6 +2,11 @@ icon: material/new-box --- +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [certificate_provider](#certificate_provider) + :material-delete-clock: [acme](#acme-字段) + !!! quote "sing-box 1.13.0 中的更改" :material-plus: [kernel_tx](#kernel_tx) @@ -49,6 +54,10 @@ icon: material/new-box "key_path": "", "kernel_tx": false, "kernel_rx": false, + "certificate_provider": "", + + // 废弃的 + "acme": { "domain": [], "data_directory": "", @@ -407,6 +416,18 @@ echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/ 启用内核 TLS 接收支持。 +#### certificate_provider + +!!! question "自 sing-box 1.14.0 起" + +==仅服务器== + +字符串或对象。 + +为字符串时,共享[证书提供者](/zh/configuration/shared/certificate-provider/)的标签。 + +为对象时,内联的证书提供者。可用类型和字段参阅[证书提供者](/zh/configuration/shared/certificate-provider/)。 + ## 自定义 TLS 支持 !!! info "QUIC 支持" @@ -465,7 +486,7 @@ ECH 密钥和配置可以通过 `sing-box generate ech-keypair` 生成。 !!! failure "已在 sing-box 1.12.0 废弃" - ECH 支持已在 sing-box 1.12.0 迁移至使用标准库,但标准库不支持后量子对等证书签名方案,因此 `pq_signature_schemes_enabled` 已被弃用且不再工作。 + `pq_signature_schemes_enabled` 已在 sing-box 1.12.0 废弃且已在 sing-box 1.13.0 中被移除。 启用对后量子对等证书签名方案的支持。 @@ -473,7 +494,7 @@ ECH 密钥和配置可以通过 `sing-box generate ech-keypair` 生成。 !!! failure "已在 sing-box 1.12.0 废弃" - `dynamic_record_sizing_disabled` 与 ECH 无关,是错误添加的,现已弃用且不再工作。 + `dynamic_record_sizing_disabled` 已在 sing-box 1.12.0 废弃且已在 sing-box 1.13.0 中被移除。 禁用 TLS 记录的自适应大小调整。 @@ -561,6 +582,10 @@ ECH 配置路径,PEM 格式。 ### ACME 字段 +!!! failure "已在 sing-box 1.14.0 废弃" + + 内联 ACME 选项已在 sing-box 1.14.0 废弃且将在 sing-box 1.16.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移内联-acme-到证书提供者)。 + #### domain 域名列表。 diff --git a/docs/deprecated.md b/docs/deprecated.md index 8e53bda6db..3faf986e08 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -4,6 +4,16 @@ icon: material/delete-alert # Deprecated Feature List +## 1.14.0 + +#### Inline ACME options in TLS + +Inline ACME options (`tls.acme`) are deprecated +and can be replaced by the ACME certificate provider, +check [Migration](../migration/#migrate-inline-acme-to-certificate-provider). + +Old fields will be removed in sing-box 1.16.0. + ## 1.12.0 #### Legacy DNS server formats @@ -28,7 +38,7 @@ so `pq_signature_schemes_enabled` has been deprecated and no longer works. Also, `dynamic_record_sizing_disabled` has nothing to do with ECH, was added by mistake, has been deprecated and no longer works. -These fields will be removed in sing-box 1.13.0. +These fields were removed in sing-box 1.13.0. ## 1.11.0 @@ -38,7 +48,7 @@ Legacy special outbounds (`block` / `dns`) are deprecated and can be replaced by rule actions, check [Migration](../migration/#migrate-legacy-special-outbounds-to-rule-actions). -Old fields will be removed in sing-box 1.13.0. +Old fields were removed in sing-box 1.13.0. #### Legacy inbound fields @@ -46,7 +56,7 @@ Legacy inbound fields (`inbound.` are deprecated and can be replaced by rule actions, check [Migration](../migration/#migrate-legacy-inbound-fields-to-rule-actions). -Old fields will be removed in sing-box 1.13.0. +Old fields were removed in sing-box 1.13.0. #### Destination override fields in direct outbound @@ -54,18 +64,20 @@ Destination override fields (`override_address` / `override_port`) in direct out and can be replaced by rule actions, check [Migration](../migration/#migrate-destination-override-fields-to-route-options). +Old fields were removed in sing-box 1.13.0. + #### WireGuard outbound WireGuard outbound is deprecated and can be replaced by endpoint, check [Migration](../migration/#migrate-wireguard-outbound-to-endpoint). -Old outbound will be removed in sing-box 1.13.0. +Old outbound was removed in sing-box 1.13.0. #### GSO option in TUN GSO has no advantages for transparent proxy scenarios, is deprecated and no longer works in TUN. -Old fields will be removed in sing-box 1.13.0. +Old fields were removed in sing-box 1.13.0. ## 1.10.0 @@ -75,12 +87,12 @@ Old fields will be removed in sing-box 1.13.0. `inet4_route_address` and `inet6_route_address` are merged into `route_address`, `inet4_route_exclude_address` and `inet6_route_exclude_address` are merged into `route_exclude_address`. -Old fields will be removed in sing-box 1.12.0. +Old fields were removed in sing-box 1.12.0. #### Match source rule items are renamed `rule_set_ipcidr_match_source` route and DNS rule items are renamed to -`rule_set_ip_cidr_match_source` and will be remove in sing-box 1.11.0. +`rule_set_ip_cidr_match_source` and were removed in sing-box 1.11.0. #### Drop support for go1.18 and go1.19 @@ -95,7 +107,7 @@ check [Migration](/migration/#migrate-cache-file-from-clash-api-to-independent-o #### GeoIP -GeoIP is deprecated and will be removed in sing-box 1.12.0. +GeoIP is deprecated and was removed in sing-box 1.12.0. The maxmind GeoIP National Database, as an IP classification database, is not entirely suitable for traffic bypassing, @@ -106,7 +118,7 @@ check [Migration](/migration/#migrate-geoip-to-rule-sets). #### Geosite -Geosite is deprecated and will be removed in sing-box 1.12.0. +Geosite is deprecated and was removed in sing-box 1.12.0. Geosite, the `domain-list-community` project maintained by V2Ray as an early traffic bypassing solution, suffers from a number of problems, including lack of maintenance, inaccurate rules, and difficult management. diff --git a/docs/deprecated.zh.md b/docs/deprecated.zh.md index 82b6db042f..e710e78ce7 100644 --- a/docs/deprecated.zh.md +++ b/docs/deprecated.zh.md @@ -4,6 +4,18 @@ icon: material/delete-alert # 废弃功能列表 +## 1.14.0 + +#### TLS 中的内联 ACME 选项 + +TLS 中的内联 ACME 选项(`tls.acme`)已废弃, +且可以通过 ACME 证书提供者替代, +参阅 [迁移指南](/zh/migration/#迁移内联-acme-到证书提供者)。 + +旧字段将在 sing-box 1.16.0 中被移除。 + +## 1.12.0 + #### 旧的 DNS 服务器格式 DNS 服务器已重构, @@ -24,7 +36,7 @@ ECH 支持已在 sing-box 1.12.0 迁移至使用标准库,但标准库不支 另外,`dynamic_record_sizing_disabled` 与 ECH 无关,是错误添加的,现已弃用且不再工作。 -相关字段将在 sing-box 1.13.0 中被移除。 +相关字段已在 sing-box 1.13.0 中被移除。 ## 1.11.0 @@ -33,41 +45,41 @@ ECH 支持已在 sing-box 1.12.0 迁移至使用标准库,但标准库不支 旧的特殊出站(`block` / `dns`)已废弃且可以通过规则动作替代, 参阅 [迁移指南](/zh/migration/#迁移旧的特殊出站到规则动作)。 -旧字段将在 sing-box 1.13.0 中被移除。 +旧字段已在 sing-box 1.13.0 中被移除。 #### 旧的入站字段 旧的入站字段(`inbound.`)已废弃且可以通过规则动作替代, 参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作)。 -旧字段将在 sing-box 1.13.0 中被移除。 +旧字段已在 sing-box 1.13.0 中被移除。 #### direct 出站中的目标地址覆盖字段 direct 出站中的目标地址覆盖字段(`override_address` / `override_port`)已废弃且可以通过规则动作替代, 参阅 [迁移指南](/zh/migration/#迁移-direct-出站中的目标地址覆盖字段到路由字段)。 -旧字段将在 sing-box 1.13.0 中被移除。 +旧字段已在 sing-box 1.13.0 中被移除。 #### WireGuard 出站 WireGuard 出站已废弃且可以通过端点替代, 参阅 [迁移指南](/zh/migration/#迁移-wireguard-出站到端点)。 -旧出站将在 sing-box 1.13.0 中被移除。 +旧出站已在 sing-box 1.13.0 中被移除。 #### TUN 的 GSO 字段 GSO 对透明代理场景没有优势,已废弃且在 TUN 中不再起作用。 -旧字段将在 sing-box 1.13.0 中被移除。 +旧字段已在 sing-box 1.13.0 中被移除。 ## 1.10.0 #### Match source 规则项已重命名 `rule_set_ipcidr_match_source` 路由和 DNS 规则项已被重命名为 -`rule_set_ip_cidr_match_source` 且将在 sing-box 1.11.0 中被移除。 +`rule_set_ip_cidr_match_source` 且已在 sing-box 1.11.0 中被移除。 #### TUN 地址字段已合并 @@ -75,7 +87,7 @@ GSO 对透明代理场景没有优势,已废弃且在 TUN 中不再起作用 `inet4_route_address` 和 `inet6_route_address` 已合并为 `route_address`, `inet4_route_exclude_address` 和 `inet6_route_exclude_address` 已合并为 `route_exclude_address`。 -旧字段将在 sing-box 1.11.0 中被移除。 +旧字段已在 sing-box 1.12.0 中被移除。 #### 移除对 go1.18 和 go1.19 的支持 @@ -90,7 +102,7 @@ Clash API 中的 `cache_file` 及相关功能已废弃且已迁移到独立的 ` #### GeoIP -GeoIP 已废弃且将在 sing-box 1.12.0 中被移除。 +GeoIP 已废弃且已在 sing-box 1.12.0 中被移除。 maxmind GeoIP 国家数据库作为 IP 分类数据库,不完全适合流量绕过, 且现有的实现均存在内存使用大与管理困难的问题。 @@ -100,7 +112,7 @@ sing-box 1.8.0 引入了[规则集](/zh/configuration/rule-set/), #### Geosite -Geosite 已废弃且将在 sing-box 1.12.0 中被移除。 +Geosite 已废弃且已在 sing-box 1.12.0 中被移除。 Geosite,即由 V2Ray 维护的 domain-list-community 项目,作为早期流量绕过解决方案, 存在着包括缺少维护、规则不准确和管理困难内的大量问题。 diff --git a/docs/migration.md b/docs/migration.md index 86074ac712..810bae190a 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -2,6 +2,83 @@ icon: material/arrange-bring-forward --- +## 1.14.0 + +### Migrate inline ACME to certificate provider + +Inline ACME options in TLS are deprecated and can be replaced by certificate providers. + +Most `tls.acme` fields can be moved into the ACME certificate provider unchanged. +See [ACME](/configuration/shared/certificate-provider/acme/) for fields newly added in sing-box 1.14.0. + +!!! info "References" + + [TLS](/configuration/shared/tls/#certificate_provider) / + [Certificate Provider](/configuration/shared/certificate-provider/) + +=== ":material-card-remove: Deprecated" + + ```json + { + "inbounds": [ + { + "type": "trojan", + "tls": { + "enabled": true, + "acme": { + "domain": ["example.com"], + "email": "admin@example.com" + } + } + } + ] + } + ``` + +=== ":material-card-multiple: Inline" + + ```json + { + "inbounds": [ + { + "type": "trojan", + "tls": { + "enabled": true, + "certificate_provider": { + "type": "acme", + "domain": ["example.com"], + "email": "admin@example.com" + } + } + } + ] + } + ``` + +=== ":material-card-multiple: Shared" + + ```json + { + "certificate_providers": [ + { + "type": "acme", + "tag": "my-cert", + "domain": ["example.com"], + "email": "admin@example.com" + } + ], + "inbounds": [ + { + "type": "trojan", + "tls": { + "enabled": true, + "certificate_provider": "my-cert" + } + } + ] + } + ``` + ## 1.12.0 ### Migrate to new DNS server formats diff --git a/docs/migration.zh.md b/docs/migration.zh.md index c08be78f5c..18e2872613 100644 --- a/docs/migration.zh.md +++ b/docs/migration.zh.md @@ -2,6 +2,83 @@ icon: material/arrange-bring-forward --- +## 1.14.0 + +### 迁移内联 ACME 到证书提供者 + +TLS 中的内联 ACME 选项已废弃,且可以被证书提供者替代。 + +`tls.acme` 的大多数字段都可以原样迁移到 ACME 证书提供者中。 +sing-box 1.14.0 新增字段参阅 [ACME](/zh/configuration/shared/certificate-provider/acme/) 页面。 + +!!! info "参考" + + [TLS](/zh/configuration/shared/tls/#certificate_provider) / + [证书提供者](/zh/configuration/shared/certificate-provider/) + +=== ":material-card-remove: 弃用的" + + ```json + { + "inbounds": [ + { + "type": "trojan", + "tls": { + "enabled": true, + "acme": { + "domain": ["example.com"], + "email": "admin@example.com" + } + } + } + ] + } + ``` + +=== ":material-card-multiple: 内联" + + ```json + { + "inbounds": [ + { + "type": "trojan", + "tls": { + "enabled": true, + "certificate_provider": { + "type": "acme", + "domain": ["example.com"], + "email": "admin@example.com" + } + } + } + ] + } + ``` + +=== ":material-card-multiple: 共享" + + ```json + { + "certificate_providers": [ + { + "type": "acme", + "tag": "my-cert", + "domain": ["example.com"], + "email": "admin@example.com" + } + ], + "inbounds": [ + { + "type": "trojan", + "tls": { + "enabled": true, + "certificate_provider": "my-cert" + } + } + ] + } + ``` + ## 1.12.0 ### 迁移到新的 DNS 服务器格式 diff --git a/experimental/deprecated/constants.go b/experimental/deprecated/constants.go index 385105d383..3526cda831 100644 --- a/experimental/deprecated/constants.go +++ b/experimental/deprecated/constants.go @@ -102,10 +102,20 @@ var OptionLegacyDomainStrategyOptions = Note{ MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-domain-strategy-options", } +var OptionInlineACME = Note{ + Name: "inline-acme-options", + Description: "inline ACME options in TLS", + DeprecatedVersion: "1.14.0", + ScheduledVersion: "1.16.0", + EnvName: "INLINE_ACME_OPTIONS", + MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-inline-acme-to-certificate-provider", +} + var Options = []Note{ OptionLegacyDNSTransport, OptionLegacyDNSFakeIPOptions, OptionOutboundDNSRuleItem, OptionMissingDomainResolver, OptionLegacyDomainStrategyOptions, + OptionInlineACME, } diff --git a/experimental/libbox/config.go b/experimental/libbox/config.go index 9d0b977567..16d7d3e7b3 100644 --- a/experimental/libbox/config.go +++ b/experimental/libbox/config.go @@ -34,7 +34,7 @@ func baseContext(platformInterface PlatformInterface) context.Context { } ctx := context.Background() ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID) - return box.Context(ctx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), dnsRegistry, include.ServiceRegistry()) + return box.Context(ctx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), dnsRegistry, include.ServiceRegistry(), include.CertificateProviderRegistry()) } func parseConfig(ctx context.Context, configContent string) (option.Options, error) { diff --git a/include/acme.go b/include/acme.go new file mode 100644 index 0000000000..093fd50823 --- /dev/null +++ b/include/acme.go @@ -0,0 +1,12 @@ +//go:build with_acme + +package include + +import ( + "github.com/sagernet/sing-box/adapter/certificate" + "github.com/sagernet/sing-box/service/acme" +) + +func registerACMECertificateProvider(registry *certificate.Registry) { + acme.RegisterCertificateProvider(registry) +} diff --git a/include/acme_stub.go b/include/acme_stub.go new file mode 100644 index 0000000000..bceab3d731 --- /dev/null +++ b/include/acme_stub.go @@ -0,0 +1,20 @@ +//go:build !with_acme + +package include + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/certificate" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +func registerACMECertificateProvider(registry *certificate.Registry) { + certificate.Register[option.ACMECertificateProviderOptions](registry, C.TypeACME, func(ctx context.Context, logger log.ContextLogger, tag string, options option.ACMECertificateProviderOptions) (adapter.CertificateProviderService, error) { + return nil, E.New(`ACME is not included in this build, rebuild with -tags with_acme`) + }) +} diff --git a/include/registry.go b/include/registry.go index f090845b51..eb22cce1fe 100644 --- a/include/registry.go +++ b/include/registry.go @@ -5,6 +5,7 @@ import ( "github.com/sagernet/sing-box" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/certificate" "github.com/sagernet/sing-box/adapter/endpoint" "github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/adapter/outbound" @@ -34,13 +35,14 @@ import ( "github.com/sagernet/sing-box/protocol/tun" "github.com/sagernet/sing-box/protocol/vless" "github.com/sagernet/sing-box/protocol/vmess" + originca "github.com/sagernet/sing-box/service/origin_ca" "github.com/sagernet/sing-box/service/resolved" "github.com/sagernet/sing-box/service/ssmapi" E "github.com/sagernet/sing/common/exceptions" ) func Context(ctx context.Context) context.Context { - return box.Context(ctx, InboundRegistry(), OutboundRegistry(), EndpointRegistry(), DNSTransportRegistry(), ServiceRegistry()) + return box.Context(ctx, InboundRegistry(), OutboundRegistry(), EndpointRegistry(), DNSTransportRegistry(), ServiceRegistry(), CertificateProviderRegistry()) } func InboundRegistry() *inbound.Registry { @@ -139,6 +141,16 @@ func ServiceRegistry() *service.Registry { return registry } +func CertificateProviderRegistry() *certificate.Registry { + registry := certificate.NewRegistry() + + registerACMECertificateProvider(registry) + registerTailscaleCertificateProvider(registry) + originca.RegisterCertificateProvider(registry) + + return registry +} + func registerStubForRemovedInbounds(registry *inbound.Registry) { inbound.Register[option.ShadowsocksInboundOptions](registry, C.TypeShadowsocksR, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (adapter.Inbound, error) { return nil, E.New("ShadowsocksR is deprecated and removed in sing-box 1.6.0") diff --git a/include/tailscale.go b/include/tailscale.go index 1757283b07..6f85aaac14 100644 --- a/include/tailscale.go +++ b/include/tailscale.go @@ -3,6 +3,7 @@ package include import ( + "github.com/sagernet/sing-box/adapter/certificate" "github.com/sagernet/sing-box/adapter/endpoint" "github.com/sagernet/sing-box/adapter/service" "github.com/sagernet/sing-box/dns" @@ -18,6 +19,10 @@ func registerTailscaleTransport(registry *dns.TransportRegistry) { tailscale.RegistryTransport(registry) } +func registerTailscaleCertificateProvider(registry *certificate.Registry) { + tailscale.RegisterCertificateProvider(registry) +} + func registerDERPService(registry *service.Registry) { derp.Register(registry) } diff --git a/include/tailscale_stub.go b/include/tailscale_stub.go index 78398875f8..e6f97f1eab 100644 --- a/include/tailscale_stub.go +++ b/include/tailscale_stub.go @@ -6,6 +6,7 @@ import ( "context" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/certificate" "github.com/sagernet/sing-box/adapter/endpoint" "github.com/sagernet/sing-box/adapter/service" C "github.com/sagernet/sing-box/constant" @@ -27,6 +28,12 @@ func registerTailscaleTransport(registry *dns.TransportRegistry) { }) } +func registerTailscaleCertificateProvider(registry *certificate.Registry) { + certificate.Register[option.TailscaleCertificateProviderOptions](registry, C.TypeTailscale, func(ctx context.Context, logger log.ContextLogger, tag string, options option.TailscaleCertificateProviderOptions) (adapter.CertificateProviderService, error) { + return nil, E.New(`Tailscale is not included in this build, rebuild with -tags with_tailscale`) + }) +} + func registerDERPService(registry *service.Registry) { service.Register[option.DERPServiceOptions](registry, C.TypeDERP, func(ctx context.Context, logger log.ContextLogger, tag string, options option.DERPServiceOptions) (adapter.Service, error) { return nil, E.New(`DERP is not included in this build, rebuild with -tags with_tailscale`) diff --git a/mkdocs.yml b/mkdocs.yml index 5f95842a5d..65c9db71f4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -122,6 +122,11 @@ nav: - Listen Fields: configuration/shared/listen.md - Dial Fields: configuration/shared/dial.md - TLS: configuration/shared/tls.md + - Certificate Provider: + - configuration/shared/certificate-provider/index.md + - ACME: configuration/shared/certificate-provider/acme.md + - Tailscale: configuration/shared/certificate-provider/tailscale.md + - Cloudflare Origin CA: configuration/shared/certificate-provider/cloudflare-origin-ca.md - DNS01 Challenge Fields: configuration/shared/dns01_challenge.md - Pre-match: configuration/shared/pre-match.md - Multiplex: configuration/shared/multiplex.md @@ -273,6 +278,7 @@ plugins: Shared: 通用 Listen Fields: 监听字段 Dial Fields: 拨号字段 + Certificate Provider Fields: 证书提供者字段 DNS01 Challenge Fields: DNS01 验证字段 Multiplex: 多路复用 V2Ray Transport: V2Ray 传输层 @@ -281,6 +287,7 @@ plugins: Endpoint: 端点 Inbound: 入站 Outbound: 出站 + Certificate Provider: 证书提供者 Manual: 手册 reconfigure_material: true diff --git a/option/acme.go b/option/acme.go new file mode 100644 index 0000000000..ea9349b724 --- /dev/null +++ b/option/acme.go @@ -0,0 +1,106 @@ +package option + +import ( + "strings" + + C "github.com/sagernet/sing-box/constant" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/json/badoption" +) + +type ACMECertificateProviderOptions struct { + Domain badoption.Listable[string] `json:"domain,omitempty"` + DataDirectory string `json:"data_directory,omitempty"` + DefaultServerName string `json:"default_server_name,omitempty"` + Email string `json:"email,omitempty"` + Provider string `json:"provider,omitempty"` + AccountKey string `json:"account_key,omitempty"` + DisableHTTPChallenge bool `json:"disable_http_challenge,omitempty"` + DisableTLSALPNChallenge bool `json:"disable_tls_alpn_challenge,omitempty"` + AlternativeHTTPPort uint16 `json:"alternative_http_port,omitempty"` + AlternativeTLSPort uint16 `json:"alternative_tls_port,omitempty"` + ExternalAccount *ACMEExternalAccountOptions `json:"external_account,omitempty"` + DNS01Challenge *ACMEProviderDNS01ChallengeOptions `json:"dns01_challenge,omitempty"` + KeyType ACMEKeyType `json:"key_type,omitempty"` + Detour string `json:"detour,omitempty"` +} + +type _ACMEProviderDNS01ChallengeOptions struct { + TTL badoption.Duration `json:"ttl,omitempty"` + PropagationDelay badoption.Duration `json:"propagation_delay,omitempty"` + PropagationTimeout badoption.Duration `json:"propagation_timeout,omitempty"` + Resolvers badoption.Listable[string] `json:"resolvers,omitempty"` + OverrideDomain string `json:"override_domain,omitempty"` + Provider string `json:"provider,omitempty"` + AliDNSOptions ACMEDNS01AliDNSOptions `json:"-"` + CloudflareOptions ACMEDNS01CloudflareOptions `json:"-"` + ACMEDNSOptions ACMEDNS01ACMEDNSOptions `json:"-"` +} + +type ACMEProviderDNS01ChallengeOptions _ACMEProviderDNS01ChallengeOptions + +func (o ACMEProviderDNS01ChallengeOptions) MarshalJSON() ([]byte, error) { + var v any + switch o.Provider { + case C.DNSProviderAliDNS: + v = o.AliDNSOptions + case C.DNSProviderCloudflare: + v = o.CloudflareOptions + case C.DNSProviderACMEDNS: + v = o.ACMEDNSOptions + case "": + return nil, E.New("missing provider type") + default: + return nil, E.New("unknown provider type: ", o.Provider) + } + return badjson.MarshallObjects((_ACMEProviderDNS01ChallengeOptions)(o), v) +} + +func (o *ACMEProviderDNS01ChallengeOptions) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_ACMEProviderDNS01ChallengeOptions)(o)) + if err != nil { + return err + } + var v any + switch o.Provider { + case C.DNSProviderAliDNS: + v = &o.AliDNSOptions + case C.DNSProviderCloudflare: + v = &o.CloudflareOptions + case C.DNSProviderACMEDNS: + v = &o.ACMEDNSOptions + case "": + return E.New("missing provider type") + default: + return E.New("unknown provider type: ", o.Provider) + } + return badjson.UnmarshallExcluded(bytes, (*_ACMEProviderDNS01ChallengeOptions)(o), v) +} + +type ACMEKeyType string + +const ( + ACMEKeyTypeED25519 = ACMEKeyType("ed25519") + ACMEKeyTypeP256 = ACMEKeyType("p256") + ACMEKeyTypeP384 = ACMEKeyType("p384") + ACMEKeyTypeRSA2048 = ACMEKeyType("rsa2048") + ACMEKeyTypeRSA4096 = ACMEKeyType("rsa4096") +) + +func (t *ACMEKeyType) UnmarshalJSON(data []byte) error { + var value string + err := json.Unmarshal(data, &value) + if err != nil { + return err + } + value = strings.ToLower(value) + switch ACMEKeyType(value) { + case "", ACMEKeyTypeED25519, ACMEKeyTypeP256, ACMEKeyTypeP384, ACMEKeyTypeRSA2048, ACMEKeyTypeRSA4096: + *t = ACMEKeyType(value) + default: + return E.New("unknown ACME key type: ", value) + } + return nil +} diff --git a/option/certificate_provider.go b/option/certificate_provider.go new file mode 100644 index 0000000000..a24abdc570 --- /dev/null +++ b/option/certificate_provider.go @@ -0,0 +1,100 @@ +package option + +import ( + "context" + + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/service" +) + +type CertificateProviderOptionsRegistry interface { + CreateOptions(providerType string) (any, bool) +} + +type _CertificateProvider struct { + Type string `json:"type"` + Tag string `json:"tag,omitempty"` + Options any `json:"-"` +} + +type CertificateProvider _CertificateProvider + +func (h *CertificateProvider) MarshalJSONContext(ctx context.Context) ([]byte, error) { + return badjson.MarshallObjectsContext(ctx, (*_CertificateProvider)(h), h.Options) +} + +func (h *CertificateProvider) UnmarshalJSONContext(ctx context.Context, content []byte) error { + err := json.UnmarshalContext(ctx, content, (*_CertificateProvider)(h)) + if err != nil { + return err + } + registry := service.FromContext[CertificateProviderOptionsRegistry](ctx) + if registry == nil { + return E.New("missing certificate provider options registry in context") + } + options, loaded := registry.CreateOptions(h.Type) + if !loaded { + return E.New("unknown certificate provider type: ", h.Type) + } + err = badjson.UnmarshallExcludedContext(ctx, content, (*_CertificateProvider)(h), options) + if err != nil { + return err + } + h.Options = options + return nil +} + +type CertificateProviderOptions struct { + Tag string `json:"-"` + Type string `json:"-"` + Options any `json:"-"` +} + +type _CertificateProviderInline struct { + Type string `json:"type"` +} + +func (o *CertificateProviderOptions) MarshalJSONContext(ctx context.Context) ([]byte, error) { + if o.Tag != "" { + return json.Marshal(o.Tag) + } + return badjson.MarshallObjectsContext(ctx, _CertificateProviderInline{Type: o.Type}, o.Options) +} + +func (o *CertificateProviderOptions) UnmarshalJSONContext(ctx context.Context, content []byte) error { + if len(content) == 0 { + return E.New("empty certificate_provider value") + } + if content[0] == '"' { + return json.UnmarshalContext(ctx, content, &o.Tag) + } + var inline _CertificateProviderInline + err := json.UnmarshalContext(ctx, content, &inline) + if err != nil { + return err + } + o.Type = inline.Type + if o.Type == "" { + return E.New("missing certificate provider type") + } + registry := service.FromContext[CertificateProviderOptionsRegistry](ctx) + if registry == nil { + return E.New("missing certificate provider options registry in context") + } + options, loaded := registry.CreateOptions(o.Type) + if !loaded { + return E.New("unknown certificate provider type: ", o.Type) + } + err = badjson.UnmarshallExcludedContext(ctx, content, &inline, options) + if err != nil { + return err + } + o.Options = options + return nil +} + +func (o *CertificateProviderOptions) IsShared() bool { + return o.Tag != "" +} diff --git a/option/options.go b/option/options.go index 8bebd48fc6..a08dcbc0f1 100644 --- a/option/options.go +++ b/option/options.go @@ -10,18 +10,19 @@ import ( ) type _Options struct { - RawMessage json.RawMessage `json:"-"` - Schema string `json:"$schema,omitempty"` - Log *LogOptions `json:"log,omitempty"` - DNS *DNSOptions `json:"dns,omitempty"` - NTP *NTPOptions `json:"ntp,omitempty"` - Certificate *CertificateOptions `json:"certificate,omitempty"` - Endpoints []Endpoint `json:"endpoints,omitempty"` - Inbounds []Inbound `json:"inbounds,omitempty"` - Outbounds []Outbound `json:"outbounds,omitempty"` - Route *RouteOptions `json:"route,omitempty"` - Services []Service `json:"services,omitempty"` - Experimental *ExperimentalOptions `json:"experimental,omitempty"` + RawMessage json.RawMessage `json:"-"` + Schema string `json:"$schema,omitempty"` + Log *LogOptions `json:"log,omitempty"` + DNS *DNSOptions `json:"dns,omitempty"` + NTP *NTPOptions `json:"ntp,omitempty"` + Certificate *CertificateOptions `json:"certificate,omitempty"` + CertificateProviders []CertificateProvider `json:"certificate_providers,omitempty"` + Endpoints []Endpoint `json:"endpoints,omitempty"` + Inbounds []Inbound `json:"inbounds,omitempty"` + Outbounds []Outbound `json:"outbounds,omitempty"` + Route *RouteOptions `json:"route,omitempty"` + Services []Service `json:"services,omitempty"` + Experimental *ExperimentalOptions `json:"experimental,omitempty"` } type Options _Options @@ -56,6 +57,25 @@ func checkOptions(options *Options) error { if err != nil { return err } + err = checkCertificateProviders(options.CertificateProviders) + if err != nil { + return err + } + return nil +} + +func checkCertificateProviders(providers []CertificateProvider) error { + seen := make(map[string]bool) + for i, provider := range providers { + tag := provider.Tag + if tag == "" { + tag = F.ToString(i) + } + if seen[tag] { + return E.New("duplicate certificate provider tag: ", tag) + } + seen[tag] = true + } return nil } diff --git a/option/origin_ca.go b/option/origin_ca.go new file mode 100644 index 0000000000..ee8b370414 --- /dev/null +++ b/option/origin_ca.go @@ -0,0 +1,76 @@ +package option + +import ( + "strings" + + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badoption" +) + +type CloudflareOriginCACertificateProviderOptions struct { + Domain badoption.Listable[string] `json:"domain,omitempty"` + DataDirectory string `json:"data_directory,omitempty"` + APIToken string `json:"api_token,omitempty"` + OriginCAKey string `json:"origin_ca_key,omitempty"` + RequestType CloudflareOriginCARequestType `json:"request_type,omitempty"` + RequestedValidity CloudflareOriginCARequestValidity `json:"requested_validity,omitempty"` + Detour string `json:"detour,omitempty"` +} + +type CloudflareOriginCARequestType string + +const ( + CloudflareOriginCARequestTypeOriginRSA = CloudflareOriginCARequestType("origin-rsa") + CloudflareOriginCARequestTypeOriginECC = CloudflareOriginCARequestType("origin-ecc") +) + +func (t *CloudflareOriginCARequestType) UnmarshalJSON(data []byte) error { + var value string + err := json.Unmarshal(data, &value) + if err != nil { + return err + } + value = strings.ToLower(value) + switch CloudflareOriginCARequestType(value) { + case "", CloudflareOriginCARequestTypeOriginRSA, CloudflareOriginCARequestTypeOriginECC: + *t = CloudflareOriginCARequestType(value) + default: + return E.New("unsupported Cloudflare Origin CA request type: ", value) + } + return nil +} + +type CloudflareOriginCARequestValidity uint16 + +const ( + CloudflareOriginCARequestValidity7 = CloudflareOriginCARequestValidity(7) + CloudflareOriginCARequestValidity30 = CloudflareOriginCARequestValidity(30) + CloudflareOriginCARequestValidity90 = CloudflareOriginCARequestValidity(90) + CloudflareOriginCARequestValidity365 = CloudflareOriginCARequestValidity(365) + CloudflareOriginCARequestValidity730 = CloudflareOriginCARequestValidity(730) + CloudflareOriginCARequestValidity1095 = CloudflareOriginCARequestValidity(1095) + CloudflareOriginCARequestValidity5475 = CloudflareOriginCARequestValidity(5475) +) + +func (v *CloudflareOriginCARequestValidity) UnmarshalJSON(data []byte) error { + var value uint16 + err := json.Unmarshal(data, &value) + if err != nil { + return err + } + switch CloudflareOriginCARequestValidity(value) { + case 0, + CloudflareOriginCARequestValidity7, + CloudflareOriginCARequestValidity30, + CloudflareOriginCARequestValidity90, + CloudflareOriginCARequestValidity365, + CloudflareOriginCARequestValidity730, + CloudflareOriginCARequestValidity1095, + CloudflareOriginCARequestValidity5475: + *v = CloudflareOriginCARequestValidity(value) + default: + return E.New("unsupported Cloudflare Origin CA requested validity: ", value) + } + return nil +} diff --git a/option/tailscale.go b/option/tailscale.go index 68a143693e..a4f82ce0de 100644 --- a/option/tailscale.go +++ b/option/tailscale.go @@ -36,6 +36,10 @@ type TailscaleDNSServerOptions struct { AcceptDefaultResolvers bool `json:"accept_default_resolvers,omitempty"` } +type TailscaleCertificateProviderOptions struct { + Endpoint string `json:"endpoint,omitempty"` +} + type DERPServiceOptions struct { ListenOptions InboundTLSOptionsContainer diff --git a/option/tls.go b/option/tls.go index 60343a15f1..dbbb7620ed 100644 --- a/option/tls.go +++ b/option/tls.go @@ -28,9 +28,13 @@ type InboundTLSOptions struct { KeyPath string `json:"key_path,omitempty"` KernelTx bool `json:"kernel_tx,omitempty"` KernelRx bool `json:"kernel_rx,omitempty"` - ACME *InboundACMEOptions `json:"acme,omitempty"` - ECH *InboundECHOptions `json:"ech,omitempty"` - Reality *InboundRealityOptions `json:"reality,omitempty"` + CertificateProvider *CertificateProviderOptions `json:"certificate_provider,omitempty"` + + // Deprecated: use certificate_provider + ACME *InboundACMEOptions `json:"acme,omitempty"` + + ECH *InboundECHOptions `json:"ech,omitempty"` + Reality *InboundRealityOptions `json:"reality,omitempty"` } type ClientAuthType tls.ClientAuthType diff --git a/protocol/tailscale/certificate_provider.go b/protocol/tailscale/certificate_provider.go new file mode 100644 index 0000000000..5ac18a3073 --- /dev/null +++ b/protocol/tailscale/certificate_provider.go @@ -0,0 +1,98 @@ +//go:build with_gvisor + +package tailscale + +import ( + "context" + "crypto/tls" + "net" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/certificate" + "github.com/sagernet/sing-box/common/dialer" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" + "github.com/sagernet/tailscale/client/local" +) + +func RegisterCertificateProvider(registry *certificate.Registry) { + certificate.Register[option.TailscaleCertificateProviderOptions](registry, C.TypeTailscale, NewCertificateProvider) +} + +var _ adapter.CertificateProviderService = (*CertificateProvider)(nil) + +type CertificateProvider struct { + certificate.Adapter + endpointTag string + endpoint *Endpoint + dialer N.Dialer + localClient *local.Client +} + +func NewCertificateProvider(ctx context.Context, _ log.ContextLogger, tag string, options option.TailscaleCertificateProviderOptions) (adapter.CertificateProviderService, error) { + if options.Endpoint == "" { + return nil, E.New("missing tailscale endpoint tag") + } + endpointManager := service.FromContext[adapter.EndpointManager](ctx) + if endpointManager == nil { + return nil, E.New("missing endpoint manager in context") + } + rawEndpoint, loaded := endpointManager.Get(options.Endpoint) + if !loaded { + return nil, E.New("endpoint not found: ", options.Endpoint) + } + endpoint, isTailscale := rawEndpoint.(*Endpoint) + if !isTailscale { + return nil, E.New("endpoint is not Tailscale: ", options.Endpoint) + } + providerDialer, err := dialer.NewWithOptions(dialer.Options{ + Context: ctx, + Options: option.DialerOptions{}, + RemoteIsDomain: true, + }) + if err != nil { + return nil, E.Cause(err, "create tailscale certificate provider dialer") + } + return &CertificateProvider{ + Adapter: certificate.NewAdapter(C.TypeTailscale, tag), + endpointTag: options.Endpoint, + endpoint: endpoint, + dialer: providerDialer, + }, nil +} + +func (p *CertificateProvider) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + localClient, err := p.endpoint.Server().LocalClient() + if err != nil { + return E.Cause(err, "initialize tailscale local client for endpoint ", p.endpointTag) + } + originalDial := localClient.Dial + localClient.Dial = func(ctx context.Context, network, addr string) (net.Conn, error) { + if originalDial != nil && addr == "local-tailscaled.sock:80" { + return originalDial(ctx, network, addr) + } + return p.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + } + p.localClient = localClient + return nil +} + +func (p *CertificateProvider) Close() error { + return nil +} + +func (p *CertificateProvider) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { + localClient := p.localClient + if localClient == nil { + return nil, E.New("Tailscale is not ready yet") + } + return localClient.GetCertificate(clientHello) +} diff --git a/service/acme/service.go b/service/acme/service.go new file mode 100644 index 0000000000..8286a19717 --- /dev/null +++ b/service/acme/service.go @@ -0,0 +1,411 @@ +//go:build with_acme + +package acme + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/json" + "net" + "net/http" + "net/url" + "reflect" + "strings" + "time" + "unsafe" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/certificate" + "github.com/sagernet/sing-box/common/dialer" + boxtls "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/ntp" + + "github.com/caddyserver/certmagic" + "github.com/caddyserver/zerossl" + "github.com/libdns/alidns" + "github.com/libdns/cloudflare" + "github.com/libdns/libdns" + "github.com/mholt/acmez/v3/acme" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +func RegisterCertificateProvider(registry *certificate.Registry) { + certificate.Register[option.ACMECertificateProviderOptions](registry, C.TypeACME, NewCertificateProvider) +} + +var ( + _ adapter.CertificateProviderService = (*Service)(nil) + _ adapter.ACMECertificateProvider = (*Service)(nil) +) + +type Service struct { + certificate.Adapter + ctx context.Context + config *certmagic.Config + cache *certmagic.Cache + domain []string + nextProtos []string +} + +func NewCertificateProvider(ctx context.Context, logger log.ContextLogger, tag string, options option.ACMECertificateProviderOptions) (adapter.CertificateProviderService, error) { + if len(options.Domain) == 0 { + return nil, E.New("missing domain") + } + var acmeServer string + switch options.Provider { + case "", "letsencrypt": + acmeServer = certmagic.LetsEncryptProductionCA + case "zerossl": + acmeServer = certmagic.ZeroSSLProductionCA + default: + if !strings.HasPrefix(options.Provider, "https://") { + return nil, E.New("unsupported ACME provider: ", options.Provider) + } + acmeServer = options.Provider + } + if acmeServer == certmagic.ZeroSSLProductionCA && + (options.ExternalAccount == nil || options.ExternalAccount.KeyID == "") && + strings.TrimSpace(options.Email) == "" && + strings.TrimSpace(options.AccountKey) == "" { + return nil, E.New("email is required to use the ZeroSSL ACME endpoint without external_account or account_key") + } + + var storage certmagic.Storage + if options.DataDirectory != "" { + storage = &certmagic.FileStorage{Path: options.DataDirectory} + } else { + storage = certmagic.Default.Storage + } + + zapLogger := zap.New(zapcore.NewCore( + zapcore.NewConsoleEncoder(boxtls.ACMEEncoderConfig()), + &boxtls.ACMELogWriter{Logger: logger}, + zap.DebugLevel, + )) + + config := &certmagic.Config{ + DefaultServerName: options.DefaultServerName, + Storage: storage, + Logger: zapLogger, + } + if options.KeyType != "" { + var keyType certmagic.KeyType + switch options.KeyType { + case option.ACMEKeyTypeED25519: + keyType = certmagic.ED25519 + case option.ACMEKeyTypeP256: + keyType = certmagic.P256 + case option.ACMEKeyTypeP384: + keyType = certmagic.P384 + case option.ACMEKeyTypeRSA2048: + keyType = certmagic.RSA2048 + case option.ACMEKeyTypeRSA4096: + keyType = certmagic.RSA4096 + default: + return nil, E.New("unsupported ACME key type: ", options.KeyType) + } + config.KeySource = certmagic.StandardKeyGenerator{KeyType: keyType} + } + + acmeIssuer := certmagic.ACMEIssuer{ + CA: acmeServer, + Email: options.Email, + AccountKeyPEM: options.AccountKey, + Agreed: true, + DisableHTTPChallenge: options.DisableHTTPChallenge, + DisableTLSALPNChallenge: options.DisableTLSALPNChallenge, + AltHTTPPort: int(options.AlternativeHTTPPort), + AltTLSALPNPort: int(options.AlternativeTLSPort), + Logger: zapLogger, + } + acmeHTTPClient, err := newACMEHTTPClient(ctx, options.Detour) + if err != nil { + return nil, err + } + dnsSolver, err := newDNSSolver(options.DNS01Challenge, zapLogger, acmeHTTPClient) + if err != nil { + return nil, err + } + if dnsSolver != nil { + acmeIssuer.DNS01Solver = dnsSolver + } + if options.ExternalAccount != nil && options.ExternalAccount.KeyID != "" { + acmeIssuer.ExternalAccount = (*acme.EAB)(options.ExternalAccount) + } + if acmeServer == certmagic.ZeroSSLProductionCA { + acmeIssuer.NewAccountFunc = func(ctx context.Context, acmeIssuer *certmagic.ACMEIssuer, account acme.Account) (acme.Account, error) { + if acmeIssuer.ExternalAccount != nil { + return account, nil + } + var err error + acmeIssuer.ExternalAccount, account, err = createZeroSSLExternalAccountBinding(ctx, acmeIssuer, account, acmeHTTPClient) + return account, err + } + } + + certmagicIssuer := certmagic.NewACMEIssuer(config, acmeIssuer) + httpClientField := reflect.ValueOf(certmagicIssuer).Elem().FieldByName("httpClient") + if !httpClientField.IsValid() || !httpClientField.CanAddr() { + return nil, E.New("certmagic ACME issuer HTTP client field is unavailable") + } + reflect.NewAt(httpClientField.Type(), unsafe.Pointer(httpClientField.UnsafeAddr())).Elem().Set(reflect.ValueOf(acmeHTTPClient)) + config.Issuers = []certmagic.Issuer{certmagicIssuer} + cache := certmagic.NewCache(certmagic.CacheOptions{ + GetConfigForCert: func(certificate certmagic.Certificate) (*certmagic.Config, error) { + return config, nil + }, + Logger: zapLogger, + }) + config = certmagic.New(cache, *config) + + var nextProtos []string + if !acmeIssuer.DisableTLSALPNChallenge && acmeIssuer.DNS01Solver == nil { + nextProtos = []string{C.ACMETLS1Protocol} + } + return &Service{ + Adapter: certificate.NewAdapter(C.TypeACME, tag), + ctx: ctx, + config: config, + cache: cache, + domain: options.Domain, + nextProtos: nextProtos, + }, nil +} + +func (s *Service) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + return s.config.ManageAsync(s.ctx, s.domain) +} + +func (s *Service) Close() error { + if s.cache != nil { + s.cache.Stop() + } + return nil +} + +func (s *Service) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { + return s.config.GetCertificate(hello) +} + +func (s *Service) GetACMENextProtos() []string { + return s.nextProtos +} + +func newDNSSolver(dnsOptions *option.ACMEProviderDNS01ChallengeOptions, logger *zap.Logger, httpClient *http.Client) (*certmagic.DNS01Solver, error) { + if dnsOptions == nil || dnsOptions.Provider == "" { + return nil, nil + } + if dnsOptions.TTL < 0 { + return nil, E.New("invalid ACME DNS01 ttl: ", dnsOptions.TTL) + } + if dnsOptions.PropagationDelay < 0 { + return nil, E.New("invalid ACME DNS01 propagation_delay: ", dnsOptions.PropagationDelay) + } + if dnsOptions.PropagationTimeout < -1 { + return nil, E.New("invalid ACME DNS01 propagation_timeout: ", dnsOptions.PropagationTimeout) + } + solver := &certmagic.DNS01Solver{ + DNSManager: certmagic.DNSManager{ + TTL: time.Duration(dnsOptions.TTL), + PropagationDelay: time.Duration(dnsOptions.PropagationDelay), + PropagationTimeout: time.Duration(dnsOptions.PropagationTimeout), + Resolvers: dnsOptions.Resolvers, + OverrideDomain: dnsOptions.OverrideDomain, + Logger: logger.Named("dns_manager"), + }, + } + switch dnsOptions.Provider { + case C.DNSProviderAliDNS: + solver.DNSProvider = &alidns.Provider{ + CredentialInfo: alidns.CredentialInfo{ + AccessKeyID: dnsOptions.AliDNSOptions.AccessKeyID, + AccessKeySecret: dnsOptions.AliDNSOptions.AccessKeySecret, + RegionID: dnsOptions.AliDNSOptions.RegionID, + SecurityToken: dnsOptions.AliDNSOptions.SecurityToken, + }, + } + case C.DNSProviderCloudflare: + solver.DNSProvider = &cloudflare.Provider{ + APIToken: dnsOptions.CloudflareOptions.APIToken, + ZoneToken: dnsOptions.CloudflareOptions.ZoneToken, + HTTPClient: httpClient, + } + case C.DNSProviderACMEDNS: + solver.DNSProvider = &acmeDNSProvider{ + username: dnsOptions.ACMEDNSOptions.Username, + password: dnsOptions.ACMEDNSOptions.Password, + subdomain: dnsOptions.ACMEDNSOptions.Subdomain, + serverURL: dnsOptions.ACMEDNSOptions.ServerURL, + httpClient: httpClient, + } + default: + return nil, E.New("unsupported ACME DNS01 provider type: ", dnsOptions.Provider) + } + return solver, nil +} + +func createZeroSSLExternalAccountBinding(ctx context.Context, acmeIssuer *certmagic.ACMEIssuer, account acme.Account, httpClient *http.Client) (*acme.EAB, acme.Account, error) { + email := strings.TrimSpace(acmeIssuer.Email) + if email == "" { + return nil, acme.Account{}, E.New("email is required to use the ZeroSSL ACME endpoint without external_account") + } + if len(account.Contact) == 0 { + account.Contact = []string{"mailto:" + email} + } + if acmeIssuer.CertObtainTimeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, acmeIssuer.CertObtainTimeout) + defer cancel() + } + + form := url.Values{"email": []string{email}} + request, err := http.NewRequestWithContext(ctx, http.MethodPost, zerossl.BaseURL+"/acme/eab-credentials-email", strings.NewReader(form.Encode())) + if err != nil { + return nil, account, E.Cause(err, "create ZeroSSL EAB request") + } + request.Header.Set("Content-Type", "application/x-www-form-urlencoded") + request.Header.Set("User-Agent", certmagic.UserAgent) + + response, err := httpClient.Do(request) + if err != nil { + return nil, account, E.Cause(err, "request ZeroSSL EAB") + } + defer response.Body.Close() + + var result struct { + Success bool `json:"success"` + Error struct { + Code int `json:"code"` + Type string `json:"type"` + } `json:"error"` + EABKID string `json:"eab_kid"` + EABHMACKey string `json:"eab_hmac_key"` + } + err = json.NewDecoder(response.Body).Decode(&result) + if err != nil { + return nil, account, E.Cause(err, "decode ZeroSSL EAB response") + } + if response.StatusCode != http.StatusOK { + return nil, account, E.New("failed getting ZeroSSL EAB credentials: HTTP ", response.StatusCode) + } + if result.Error.Code != 0 { + return nil, account, E.New("failed getting ZeroSSL EAB credentials: ", result.Error.Type, " (code ", result.Error.Code, ")") + } + + acmeIssuer.Logger.Info("generated ZeroSSL EAB credentials", zap.String("key_id", result.EABKID)) + + return &acme.EAB{ + KeyID: result.EABKID, + MACKey: result.EABHMACKey, + }, account, nil +} + +func newACMEHTTPClient(ctx context.Context, detour string) (*http.Client, error) { + outboundDialer, err := dialer.NewWithOptions(dialer.Options{ + Context: ctx, + Options: option.DialerOptions{ + Detour: detour, + }, + RemoteIsDomain: true, + }) + if err != nil { + return nil, E.Cause(err, "create ACME provider dialer") + } + return &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return outboundDialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + }, + TLSClientConfig: &tls.Config{ + RootCAs: adapter.RootPoolFromContext(ctx), + Time: ntp.TimeFuncFromContext(ctx), + }, + // from certmagic defaults (acmeissuer.go) + TLSHandshakeTimeout: 30 * time.Second, + ResponseHeaderTimeout: 30 * time.Second, + ExpectContinueTimeout: 2 * time.Second, + ForceAttemptHTTP2: true, + }, + Timeout: certmagic.HTTPTimeout, + }, nil +} + +type acmeDNSProvider struct { + username string + password string + subdomain string + serverURL string + httpClient *http.Client +} + +type acmeDNSRecord struct { + resourceRecord libdns.RR +} + +func (r acmeDNSRecord) RR() libdns.RR { + return r.resourceRecord +} + +func (p *acmeDNSProvider) AppendRecords(ctx context.Context, _ string, records []libdns.Record) ([]libdns.Record, error) { + if p.username == "" { + return nil, E.New("ACME-DNS username cannot be empty") + } + if p.password == "" { + return nil, E.New("ACME-DNS password cannot be empty") + } + if p.subdomain == "" { + return nil, E.New("ACME-DNS subdomain cannot be empty") + } + if p.serverURL == "" { + return nil, E.New("ACME-DNS server_url cannot be empty") + } + appendedRecords := make([]libdns.Record, 0, len(records)) + for _, record := range records { + resourceRecord := record.RR() + if resourceRecord.Type != "TXT" { + return appendedRecords, E.New("ACME-DNS only supports adding TXT records") + } + requestBody, err := json.Marshal(map[string]string{ + "subdomain": p.subdomain, + "txt": resourceRecord.Data, + }) + if err != nil { + return appendedRecords, E.Cause(err, "marshal ACME-DNS update request") + } + request, err := http.NewRequestWithContext(ctx, http.MethodPost, p.serverURL+"/update", bytes.NewReader(requestBody)) + if err != nil { + return appendedRecords, E.Cause(err, "create ACME-DNS update request") + } + request.Header.Set("X-Api-User", p.username) + request.Header.Set("X-Api-Key", p.password) + request.Header.Set("Content-Type", "application/json") + response, err := p.httpClient.Do(request) + if err != nil { + return appendedRecords, E.Cause(err, "update ACME-DNS record") + } + _ = response.Body.Close() + if response.StatusCode != http.StatusOK { + return appendedRecords, E.New("update ACME-DNS record: HTTP ", response.StatusCode) + } + appendedRecords = append(appendedRecords, acmeDNSRecord{resourceRecord: libdns.RR{ + Type: "TXT", + Name: resourceRecord.Name, + Data: resourceRecord.Data, + }}) + } + return appendedRecords, nil +} + +func (p *acmeDNSProvider) DeleteRecords(context.Context, string, []libdns.Record) ([]libdns.Record, error) { + return nil, nil +} diff --git a/service/acme/stub.go b/service/acme/stub.go new file mode 100644 index 0000000000..43a58d6449 --- /dev/null +++ b/service/acme/stub.go @@ -0,0 +1,3 @@ +//go:build !with_acme + +package acme diff --git a/service/origin_ca/service.go b/service/origin_ca/service.go new file mode 100644 index 0000000000..85588c37d5 --- /dev/null +++ b/service/origin_ca/service.go @@ -0,0 +1,618 @@ +package originca + +import ( + "bytes" + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/json" + "encoding/pem" + "errors" + "io" + "io/fs" + "net" + "net/http" + "slices" + "strconv" + "strings" + "sync" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/certificate" + "github.com/sagernet/sing-box/common/dialer" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/ntp" + + "github.com/caddyserver/certmagic" +) + +const ( + cloudflareOriginCAEndpoint = "https://api.cloudflare.com/client/v4/certificates" + defaultRequestedValidity = option.CloudflareOriginCARequestValidity5475 + // min of 30 days and certmagic's 1/3 lifetime ratio (maintain.go) + defaultRenewBefore = 30 * 24 * time.Hour + // from certmagic retry backoff range (async.go) + minimumRenewRetryDelay = time.Minute + maximumRenewRetryDelay = time.Hour + storageLockPrefix = "cloudflare-origin-ca" +) + +func RegisterCertificateProvider(registry *certificate.Registry) { + certificate.Register[option.CloudflareOriginCACertificateProviderOptions](registry, C.TypeCloudflareOriginCA, NewCertificateProvider) +} + +var _ adapter.CertificateProviderService = (*Service)(nil) + +type Service struct { + certificate.Adapter + logger log.ContextLogger + ctx context.Context + cancel context.CancelFunc + done chan struct{} + timeFunc func() time.Time + httpClient *http.Client + storage certmagic.Storage + storageIssuerKey string + storageNamesKey string + storageLockKey string + apiToken string + originCAKey string + domain []string + requestType option.CloudflareOriginCARequestType + requestedValidity option.CloudflareOriginCARequestValidity + + access sync.RWMutex + currentCertificate *tls.Certificate + currentLeaf *x509.Certificate +} + +func NewCertificateProvider(ctx context.Context, logger log.ContextLogger, tag string, options option.CloudflareOriginCACertificateProviderOptions) (adapter.CertificateProviderService, error) { + domain, err := normalizeHostnames(options.Domain) + if err != nil { + return nil, err + } + if len(domain) == 0 { + return nil, E.New("missing domain") + } + apiToken := strings.TrimSpace(options.APIToken) + originCAKey := strings.TrimSpace(options.OriginCAKey) + switch { + case apiToken == "" && originCAKey == "": + return nil, E.New("api_token or origin_ca_key is required") + case apiToken != "" && originCAKey != "": + return nil, E.New("api_token and origin_ca_key are mutually exclusive") + } + requestType := options.RequestType + if requestType == "" { + requestType = option.CloudflareOriginCARequestTypeOriginRSA + } + requestedValidity := options.RequestedValidity + if requestedValidity == 0 { + requestedValidity = defaultRequestedValidity + } + ctx, cancel := context.WithCancel(ctx) + serviceDialer, err := dialer.NewWithOptions(dialer.Options{ + Context: ctx, + Options: option.DialerOptions{ + Detour: options.Detour, + }, + RemoteIsDomain: true, + }) + if err != nil { + cancel() + return nil, E.Cause(err, "create Cloudflare Origin CA dialer") + } + var storage certmagic.Storage + if options.DataDirectory != "" { + storage = &certmagic.FileStorage{Path: options.DataDirectory} + } else { + storage = certmagic.Default.Storage + } + timeFunc := ntp.TimeFuncFromContext(ctx) + if timeFunc == nil { + timeFunc = time.Now + } + storageIssuerKey := C.TypeCloudflareOriginCA + "-" + string(requestType) + storageNamesKey := (&certmagic.CertificateResource{SANs: slices.Clone(domain)}).NamesKey() + storageLockKey := strings.Join([]string{ + storageLockPrefix, + certmagic.StorageKeys.Safe(storageIssuerKey), + certmagic.StorageKeys.Safe(storageNamesKey), + }, "/") + return &Service{ + Adapter: certificate.NewAdapter(C.TypeCloudflareOriginCA, tag), + logger: logger, + ctx: ctx, + cancel: cancel, + timeFunc: timeFunc, + httpClient: &http.Client{Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return serviceDialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + }, + TLSClientConfig: &tls.Config{ + RootCAs: adapter.RootPoolFromContext(ctx), + Time: timeFunc, + }, + ForceAttemptHTTP2: true, + }}, + storage: storage, + storageIssuerKey: storageIssuerKey, + storageNamesKey: storageNamesKey, + storageLockKey: storageLockKey, + apiToken: apiToken, + originCAKey: originCAKey, + domain: domain, + requestType: requestType, + requestedValidity: requestedValidity, + }, nil +} + +func (s *Service) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + cachedCertificate, cachedLeaf, err := s.loadCachedCertificate() + if err != nil { + s.logger.Warn(E.Cause(err, "load cached Cloudflare Origin CA certificate")) + } else if cachedCertificate != nil { + s.setCurrentCertificate(cachedCertificate, cachedLeaf) + } + if cachedCertificate == nil { + err = s.issueAndStoreCertificate() + if err != nil { + return err + } + } else if s.shouldRenew(cachedLeaf, s.timeFunc()) { + err = s.issueAndStoreCertificate() + if err != nil { + s.logger.Warn(E.Cause(err, "renew cached Cloudflare Origin CA certificate")) + } + } + s.done = make(chan struct{}) + go s.refreshLoop() + return nil +} + +func (s *Service) Close() error { + s.cancel() + if done := s.done; done != nil { + <-done + } + if transport, loaded := s.httpClient.Transport.(*http.Transport); loaded { + transport.CloseIdleConnections() + } + return nil +} + +func (s *Service) GetCertificate(_ *tls.ClientHelloInfo) (*tls.Certificate, error) { + s.access.RLock() + certificate := s.currentCertificate + s.access.RUnlock() + if certificate == nil { + return nil, E.New("Cloudflare Origin CA certificate is unavailable") + } + return certificate, nil +} + +func (s *Service) refreshLoop() { + defer close(s.done) + var retryDelay time.Duration + for { + waitDuration := retryDelay + if waitDuration == 0 { + s.access.RLock() + leaf := s.currentLeaf + s.access.RUnlock() + if leaf == nil { + waitDuration = minimumRenewRetryDelay + } else { + refreshAt := leaf.NotAfter.Add(-s.effectiveRenewBefore(leaf)) + waitDuration = refreshAt.Sub(s.timeFunc()) + if waitDuration < minimumRenewRetryDelay { + waitDuration = minimumRenewRetryDelay + } + } + } + timer := time.NewTimer(waitDuration) + select { + case <-s.ctx.Done(): + if !timer.Stop() { + select { + case <-timer.C: + default: + } + } + return + case <-timer.C: + } + err := s.issueAndStoreCertificate() + if err != nil { + s.logger.Error(E.Cause(err, "renew Cloudflare Origin CA certificate")) + s.access.RLock() + leaf := s.currentLeaf + s.access.RUnlock() + if leaf == nil { + retryDelay = minimumRenewRetryDelay + } else { + remaining := leaf.NotAfter.Sub(s.timeFunc()) + switch { + case remaining <= minimumRenewRetryDelay: + retryDelay = minimumRenewRetryDelay + case remaining < maximumRenewRetryDelay: + retryDelay = max(remaining/2, minimumRenewRetryDelay) + default: + retryDelay = maximumRenewRetryDelay + } + } + continue + } + retryDelay = 0 + } +} + +func (s *Service) shouldRenew(leaf *x509.Certificate, now time.Time) bool { + return !now.Before(leaf.NotAfter.Add(-s.effectiveRenewBefore(leaf))) +} + +func (s *Service) effectiveRenewBefore(leaf *x509.Certificate) time.Duration { + lifetime := leaf.NotAfter.Sub(leaf.NotBefore) + if lifetime <= 0 { + return 0 + } + return min(lifetime/3, defaultRenewBefore) +} + +func (s *Service) issueAndStoreCertificate() error { + err := s.storage.Lock(s.ctx, s.storageLockKey) + if err != nil { + return E.Cause(err, "lock Cloudflare Origin CA certificate storage") + } + defer func() { + err = s.storage.Unlock(context.WithoutCancel(s.ctx), s.storageLockKey) + if err != nil { + s.logger.Warn(E.Cause(err, "unlock Cloudflare Origin CA certificate storage")) + } + }() + cachedCertificate, cachedLeaf, err := s.loadCachedCertificate() + if err != nil { + s.logger.Warn(E.Cause(err, "load cached Cloudflare Origin CA certificate")) + } else if cachedCertificate != nil && !s.shouldRenew(cachedLeaf, s.timeFunc()) { + s.setCurrentCertificate(cachedCertificate, cachedLeaf) + return nil + } + certificatePEM, privateKeyPEM, tlsCertificate, leaf, err := s.requestCertificate(s.ctx) + if err != nil { + return err + } + issuerData, err := json.Marshal(originCAIssuerData{ + RequestType: s.requestType, + RequestedValidity: s.requestedValidity, + }) + if err != nil { + return E.Cause(err, "encode Cloudflare Origin CA certificate metadata") + } + err = storeCertificateResource(s.ctx, s.storage, s.storageIssuerKey, certmagic.CertificateResource{ + SANs: slices.Clone(s.domain), + CertificatePEM: certificatePEM, + PrivateKeyPEM: privateKeyPEM, + IssuerData: issuerData, + }) + if err != nil { + return E.Cause(err, "store Cloudflare Origin CA certificate") + } + s.setCurrentCertificate(tlsCertificate, leaf) + s.logger.Info("updated Cloudflare Origin CA certificate, expires at ", leaf.NotAfter.Format(time.RFC3339)) + return nil +} + +func (s *Service) requestCertificate(ctx context.Context) ([]byte, []byte, *tls.Certificate, *x509.Certificate, error) { + var privateKey crypto.Signer + switch s.requestType { + case option.CloudflareOriginCARequestTypeOriginRSA: + rsaKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, nil, nil, err + } + privateKey = rsaKey + case option.CloudflareOriginCARequestTypeOriginECC: + ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, nil, nil, err + } + privateKey = ecKey + default: + return nil, nil, nil, nil, E.New("unsupported Cloudflare Origin CA request type: ", s.requestType) + } + privateKeyDER, err := x509.MarshalPKCS8PrivateKey(privateKey) + if err != nil { + return nil, nil, nil, nil, E.Cause(err, "encode private key") + } + privateKeyPEM := pem.EncodeToMemory(&pem.Block{ + Type: "PRIVATE KEY", + Bytes: privateKeyDER, + }) + certificateRequestDER, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ + Subject: pkix.Name{CommonName: s.domain[0]}, + DNSNames: s.domain, + }, privateKey) + if err != nil { + return nil, nil, nil, nil, E.Cause(err, "create certificate request") + } + certificateRequestPEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE REQUEST", + Bytes: certificateRequestDER, + }) + requestBody, err := json.Marshal(originCARequest{ + CSR: string(certificateRequestPEM), + Hostnames: s.domain, + RequestType: string(s.requestType), + RequestedValidity: uint16(s.requestedValidity), + }) + if err != nil { + return nil, nil, nil, nil, E.Cause(err, "marshal request") + } + request, err := http.NewRequestWithContext(ctx, http.MethodPost, cloudflareOriginCAEndpoint, bytes.NewReader(requestBody)) + if err != nil { + return nil, nil, nil, nil, E.Cause(err, "create request") + } + request.Header.Set("Accept", "application/json") + request.Header.Set("Content-Type", "application/json") + request.Header.Set("User-Agent", "sing-box/"+C.Version) + if s.apiToken != "" { + request.Header.Set("Authorization", "Bearer "+s.apiToken) + } else { + request.Header.Set("X-Auth-User-Service-Key", s.originCAKey) + } + response, err := s.httpClient.Do(request) + if err != nil { + return nil, nil, nil, nil, E.Cause(err, "request certificate from Cloudflare") + } + defer response.Body.Close() + responseBody, err := io.ReadAll(response.Body) + if err != nil { + return nil, nil, nil, nil, E.Cause(err, "read Cloudflare response") + } + var responseEnvelope originCAResponse + err = json.Unmarshal(responseBody, &responseEnvelope) + if err != nil && response.StatusCode >= http.StatusOK && response.StatusCode < http.StatusMultipleChoices { + return nil, nil, nil, nil, E.Cause(err, "decode Cloudflare response") + } + if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices { + return nil, nil, nil, nil, buildOriginCAError(response.StatusCode, responseEnvelope.Errors, responseBody) + } + if !responseEnvelope.Success { + return nil, nil, nil, nil, buildOriginCAError(response.StatusCode, responseEnvelope.Errors, responseBody) + } + if responseEnvelope.Result.Certificate == "" { + return nil, nil, nil, nil, E.New("Cloudflare Origin CA response is missing certificate data") + } + certificatePEM := []byte(responseEnvelope.Result.Certificate) + tlsCertificate, leaf, err := parseKeyPair(certificatePEM, privateKeyPEM) + if err != nil { + return nil, nil, nil, nil, E.Cause(err, "parse issued certificate") + } + if !s.matchesCertificate(leaf) { + return nil, nil, nil, nil, E.New("issued Cloudflare Origin CA certificate does not match requested hostnames or key type") + } + return certificatePEM, privateKeyPEM, tlsCertificate, leaf, nil +} + +func (s *Service) loadCachedCertificate() (*tls.Certificate, *x509.Certificate, error) { + certificateResource, err := loadCertificateResource(s.ctx, s.storage, s.storageIssuerKey, s.storageNamesKey) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return nil, nil, nil + } + return nil, nil, err + } + tlsCertificate, leaf, err := parseKeyPair(certificateResource.CertificatePEM, certificateResource.PrivateKeyPEM) + if err != nil { + return nil, nil, E.Cause(err, "parse cached key pair") + } + if s.timeFunc().After(leaf.NotAfter) { + return nil, nil, nil + } + if !s.matchesCertificate(leaf) { + return nil, nil, nil + } + return tlsCertificate, leaf, nil +} + +func (s *Service) matchesCertificate(leaf *x509.Certificate) bool { + if leaf == nil { + return false + } + leafHostnames := leaf.DNSNames + if len(leafHostnames) == 0 && leaf.Subject.CommonName != "" { + leafHostnames = []string{leaf.Subject.CommonName} + } + normalizedLeafHostnames, err := normalizeHostnames(leafHostnames) + if err != nil { + return false + } + if !slices.Equal(normalizedLeafHostnames, s.domain) { + return false + } + switch s.requestType { + case option.CloudflareOriginCARequestTypeOriginRSA: + return leaf.PublicKeyAlgorithm == x509.RSA + case option.CloudflareOriginCARequestTypeOriginECC: + return leaf.PublicKeyAlgorithm == x509.ECDSA + default: + return false + } +} + +func (s *Service) setCurrentCertificate(certificate *tls.Certificate, leaf *x509.Certificate) { + s.access.Lock() + s.currentCertificate = certificate + s.currentLeaf = leaf + s.access.Unlock() +} + +func normalizeHostnames(hostnames []string) ([]string, error) { + normalizedHostnames := make([]string, 0, len(hostnames)) + seen := make(map[string]struct{}, len(hostnames)) + for _, hostname := range hostnames { + normalizedHostname := strings.ToLower(strings.TrimSpace(strings.TrimSuffix(hostname, "."))) + if normalizedHostname == "" { + return nil, E.New("hostname is empty") + } + if net.ParseIP(normalizedHostname) != nil { + return nil, E.New("hostname cannot be an IP address: ", normalizedHostname) + } + if strings.Contains(normalizedHostname, "*") { + if !strings.HasPrefix(normalizedHostname, "*.") || strings.Count(normalizedHostname, "*") != 1 { + return nil, E.New("invalid wildcard hostname: ", normalizedHostname) + } + suffix := strings.TrimPrefix(normalizedHostname, "*.") + if strings.Count(suffix, ".") == 0 { + return nil, E.New("wildcard hostname must cover a multi-label domain: ", normalizedHostname) + } + normalizedHostname = "*." + suffix + } + if _, loaded := seen[normalizedHostname]; loaded { + continue + } + seen[normalizedHostname] = struct{}{} + normalizedHostnames = append(normalizedHostnames, normalizedHostname) + } + slices.Sort(normalizedHostnames) + return normalizedHostnames, nil +} + +func parseKeyPair(certificatePEM []byte, privateKeyPEM []byte) (*tls.Certificate, *x509.Certificate, error) { + keyPair, err := tls.X509KeyPair(certificatePEM, privateKeyPEM) + if err != nil { + return nil, nil, err + } + if len(keyPair.Certificate) == 0 { + return nil, nil, E.New("certificate chain is empty") + } + leaf, err := x509.ParseCertificate(keyPair.Certificate[0]) + if err != nil { + return nil, nil, err + } + keyPair.Leaf = leaf + return &keyPair, leaf, nil +} + +func storeCertificateResource(ctx context.Context, storage certmagic.Storage, issuerKey string, certificateResource certmagic.CertificateResource) error { + metaBytes, err := json.MarshalIndent(certificateResource, "", "\t") + if err != nil { + return err + } + namesKey := certificateResource.NamesKey() + keyValueList := []struct { + key string + value []byte + }{ + { + key: certmagic.StorageKeys.SitePrivateKey(issuerKey, namesKey), + value: certificateResource.PrivateKeyPEM, + }, + { + key: certmagic.StorageKeys.SiteCert(issuerKey, namesKey), + value: certificateResource.CertificatePEM, + }, + { + key: certmagic.StorageKeys.SiteMeta(issuerKey, namesKey), + value: metaBytes, + }, + } + for i, item := range keyValueList { + err = storage.Store(ctx, item.key, item.value) + if err != nil { + for j := i - 1; j >= 0; j-- { + storage.Delete(ctx, keyValueList[j].key) + } + return err + } + } + return nil +} + +func loadCertificateResource(ctx context.Context, storage certmagic.Storage, issuerKey string, namesKey string) (certmagic.CertificateResource, error) { + privateKeyPEM, err := storage.Load(ctx, certmagic.StorageKeys.SitePrivateKey(issuerKey, namesKey)) + if err != nil { + return certmagic.CertificateResource{}, err + } + certificatePEM, err := storage.Load(ctx, certmagic.StorageKeys.SiteCert(issuerKey, namesKey)) + if err != nil { + return certmagic.CertificateResource{}, err + } + metaBytes, err := storage.Load(ctx, certmagic.StorageKeys.SiteMeta(issuerKey, namesKey)) + if err != nil { + return certmagic.CertificateResource{}, err + } + var certificateResource certmagic.CertificateResource + err = json.Unmarshal(metaBytes, &certificateResource) + if err != nil { + return certmagic.CertificateResource{}, E.Cause(err, "decode Cloudflare Origin CA certificate metadata") + } + certificateResource.PrivateKeyPEM = privateKeyPEM + certificateResource.CertificatePEM = certificatePEM + return certificateResource, nil +} + +func buildOriginCAError(statusCode int, responseErrors []originCAResponseError, responseBody []byte) error { + if len(responseErrors) > 0 { + messageList := make([]string, 0, len(responseErrors)) + for _, responseError := range responseErrors { + if responseError.Message == "" { + continue + } + if responseError.Code != 0 { + messageList = append(messageList, responseError.Message+" (code "+strconv.Itoa(responseError.Code)+")") + } else { + messageList = append(messageList, responseError.Message) + } + } + if len(messageList) > 0 { + return E.New("Cloudflare Origin CA request failed: HTTP ", statusCode, " ", strings.Join(messageList, ", ")) + } + } + responseText := strings.TrimSpace(string(responseBody)) + if responseText == "" { + return E.New("Cloudflare Origin CA request failed: HTTP ", statusCode) + } + return E.New("Cloudflare Origin CA request failed: HTTP ", statusCode, " ", responseText) +} + +type originCARequest struct { + CSR string `json:"csr"` + Hostnames []string `json:"hostnames"` + RequestType string `json:"request_type"` + RequestedValidity uint16 `json:"requested_validity"` +} + +type originCAResponse struct { + Success bool `json:"success"` + Errors []originCAResponseError `json:"errors"` + Result originCAResponseResult `json:"result"` +} + +type originCAResponseError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +type originCAResponseResult struct { + Certificate string `json:"certificate"` +} + +type originCAIssuerData struct { + RequestType option.CloudflareOriginCARequestType `json:"request_type,omitempty"` + RequestedValidity option.CloudflareOriginCARequestValidity `json:"requested_validity,omitempty"` +} From 0155352ff14fa4fa69a9bbcddd3ba869f561be79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 30 Mar 2026 23:45:16 +0800 Subject: [PATCH 06/93] Add BBR profile and hop interval randomization for Hysteria2 --- docs/configuration/inbound/hysteria2.md | 13 ++++++++++ docs/configuration/inbound/hysteria2.zh.md | 13 ++++++++++ docs/configuration/outbound/hysteria2.md | 27 +++++++++++++++++++-- docs/configuration/outbound/hysteria2.zh.md | 25 ++++++++++++++++++- go.mod | 6 ++--- go.sum | 4 +-- option/hysteria2.go | 19 +++++++++------ protocol/hysteria2/inbound.go | 1 + protocol/hysteria2/outbound.go | 2 ++ 9 files changed, 94 insertions(+), 16 deletions(-) diff --git a/docs/configuration/inbound/hysteria2.md b/docs/configuration/inbound/hysteria2.md index 3b7332b064..8426be2459 100644 --- a/docs/configuration/inbound/hysteria2.md +++ b/docs/configuration/inbound/hysteria2.md @@ -2,6 +2,10 @@ icon: material/alert-decagram --- +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [bbr_profile](#bbr_profile) + !!! quote "Changes in sing-box 1.11.0" :material-alert: [masquerade](#masquerade) @@ -31,6 +35,7 @@ icon: material/alert-decagram "ignore_client_bandwidth": false, "tls": {}, "masquerade": "", // or {} + "bbr_profile": "", "brutal_debug": false } ``` @@ -141,6 +146,14 @@ Fixed response headers. Fixed response content. +#### bbr_profile + +!!! question "Since sing-box 1.14.0" + +BBR congestion control algorithm profile, one of `conservative` `standard` `aggressive`. + +`standard` is used by default. + #### brutal_debug Enable debug information logging for Hysteria Brutal CC. diff --git a/docs/configuration/inbound/hysteria2.zh.md b/docs/configuration/inbound/hysteria2.zh.md index 35a3c25bc7..0c5e918ed9 100644 --- a/docs/configuration/inbound/hysteria2.zh.md +++ b/docs/configuration/inbound/hysteria2.zh.md @@ -2,6 +2,10 @@ icon: material/alert-decagram --- +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [bbr_profile](#bbr_profile) + !!! quote "sing-box 1.11.0 中的更改" :material-alert: [masquerade](#masquerade) @@ -31,6 +35,7 @@ icon: material/alert-decagram "ignore_client_bandwidth": false, "tls": {}, "masquerade": "", // 或 {} + "bbr_profile": "", "brutal_debug": false } ``` @@ -138,6 +143,14 @@ HTTP3 服务器认证失败时的行为 (对象配置)。 固定响应内容。 +#### bbr_profile + +!!! question "自 sing-box 1.14.0 起" + +BBR 拥塞控制算法配置,可选 `conservative` `standard` `aggressive`。 + +默认使用 `standard`。 + #### brutal_debug 启用 Hysteria Brutal CC 的调试信息日志记录。 diff --git a/docs/configuration/outbound/hysteria2.md b/docs/configuration/outbound/hysteria2.md index dc0a496500..a71dd1e070 100644 --- a/docs/configuration/outbound/hysteria2.md +++ b/docs/configuration/outbound/hysteria2.md @@ -1,3 +1,8 @@ +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [hop_interval_max](#hop_interval_max) + :material-plus: [bbr_profile](#bbr_profile) + !!! quote "Changes in sing-box 1.11.0" :material-plus: [server_ports](#server_ports) @@ -9,13 +14,14 @@ { "type": "hysteria2", "tag": "hy2-out", - + "server": "127.0.0.1", "server_port": 1080, "server_ports": [ "2080:3000" ], "hop_interval": "", + "hop_interval_max": "", "up_mbps": 100, "down_mbps": 100, "obfs": { @@ -25,8 +31,9 @@ "password": "goofy_ahh_password", "network": "tcp", "tls": {}, + "bbr_profile": "", "brutal_debug": false, - + ... // Dial Fields } ``` @@ -75,6 +82,14 @@ Port hopping interval. `30s` is used by default. +#### hop_interval_max + +!!! question "Since sing-box 1.14.0" + +Maximum port hopping interval, used for randomization. + +If set, the actual hop interval will be randomly chosen between `hop_interval` and `hop_interval_max`. + #### up_mbps, down_mbps Max bandwidth, in Mbps. @@ -109,6 +124,14 @@ Both is enabled by default. TLS configuration, see [TLS](/configuration/shared/tls/#outbound). +#### bbr_profile + +!!! question "Since sing-box 1.14.0" + +BBR congestion control algorithm profile, one of `conservative` `standard` `aggressive`. + +`standard` is used by default. + #### brutal_debug Enable debug information logging for Hysteria Brutal CC. diff --git a/docs/configuration/outbound/hysteria2.zh.md b/docs/configuration/outbound/hysteria2.zh.md index bc77f4ec92..0fb17bbdc3 100644 --- a/docs/configuration/outbound/hysteria2.zh.md +++ b/docs/configuration/outbound/hysteria2.zh.md @@ -1,3 +1,8 @@ +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [hop_interval_max](#hop_interval_max) + :material-plus: [bbr_profile](#bbr_profile) + !!! quote "sing-box 1.11.0 中的更改" :material-plus: [server_ports](#server_ports) @@ -16,6 +21,7 @@ "2080:3000" ], "hop_interval": "", + "hop_interval_max": "", "up_mbps": 100, "down_mbps": 100, "obfs": { @@ -25,8 +31,9 @@ "password": "goofy_ahh_password", "network": "tcp", "tls": {}, + "bbr_profile": "", "brutal_debug": false, - + ... // 拨号字段 } ``` @@ -73,6 +80,14 @@ 默认使用 `30s`。 +#### hop_interval_max + +!!! question "自 sing-box 1.14.0 起" + +最大端口跳跃间隔,用于随机化。 + +如果设置,实际跳跃间隔将在 `hop_interval` 和 `hop_interval_max` 之间随机选择。 + #### up_mbps, down_mbps 最大带宽。 @@ -107,6 +122,14 @@ QUIC 流量混淆器密码. TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 +#### bbr_profile + +!!! question "自 sing-box 1.14.0 起" + +BBR 拥塞控制算法配置,可选 `conservative` `standard` `aggressive`。 + +默认使用 `standard`。 + #### brutal_debug 启用 Hysteria Brutal CC 的调试信息日志记录。 diff --git a/go.mod b/go.mod index db27120bd5..46aadde68a 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/anthropics/anthropic-sdk-go v1.26.0 github.com/anytls/sing-anytls v0.0.11 github.com/caddyserver/certmagic v0.25.2 + github.com/caddyserver/zerossl v0.1.5 github.com/coder/websocket v1.8.14 github.com/cretz/bine v0.2.0 github.com/database64128/tfo-go/v2 v2.3.2 @@ -19,6 +20,7 @@ require ( github.com/libdns/acmedns v0.5.0 github.com/libdns/alidns v1.0.6 github.com/libdns/cloudflare v0.2.2 + github.com/libdns/libdns v1.1.1 github.com/logrusorgru/aurora v2.0.3+incompatible github.com/mdlayher/netlink v1.9.0 github.com/metacubex/utls v1.8.4 @@ -37,7 +39,7 @@ require ( github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 github.com/sagernet/sing v0.8.10-0.20260421111925-3e730f2301b4 github.com/sagernet/sing-mux v0.3.4 - github.com/sagernet/sing-quic v0.6.1 + github.com/sagernet/sing-quic v0.6.2-0.20260330152607-bf674c163212 github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 @@ -69,7 +71,6 @@ require ( github.com/akutz/memconn v0.1.0 // indirect github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect github.com/andybalholm/brotli v1.1.0 // indirect - github.com/caddyserver/zerossl v0.1.5 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect github.com/database64128/netx-go v0.1.1 // indirect @@ -96,7 +97,6 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect - github.com/libdns/libdns v1.1.1 // indirect github.com/mdlayher/socket v0.5.1 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect diff --git a/go.sum b/go.sum index 3b1f4c2098..263305fde8 100644 --- a/go.sum +++ b/go.sum @@ -240,8 +240,8 @@ github.com/sagernet/sing v0.8.10-0.20260421111925-3e730f2301b4 h1:pqb1VEG6BXNTid github.com/sagernet/sing v0.8.10-0.20260421111925-3e730f2301b4/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s= github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= -github.com/sagernet/sing-quic v0.6.1 h1:lx0tcm99wIA1RkyvILNzRSsMy1k7TTQYIhx71E/WBlw= -github.com/sagernet/sing-quic v0.6.1/go.mod h1:K5bWvITOm4vE10fwLfrWpw27bCoVJ+tfQ79tOWg+Ko8= +github.com/sagernet/sing-quic v0.6.2-0.20260330152607-bf674c163212 h1:7mFOUqy+DyOj7qKGd1X54UMXbnbJiiMileK/tn17xYc= +github.com/sagernet/sing-quic v0.6.2-0.20260330152607-bf674c163212/go.mod h1:K5bWvITOm4vE10fwLfrWpw27bCoVJ+tfQ79tOWg+Ko8= github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE= github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI= github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo= diff --git a/option/hysteria2.go b/option/hysteria2.go index a014513630..e31c8de345 100644 --- a/option/hysteria2.go +++ b/option/hysteria2.go @@ -19,6 +19,7 @@ type Hysteria2InboundOptions struct { IgnoreClientBandwidth bool `json:"ignore_client_bandwidth,omitempty"` InboundTLSOptionsContainer Masquerade *Hysteria2Masquerade `json:"masquerade,omitempty"` + BBRProfile string `json:"bbr_profile,omitempty"` BrutalDebug bool `json:"brutal_debug,omitempty"` } @@ -112,13 +113,15 @@ type Hysteria2MasqueradeString struct { type Hysteria2OutboundOptions struct { DialerOptions ServerOptions - ServerPorts badoption.Listable[string] `json:"server_ports,omitempty"` - HopInterval badoption.Duration `json:"hop_interval,omitempty"` - UpMbps int `json:"up_mbps,omitempty"` - DownMbps int `json:"down_mbps,omitempty"` - Obfs *Hysteria2Obfs `json:"obfs,omitempty"` - Password string `json:"password,omitempty"` - Network NetworkList `json:"network,omitempty"` + ServerPorts badoption.Listable[string] `json:"server_ports,omitempty"` + HopInterval badoption.Duration `json:"hop_interval,omitempty"` + HopIntervalMax badoption.Duration `json:"hop_interval_max,omitempty"` + UpMbps int `json:"up_mbps,omitempty"` + DownMbps int `json:"down_mbps,omitempty"` + Obfs *Hysteria2Obfs `json:"obfs,omitempty"` + Password string `json:"password,omitempty"` + Network NetworkList `json:"network,omitempty"` OutboundTLSOptionsContainer - BrutalDebug bool `json:"brutal_debug,omitempty"` + BBRProfile string `json:"bbr_profile,omitempty"` + BrutalDebug bool `json:"brutal_debug,omitempty"` } diff --git a/protocol/hysteria2/inbound.go b/protocol/hysteria2/inbound.go index bb5980701f..5fe8848d9a 100644 --- a/protocol/hysteria2/inbound.go +++ b/protocol/hysteria2/inbound.go @@ -125,6 +125,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo UDPTimeout: udpTimeout, Handler: inbound, MasqueradeHandler: masqueradeHandler, + BBRProfile: options.BBRProfile, }) if err != nil { return nil, err diff --git a/protocol/hysteria2/outbound.go b/protocol/hysteria2/outbound.go index d4382fdcdf..4a0c9f2430 100644 --- a/protocol/hysteria2/outbound.go +++ b/protocol/hysteria2/outbound.go @@ -73,12 +73,14 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL ServerAddress: options.ServerOptions.Build(), ServerPorts: options.ServerPorts, HopInterval: time.Duration(options.HopInterval), + HopIntervalMax: time.Duration(options.HopIntervalMax), SendBPS: uint64(options.UpMbps * hysteria.MbpsToBps), ReceiveBPS: uint64(options.DownMbps * hysteria.MbpsToBps), SalamanderPassword: salamanderPassword, Password: options.Password, TLSConfig: tlsConfig, UDPDisabled: !common.Contains(networkList, N.NetworkUDP), + BBRProfile: options.BBRProfile, }) if err != nil { return nil, err From 26ddb928d93f1a793300a2b2b2ef8b30d9015231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 2 Apr 2026 16:39:34 +0800 Subject: [PATCH 07/93] platform: Add OOM Report & Crash Report --- daemon/instance.go | 9 +- daemon/platform.go | 1 + daemon/started_service.go | 68 ++- daemon/started_service.pb.go | 343 +++++++++------ daemon/started_service.proto | 13 +- daemon/started_service_grpc.pb.go | 78 ++++ experimental/libbox/command_client.go | 25 ++ experimental/libbox/command_server.go | 22 +- experimental/libbox/config.go | 6 + experimental/libbox/debug.go | 9 + .../libbox/internal/oomprofile/builder.go | 390 ++++++++++++++++++ .../internal/oomprofile/defs_darwin_amd64.go | 24 ++ .../internal/oomprofile/defs_darwin_arm64.go | 24 ++ .../libbox/internal/oomprofile/linkname.go | 47 +++ .../internal/oomprofile/mapping_darwin.go | 57 +++ .../internal/oomprofile/mapping_linux.go | 13 + .../internal/oomprofile/mapping_windows.go | 58 +++ .../libbox/internal/oomprofile/oomprofile.go | 380 +++++++++++++++++ .../libbox/internal/oomprofile/protobuf.go | 120 ++++++ experimental/libbox/log.go | 142 ++++++- experimental/libbox/memory.go | 26 -- experimental/libbox/oom_report.go | 141 +++++++ experimental/libbox/report.go | 97 +++++ experimental/libbox/setup.go | 43 +- option/oom_killer.go | 11 +- service/oomkiller/config.go | 51 --- service/oomkiller/policy.go | 46 +++ service/oomkiller/service.go | 193 ++------- service/oomkiller/service_darwin.go | 103 +++++ service/oomkiller/service_stub.go | 63 +-- service/oomkiller/service_timer.go | 158 ------- service/oomkiller/timer.go | 325 +++++++++++++++ 32 files changed, 2487 insertions(+), 599 deletions(-) create mode 100644 experimental/libbox/debug.go create mode 100644 experimental/libbox/internal/oomprofile/builder.go create mode 100644 experimental/libbox/internal/oomprofile/defs_darwin_amd64.go create mode 100644 experimental/libbox/internal/oomprofile/defs_darwin_arm64.go create mode 100644 experimental/libbox/internal/oomprofile/linkname.go create mode 100644 experimental/libbox/internal/oomprofile/mapping_darwin.go create mode 100644 experimental/libbox/internal/oomprofile/mapping_linux.go create mode 100644 experimental/libbox/internal/oomprofile/mapping_windows.go create mode 100644 experimental/libbox/internal/oomprofile/oomprofile.go create mode 100644 experimental/libbox/internal/oomprofile/protobuf.go delete mode 100644 experimental/libbox/memory.go create mode 100644 experimental/libbox/oom_report.go create mode 100644 experimental/libbox/report.go delete mode 100644 service/oomkiller/config.go create mode 100644 service/oomkiller/policy.go create mode 100644 service/oomkiller/service_darwin.go delete mode 100644 service/oomkiller/service_timer.go create mode 100644 service/oomkiller/timer.go diff --git a/daemon/instance.go b/daemon/instance.go index 3acf75ccf9..f16e594e2c 100644 --- a/daemon/instance.go +++ b/daemon/instance.go @@ -87,12 +87,17 @@ func (s *StartedService) newInstance(profileContent string, overrideOptions *Ove } } } - if s.oomKiller && C.IsIos { + if s.oomKillerEnabled { if !common.Any(options.Services, func(it option.Service) bool { return it.Type == C.TypeOOMKiller }) { + oomOptions := &option.OOMKillerServiceOptions{ + KillerDisabled: s.oomKillerDisabled, + MemoryLimitOverride: s.oomMemoryLimit, + } options.Services = append(options.Services, option.Service{ - Type: C.TypeOOMKiller, + Type: C.TypeOOMKiller, + Options: oomOptions, }) } } diff --git a/daemon/platform.go b/daemon/platform.go index 37906aff08..ae954c5785 100644 --- a/daemon/platform.go +++ b/daemon/platform.go @@ -5,5 +5,6 @@ type PlatformHandler interface { ServiceReload() error SystemProxyStatus() (*SystemProxyStatus, error) SetSystemProxyEnabled(enabled bool) error + TriggerNativeCrash() error WriteDebugMessage(message string) } diff --git a/daemon/started_service.go b/daemon/started_service.go index c260e8cb71..9622d88b40 100644 --- a/daemon/started_service.go +++ b/daemon/started_service.go @@ -14,6 +14,7 @@ import ( "github.com/sagernet/sing-box/experimental/deprecated" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/protocol/group" + "github.com/sagernet/sing-box/service/oomkiller" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/batch" E "github.com/sagernet/sing/common/exceptions" @@ -24,6 +25,8 @@ import ( "github.com/gofrs/uuid/v5" "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/emptypb" ) @@ -32,10 +35,12 @@ var _ StartedServiceServer = (*StartedService)(nil) type StartedService struct { ctx context.Context // platform adapter.PlatformInterface - handler PlatformHandler - debug bool - logMaxLines int - oomKiller bool + handler PlatformHandler + debug bool + logMaxLines int + oomKillerEnabled bool + oomKillerDisabled bool + oomMemoryLimit uint64 // workingDirectory string // tempDirectory string // userID int @@ -64,10 +69,12 @@ type StartedService struct { type ServiceOptions struct { Context context.Context // Platform adapter.PlatformInterface - Handler PlatformHandler - Debug bool - LogMaxLines int - OOMKiller bool + Handler PlatformHandler + Debug bool + LogMaxLines int + OOMKillerEnabled bool + OOMKillerDisabled bool + OOMMemoryLimit uint64 // WorkingDirectory string // TempDirectory string // UserID int @@ -79,10 +86,12 @@ func NewStartedService(options ServiceOptions) *StartedService { s := &StartedService{ ctx: options.Context, // platform: options.Platform, - handler: options.Handler, - debug: options.Debug, - logMaxLines: options.LogMaxLines, - oomKiller: options.OOMKiller, + handler: options.Handler, + debug: options.Debug, + logMaxLines: options.LogMaxLines, + oomKillerEnabled: options.OOMKillerEnabled, + oomKillerDisabled: options.OOMKillerDisabled, + oomMemoryLimit: options.OOMMemoryLimit, // workingDirectory: options.WorkingDirectory, // tempDirectory: options.TempDirectory, // userID: options.UserID, @@ -685,6 +694,41 @@ func (s *StartedService) SetSystemProxyEnabled(ctx context.Context, request *Set return nil, err } +func (s *StartedService) TriggerDebugCrash(ctx context.Context, request *DebugCrashRequest) (*emptypb.Empty, error) { + if !s.debug { + return nil, status.Error(codes.PermissionDenied, "debug crash trigger unavailable") + } + if request == nil { + return nil, status.Error(codes.InvalidArgument, "missing debug crash request") + } + switch request.Type { + case DebugCrashRequest_GO: + time.AfterFunc(200*time.Millisecond, func() { + panic("debug go crash") + }) + case DebugCrashRequest_NATIVE: + err := s.handler.TriggerNativeCrash() + if err != nil { + return nil, err + } + default: + return nil, status.Error(codes.InvalidArgument, "unknown debug crash type") + } + return &emptypb.Empty{}, nil +} + +func (s *StartedService) TriggerOOMReport(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) { + instance := s.Instance() + if instance == nil { + return nil, status.Error(codes.FailedPrecondition, "service not started") + } + reporter := service.FromContext[oomkiller.OOMReporter](instance.ctx) + if reporter == nil { + return nil, status.Error(codes.Unavailable, "OOM reporter not available") + } + return &emptypb.Empty{}, reporter.WriteReport(memory.Total()) +} + func (s *StartedService) SubscribeConnections(request *SubscribeConnectionsRequest, server grpc.ServerStreamingServer[ConnectionEvents]) error { err := s.waitForStarted(server.Context()) if err != nil { diff --git a/daemon/started_service.pb.go b/daemon/started_service.pb.go index 927fb5149d..403ba66050 100644 --- a/daemon/started_service.pb.go +++ b/daemon/started_service.pb.go @@ -182,6 +182,52 @@ func (ServiceStatus_Type) EnumDescriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{0, 0} } +type DebugCrashRequest_Type int32 + +const ( + DebugCrashRequest_GO DebugCrashRequest_Type = 0 + DebugCrashRequest_NATIVE DebugCrashRequest_Type = 1 +) + +// Enum value maps for DebugCrashRequest_Type. +var ( + DebugCrashRequest_Type_name = map[int32]string{ + 0: "GO", + 1: "NATIVE", + } + DebugCrashRequest_Type_value = map[string]int32{ + "GO": 0, + "NATIVE": 1, + } +) + +func (x DebugCrashRequest_Type) Enum() *DebugCrashRequest_Type { + p := new(DebugCrashRequest_Type) + *p = x + return p +} + +func (x DebugCrashRequest_Type) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (DebugCrashRequest_Type) Descriptor() protoreflect.EnumDescriptor { + return file_daemon_started_service_proto_enumTypes[3].Descriptor() +} + +func (DebugCrashRequest_Type) Type() protoreflect.EnumType { + return &file_daemon_started_service_proto_enumTypes[3] +} + +func (x DebugCrashRequest_Type) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use DebugCrashRequest_Type.Descriptor instead. +func (DebugCrashRequest_Type) EnumDescriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{16, 0} +} + type ServiceStatus struct { state protoimpl.MessageState `protogen:"open.v1"` Status ServiceStatus_Type `protobuf:"varint,1,opt,name=status,proto3,enum=daemon.ServiceStatus_Type" json:"status,omitempty"` @@ -1062,6 +1108,50 @@ func (x *SetSystemProxyEnabledRequest) GetEnabled() bool { return false } +type DebugCrashRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type DebugCrashRequest_Type `protobuf:"varint,1,opt,name=type,proto3,enum=daemon.DebugCrashRequest_Type" json:"type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DebugCrashRequest) Reset() { + *x = DebugCrashRequest{} + mi := &file_daemon_started_service_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DebugCrashRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DebugCrashRequest) ProtoMessage() {} + +func (x *DebugCrashRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DebugCrashRequest.ProtoReflect.Descriptor instead. +func (*DebugCrashRequest) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{16} +} + +func (x *DebugCrashRequest) GetType() DebugCrashRequest_Type { + if x != nil { + return x.Type + } + return DebugCrashRequest_GO +} + type SubscribeConnectionsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Interval int64 `protobuf:"varint,1,opt,name=interval,proto3" json:"interval,omitempty"` @@ -1071,7 +1161,7 @@ type SubscribeConnectionsRequest struct { func (x *SubscribeConnectionsRequest) Reset() { *x = SubscribeConnectionsRequest{} - mi := &file_daemon_started_service_proto_msgTypes[16] + mi := &file_daemon_started_service_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1083,7 +1173,7 @@ func (x *SubscribeConnectionsRequest) String() string { func (*SubscribeConnectionsRequest) ProtoMessage() {} func (x *SubscribeConnectionsRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[16] + mi := &file_daemon_started_service_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1096,7 +1186,7 @@ func (x *SubscribeConnectionsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SubscribeConnectionsRequest.ProtoReflect.Descriptor instead. func (*SubscribeConnectionsRequest) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{16} + return file_daemon_started_service_proto_rawDescGZIP(), []int{17} } func (x *SubscribeConnectionsRequest) GetInterval() int64 { @@ -1120,7 +1210,7 @@ type ConnectionEvent struct { func (x *ConnectionEvent) Reset() { *x = ConnectionEvent{} - mi := &file_daemon_started_service_proto_msgTypes[17] + mi := &file_daemon_started_service_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1132,7 +1222,7 @@ func (x *ConnectionEvent) String() string { func (*ConnectionEvent) ProtoMessage() {} func (x *ConnectionEvent) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[17] + mi := &file_daemon_started_service_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1145,7 +1235,7 @@ func (x *ConnectionEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use ConnectionEvent.ProtoReflect.Descriptor instead. func (*ConnectionEvent) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{17} + return file_daemon_started_service_proto_rawDescGZIP(), []int{18} } func (x *ConnectionEvent) GetType() ConnectionEventType { @@ -1200,7 +1290,7 @@ type ConnectionEvents struct { func (x *ConnectionEvents) Reset() { *x = ConnectionEvents{} - mi := &file_daemon_started_service_proto_msgTypes[18] + mi := &file_daemon_started_service_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1212,7 +1302,7 @@ func (x *ConnectionEvents) String() string { func (*ConnectionEvents) ProtoMessage() {} func (x *ConnectionEvents) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[18] + mi := &file_daemon_started_service_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1225,7 +1315,7 @@ func (x *ConnectionEvents) ProtoReflect() protoreflect.Message { // Deprecated: Use ConnectionEvents.ProtoReflect.Descriptor instead. func (*ConnectionEvents) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{18} + return file_daemon_started_service_proto_rawDescGZIP(), []int{19} } func (x *ConnectionEvents) GetEvents() []*ConnectionEvent { @@ -1272,7 +1362,7 @@ type Connection struct { func (x *Connection) Reset() { *x = Connection{} - mi := &file_daemon_started_service_proto_msgTypes[19] + mi := &file_daemon_started_service_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1284,7 +1374,7 @@ func (x *Connection) String() string { func (*Connection) ProtoMessage() {} func (x *Connection) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[19] + mi := &file_daemon_started_service_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1297,7 +1387,7 @@ func (x *Connection) ProtoReflect() protoreflect.Message { // Deprecated: Use Connection.ProtoReflect.Descriptor instead. func (*Connection) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{19} + return file_daemon_started_service_proto_rawDescGZIP(), []int{20} } func (x *Connection) GetId() string { @@ -1467,7 +1557,7 @@ type ProcessInfo struct { func (x *ProcessInfo) Reset() { *x = ProcessInfo{} - mi := &file_daemon_started_service_proto_msgTypes[20] + mi := &file_daemon_started_service_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1479,7 +1569,7 @@ func (x *ProcessInfo) String() string { func (*ProcessInfo) ProtoMessage() {} func (x *ProcessInfo) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[20] + mi := &file_daemon_started_service_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1492,7 +1582,7 @@ func (x *ProcessInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use ProcessInfo.ProtoReflect.Descriptor instead. func (*ProcessInfo) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{20} + return file_daemon_started_service_proto_rawDescGZIP(), []int{21} } func (x *ProcessInfo) GetProcessId() uint32 { @@ -1539,7 +1629,7 @@ type CloseConnectionRequest struct { func (x *CloseConnectionRequest) Reset() { *x = CloseConnectionRequest{} - mi := &file_daemon_started_service_proto_msgTypes[21] + mi := &file_daemon_started_service_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1551,7 +1641,7 @@ func (x *CloseConnectionRequest) String() string { func (*CloseConnectionRequest) ProtoMessage() {} func (x *CloseConnectionRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[21] + mi := &file_daemon_started_service_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1564,7 +1654,7 @@ func (x *CloseConnectionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CloseConnectionRequest.ProtoReflect.Descriptor instead. func (*CloseConnectionRequest) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{21} + return file_daemon_started_service_proto_rawDescGZIP(), []int{22} } func (x *CloseConnectionRequest) GetId() string { @@ -1583,7 +1673,7 @@ type DeprecatedWarnings struct { func (x *DeprecatedWarnings) Reset() { *x = DeprecatedWarnings{} - mi := &file_daemon_started_service_proto_msgTypes[22] + mi := &file_daemon_started_service_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1595,7 +1685,7 @@ func (x *DeprecatedWarnings) String() string { func (*DeprecatedWarnings) ProtoMessage() {} func (x *DeprecatedWarnings) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[22] + mi := &file_daemon_started_service_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1608,7 +1698,7 @@ func (x *DeprecatedWarnings) ProtoReflect() protoreflect.Message { // Deprecated: Use DeprecatedWarnings.ProtoReflect.Descriptor instead. func (*DeprecatedWarnings) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{22} + return file_daemon_started_service_proto_rawDescGZIP(), []int{23} } func (x *DeprecatedWarnings) GetWarnings() []*DeprecatedWarning { @@ -1629,7 +1719,7 @@ type DeprecatedWarning struct { func (x *DeprecatedWarning) Reset() { *x = DeprecatedWarning{} - mi := &file_daemon_started_service_proto_msgTypes[23] + mi := &file_daemon_started_service_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1641,7 +1731,7 @@ func (x *DeprecatedWarning) String() string { func (*DeprecatedWarning) ProtoMessage() {} func (x *DeprecatedWarning) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[23] + mi := &file_daemon_started_service_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1654,7 +1744,7 @@ func (x *DeprecatedWarning) ProtoReflect() protoreflect.Message { // Deprecated: Use DeprecatedWarning.ProtoReflect.Descriptor instead. func (*DeprecatedWarning) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{23} + return file_daemon_started_service_proto_rawDescGZIP(), []int{24} } func (x *DeprecatedWarning) GetMessage() string { @@ -1687,7 +1777,7 @@ type StartedAt struct { func (x *StartedAt) Reset() { *x = StartedAt{} - mi := &file_daemon_started_service_proto_msgTypes[24] + mi := &file_daemon_started_service_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1699,7 +1789,7 @@ func (x *StartedAt) String() string { func (*StartedAt) ProtoMessage() {} func (x *StartedAt) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[24] + mi := &file_daemon_started_service_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1712,7 +1802,7 @@ func (x *StartedAt) ProtoReflect() protoreflect.Message { // Deprecated: Use StartedAt.ProtoReflect.Descriptor instead. func (*StartedAt) Descriptor() ([]byte, []int) { - return file_daemon_started_service_proto_rawDescGZIP(), []int{24} + return file_daemon_started_service_proto_rawDescGZIP(), []int{25} } func (x *StartedAt) GetStartedAt() int64 { @@ -1732,7 +1822,7 @@ type Log_Message struct { func (x *Log_Message) Reset() { *x = Log_Message{} - mi := &file_daemon_started_service_proto_msgTypes[25] + mi := &file_daemon_started_service_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1744,7 +1834,7 @@ func (x *Log_Message) String() string { func (*Log_Message) ProtoMessage() {} func (x *Log_Message) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[25] + mi := &file_daemon_started_service_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1845,7 +1935,13 @@ const file_daemon_started_service_proto_rawDesc = "" + "\tavailable\x18\x01 \x01(\bR\tavailable\x12\x18\n" + "\aenabled\x18\x02 \x01(\bR\aenabled\"8\n" + "\x1cSetSystemProxyEnabledRequest\x12\x18\n" + - "\aenabled\x18\x01 \x01(\bR\aenabled\"9\n" + + "\aenabled\x18\x01 \x01(\bR\aenabled\"c\n" + + "\x11DebugCrashRequest\x122\n" + + "\x04type\x18\x01 \x01(\x0e2\x1e.daemon.DebugCrashRequest.TypeR\x04type\"\x1a\n" + + "\x04Type\x12\x06\n" + + "\x02GO\x10\x00\x12\n" + + "\n" + + "\x06NATIVE\x10\x01\"9\n" + "\x1bSubscribeConnectionsRequest\x12\x1a\n" + "\binterval\x18\x01 \x01(\x03R\binterval\"\xea\x01\n" + "\x0fConnectionEvent\x12/\n" + @@ -1912,7 +2008,7 @@ const file_daemon_started_service_proto_rawDesc = "" + "\x13ConnectionEventType\x12\x18\n" + "\x14CONNECTION_EVENT_NEW\x10\x00\x12\x1b\n" + "\x17CONNECTION_EVENT_UPDATE\x10\x01\x12\x1b\n" + - "\x17CONNECTION_EVENT_CLOSED\x10\x022\xe5\v\n" + + "\x17CONNECTION_EVENT_CLOSED\x10\x022\xf5\f\n" + "\x0eStartedService\x12=\n" + "\vStopService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12?\n" + "\rReloadService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12K\n" + @@ -1929,7 +2025,9 @@ const file_daemon_started_service_proto_rawDesc = "" + "\x0eSelectOutbound\x12\x1d.daemon.SelectOutboundRequest\x1a\x16.google.protobuf.Empty\"\x00\x12I\n" + "\x0eSetGroupExpand\x12\x1d.daemon.SetGroupExpandRequest\x1a\x16.google.protobuf.Empty\"\x00\x12K\n" + "\x14GetSystemProxyStatus\x12\x16.google.protobuf.Empty\x1a\x19.daemon.SystemProxyStatus\"\x00\x12W\n" + - "\x15SetSystemProxyEnabled\x12$.daemon.SetSystemProxyEnabledRequest\x1a\x16.google.protobuf.Empty\"\x00\x12Y\n" + + "\x15SetSystemProxyEnabled\x12$.daemon.SetSystemProxyEnabledRequest\x1a\x16.google.protobuf.Empty\"\x00\x12H\n" + + "\x11TriggerDebugCrash\x12\x19.daemon.DebugCrashRequest\x1a\x16.google.protobuf.Empty\"\x00\x12D\n" + + "\x10TriggerOOMReport\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12Y\n" + "\x14SubscribeConnections\x12#.daemon.SubscribeConnectionsRequest\x1a\x18.daemon.ConnectionEvents\"\x000\x01\x12K\n" + "\x0fCloseConnection\x12\x1e.daemon.CloseConnectionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12G\n" + "\x13CloseAllConnections\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12M\n" + @@ -1949,101 +2047,108 @@ func file_daemon_started_service_proto_rawDescGZIP() []byte { } var ( - file_daemon_started_service_proto_enumTypes = make([]protoimpl.EnumInfo, 3) - file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 26) + file_daemon_started_service_proto_enumTypes = make([]protoimpl.EnumInfo, 4) + file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 27) file_daemon_started_service_proto_goTypes = []any{ (LogLevel)(0), // 0: daemon.LogLevel (ConnectionEventType)(0), // 1: daemon.ConnectionEventType (ServiceStatus_Type)(0), // 2: daemon.ServiceStatus.Type - (*ServiceStatus)(nil), // 3: daemon.ServiceStatus - (*ReloadServiceRequest)(nil), // 4: daemon.ReloadServiceRequest - (*SubscribeStatusRequest)(nil), // 5: daemon.SubscribeStatusRequest - (*Log)(nil), // 6: daemon.Log - (*DefaultLogLevel)(nil), // 7: daemon.DefaultLogLevel - (*Status)(nil), // 8: daemon.Status - (*Groups)(nil), // 9: daemon.Groups - (*Group)(nil), // 10: daemon.Group - (*GroupItem)(nil), // 11: daemon.GroupItem - (*URLTestRequest)(nil), // 12: daemon.URLTestRequest - (*SelectOutboundRequest)(nil), // 13: daemon.SelectOutboundRequest - (*SetGroupExpandRequest)(nil), // 14: daemon.SetGroupExpandRequest - (*ClashMode)(nil), // 15: daemon.ClashMode - (*ClashModeStatus)(nil), // 16: daemon.ClashModeStatus - (*SystemProxyStatus)(nil), // 17: daemon.SystemProxyStatus - (*SetSystemProxyEnabledRequest)(nil), // 18: daemon.SetSystemProxyEnabledRequest - (*SubscribeConnectionsRequest)(nil), // 19: daemon.SubscribeConnectionsRequest - (*ConnectionEvent)(nil), // 20: daemon.ConnectionEvent - (*ConnectionEvents)(nil), // 21: daemon.ConnectionEvents - (*Connection)(nil), // 22: daemon.Connection - (*ProcessInfo)(nil), // 23: daemon.ProcessInfo - (*CloseConnectionRequest)(nil), // 24: daemon.CloseConnectionRequest - (*DeprecatedWarnings)(nil), // 25: daemon.DeprecatedWarnings - (*DeprecatedWarning)(nil), // 26: daemon.DeprecatedWarning - (*StartedAt)(nil), // 27: daemon.StartedAt - (*Log_Message)(nil), // 28: daemon.Log.Message - (*emptypb.Empty)(nil), // 29: google.protobuf.Empty + (DebugCrashRequest_Type)(0), // 3: daemon.DebugCrashRequest.Type + (*ServiceStatus)(nil), // 4: daemon.ServiceStatus + (*ReloadServiceRequest)(nil), // 5: daemon.ReloadServiceRequest + (*SubscribeStatusRequest)(nil), // 6: daemon.SubscribeStatusRequest + (*Log)(nil), // 7: daemon.Log + (*DefaultLogLevel)(nil), // 8: daemon.DefaultLogLevel + (*Status)(nil), // 9: daemon.Status + (*Groups)(nil), // 10: daemon.Groups + (*Group)(nil), // 11: daemon.Group + (*GroupItem)(nil), // 12: daemon.GroupItem + (*URLTestRequest)(nil), // 13: daemon.URLTestRequest + (*SelectOutboundRequest)(nil), // 14: daemon.SelectOutboundRequest + (*SetGroupExpandRequest)(nil), // 15: daemon.SetGroupExpandRequest + (*ClashMode)(nil), // 16: daemon.ClashMode + (*ClashModeStatus)(nil), // 17: daemon.ClashModeStatus + (*SystemProxyStatus)(nil), // 18: daemon.SystemProxyStatus + (*SetSystemProxyEnabledRequest)(nil), // 19: daemon.SetSystemProxyEnabledRequest + (*DebugCrashRequest)(nil), // 20: daemon.DebugCrashRequest + (*SubscribeConnectionsRequest)(nil), // 21: daemon.SubscribeConnectionsRequest + (*ConnectionEvent)(nil), // 22: daemon.ConnectionEvent + (*ConnectionEvents)(nil), // 23: daemon.ConnectionEvents + (*Connection)(nil), // 24: daemon.Connection + (*ProcessInfo)(nil), // 25: daemon.ProcessInfo + (*CloseConnectionRequest)(nil), // 26: daemon.CloseConnectionRequest + (*DeprecatedWarnings)(nil), // 27: daemon.DeprecatedWarnings + (*DeprecatedWarning)(nil), // 28: daemon.DeprecatedWarning + (*StartedAt)(nil), // 29: daemon.StartedAt + (*Log_Message)(nil), // 30: daemon.Log.Message + (*emptypb.Empty)(nil), // 31: google.protobuf.Empty } ) var file_daemon_started_service_proto_depIdxs = []int32{ 2, // 0: daemon.ServiceStatus.status:type_name -> daemon.ServiceStatus.Type - 28, // 1: daemon.Log.messages:type_name -> daemon.Log.Message + 30, // 1: daemon.Log.messages:type_name -> daemon.Log.Message 0, // 2: daemon.DefaultLogLevel.level:type_name -> daemon.LogLevel - 10, // 3: daemon.Groups.group:type_name -> daemon.Group - 11, // 4: daemon.Group.items:type_name -> daemon.GroupItem - 1, // 5: daemon.ConnectionEvent.type:type_name -> daemon.ConnectionEventType - 22, // 6: daemon.ConnectionEvent.connection:type_name -> daemon.Connection - 20, // 7: daemon.ConnectionEvents.events:type_name -> daemon.ConnectionEvent - 23, // 8: daemon.Connection.processInfo:type_name -> daemon.ProcessInfo - 26, // 9: daemon.DeprecatedWarnings.warnings:type_name -> daemon.DeprecatedWarning - 0, // 10: daemon.Log.Message.level:type_name -> daemon.LogLevel - 29, // 11: daemon.StartedService.StopService:input_type -> google.protobuf.Empty - 29, // 12: daemon.StartedService.ReloadService:input_type -> google.protobuf.Empty - 29, // 13: daemon.StartedService.SubscribeServiceStatus:input_type -> google.protobuf.Empty - 29, // 14: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty - 29, // 15: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty - 29, // 16: daemon.StartedService.ClearLogs:input_type -> google.protobuf.Empty - 5, // 17: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest - 29, // 18: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty - 29, // 19: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty - 29, // 20: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty - 15, // 21: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode - 12, // 22: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest - 13, // 23: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest - 14, // 24: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest - 29, // 25: daemon.StartedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty - 18, // 26: daemon.StartedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest - 19, // 27: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest - 24, // 28: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest - 29, // 29: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty - 29, // 30: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty - 29, // 31: daemon.StartedService.GetStartedAt:input_type -> google.protobuf.Empty - 29, // 32: daemon.StartedService.StopService:output_type -> google.protobuf.Empty - 29, // 33: daemon.StartedService.ReloadService:output_type -> google.protobuf.Empty - 3, // 34: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus - 6, // 35: daemon.StartedService.SubscribeLog:output_type -> daemon.Log - 7, // 36: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel - 29, // 37: daemon.StartedService.ClearLogs:output_type -> google.protobuf.Empty - 8, // 38: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status - 9, // 39: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups - 16, // 40: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus - 15, // 41: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode - 29, // 42: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty - 29, // 43: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty - 29, // 44: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty - 29, // 45: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty - 17, // 46: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus - 29, // 47: daemon.StartedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty - 21, // 48: daemon.StartedService.SubscribeConnections:output_type -> daemon.ConnectionEvents - 29, // 49: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty - 29, // 50: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty - 25, // 51: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings - 27, // 52: daemon.StartedService.GetStartedAt:output_type -> daemon.StartedAt - 32, // [32:53] is the sub-list for method output_type - 11, // [11:32] is the sub-list for method input_type - 11, // [11:11] is the sub-list for extension type_name - 11, // [11:11] is the sub-list for extension extendee - 0, // [0:11] is the sub-list for field type_name + 11, // 3: daemon.Groups.group:type_name -> daemon.Group + 12, // 4: daemon.Group.items:type_name -> daemon.GroupItem + 3, // 5: daemon.DebugCrashRequest.type:type_name -> daemon.DebugCrashRequest.Type + 1, // 6: daemon.ConnectionEvent.type:type_name -> daemon.ConnectionEventType + 24, // 7: daemon.ConnectionEvent.connection:type_name -> daemon.Connection + 22, // 8: daemon.ConnectionEvents.events:type_name -> daemon.ConnectionEvent + 25, // 9: daemon.Connection.processInfo:type_name -> daemon.ProcessInfo + 28, // 10: daemon.DeprecatedWarnings.warnings:type_name -> daemon.DeprecatedWarning + 0, // 11: daemon.Log.Message.level:type_name -> daemon.LogLevel + 31, // 12: daemon.StartedService.StopService:input_type -> google.protobuf.Empty + 31, // 13: daemon.StartedService.ReloadService:input_type -> google.protobuf.Empty + 31, // 14: daemon.StartedService.SubscribeServiceStatus:input_type -> google.protobuf.Empty + 31, // 15: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty + 31, // 16: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty + 31, // 17: daemon.StartedService.ClearLogs:input_type -> google.protobuf.Empty + 6, // 18: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest + 31, // 19: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty + 31, // 20: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty + 31, // 21: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty + 16, // 22: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode + 13, // 23: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest + 14, // 24: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest + 15, // 25: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest + 31, // 26: daemon.StartedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty + 19, // 27: daemon.StartedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest + 20, // 28: daemon.StartedService.TriggerDebugCrash:input_type -> daemon.DebugCrashRequest + 31, // 29: daemon.StartedService.TriggerOOMReport:input_type -> google.protobuf.Empty + 21, // 30: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest + 26, // 31: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest + 31, // 32: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty + 31, // 33: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty + 31, // 34: daemon.StartedService.GetStartedAt:input_type -> google.protobuf.Empty + 31, // 35: daemon.StartedService.StopService:output_type -> google.protobuf.Empty + 31, // 36: daemon.StartedService.ReloadService:output_type -> google.protobuf.Empty + 4, // 37: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus + 7, // 38: daemon.StartedService.SubscribeLog:output_type -> daemon.Log + 8, // 39: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel + 31, // 40: daemon.StartedService.ClearLogs:output_type -> google.protobuf.Empty + 9, // 41: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status + 10, // 42: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups + 17, // 43: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus + 16, // 44: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode + 31, // 45: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty + 31, // 46: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty + 31, // 47: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty + 31, // 48: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty + 18, // 49: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus + 31, // 50: daemon.StartedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty + 31, // 51: daemon.StartedService.TriggerDebugCrash:output_type -> google.protobuf.Empty + 31, // 52: daemon.StartedService.TriggerOOMReport:output_type -> google.protobuf.Empty + 23, // 53: daemon.StartedService.SubscribeConnections:output_type -> daemon.ConnectionEvents + 31, // 54: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty + 31, // 55: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty + 27, // 56: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings + 29, // 57: daemon.StartedService.GetStartedAt:output_type -> daemon.StartedAt + 35, // [35:58] is the sub-list for method output_type + 12, // [12:35] is the sub-list for method input_type + 12, // [12:12] is the sub-list for extension type_name + 12, // [12:12] is the sub-list for extension extendee + 0, // [0:12] is the sub-list for field type_name } func init() { file_daemon_started_service_proto_init() } @@ -2056,8 +2161,8 @@ func file_daemon_started_service_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_started_service_proto_rawDesc), len(file_daemon_started_service_proto_rawDesc)), - NumEnums: 3, - NumMessages: 26, + NumEnums: 4, + NumMessages: 27, NumExtensions: 0, NumServices: 1, }, diff --git a/daemon/started_service.proto b/daemon/started_service.proto index 8a76081ab5..3434c3f19d 100644 --- a/daemon/started_service.proto +++ b/daemon/started_service.proto @@ -26,6 +26,8 @@ service StartedService { rpc GetSystemProxyStatus(google.protobuf.Empty) returns(SystemProxyStatus) {} rpc SetSystemProxyEnabled(SetSystemProxyEnabledRequest) returns(google.protobuf.Empty) {} + rpc TriggerDebugCrash(DebugCrashRequest) returns(google.protobuf.Empty) {} + rpc TriggerOOMReport(google.protobuf.Empty) returns(google.protobuf.Empty) {} rpc SubscribeConnections(SubscribeConnectionsRequest) returns(stream ConnectionEvents) {} rpc CloseConnection(CloseConnectionRequest) returns(google.protobuf.Empty) {} @@ -141,6 +143,15 @@ message SetSystemProxyEnabledRequest { bool enabled = 1; } +message DebugCrashRequest { + enum Type { + GO = 0; + NATIVE = 1; + } + + Type type = 1; +} + message SubscribeConnectionsRequest { int64 interval = 1; } @@ -214,4 +225,4 @@ message DeprecatedWarning { message StartedAt { int64 startedAt = 1; -} \ No newline at end of file +} diff --git a/daemon/started_service_grpc.pb.go b/daemon/started_service_grpc.pb.go index 438cca5c35..bdf81e4a64 100644 --- a/daemon/started_service_grpc.pb.go +++ b/daemon/started_service_grpc.pb.go @@ -31,6 +31,8 @@ const ( StartedService_SetGroupExpand_FullMethodName = "/daemon.StartedService/SetGroupExpand" StartedService_GetSystemProxyStatus_FullMethodName = "/daemon.StartedService/GetSystemProxyStatus" StartedService_SetSystemProxyEnabled_FullMethodName = "/daemon.StartedService/SetSystemProxyEnabled" + StartedService_TriggerDebugCrash_FullMethodName = "/daemon.StartedService/TriggerDebugCrash" + StartedService_TriggerOOMReport_FullMethodName = "/daemon.StartedService/TriggerOOMReport" StartedService_SubscribeConnections_FullMethodName = "/daemon.StartedService/SubscribeConnections" StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection" StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections" @@ -58,6 +60,8 @@ type StartedServiceClient interface { SetGroupExpand(ctx context.Context, in *SetGroupExpandRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) GetSystemProxyStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SystemProxyStatus, error) SetSystemProxyEnabled(ctx context.Context, in *SetSystemProxyEnabledRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + TriggerDebugCrash(ctx context.Context, in *DebugCrashRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + TriggerOOMReport(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ConnectionEvents], error) CloseConnection(ctx context.Context, in *CloseConnectionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) CloseAllConnections(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) @@ -278,6 +282,26 @@ func (c *startedServiceClient) SetSystemProxyEnabled(ctx context.Context, in *Se return out, nil } +func (c *startedServiceClient) TriggerDebugCrash(ctx context.Context, in *DebugCrashRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, StartedService_TriggerDebugCrash_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *startedServiceClient) TriggerOOMReport(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, StartedService_TriggerOOMReport_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *startedServiceClient) SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ConnectionEvents], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[5], StartedService_SubscribeConnections_FullMethodName, cOpts...) @@ -357,6 +381,8 @@ type StartedServiceServer interface { SetGroupExpand(context.Context, *SetGroupExpandRequest) (*emptypb.Empty, error) GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error) SetSystemProxyEnabled(context.Context, *SetSystemProxyEnabledRequest) (*emptypb.Empty, error) + TriggerDebugCrash(context.Context, *DebugCrashRequest) (*emptypb.Empty, error) + TriggerOOMReport(context.Context, *emptypb.Empty) (*emptypb.Empty, error) SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[ConnectionEvents]) error CloseConnection(context.Context, *CloseConnectionRequest) (*emptypb.Empty, error) CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error) @@ -436,6 +462,14 @@ func (UnimplementedStartedServiceServer) SetSystemProxyEnabled(context.Context, return nil, status.Error(codes.Unimplemented, "method SetSystemProxyEnabled not implemented") } +func (UnimplementedStartedServiceServer) TriggerDebugCrash(context.Context, *DebugCrashRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method TriggerDebugCrash not implemented") +} + +func (UnimplementedStartedServiceServer) TriggerOOMReport(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method TriggerOOMReport not implemented") +} + func (UnimplementedStartedServiceServer) SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[ConnectionEvents]) error { return status.Error(codes.Unimplemented, "method SubscribeConnections not implemented") } @@ -729,6 +763,42 @@ func _StartedService_SetSystemProxyEnabled_Handler(srv interface{}, ctx context. return interceptor(ctx, in, info, handler) } +func _StartedService_TriggerDebugCrash_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DebugCrashRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StartedServiceServer).TriggerDebugCrash(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StartedService_TriggerDebugCrash_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StartedServiceServer).TriggerDebugCrash(ctx, req.(*DebugCrashRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StartedService_TriggerOOMReport_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StartedServiceServer).TriggerOOMReport(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StartedService_TriggerOOMReport_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StartedServiceServer).TriggerOOMReport(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + func _StartedService_SubscribeConnections_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(SubscribeConnectionsRequest) if err := stream.RecvMsg(m); err != nil { @@ -863,6 +933,14 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{ MethodName: "SetSystemProxyEnabled", Handler: _StartedService_SetSystemProxyEnabled_Handler, }, + { + MethodName: "TriggerDebugCrash", + Handler: _StartedService_TriggerDebugCrash_Handler, + }, + { + MethodName: "TriggerOOMReport", + Handler: _StartedService_TriggerOOMReport_Handler, + }, { MethodName: "CloseConnection", Handler: _StartedService_CloseConnection_Handler, diff --git a/experimental/libbox/command_client.go b/experimental/libbox/command_client.go index a5077bea99..114198a146 100644 --- a/experimental/libbox/command_client.go +++ b/experimental/libbox/command_client.go @@ -540,6 +540,31 @@ func (c *CommandClient) SetSystemProxyEnabled(isEnabled bool) error { return err } +func (c *CommandClient) TriggerGoCrash() error { + _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { + return client.TriggerDebugCrash(context.Background(), &daemon.DebugCrashRequest{ + Type: daemon.DebugCrashRequest_GO, + }) + }) + return err +} + +func (c *CommandClient) TriggerNativeCrash() error { + _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { + return client.TriggerDebugCrash(context.Background(), &daemon.DebugCrashRequest{ + Type: daemon.DebugCrashRequest_NATIVE, + }) + }) + return err +} + +func (c *CommandClient) TriggerOOMReport() error { + _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { + return client.TriggerOOMReport(context.Background(), &emptypb.Empty{}) + }) + return err +} + func (c *CommandClient) GetDeprecatedNotes() (DeprecatedNoteIterator, error) { return callWithResult(c, func(client daemon.StartedServiceClient) (DeprecatedNoteIterator, error) { warnings, err := client.GetDeprecatedWarnings(context.Background(), &emptypb.Empty{}) diff --git a/experimental/libbox/command_server.go b/experimental/libbox/command_server.go index 1c2412b697..c093cd6da4 100644 --- a/experimental/libbox/command_server.go +++ b/experimental/libbox/command_server.go @@ -39,6 +39,7 @@ type CommandServerHandler interface { ServiceReload() error GetSystemProxyStatus() (*SystemProxyStatus, error) SetSystemProxyEnabled(enabled bool) error + TriggerNativeCrash() error WriteDebugMessage(message string) } @@ -57,10 +58,12 @@ func NewCommandServer(handler CommandServerHandler, platformInterface PlatformIn server.StartedService = daemon.NewStartedService(daemon.ServiceOptions{ Context: ctx, // Platform: platformWrapper, - Handler: (*platformHandler)(server), - Debug: sDebug, - LogMaxLines: sLogMaxLines, - OOMKiller: memoryLimitEnabled, + Handler: (*platformHandler)(server), + Debug: sDebug, + LogMaxLines: sLogMaxLines, + OOMKillerEnabled: sOOMKillerEnabled, + OOMKillerDisabled: sOOMKillerDisabled, + OOMMemoryLimit: uint64(sOOMMemoryLimit), // WorkingDirectory: sWorkingPath, // TempDirectory: sTempPath, // UserID: sUserID, @@ -170,11 +173,16 @@ type OverrideOptions struct { } func (s *CommandServer) StartOrReloadService(configContent string, options *OverrideOptions) error { - return s.StartedService.StartOrReloadService(configContent, &daemon.OverrideOptions{ + saveConfigSnapshot(configContent) + err := s.StartedService.StartOrReloadService(configContent, &daemon.OverrideOptions{ AutoRedirect: options.AutoRedirect, IncludePackage: iteratorToArray(options.IncludePackage), ExcludePackage: iteratorToArray(options.ExcludePackage), }) + if err != nil { + return err + } + return nil } func (s *CommandServer) CloseService() error { @@ -271,6 +279,10 @@ func (h *platformHandler) SetSystemProxyEnabled(enabled bool) error { return (*CommandServer)(h).handler.SetSystemProxyEnabled(enabled) } +func (h *platformHandler) TriggerNativeCrash() error { + return (*CommandServer)(h).handler.TriggerNativeCrash() +} + func (h *platformHandler) WriteDebugMessage(message string) { (*CommandServer)(h).handler.WriteDebugMessage(message) } diff --git a/experimental/libbox/config.go b/experimental/libbox/config.go index 16d7d3e7b3..8010e5335f 100644 --- a/experimental/libbox/config.go +++ b/experimental/libbox/config.go @@ -13,6 +13,7 @@ import ( "github.com/sagernet/sing-box/include" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/service/oomkiller" tun "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" @@ -23,6 +24,8 @@ import ( "github.com/sagernet/sing/service/filemanager" ) +var sOOMReporter oomkiller.OOMReporter + func baseContext(platformInterface PlatformInterface) context.Context { dnsRegistry := include.DNSTransportRegistry() if platformInterface != nil { @@ -34,6 +37,9 @@ func baseContext(platformInterface PlatformInterface) context.Context { } ctx := context.Background() ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID) + if sOOMReporter != nil { + ctx = service.ContextWith[oomkiller.OOMReporter](ctx, sOOMReporter) + } return box.Context(ctx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), dnsRegistry, include.ServiceRegistry(), include.CertificateProviderRegistry()) } diff --git a/experimental/libbox/debug.go b/experimental/libbox/debug.go new file mode 100644 index 0000000000..63f2b49e98 --- /dev/null +++ b/experimental/libbox/debug.go @@ -0,0 +1,9 @@ +package libbox + +import "time" + +func TriggerGoPanic() { + time.AfterFunc(200*time.Millisecond, func() { + panic("debug go crash") + }) +} diff --git a/experimental/libbox/internal/oomprofile/builder.go b/experimental/libbox/internal/oomprofile/builder.go new file mode 100644 index 0000000000..1f59078a23 --- /dev/null +++ b/experimental/libbox/internal/oomprofile/builder.go @@ -0,0 +1,390 @@ +//go:build darwin || linux || windows + +package oomprofile + +import ( + "fmt" + "io" + "runtime" + "time" +) + +const ( + tagProfile_SampleType = 1 + tagProfile_Sample = 2 + tagProfile_Mapping = 3 + tagProfile_Location = 4 + tagProfile_Function = 5 + tagProfile_StringTable = 6 + tagProfile_TimeNanos = 9 + tagProfile_PeriodType = 11 + tagProfile_Period = 12 + tagProfile_DefaultSampleType = 14 + + tagValueType_Type = 1 + tagValueType_Unit = 2 + + tagSample_Location = 1 + tagSample_Value = 2 + tagSample_Label = 3 + + tagLabel_Key = 1 + tagLabel_Str = 2 + tagLabel_Num = 3 + + tagMapping_ID = 1 + tagMapping_Start = 2 + tagMapping_Limit = 3 + tagMapping_Offset = 4 + tagMapping_Filename = 5 + tagMapping_BuildID = 6 + tagMapping_HasFunctions = 7 + tagMapping_HasFilenames = 8 + tagMapping_HasLineNumbers = 9 + tagMapping_HasInlineFrames = 10 + + tagLocation_ID = 1 + tagLocation_MappingID = 2 + tagLocation_Address = 3 + tagLocation_Line = 4 + + tagLine_FunctionID = 1 + tagLine_Line = 2 + + tagFunction_ID = 1 + tagFunction_Name = 2 + tagFunction_SystemName = 3 + tagFunction_Filename = 4 + tagFunction_StartLine = 5 +) + +type memMap struct { + start uintptr + end uintptr + offset uint64 + file string + buildID string + funcs symbolizeFlag + fake bool +} + +type symbolizeFlag uint8 + +const ( + lookupTried symbolizeFlag = 1 << iota + lookupFailed +) + +func newProfileBuilder(w io.Writer) *profileBuilder { + builder := &profileBuilder{ + start: time.Now(), + w: w, + strings: []string{""}, + stringMap: map[string]int{"": 0}, + locs: map[uintptr]locInfo{}, + funcs: map[string]int{}, + } + builder.readMapping() + return builder +} + +func (b *profileBuilder) stringIndex(s string) int64 { + id, ok := b.stringMap[s] + if !ok { + id = len(b.strings) + b.strings = append(b.strings, s) + b.stringMap[s] = id + } + return int64(id) +} + +func (b *profileBuilder) flush() { + const dataFlush = 4096 + if b.err != nil || b.pb.nest != 0 || len(b.pb.data) <= dataFlush { + return + } + + _, b.err = b.w.Write(b.pb.data) + b.pb.data = b.pb.data[:0] +} + +func (b *profileBuilder) pbValueType(tag int, typ string, unit string) { + start := b.pb.startMessage() + b.pb.int64(tagValueType_Type, b.stringIndex(typ)) + b.pb.int64(tagValueType_Unit, b.stringIndex(unit)) + b.pb.endMessage(tag, start) +} + +func (b *profileBuilder) pbSample(values []int64, locs []uint64, labels func()) { + start := b.pb.startMessage() + b.pb.int64s(tagSample_Value, values) + b.pb.uint64s(tagSample_Location, locs) + if labels != nil { + labels() + } + b.pb.endMessage(tagProfile_Sample, start) + b.flush() +} + +func (b *profileBuilder) pbLabel(tag int, key string, str string, num int64) { + start := b.pb.startMessage() + b.pb.int64Opt(tagLabel_Key, b.stringIndex(key)) + b.pb.int64Opt(tagLabel_Str, b.stringIndex(str)) + b.pb.int64Opt(tagLabel_Num, num) + b.pb.endMessage(tag, start) +} + +func (b *profileBuilder) pbLine(tag int, funcID uint64, line int64) { + start := b.pb.startMessage() + b.pb.uint64Opt(tagLine_FunctionID, funcID) + b.pb.int64Opt(tagLine_Line, line) + b.pb.endMessage(tag, start) +} + +func (b *profileBuilder) pbMapping(tag int, id uint64, base uint64, limit uint64, offset uint64, file string, buildID string, hasFuncs bool) { + start := b.pb.startMessage() + b.pb.uint64Opt(tagMapping_ID, id) + b.pb.uint64Opt(tagMapping_Start, base) + b.pb.uint64Opt(tagMapping_Limit, limit) + b.pb.uint64Opt(tagMapping_Offset, offset) + b.pb.int64Opt(tagMapping_Filename, b.stringIndex(file)) + b.pb.int64Opt(tagMapping_BuildID, b.stringIndex(buildID)) + if hasFuncs { + b.pb.bool(tagMapping_HasFunctions, true) + } + b.pb.endMessage(tag, start) +} + +func (b *profileBuilder) build() error { + if b.err != nil { + return b.err + } + + b.pb.int64Opt(tagProfile_TimeNanos, b.start.UnixNano()) + for i, mapping := range b.mem { + hasFunctions := mapping.funcs == lookupTried + b.pbMapping(tagProfile_Mapping, uint64(i+1), uint64(mapping.start), uint64(mapping.end), mapping.offset, mapping.file, mapping.buildID, hasFunctions) + } + b.pb.strings(tagProfile_StringTable, b.strings) + if b.err != nil { + return b.err + } + _, err := b.w.Write(b.pb.data) + return err +} + +func allFrames(addr uintptr) ([]runtime.Frame, symbolizeFlag) { + frames := runtime.CallersFrames([]uintptr{addr}) + frame, more := frames.Next() + if frame.Function == "runtime.goexit" { + return nil, 0 + } + + result := lookupTried + if frame.PC == 0 || frame.Function == "" || frame.File == "" || frame.Line == 0 { + result |= lookupFailed + } + if frame.PC == 0 { + frame.PC = addr - 1 + } + + ret := []runtime.Frame{frame} + for frame.Function != "runtime.goexit" && more { + frame, more = frames.Next() + ret = append(ret, frame) + } + return ret, result +} + +type locInfo struct { + id uint64 + + pcs []uintptr + + firstPCFrames []runtime.Frame + firstPCSymbolizeResult symbolizeFlag +} + +func (b *profileBuilder) appendLocsForStack(locs []uint64, stk []uintptr) []uint64 { + b.deck.reset() + origStk := stk + stk = runtimeExpandFinalInlineFrame(stk) + + for len(stk) > 0 { + addr := stk[0] + if loc, ok := b.locs[addr]; ok { + if len(b.deck.pcs) > 0 { + if b.deck.tryAdd(addr, loc.firstPCFrames, loc.firstPCSymbolizeResult) { + stk = stk[1:] + continue + } + } + if id := b.emitLocation(); id > 0 { + locs = append(locs, id) + } + locs = append(locs, loc.id) + if len(loc.pcs) > len(stk) { + panic(fmt.Sprintf("stack too short to match cached location; stk = %#x, loc.pcs = %#x, original stk = %#x", stk, loc.pcs, origStk)) + } + stk = stk[len(loc.pcs):] + continue + } + + frames, symbolizeResult := allFrames(addr) + if len(frames) == 0 { + if id := b.emitLocation(); id > 0 { + locs = append(locs, id) + } + stk = stk[1:] + continue + } + + if b.deck.tryAdd(addr, frames, symbolizeResult) { + stk = stk[1:] + continue + } + if id := b.emitLocation(); id > 0 { + locs = append(locs, id) + } + + if loc, ok := b.locs[addr]; ok { + locs = append(locs, loc.id) + stk = stk[len(loc.pcs):] + } else { + b.deck.tryAdd(addr, frames, symbolizeResult) + stk = stk[1:] + } + } + if id := b.emitLocation(); id > 0 { + locs = append(locs, id) + } + return locs +} + +type pcDeck struct { + pcs []uintptr + frames []runtime.Frame + symbolizeResult symbolizeFlag + + firstPCFrames int + firstPCSymbolizeResult symbolizeFlag +} + +func (d *pcDeck) reset() { + d.pcs = d.pcs[:0] + d.frames = d.frames[:0] + d.symbolizeResult = 0 + d.firstPCFrames = 0 + d.firstPCSymbolizeResult = 0 +} + +func (d *pcDeck) tryAdd(pc uintptr, frames []runtime.Frame, symbolizeResult symbolizeFlag) bool { + if existing := len(d.frames); existing > 0 { + newFrame := frames[0] + last := d.frames[existing-1] + if last.Func != nil { + return false + } + if last.Entry == 0 || newFrame.Entry == 0 { + return false + } + if last.Entry != newFrame.Entry { + return false + } + if runtimeFrameSymbolName(&last) == runtimeFrameSymbolName(&newFrame) { + return false + } + } + + d.pcs = append(d.pcs, pc) + d.frames = append(d.frames, frames...) + d.symbolizeResult |= symbolizeResult + if len(d.pcs) == 1 { + d.firstPCFrames = len(d.frames) + d.firstPCSymbolizeResult = symbolizeResult + } + return true +} + +func (b *profileBuilder) emitLocation() uint64 { + if len(b.deck.pcs) == 0 { + return 0 + } + defer b.deck.reset() + + addr := b.deck.pcs[0] + firstFrame := b.deck.frames[0] + + type newFunc struct { + id uint64 + name string + file string + startLine int64 + } + + newFuncs := make([]newFunc, 0, 8) + id := uint64(len(b.locs)) + 1 + b.locs[addr] = locInfo{ + id: id, + pcs: append([]uintptr{}, b.deck.pcs...), + firstPCFrames: append([]runtime.Frame{}, b.deck.frames[:b.deck.firstPCFrames]...), + firstPCSymbolizeResult: b.deck.firstPCSymbolizeResult, + } + + start := b.pb.startMessage() + b.pb.uint64Opt(tagLocation_ID, id) + b.pb.uint64Opt(tagLocation_Address, uint64(firstFrame.PC)) + for _, frame := range b.deck.frames { + funcName := runtimeFrameSymbolName(&frame) + funcID := uint64(b.funcs[funcName]) + if funcID == 0 { + funcID = uint64(len(b.funcs)) + 1 + b.funcs[funcName] = int(funcID) + newFuncs = append(newFuncs, newFunc{ + id: funcID, + name: funcName, + file: frame.File, + startLine: int64(runtimeFrameStartLine(&frame)), + }) + } + b.pbLine(tagLocation_Line, funcID, int64(frame.Line)) + } + for i := range b.mem { + if (b.mem[i].start <= addr && addr < b.mem[i].end) || b.mem[i].fake { + b.pb.uint64Opt(tagLocation_MappingID, uint64(i+1)) + mapping := b.mem[i] + mapping.funcs |= b.deck.symbolizeResult + b.mem[i] = mapping + break + } + } + b.pb.endMessage(tagProfile_Location, start) + + for _, fn := range newFuncs { + start := b.pb.startMessage() + b.pb.uint64Opt(tagFunction_ID, fn.id) + b.pb.int64Opt(tagFunction_Name, b.stringIndex(fn.name)) + b.pb.int64Opt(tagFunction_SystemName, b.stringIndex(fn.name)) + b.pb.int64Opt(tagFunction_Filename, b.stringIndex(fn.file)) + b.pb.int64Opt(tagFunction_StartLine, fn.startLine) + b.pb.endMessage(tagProfile_Function, start) + } + + b.flush() + return id +} + +func (b *profileBuilder) addMapping(lo uint64, hi uint64, offset uint64, file string, buildID string) { + b.addMappingEntry(lo, hi, offset, file, buildID, false) +} + +func (b *profileBuilder) addMappingEntry(lo uint64, hi uint64, offset uint64, file string, buildID string, fake bool) { + b.mem = append(b.mem, memMap{ + start: uintptr(lo), + end: uintptr(hi), + offset: offset, + file: file, + buildID: buildID, + fake: fake, + }) +} diff --git a/experimental/libbox/internal/oomprofile/defs_darwin_amd64.go b/experimental/libbox/internal/oomprofile/defs_darwin_amd64.go new file mode 100644 index 0000000000..8a30074ca2 --- /dev/null +++ b/experimental/libbox/internal/oomprofile/defs_darwin_amd64.go @@ -0,0 +1,24 @@ +//go:build darwin && amd64 + +package oomprofile + +type machVMRegionBasicInfoData struct { + Protection int32 + MaxProtection int32 + Inheritance uint32 + Shared uint32 + Reserved uint32 + Offset [8]byte + Behavior int32 + UserWiredCount uint16 + PadCgo1 [2]byte +} + +const ( + _VM_PROT_READ = 0x1 + _VM_PROT_EXECUTE = 0x4 + + _MACH_SEND_INVALID_DEST = 0x10000003 + + _MAXPATHLEN = 0x400 +) diff --git a/experimental/libbox/internal/oomprofile/defs_darwin_arm64.go b/experimental/libbox/internal/oomprofile/defs_darwin_arm64.go new file mode 100644 index 0000000000..2fd4659001 --- /dev/null +++ b/experimental/libbox/internal/oomprofile/defs_darwin_arm64.go @@ -0,0 +1,24 @@ +//go:build darwin && arm64 + +package oomprofile + +type machVMRegionBasicInfoData struct { + Protection int32 + MaxProtection int32 + Inheritance uint32 + Shared int32 + Reserved int32 + Offset [8]byte + Behavior int32 + UserWiredCount uint16 + PadCgo1 [2]byte +} + +const ( + _VM_PROT_READ = 0x1 + _VM_PROT_EXECUTE = 0x4 + + _MACH_SEND_INVALID_DEST = 0x10000003 + + _MAXPATHLEN = 0x400 +) diff --git a/experimental/libbox/internal/oomprofile/linkname.go b/experimental/libbox/internal/oomprofile/linkname.go new file mode 100644 index 0000000000..2a5e10ed10 --- /dev/null +++ b/experimental/libbox/internal/oomprofile/linkname.go @@ -0,0 +1,47 @@ +//go:build darwin || linux || windows + +package oomprofile + +import ( + "runtime" + _ "runtime/pprof" + "unsafe" + + _ "unsafe" +) + +//go:linkname runtimeMemProfileInternal runtime.pprof_memProfileInternal +func runtimeMemProfileInternal(p []memProfileRecord, inuseZero bool) (n int, ok bool) + +//go:linkname runtimeBlockProfileInternal runtime.pprof_blockProfileInternal +func runtimeBlockProfileInternal(p []blockProfileRecord) (n int, ok bool) + +//go:linkname runtimeMutexProfileInternal runtime.pprof_mutexProfileInternal +func runtimeMutexProfileInternal(p []blockProfileRecord) (n int, ok bool) + +//go:linkname runtimeThreadCreateInternal runtime.pprof_threadCreateInternal +func runtimeThreadCreateInternal(p []stackRecord) (n int, ok bool) + +//go:linkname runtimeGoroutineProfileWithLabels runtime.pprof_goroutineProfileWithLabels +func runtimeGoroutineProfileWithLabels(p []stackRecord, labels []unsafe.Pointer) (n int, ok bool) + +//go:linkname runtimeCyclesPerSecond runtime/pprof.runtime_cyclesPerSecond +func runtimeCyclesPerSecond() int64 + +//go:linkname runtimeMakeProfStack runtime.pprof_makeProfStack +func runtimeMakeProfStack() []uintptr + +//go:linkname runtimeFrameStartLine runtime/pprof.runtime_FrameStartLine +func runtimeFrameStartLine(f *runtime.Frame) int + +//go:linkname runtimeFrameSymbolName runtime/pprof.runtime_FrameSymbolName +func runtimeFrameSymbolName(f *runtime.Frame) string + +//go:linkname runtimeExpandFinalInlineFrame runtime/pprof.runtime_expandFinalInlineFrame +func runtimeExpandFinalInlineFrame(stk []uintptr) []uintptr + +//go:linkname stdParseProcSelfMaps runtime/pprof.parseProcSelfMaps +func stdParseProcSelfMaps(data []byte, addMapping func(lo uint64, hi uint64, offset uint64, file string, buildID string)) + +//go:linkname stdELFBuildID runtime/pprof.elfBuildID +func stdELFBuildID(file string) (string, error) diff --git a/experimental/libbox/internal/oomprofile/mapping_darwin.go b/experimental/libbox/internal/oomprofile/mapping_darwin.go new file mode 100644 index 0000000000..8d5d854029 --- /dev/null +++ b/experimental/libbox/internal/oomprofile/mapping_darwin.go @@ -0,0 +1,57 @@ +//go:build darwin + +package oomprofile + +import ( + "encoding/binary" + "os" + "unsafe" + + _ "unsafe" +) + +func isExecutable(protection int32) bool { + return (protection&_VM_PROT_EXECUTE) != 0 && (protection&_VM_PROT_READ) != 0 +} + +func (b *profileBuilder) readMapping() { + if !machVMInfo(b.addMapping) { + b.addMappingEntry(0, 0, 0, "", "", true) + } +} + +func machVMInfo(addMapping func(lo uint64, hi uint64, off uint64, file string, buildID string)) bool { + added := false + addr := uint64(0x1) + for { + var regionSize uint64 + var info machVMRegionBasicInfoData + kr := machVMRegion(&addr, ®ionSize, unsafe.Pointer(&info)) + if kr != 0 { + if kr == _MACH_SEND_INVALID_DEST { + return true + } + return added + } + if isExecutable(info.Protection) { + addMapping(addr, addr+regionSize, binary.LittleEndian.Uint64(info.Offset[:]), regionFilename(addr), "") + added = true + } + addr += regionSize + } +} + +func regionFilename(address uint64) string { + buf := make([]byte, _MAXPATHLEN) + n := procRegionFilename(os.Getpid(), address, unsafe.SliceData(buf), int64(cap(buf))) + if n == 0 { + return "" + } + return string(buf[:n]) +} + +//go:linkname machVMRegion runtime/pprof.mach_vm_region +func machVMRegion(address *uint64, regionSize *uint64, info unsafe.Pointer) int32 + +//go:linkname procRegionFilename runtime/pprof.proc_regionfilename +func procRegionFilename(pid int, address uint64, buf *byte, buflen int64) int32 diff --git a/experimental/libbox/internal/oomprofile/mapping_linux.go b/experimental/libbox/internal/oomprofile/mapping_linux.go new file mode 100644 index 0000000000..cc9b03a6d1 --- /dev/null +++ b/experimental/libbox/internal/oomprofile/mapping_linux.go @@ -0,0 +1,13 @@ +//go:build linux + +package oomprofile + +import "os" + +func (b *profileBuilder) readMapping() { + data, _ := os.ReadFile("/proc/self/maps") + stdParseProcSelfMaps(data, b.addMapping) + if len(b.mem) == 0 { + b.addMappingEntry(0, 0, 0, "", "", true) + } +} diff --git a/experimental/libbox/internal/oomprofile/mapping_windows.go b/experimental/libbox/internal/oomprofile/mapping_windows.go new file mode 100644 index 0000000000..68303d895d --- /dev/null +++ b/experimental/libbox/internal/oomprofile/mapping_windows.go @@ -0,0 +1,58 @@ +//go:build windows + +package oomprofile + +import ( + "errors" + "os" + + "golang.org/x/sys/windows" +) + +func (b *profileBuilder) readMapping() { + snapshot, err := createModuleSnapshot() + if err != nil { + b.addMappingEntry(0, 0, 0, "", "", true) + return + } + defer windows.CloseHandle(snapshot) + + var module windows.ModuleEntry32 + module.Size = uint32(windows.SizeofModuleEntry32) + err = windows.Module32First(snapshot, &module) + if err != nil { + b.addMappingEntry(0, 0, 0, "", "", true) + return + } + for err == nil { + exe := windows.UTF16ToString(module.ExePath[:]) + b.addMappingEntry( + uint64(module.ModBaseAddr), + uint64(module.ModBaseAddr)+uint64(module.ModBaseSize), + 0, + exe, + peBuildID(exe), + false, + ) + err = windows.Module32Next(snapshot, &module) + } +} + +func createModuleSnapshot() (windows.Handle, error) { + for { + snapshot, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPMODULE|windows.TH32CS_SNAPMODULE32, uint32(windows.GetCurrentProcessId())) + var errno windows.Errno + if err != nil && errors.As(err, &errno) && errno == windows.ERROR_BAD_LENGTH { + continue + } + return snapshot, err + } +} + +func peBuildID(file string) string { + info, err := os.Stat(file) + if err != nil { + return file + } + return file + info.ModTime().String() +} diff --git a/experimental/libbox/internal/oomprofile/oomprofile.go b/experimental/libbox/internal/oomprofile/oomprofile.go new file mode 100644 index 0000000000..f26d3b5894 --- /dev/null +++ b/experimental/libbox/internal/oomprofile/oomprofile.go @@ -0,0 +1,380 @@ +//go:build darwin || linux || windows + +package oomprofile + +import ( + "fmt" + "io" + "math" + "os" + "path/filepath" + "runtime" + "sort" + "strings" + "time" + "unsafe" +) + +type stackRecord struct { + Stack []uintptr +} + +type memProfileRecord struct { + AllocBytes, FreeBytes int64 + AllocObjects, FreeObjects int64 + Stack []uintptr +} + +func (r *memProfileRecord) InUseBytes() int64 { + return r.AllocBytes - r.FreeBytes +} + +func (r *memProfileRecord) InUseObjects() int64 { + return r.AllocObjects - r.FreeObjects +} + +type blockProfileRecord struct { + Count int64 + Cycles int64 + Stack []uintptr +} + +type label struct { + key string + value string +} + +type labelSet struct { + list []label +} + +type labelMap struct { + labelSet +} + +func WriteFile(destPath string, name string) (string, error) { + writer, ok := profileWriters[name] + if !ok { + return "", fmt.Errorf("unsupported profile %q", name) + } + + filePath := filepath.Join(destPath, name+".pb") + file, err := os.Create(filePath) + if err != nil { + return "", err + } + defer file.Close() + + if err := writer(file); err != nil { + _ = os.Remove(filePath) + return "", err + } + if err := file.Close(); err != nil { + _ = os.Remove(filePath) + return "", err + } + return filePath, nil +} + +var profileWriters = map[string]func(io.Writer) error{ + "allocs": writeAlloc, + "block": writeBlock, + "goroutine": writeGoroutine, + "heap": writeHeap, + "mutex": writeMutex, + "threadcreate": writeThreadCreate, +} + +func writeHeap(w io.Writer) error { + return writeHeapInternal(w, "") +} + +func writeAlloc(w io.Writer) error { + return writeHeapInternal(w, "alloc_space") +} + +func writeHeapInternal(w io.Writer, defaultSampleType string) error { + var profile []memProfileRecord + n, ok := runtimeMemProfileInternal(nil, true) + for { + profile = make([]memProfileRecord, n+50) + n, ok = runtimeMemProfileInternal(profile, true) + if ok { + profile = profile[:n] + break + } + } + return writeHeapProto(w, profile, int64(runtime.MemProfileRate), defaultSampleType) +} + +func writeGoroutine(w io.Writer) error { + return writeRuntimeProfile(w, "goroutine", runtimeGoroutineProfileWithLabels) +} + +func writeThreadCreate(w io.Writer) error { + return writeRuntimeProfile(w, "threadcreate", func(p []stackRecord, _ []unsafe.Pointer) (int, bool) { + return runtimeThreadCreateInternal(p) + }) +} + +func writeRuntimeProfile(w io.Writer, name string, fetch func([]stackRecord, []unsafe.Pointer) (int, bool)) error { + var profile []stackRecord + var labels []unsafe.Pointer + + n, ok := fetch(nil, nil) + for { + profile = make([]stackRecord, n+10) + labels = make([]unsafe.Pointer, n+10) + n, ok = fetch(profile, labels) + if ok { + profile = profile[:n] + labels = labels[:n] + break + } + } + + return writeCountProfile(w, name, &runtimeProfile{profile, labels}) +} + +func writeBlock(w io.Writer) error { + return writeCycleProfile(w, "contentions", "delay", runtimeBlockProfileInternal) +} + +func writeMutex(w io.Writer) error { + return writeCycleProfile(w, "contentions", "delay", runtimeMutexProfileInternal) +} + +func writeCycleProfile(w io.Writer, countName string, cycleName string, fetch func([]blockProfileRecord) (int, bool)) error { + var profile []blockProfileRecord + n, ok := fetch(nil) + for { + profile = make([]blockProfileRecord, n+50) + n, ok = fetch(profile) + if ok { + profile = profile[:n] + break + } + } + + sort.Slice(profile, func(i, j int) bool { + return profile[i].Cycles > profile[j].Cycles + }) + + builder := newProfileBuilder(w) + builder.pbValueType(tagProfile_PeriodType, countName, "count") + builder.pb.int64Opt(tagProfile_Period, 1) + builder.pbValueType(tagProfile_SampleType, countName, "count") + builder.pbValueType(tagProfile_SampleType, cycleName, "nanoseconds") + + cpuGHz := float64(runtimeCyclesPerSecond()) / 1e9 + values := []int64{0, 0} + var locs []uint64 + expandedStack := runtimeMakeProfStack() + for _, record := range profile { + values[0] = record.Count + if cpuGHz > 0 { + values[1] = int64(float64(record.Cycles) / cpuGHz) + } else { + values[1] = 0 + } + n := expandInlinedFrames(expandedStack, record.Stack) + locs = builder.appendLocsForStack(locs[:0], expandedStack[:n]) + builder.pbSample(values, locs, nil) + } + + return builder.build() +} + +type countProfile interface { + Len() int + Stack(i int) []uintptr + Label(i int) *labelMap +} + +type runtimeProfile struct { + stk []stackRecord + labels []unsafe.Pointer +} + +func (p *runtimeProfile) Len() int { + return len(p.stk) +} + +func (p *runtimeProfile) Stack(i int) []uintptr { + return p.stk[i].Stack +} + +func (p *runtimeProfile) Label(i int) *labelMap { + return (*labelMap)(p.labels[i]) +} + +func writeCountProfile(w io.Writer, name string, profile countProfile) error { + var buf strings.Builder + key := func(stk []uintptr, labels *labelMap) string { + buf.Reset() + buf.WriteByte('@') + for _, pc := range stk { + fmt.Fprintf(&buf, " %#x", pc) + } + if labels != nil { + buf.WriteString("\n# labels:") + for _, label := range labels.list { + fmt.Fprintf(&buf, " %q:%q", label.key, label.value) + } + } + return buf.String() + } + + counts := make(map[string]int) + index := make(map[string]int) + var keys []string + for i := 0; i < profile.Len(); i++ { + k := key(profile.Stack(i), profile.Label(i)) + if counts[k] == 0 { + index[k] = i + keys = append(keys, k) + } + counts[k]++ + } + + sort.Sort(&keysByCount{keys: keys, count: counts}) + + builder := newProfileBuilder(w) + builder.pbValueType(tagProfile_PeriodType, name, "count") + builder.pb.int64Opt(tagProfile_Period, 1) + builder.pbValueType(tagProfile_SampleType, name, "count") + + values := []int64{0} + var locs []uint64 + for _, k := range keys { + values[0] = int64(counts[k]) + idx := index[k] + locs = builder.appendLocsForStack(locs[:0], profile.Stack(idx)) + + var labels func() + if profile.Label(idx) != nil { + labels = func() { + for _, label := range profile.Label(idx).list { + builder.pbLabel(tagSample_Label, label.key, label.value, 0) + } + } + } + builder.pbSample(values, locs, labels) + } + + return builder.build() +} + +type keysByCount struct { + keys []string + count map[string]int +} + +func (x *keysByCount) Len() int { + return len(x.keys) +} + +func (x *keysByCount) Swap(i int, j int) { + x.keys[i], x.keys[j] = x.keys[j], x.keys[i] +} + +func (x *keysByCount) Less(i int, j int) bool { + ki, kj := x.keys[i], x.keys[j] + ci, cj := x.count[ki], x.count[kj] + if ci != cj { + return ci > cj + } + return ki < kj +} + +func expandInlinedFrames(dst []uintptr, pcs []uintptr) int { + frames := runtime.CallersFrames(pcs) + var n int + for n < len(dst) { + frame, more := frames.Next() + dst[n] = frame.PC + 1 + n++ + if !more { + break + } + } + return n +} + +func writeHeapProto(w io.Writer, profile []memProfileRecord, rate int64, defaultSampleType string) error { + builder := newProfileBuilder(w) + builder.pbValueType(tagProfile_PeriodType, "space", "bytes") + builder.pb.int64Opt(tagProfile_Period, rate) + builder.pbValueType(tagProfile_SampleType, "alloc_objects", "count") + builder.pbValueType(tagProfile_SampleType, "alloc_space", "bytes") + builder.pbValueType(tagProfile_SampleType, "inuse_objects", "count") + builder.pbValueType(tagProfile_SampleType, "inuse_space", "bytes") + if defaultSampleType != "" { + builder.pb.int64Opt(tagProfile_DefaultSampleType, builder.stringIndex(defaultSampleType)) + } + + values := []int64{0, 0, 0, 0} + var locs []uint64 + for _, record := range profile { + hideRuntime := true + for tries := 0; tries < 2; tries++ { + stk := record.Stack + if hideRuntime { + for i, addr := range stk { + if f := runtime.FuncForPC(addr); f != nil && (strings.HasPrefix(f.Name(), "runtime.") || strings.HasPrefix(f.Name(), "internal/runtime/")) { + continue + } + stk = stk[i:] + break + } + } + locs = builder.appendLocsForStack(locs[:0], stk) + if len(locs) > 0 { + break + } + hideRuntime = false + } + + values[0], values[1] = scaleHeapSample(record.AllocObjects, record.AllocBytes, rate) + values[2], values[3] = scaleHeapSample(record.InUseObjects(), record.InUseBytes(), rate) + + var blockSize int64 + if record.AllocObjects > 0 { + blockSize = record.AllocBytes / record.AllocObjects + } + builder.pbSample(values, locs, func() { + if blockSize != 0 { + builder.pbLabel(tagSample_Label, "bytes", "", blockSize) + } + }) + } + + return builder.build() +} + +func scaleHeapSample(count int64, size int64, rate int64) (int64, int64) { + if count == 0 || size == 0 { + return 0, 0 + } + if rate <= 1 { + return count, size + } + + avgSize := float64(size) / float64(count) + scale := 1 / (1 - math.Exp(-avgSize/float64(rate))) + return int64(float64(count) * scale), int64(float64(size) * scale) +} + +type profileBuilder struct { + start time.Time + w io.Writer + err error + + pb protobuf + strings []string + stringMap map[string]int + locs map[uintptr]locInfo + funcs map[string]int + mem []memMap + deck pcDeck +} diff --git a/experimental/libbox/internal/oomprofile/protobuf.go b/experimental/libbox/internal/oomprofile/protobuf.go new file mode 100644 index 0000000000..0f06e00d50 --- /dev/null +++ b/experimental/libbox/internal/oomprofile/protobuf.go @@ -0,0 +1,120 @@ +//go:build darwin || linux || windows + +package oomprofile + +type protobuf struct { + data []byte + tmp [16]byte + nest int +} + +func (b *protobuf) varint(x uint64) { + for x >= 128 { + b.data = append(b.data, byte(x)|0x80) + x >>= 7 + } + b.data = append(b.data, byte(x)) +} + +func (b *protobuf) length(tag int, length int) { + b.varint(uint64(tag)<<3 | 2) + b.varint(uint64(length)) +} + +func (b *protobuf) uint64(tag int, x uint64) { + b.varint(uint64(tag)<<3 | 0) + b.varint(x) +} + +func (b *protobuf) uint64s(tag int, x []uint64) { + if len(x) > 2 { + n1 := len(b.data) + for _, u := range x { + b.varint(u) + } + n2 := len(b.data) + b.length(tag, n2-n1) + n3 := len(b.data) + copy(b.tmp[:], b.data[n2:n3]) + copy(b.data[n1+(n3-n2):], b.data[n1:n2]) + copy(b.data[n1:], b.tmp[:n3-n2]) + return + } + for _, u := range x { + b.uint64(tag, u) + } +} + +func (b *protobuf) uint64Opt(tag int, x uint64) { + if x == 0 { + return + } + b.uint64(tag, x) +} + +func (b *protobuf) int64(tag int, x int64) { + b.uint64(tag, uint64(x)) +} + +func (b *protobuf) int64Opt(tag int, x int64) { + if x == 0 { + return + } + b.int64(tag, x) +} + +func (b *protobuf) int64s(tag int, x []int64) { + if len(x) > 2 { + n1 := len(b.data) + for _, u := range x { + b.varint(uint64(u)) + } + n2 := len(b.data) + b.length(tag, n2-n1) + n3 := len(b.data) + copy(b.tmp[:], b.data[n2:n3]) + copy(b.data[n1+(n3-n2):], b.data[n1:n2]) + copy(b.data[n1:], b.tmp[:n3-n2]) + return + } + for _, u := range x { + b.int64(tag, u) + } +} + +func (b *protobuf) bool(tag int, x bool) { + if x { + b.uint64(tag, 1) + } else { + b.uint64(tag, 0) + } +} + +func (b *protobuf) string(tag int, x string) { + b.length(tag, len(x)) + b.data = append(b.data, x...) +} + +func (b *protobuf) strings(tag int, x []string) { + for _, s := range x { + b.string(tag, s) + } +} + +type msgOffset int + +func (b *protobuf) startMessage() msgOffset { + b.nest++ + return msgOffset(len(b.data)) +} + +func (b *protobuf) endMessage(tag int, start msgOffset) { + n1 := int(start) + n2 := len(b.data) + b.length(tag, n2-n1) + n3 := len(b.data) + copy(b.tmp[:], b.data[n2:n3]) + copy(b.data[n1+(n3-n2):], b.data[n1:n2]) + copy(b.data[n1:], b.tmp[:n3-n2]) + b.nest-- +} diff --git a/experimental/libbox/log.go b/experimental/libbox/log.go index ff33f08133..e275d7e6b0 100644 --- a/experimental/libbox/log.go +++ b/experimental/libbox/log.go @@ -1,24 +1,76 @@ -//go:build darwin || linux +//go:build darwin || linux || windows package libbox import ( + "archive/zip" + "io" + "io/fs" "os" + "path/filepath" "runtime" "runtime/debug" + "time" ) -var crashOutputFile *os.File +type crashReportMetadata struct { + reportMetadata + CrashedAt string `json:"crashedAt,omitempty"` + SignalName string `json:"signalName,omitempty"` + SignalCode string `json:"signalCode,omitempty"` + ExceptionName string `json:"exceptionName,omitempty"` + ExceptionReason string `json:"exceptionReason,omitempty"` +} + +func archiveCrashReport(path string, crashReportsDir string) { + content, err := os.ReadFile(path) + if err != nil || len(content) == 0 { + return + } + + info, _ := os.Stat(path) + crashTime := time.Now().UTC() + if info != nil { + crashTime = info.ModTime().UTC() + } + + initReportDir(crashReportsDir) + destPath, err := nextAvailableReportPath(crashReportsDir, crashTime) + if err != nil { + return + } + initReportDir(destPath) -func RedirectStderr(path string) error { - if stats, err := os.Stat(path); err == nil && stats.Size() > 0 { - _ = os.Rename(path, path+".old") + writeReportFile(destPath, "go.log", content) + metadata := crashReportMetadata{ + reportMetadata: baseReportMetadata(), + CrashedAt: crashTime.Format(time.RFC3339), } + writeReportMetadata(destPath, metadata) + os.Remove(path) + copyConfigSnapshot(destPath) +} + +func configSnapshotPath() string { + return filepath.Join(sBasePath, "configuration.json") +} + +func saveConfigSnapshot(configContent string) { + snapshotPath := configSnapshotPath() + os.WriteFile(snapshotPath, []byte(configContent), 0o666) + chownReport(snapshotPath) +} + +func redirectStderr(path string) error { + crashReportsDir := filepath.Join(sWorkingPath, "crash_reports") + archiveCrashReport(path, crashReportsDir) + archiveCrashReport(path+".old", crashReportsDir) + outputFile, err := os.Create(path) if err != nil { return err } - if runtime.GOOS != "android" { + if runtime.GOOS != "android" && runtime.GOOS != "windows" { err = outputFile.Chown(sUserID, sGroupID) if err != nil { outputFile.Close() @@ -26,12 +78,88 @@ func RedirectStderr(path string) error { return err } } + err = debug.SetCrashOutput(outputFile, debug.CrashOptions{}) if err != nil { outputFile.Close() os.Remove(outputFile.Name()) return err } - crashOutputFile = outputFile + _ = outputFile.Close() return nil } + +func CreateZipArchive(sourcePath string, destinationPath string) error { + sourceInfo, err := os.Stat(sourcePath) + if err != nil { + return err + } + if !sourceInfo.IsDir() { + return os.ErrInvalid + } + + destinationFile, err := os.Create(destinationPath) + if err != nil { + return err + } + defer func() { + _ = destinationFile.Close() + }() + + zipWriter := zip.NewWriter(destinationFile) + + rootName := filepath.Base(sourcePath) + err = filepath.WalkDir(sourcePath, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + relativePath, err := filepath.Rel(sourcePath, path) + if err != nil { + return err + } + if relativePath == "." { + return nil + } + + archivePath := filepath.ToSlash(filepath.Join(rootName, relativePath)) + if d.IsDir() { + _, err = zipWriter.Create(archivePath + "/") + return err + } + + fileInfo, err := d.Info() + if err != nil { + return err + } + header, err := zip.FileInfoHeader(fileInfo) + if err != nil { + return err + } + header.Name = archivePath + header.Method = zip.Deflate + + writer, err := zipWriter.CreateHeader(header) + if err != nil { + return err + } + + sourceFile, err := os.Open(path) + if err != nil { + return err + } + + _, err = io.Copy(writer, sourceFile) + closeErr := sourceFile.Close() + if err != nil { + return err + } + return closeErr + }) + if err != nil { + _ = zipWriter.Close() + return err + } + + return zipWriter.Close() +} diff --git a/experimental/libbox/memory.go b/experimental/libbox/memory.go deleted file mode 100644 index b0b87f73f9..0000000000 --- a/experimental/libbox/memory.go +++ /dev/null @@ -1,26 +0,0 @@ -package libbox - -import ( - "math" - runtimeDebug "runtime/debug" - - C "github.com/sagernet/sing-box/constant" -) - -var memoryLimitEnabled bool - -func SetMemoryLimit(enabled bool) { - memoryLimitEnabled = enabled - const memoryLimitGo = 45 * 1024 * 1024 - if enabled { - runtimeDebug.SetGCPercent(10) - if C.IsIos { - runtimeDebug.SetMemoryLimit(memoryLimitGo) - } - } else { - runtimeDebug.SetGCPercent(100) - if C.IsIos { - runtimeDebug.SetMemoryLimit(math.MaxInt64) - } - } -} diff --git a/experimental/libbox/oom_report.go b/experimental/libbox/oom_report.go new file mode 100644 index 0000000000..e96c3e875d --- /dev/null +++ b/experimental/libbox/oom_report.go @@ -0,0 +1,141 @@ +//go:build darwin || linux || windows + +package libbox + +import ( + "os" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/sagernet/sing-box/experimental/libbox/internal/oomprofile" + "github.com/sagernet/sing-box/service/oomkiller" + "github.com/sagernet/sing/common/byteformats" + "github.com/sagernet/sing/common/memory" +) + +func init() { + sOOMReporter = &oomReporter{} +} + +var oomReportProfiles = []string{ + "allocs", + "block", + "goroutine", + "heap", + "mutex", + "threadcreate", +} + +type oomReportMetadata struct { + reportMetadata + RecordedAt string `json:"recordedAt"` + MemoryUsage string `json:"memoryUsage"` + AvailableMemory string `json:"availableMemory,omitempty"` + // Heap + HeapAlloc string `json:"heapAlloc,omitempty"` + HeapObjects uint64 `json:"heapObjects,omitempty,string"` + HeapInuse string `json:"heapInuse,omitempty"` + HeapIdle string `json:"heapIdle,omitempty"` + HeapReleased string `json:"heapReleased,omitempty"` + HeapSys string `json:"heapSys,omitempty"` + // Stack + StackInuse string `json:"stackInuse,omitempty"` + StackSys string `json:"stackSys,omitempty"` + // Runtime metadata + MSpanInuse string `json:"mSpanInuse,omitempty"` + MSpanSys string `json:"mSpanSys,omitempty"` + MCacheSys string `json:"mCacheSys,omitempty"` + BuckHashSys string `json:"buckHashSys,omitempty"` + GCSys string `json:"gcSys,omitempty"` + OtherSys string `json:"otherSys,omitempty"` + Sys string `json:"sys,omitempty"` + // GC & runtime + TotalAlloc string `json:"totalAlloc,omitempty"` + NumGC uint32 `json:"numGC,omitempty,string"` + NumGoroutine int `json:"numGoroutine,omitempty,string"` + NextGC string `json:"nextGC,omitempty"` + LastGC string `json:"lastGC,omitempty"` +} + +type oomReporter struct{} + +var _ oomkiller.OOMReporter = (*oomReporter)(nil) + +func (r *oomReporter) WriteReport(memoryUsage uint64) error { + now := time.Now().UTC() + reportsDir := filepath.Join(sWorkingPath, "oom_reports") + err := os.MkdirAll(reportsDir, 0o777) + if err != nil { + return err + } + chownReport(reportsDir) + + destPath, err := nextAvailableReportPath(reportsDir, now) + if err != nil { + return err + } + err = os.MkdirAll(destPath, 0o777) + if err != nil { + return err + } + chownReport(destPath) + + for _, name := range oomReportProfiles { + writeOOMProfile(destPath, name) + } + + writeReportFile(destPath, "cmdline", []byte(strings.Join(os.Args, "\000"))) + + var memStats runtime.MemStats + runtime.ReadMemStats(&memStats) + + metadata := oomReportMetadata{ + reportMetadata: baseReportMetadata(), + RecordedAt: now.Format(time.RFC3339), + MemoryUsage: byteformats.FormatMemoryBytes(memoryUsage), + // Heap + HeapAlloc: byteformats.FormatMemoryBytes(memStats.HeapAlloc), + HeapObjects: memStats.HeapObjects, + HeapInuse: byteformats.FormatMemoryBytes(memStats.HeapInuse), + HeapIdle: byteformats.FormatMemoryBytes(memStats.HeapIdle), + HeapReleased: byteformats.FormatMemoryBytes(memStats.HeapReleased), + HeapSys: byteformats.FormatMemoryBytes(memStats.HeapSys), + // Stack + StackInuse: byteformats.FormatMemoryBytes(memStats.StackInuse), + StackSys: byteformats.FormatMemoryBytes(memStats.StackSys), + // Runtime metadata + MSpanInuse: byteformats.FormatMemoryBytes(memStats.MSpanInuse), + MSpanSys: byteformats.FormatMemoryBytes(memStats.MSpanSys), + MCacheSys: byteformats.FormatMemoryBytes(memStats.MCacheSys), + BuckHashSys: byteformats.FormatMemoryBytes(memStats.BuckHashSys), + GCSys: byteformats.FormatMemoryBytes(memStats.GCSys), + OtherSys: byteformats.FormatMemoryBytes(memStats.OtherSys), + Sys: byteformats.FormatMemoryBytes(memStats.Sys), + // GC & runtime + TotalAlloc: byteformats.FormatMemoryBytes(memStats.TotalAlloc), + NumGC: memStats.NumGC, + NumGoroutine: runtime.NumGoroutine(), + NextGC: byteformats.FormatMemoryBytes(memStats.NextGC), + } + if memStats.LastGC > 0 { + metadata.LastGC = time.Unix(0, int64(memStats.LastGC)).UTC().Format(time.RFC3339) + } + availableMemory := memory.Available() + if availableMemory > 0 { + metadata.AvailableMemory = byteformats.FormatMemoryBytes(availableMemory) + } + writeReportMetadata(destPath, metadata) + copyConfigSnapshot(destPath) + + return nil +} + +func writeOOMProfile(destPath string, name string) { + filePath, err := oomprofile.WriteFile(destPath, name) + if err != nil { + return + } + chownReport(filePath) +} diff --git a/experimental/libbox/report.go b/experimental/libbox/report.go new file mode 100644 index 0000000000..816dcac425 --- /dev/null +++ b/experimental/libbox/report.go @@ -0,0 +1,97 @@ +//go:build darwin || linux || windows + +package libbox + +import ( + "bytes" + "encoding/json" + "os" + "path/filepath" + "runtime" + "strconv" + "time" + + C "github.com/sagernet/sing-box/constant" + E "github.com/sagernet/sing/common/exceptions" +) + +type reportMetadata struct { + Source string `json:"source,omitempty"` + BundleIdentifier string `json:"bundleIdentifier,omitempty"` + ProcessName string `json:"processName,omitempty"` + ProcessPath string `json:"processPath,omitempty"` + StartedAt string `json:"startedAt,omitempty"` + AppVersion string `json:"appVersion,omitempty"` + AppMarketingVersion string `json:"appMarketingVersion,omitempty"` + CoreVersion string `json:"coreVersion,omitempty"` + GoVersion string `json:"goVersion,omitempty"` +} + +func baseReportMetadata() reportMetadata { + processPath, _ := os.Executable() + processName := filepath.Base(processPath) + if processName == "." { + processName = "" + } + return reportMetadata{ + Source: sCrashReportSource, + ProcessName: processName, + ProcessPath: processPath, + CoreVersion: C.Version, + GoVersion: GoVersion(), + } +} + +func writeReportFile(destPath string, name string, content []byte) { + filePath := filepath.Join(destPath, name) + os.WriteFile(filePath, content, 0o666) + chownReport(filePath) +} + +func writeReportMetadata(destPath string, metadata any) { + data, err := json.Marshal(metadata) + if err != nil { + return + } + writeReportFile(destPath, "metadata.json", data) +} + +func copyConfigSnapshot(destPath string) { + snapshotPath := configSnapshotPath() + content, err := os.ReadFile(snapshotPath) + if err != nil { + return + } + if len(bytes.TrimSpace(content)) == 0 { + return + } + writeReportFile(destPath, "configuration.json", content) +} + +func initReportDir(path string) { + os.MkdirAll(path, 0o777) + chownReport(path) +} + +func chownReport(path string) { + if runtime.GOOS != "android" && runtime.GOOS != "windows" { + os.Chown(path, sUserID, sGroupID) + } +} + +func nextAvailableReportPath(reportsDir string, timestamp time.Time) (string, error) { + destName := timestamp.Format("2006-01-02T15-04-05") + destPath := filepath.Join(reportsDir, destName) + _, err := os.Stat(destPath) + if os.IsNotExist(err) { + return destPath, nil + } + for i := 1; i <= 1000; i++ { + suffixedPath := filepath.Join(reportsDir, destName+"-"+strconv.Itoa(i)) + _, err = os.Stat(suffixedPath) + if os.IsNotExist(err) { + return suffixedPath, nil + } + } + return "", E.New("no available report path for ", destName) +} diff --git a/experimental/libbox/setup.go b/experimental/libbox/setup.go index 5063ce6db2..5b4b375d88 100644 --- a/experimental/libbox/setup.go +++ b/experimental/libbox/setup.go @@ -1,13 +1,17 @@ package libbox import ( + "math" "os" + "path/filepath" + "runtime" "runtime/debug" "time" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental/locale" "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/service/oomkiller" "github.com/sagernet/sing/common/byteformats" ) @@ -22,6 +26,10 @@ var ( sCommandServerSecret string sLogMaxLines int sDebug bool + sCrashReportSource string + sOOMKillerEnabled bool + sOOMKillerDisabled bool + sOOMMemoryLimit int64 ) func init() { @@ -38,9 +46,13 @@ type SetupOptions struct { CommandServerSecret string LogMaxLines int Debug bool + CrashReportSource string + OomKillerEnabled bool + OomKillerDisabled bool + OomMemoryLimit int64 } -func Setup(options *SetupOptions) error { +func applySetupOptions(options *SetupOptions) { sBasePath = options.BasePath sWorkingPath = options.WorkingPath sTempPath = options.TempPath @@ -56,10 +68,33 @@ func Setup(options *SetupOptions) error { sCommandServerSecret = options.CommandServerSecret sLogMaxLines = options.LogMaxLines sDebug = options.Debug + sCrashReportSource = options.CrashReportSource + ReloadSetupOptions(options) +} + +func ReloadSetupOptions(options *SetupOptions) { + sOOMKillerEnabled = options.OomKillerEnabled + sOOMKillerDisabled = options.OomKillerDisabled + sOOMMemoryLimit = options.OomMemoryLimit + if sOOMKillerEnabled { + if sOOMMemoryLimit == 0 && C.IsIos { + sOOMMemoryLimit = oomkiller.DefaultAppleNetworkExtensionMemoryLimit + } + if sOOMMemoryLimit > 0 { + debug.SetMemoryLimit(sOOMMemoryLimit * 3 / 4) + } else { + debug.SetMemoryLimit(math.MaxInt64) + } + } else { + debug.SetMemoryLimit(math.MaxInt64) + } +} +func Setup(options *SetupOptions) error { + applySetupOptions(options) os.MkdirAll(sWorkingPath, 0o777) os.MkdirAll(sTempPath, 0o777) - return nil + return redirectStderr(filepath.Join(sWorkingPath, "CrashReport-"+sCrashReportSource+".log")) } func SetLocale(localeId string) { @@ -70,6 +105,10 @@ func Version() string { return C.Version } +func GoVersion() string { + return runtime.Version() + ", " + runtime.GOOS + "/" + runtime.GOARCH +} + func FormatBytes(length int64) string { return byteformats.FormatKBytes(uint64(length)) } diff --git a/option/oom_killer.go b/option/oom_killer.go index 2032ed09ab..1183b502b7 100644 --- a/option/oom_killer.go +++ b/option/oom_killer.go @@ -6,9 +6,10 @@ import ( ) type OOMKillerServiceOptions struct { - MemoryLimit *byteformats.MemoryBytes `json:"memory_limit,omitempty"` - SafetyMargin *byteformats.MemoryBytes `json:"safety_margin,omitempty"` - MinInterval badoption.Duration `json:"min_interval,omitempty"` - MaxInterval badoption.Duration `json:"max_interval,omitempty"` - ChecksBeforeLimit int `json:"checks_before_limit,omitempty"` + MemoryLimit *byteformats.MemoryBytes `json:"memory_limit,omitempty"` + SafetyMargin *byteformats.MemoryBytes `json:"safety_margin,omitempty"` + MinInterval badoption.Duration `json:"min_interval,omitempty"` + MaxInterval badoption.Duration `json:"max_interval,omitempty"` + KillerDisabled bool `json:"-"` + MemoryLimitOverride uint64 `json:"-"` } diff --git a/service/oomkiller/config.go b/service/oomkiller/config.go deleted file mode 100644 index 693ced995b..0000000000 --- a/service/oomkiller/config.go +++ /dev/null @@ -1,51 +0,0 @@ -package oomkiller - -import ( - "time" - - "github.com/sagernet/sing-box/option" - E "github.com/sagernet/sing/common/exceptions" -) - -func buildTimerConfig(options option.OOMKillerServiceOptions, memoryLimit uint64, useAvailable bool) (timerConfig, error) { - safetyMargin := uint64(defaultSafetyMargin) - if options.SafetyMargin != nil && options.SafetyMargin.Value() > 0 { - safetyMargin = options.SafetyMargin.Value() - } - - minInterval := defaultMinInterval - if options.MinInterval != 0 { - minInterval = time.Duration(options.MinInterval.Build()) - if minInterval <= 0 { - return timerConfig{}, E.New("min_interval must be greater than 0") - } - } - - maxInterval := defaultMaxInterval - if options.MaxInterval != 0 { - maxInterval = time.Duration(options.MaxInterval.Build()) - if maxInterval <= 0 { - return timerConfig{}, E.New("max_interval must be greater than 0") - } - } - if maxInterval < minInterval { - return timerConfig{}, E.New("max_interval must be greater than or equal to min_interval") - } - - checksBeforeLimit := defaultChecksBeforeLimit - if options.ChecksBeforeLimit != 0 { - checksBeforeLimit = options.ChecksBeforeLimit - if checksBeforeLimit <= 0 { - return timerConfig{}, E.New("checks_before_limit must be greater than 0") - } - } - - return timerConfig{ - memoryLimit: memoryLimit, - safetyMargin: safetyMargin, - minInterval: minInterval, - maxInterval: maxInterval, - checksBeforeLimit: checksBeforeLimit, - useAvailable: useAvailable, - }, nil -} diff --git a/service/oomkiller/policy.go b/service/oomkiller/policy.go new file mode 100644 index 0000000000..aa74430157 --- /dev/null +++ b/service/oomkiller/policy.go @@ -0,0 +1,46 @@ +package oomkiller + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/memory" + "github.com/sagernet/sing/service" +) + +const DefaultAppleNetworkExtensionMemoryLimit = 50 * 1024 * 1024 + +type policyMode uint8 + +const ( + policyModeNone policyMode = iota + policyModeMemoryLimit + policyModeAvailable + policyModeNetworkExtension +) + +func (m policyMode) hasTimerMode() bool { + return m != policyModeNone +} + +func resolvePolicyMode(ctx context.Context, options option.OOMKillerServiceOptions) (uint64, policyMode) { + platformInterface := service.FromContext[adapter.PlatformInterface](ctx) + if C.IsIos && platformInterface != nil && platformInterface.UnderNetworkExtension() { + return DefaultAppleNetworkExtensionMemoryLimit, policyModeNetworkExtension + } + if options.MemoryLimitOverride > 0 { + return options.MemoryLimitOverride, policyModeMemoryLimit + } + if options.MemoryLimit != nil { + memoryLimit := options.MemoryLimit.Value() + if memoryLimit > 0 { + return memoryLimit, policyModeMemoryLimit + } + } + if memory.AvailableAvailable() { + return 0, policyModeAvailable + } + return 0, policyModeNone +} diff --git a/service/oomkiller/service.go b/service/oomkiller/service.go index c3612d9260..ec3838d2bf 100644 --- a/service/oomkiller/service.go +++ b/service/oomkiller/service.go @@ -1,192 +1,83 @@ -//go:build darwin && cgo - package oomkiller -/* -#include - -static dispatch_source_t memoryPressureSource; - -extern void goMemoryPressureCallback(unsigned long status); - -static void startMemoryPressureMonitor() { - memoryPressureSource = dispatch_source_create( - DISPATCH_SOURCE_TYPE_MEMORYPRESSURE, - 0, - DISPATCH_MEMORYPRESSURE_WARN | DISPATCH_MEMORYPRESSURE_CRITICAL, - dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0) - ); - dispatch_source_set_event_handler(memoryPressureSource, ^{ - unsigned long status = dispatch_source_get_data(memoryPressureSource); - goMemoryPressureCallback(status); - }); - dispatch_activate(memoryPressureSource); -} - -static void stopMemoryPressureMonitor() { - if (memoryPressureSource) { - dispatch_source_cancel(memoryPressureSource); - memoryPressureSource = NULL; - } -} -*/ -import "C" - import ( "context" - runtimeDebug "runtime/debug" - "sync" + "sync/atomic" + "time" "github.com/sagernet/sing-box/adapter" boxService "github.com/sagernet/sing-box/adapter/service" boxConstant "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing/common/memory" "github.com/sagernet/sing/service" ) +type OOMReporter interface { + WriteReport(memoryUsage uint64) error +} + func RegisterService(registry *boxService.Registry) { boxService.Register[option.OOMKillerServiceOptions](registry, boxConstant.TypeOOMKiller, NewService) } -var ( - globalAccess sync.Mutex - globalServices []*Service -) - type Service struct { boxService.Adapter - logger log.ContextLogger - router adapter.Router - memoryLimit uint64 - hasTimerMode bool - useAvailable bool - timerConfig timerConfig - adaptiveTimer *adaptiveTimer + ctx context.Context + logger log.ContextLogger + router adapter.Router + timerConfig timerConfig + adaptiveTimer *adaptiveTimer + lastReportTime atomic.Int64 } func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.OOMKillerServiceOptions) (adapter.Service, error) { - s := &Service{ - Adapter: boxService.NewAdapter(boxConstant.TypeOOMKiller, tag), - logger: logger, - router: service.FromContext[adapter.Router](ctx), - } - - if options.MemoryLimit != nil { - s.memoryLimit = options.MemoryLimit.Value() - if s.memoryLimit > 0 { - s.hasTimerMode = true - } - } - - config, err := buildTimerConfig(options, s.memoryLimit, s.useAvailable) + memoryLimit, mode := resolvePolicyMode(ctx, options) + config, err := buildTimerConfig(options, memoryLimit, mode, options.KillerDisabled) if err != nil { return nil, err } - s.timerConfig = config - - return s, nil + return &Service{ + Adapter: boxService.NewAdapter(boxConstant.TypeOOMKiller, tag), + ctx: ctx, + logger: logger, + router: service.FromContext[adapter.Router](ctx), + timerConfig: config, + }, nil } -func (s *Service) Start(stage adapter.StartStage) error { - if stage != adapter.StartStateStart { - return nil - } - - if s.hasTimerMode { - s.adaptiveTimer = newAdaptiveTimer(s.logger, s.router, s.timerConfig) - if s.memoryLimit > 0 { - s.logger.Info("started memory monitor with limit: ", s.memoryLimit/(1024*1024), " MiB") - } else { - s.logger.Info("started memory monitor with available memory detection") - } - } else { - s.logger.Info("started memory pressure monitor") - } - - globalAccess.Lock() - isFirst := len(globalServices) == 0 - globalServices = append(globalServices, s) - globalAccess.Unlock() +func (s *Service) createTimer() { + s.adaptiveTimer = newAdaptiveTimer(s.logger, s.router, s.timerConfig, s.writeOOMReport) +} - if isFirst { - C.startMemoryPressureMonitor() - } - return nil +func (s *Service) startTimer() { + s.createTimer() + s.adaptiveTimer.start() } -func (s *Service) Close() error { +func (s *Service) stopTimer() { if s.adaptiveTimer != nil { s.adaptiveTimer.stop() } - globalAccess.Lock() - for i, svc := range globalServices { - if svc == s { - globalServices = append(globalServices[:i], globalServices[i+1:]...) - break - } - } - isLast := len(globalServices) == 0 - globalAccess.Unlock() - if isLast { - C.stopMemoryPressureMonitor() - } - return nil } -//export goMemoryPressureCallback -func goMemoryPressureCallback(status C.ulong) { - globalAccess.Lock() - services := make([]*Service, len(globalServices)) - copy(services, globalServices) - globalAccess.Unlock() - if len(services) == 0 { +func (s *Service) writeOOMReport(memoryUsage uint64) { + now := time.Now().Unix() + lastReport := s.lastReportTime.Load() + if now-lastReport < 3600 { return } - criticalFlag := C.ulong(C.DISPATCH_MEMORYPRESSURE_CRITICAL) - warnFlag := C.ulong(C.DISPATCH_MEMORYPRESSURE_WARN) - isCritical := status&criticalFlag != 0 - isWarning := status&warnFlag != 0 - var level string - switch { - case isCritical: - level = "critical" - case isWarning: - level = "warning" - default: - level = "normal" + if !s.lastReportTime.CompareAndSwap(lastReport, now) { + return } - var freeOSMemory bool - for _, s := range services { - usage := memory.Total() - if s.hasTimerMode { - if isCritical { - s.logger.Warn("memory pressure: ", level, ", usage: ", usage/(1024*1024), " MiB") - if s.adaptiveTimer != nil { - s.adaptiveTimer.startNow() - } - } else if isWarning { - s.logger.Warn("memory pressure: ", level, ", usage: ", usage/(1024*1024), " MiB") - } else { - s.logger.Debug("memory pressure: ", level, ", usage: ", usage/(1024*1024), " MiB") - if s.adaptiveTimer != nil { - s.adaptiveTimer.stop() - } - } - } else { - if isCritical { - s.logger.Error("memory pressure: ", level, ", usage: ", usage/(1024*1024), " MiB, resetting network") - s.router.ResetNetwork() - freeOSMemory = true - } else if isWarning { - s.logger.Warn("memory pressure: ", level, ", usage: ", usage/(1024*1024), " MiB") - } else { - s.logger.Debug("memory pressure: ", level, ", usage: ", usage/(1024*1024), " MiB") - } - } + reporter := service.FromContext[OOMReporter](s.ctx) + if reporter == nil { + return } - if freeOSMemory { - runtimeDebug.FreeOSMemory() + err := reporter.WriteReport(memoryUsage) + if err != nil { + s.logger.Warn("failed to write OOM report: ", err) + } else { + s.logger.Info("OOM report saved") } } diff --git a/service/oomkiller/service_darwin.go b/service/oomkiller/service_darwin.go new file mode 100644 index 0000000000..7c957dcefb --- /dev/null +++ b/service/oomkiller/service_darwin.go @@ -0,0 +1,103 @@ +//go:build darwin && cgo + +package oomkiller + +/* +#include + +static dispatch_source_t memoryPressureSource; + +extern void goMemoryPressureCallback(unsigned long status); + +static void startMemoryPressureMonitor() { + memoryPressureSource = dispatch_source_create( + DISPATCH_SOURCE_TYPE_MEMORYPRESSURE, + 0, + DISPATCH_MEMORYPRESSURE_CRITICAL, + dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0) + ); + dispatch_source_set_event_handler(memoryPressureSource, ^{ + unsigned long status = dispatch_source_get_data(memoryPressureSource); + goMemoryPressureCallback(status); + }); + dispatch_activate(memoryPressureSource); +} + +static void stopMemoryPressureMonitor() { + if (memoryPressureSource) { + dispatch_source_cancel(memoryPressureSource); + memoryPressureSource = NULL; + } +} +*/ +import "C" + +import ( + "sync" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common/byteformats" + E "github.com/sagernet/sing/common/exceptions" +) + +var ( + globalAccess sync.Mutex + globalServices []*Service +) + +func (s *Service) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + if s.timerConfig.policyMode == policyModeNetworkExtension { + s.createTimer() + globalAccess.Lock() + isFirst := len(globalServices) == 0 + globalServices = append(globalServices, s) + globalAccess.Unlock() + if isFirst { + C.startMemoryPressureMonitor() + } + return nil + } + if !s.timerConfig.policyMode.hasTimerMode() { + return E.New("memory pressure monitoring is not available on this platform without memory_limit") + } + s.startTimer() + return nil +} + +func (s *Service) Close() error { + s.stopTimer() + if s.timerConfig.policyMode == policyModeNetworkExtension { + globalAccess.Lock() + for i, svc := range globalServices { + if svc == s { + globalServices = append(globalServices[:i], globalServices[i+1:]...) + break + } + } + isLast := len(globalServices) == 0 + globalAccess.Unlock() + if isLast { + C.stopMemoryPressureMonitor() + } + } + return nil +} + +//export goMemoryPressureCallback +func goMemoryPressureCallback(status C.ulong) { + globalAccess.Lock() + services := make([]*Service, len(globalServices)) + copy(services, globalServices) + globalAccess.Unlock() + if len(services) == 0 { + return + } + sample := readMemorySample(policyModeNetworkExtension) + for _, s := range services { + s.logger.Warn("memory pressure: critical, usage: ", byteformats.FormatMemoryBytes(sample.usage)) + s.adaptiveTimer.notifyPressure() + } +} diff --git a/service/oomkiller/service_stub.go b/service/oomkiller/service_stub.go index 13348bac10..5eaf82046a 100644 --- a/service/oomkiller/service_stub.go +++ b/service/oomkiller/service_stub.go @@ -3,79 +3,22 @@ package oomkiller import ( - "context" - "github.com/sagernet/sing-box/adapter" - boxService "github.com/sagernet/sing-box/adapter/service" - boxConstant "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/common/memory" - "github.com/sagernet/sing/service" ) -func RegisterService(registry *boxService.Registry) { - boxService.Register[option.OOMKillerServiceOptions](registry, boxConstant.TypeOOMKiller, NewService) -} - -type Service struct { - boxService.Adapter - logger log.ContextLogger - router adapter.Router - adaptiveTimer *adaptiveTimer - timerConfig timerConfig - hasTimerMode bool - useAvailable bool - memoryLimit uint64 -} - -func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.OOMKillerServiceOptions) (adapter.Service, error) { - s := &Service{ - Adapter: boxService.NewAdapter(boxConstant.TypeOOMKiller, tag), - logger: logger, - router: service.FromContext[adapter.Router](ctx), - } - - if options.MemoryLimit != nil { - s.memoryLimit = options.MemoryLimit.Value() - } - if s.memoryLimit > 0 { - s.hasTimerMode = true - } else if memory.AvailableSupported() { - s.useAvailable = true - s.hasTimerMode = true - } - - config, err := buildTimerConfig(options, s.memoryLimit, s.useAvailable) - if err != nil { - return nil, err - } - s.timerConfig = config - - return s, nil -} - func (s *Service) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } - if !s.hasTimerMode { + if !s.timerConfig.policyMode.hasTimerMode() { return E.New("memory pressure monitoring is not available on this platform without memory_limit") } - s.adaptiveTimer = newAdaptiveTimer(s.logger, s.router, s.timerConfig) - s.adaptiveTimer.start(0) - if s.useAvailable { - s.logger.Info("started memory monitor with available memory detection") - } else { - s.logger.Info("started memory monitor with limit: ", s.memoryLimit/(1024*1024), " MiB") - } + s.startTimer() return nil } func (s *Service) Close() error { - if s.adaptiveTimer != nil { - s.adaptiveTimer.stop() - } + s.stopTimer() return nil } diff --git a/service/oomkiller/service_timer.go b/service/oomkiller/service_timer.go deleted file mode 100644 index 315e171564..0000000000 --- a/service/oomkiller/service_timer.go +++ /dev/null @@ -1,158 +0,0 @@ -package oomkiller - -import ( - runtimeDebug "runtime/debug" - "sync" - "time" - - "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing/common/memory" -) - -const ( - defaultChecksBeforeLimit = 4 - defaultMinInterval = 500 * time.Millisecond - defaultMaxInterval = 10 * time.Second - defaultSafetyMargin = 5 * 1024 * 1024 -) - -type adaptiveTimer struct { - logger log.ContextLogger - router adapter.Router - memoryLimit uint64 - safetyMargin uint64 - minInterval time.Duration - maxInterval time.Duration - checksBeforeLimit int - useAvailable bool - - access sync.Mutex - timer *time.Timer - previousUsage uint64 - lastInterval time.Duration -} - -type timerConfig struct { - memoryLimit uint64 - safetyMargin uint64 - minInterval time.Duration - maxInterval time.Duration - checksBeforeLimit int - useAvailable bool -} - -func newAdaptiveTimer(logger log.ContextLogger, router adapter.Router, config timerConfig) *adaptiveTimer { - return &adaptiveTimer{ - logger: logger, - router: router, - memoryLimit: config.memoryLimit, - safetyMargin: config.safetyMargin, - minInterval: config.minInterval, - maxInterval: config.maxInterval, - checksBeforeLimit: config.checksBeforeLimit, - useAvailable: config.useAvailable, - } -} - -func (t *adaptiveTimer) start(_ uint64) { - t.access.Lock() - defer t.access.Unlock() - t.startLocked() -} - -func (t *adaptiveTimer) startNow() { - t.access.Lock() - t.startLocked() - t.access.Unlock() - t.poll() -} - -func (t *adaptiveTimer) startLocked() { - if t.timer != nil { - return - } - t.previousUsage = memory.Total() - t.lastInterval = t.minInterval - t.timer = time.AfterFunc(t.minInterval, t.poll) -} - -func (t *adaptiveTimer) stop() { - t.access.Lock() - defer t.access.Unlock() - t.stopLocked() -} - -func (t *adaptiveTimer) stopLocked() { - if t.timer != nil { - t.timer.Stop() - t.timer = nil - } -} - -func (t *adaptiveTimer) running() bool { - t.access.Lock() - defer t.access.Unlock() - return t.timer != nil -} - -func (t *adaptiveTimer) poll() { - t.access.Lock() - defer t.access.Unlock() - if t.timer == nil { - return - } - - usage := memory.Total() - delta := int64(usage) - int64(t.previousUsage) - t.previousUsage = usage - - var remaining uint64 - var triggered bool - - if t.memoryLimit > 0 { - if usage >= t.memoryLimit { - remaining = 0 - triggered = true - } else { - remaining = t.memoryLimit - usage - } - } else if t.useAvailable { - available := memory.Available() - if available <= t.safetyMargin { - remaining = 0 - triggered = true - } else { - remaining = available - t.safetyMargin - } - } else { - remaining = 0 - } - - if triggered { - t.logger.Error("memory threshold reached, usage: ", usage/(1024*1024), " MiB, resetting network") - t.router.ResetNetwork() - runtimeDebug.FreeOSMemory() - } - - var interval time.Duration - if triggered { - interval = t.maxInterval - } else if delta <= 0 { - interval = t.maxInterval - } else if t.checksBeforeLimit <= 0 { - interval = t.maxInterval - } else { - timeToLimit := time.Duration(float64(remaining) / float64(delta) * float64(t.lastInterval)) - interval = timeToLimit / time.Duration(t.checksBeforeLimit) - if interval < t.minInterval { - interval = t.minInterval - } - if interval > t.maxInterval { - interval = t.maxInterval - } - } - - t.lastInterval = interval - t.timer.Reset(interval) -} diff --git a/service/oomkiller/timer.go b/service/oomkiller/timer.go new file mode 100644 index 0000000000..146ecc3547 --- /dev/null +++ b/service/oomkiller/timer.go @@ -0,0 +1,325 @@ +package oomkiller + +import ( + runtimeDebug "runtime/debug" + "sync" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/byteformats" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/memory" +) + +const ( + defaultMinInterval = 100 * time.Millisecond + defaultArmedInterval = time.Second + defaultMaxInterval = 10 * time.Second + defaultSafetyMargin = 5 * 1024 * 1024 + defaultAvailableTriggerMarginMin = 32 * 1024 * 1024 + defaultAvailableTriggerMarginMax = 128 * 1024 * 1024 +) + +type pressureState uint8 + +const ( + pressureStateNormal pressureState = iota + pressureStateArmed + pressureStateTriggered +) + +type memorySample struct { + usage uint64 + available uint64 + availableKnown bool +} + +type pressureThresholds struct { + trigger uint64 + armed uint64 + resume uint64 +} + +type timerConfig struct { + memoryLimit uint64 + safetyMargin uint64 + hasSafetyMargin bool + minInterval time.Duration + armedInterval time.Duration + maxInterval time.Duration + policyMode policyMode + killerDisabled bool +} + +func buildTimerConfig(options option.OOMKillerServiceOptions, memoryLimit uint64, policyMode policyMode, killerDisabled bool) (timerConfig, error) { + minInterval := defaultMinInterval + if options.MinInterval != 0 { + minInterval = time.Duration(options.MinInterval.Build()) + if minInterval <= 0 { + return timerConfig{}, E.New("min_interval must be greater than 0") + } + } + + maxInterval := defaultMaxInterval + if options.MaxInterval != 0 { + maxInterval = time.Duration(options.MaxInterval.Build()) + if maxInterval <= 0 { + return timerConfig{}, E.New("max_interval must be greater than 0") + } + } + if maxInterval < minInterval { + return timerConfig{}, E.New("max_interval must be greater than or equal to min_interval") + } + + var ( + safetyMargin uint64 + hasSafetyMargin bool + ) + if options.SafetyMargin != nil && options.SafetyMargin.Value() > 0 { + safetyMargin = options.SafetyMargin.Value() + hasSafetyMargin = true + } else if memoryLimit > 0 { + safetyMargin = defaultSafetyMargin + hasSafetyMargin = true + } + + return timerConfig{ + memoryLimit: memoryLimit, + safetyMargin: safetyMargin, + hasSafetyMargin: hasSafetyMargin, + minInterval: minInterval, + armedInterval: max(min(defaultArmedInterval, maxInterval), minInterval), + maxInterval: maxInterval, + policyMode: policyMode, + killerDisabled: killerDisabled, + }, nil +} + +type adaptiveTimer struct { + timerConfig + logger log.ContextLogger + router adapter.Router + onTriggered func(uint64) + limitThresholds pressureThresholds + + access sync.Mutex + timer *time.Timer + state pressureState + forceMinInterval bool + pendingPressureBaseline bool + pressureBaseline memorySample + pressureBaselineTime time.Time +} + +func newAdaptiveTimer(logger log.ContextLogger, router adapter.Router, config timerConfig, onTriggered func(uint64)) *adaptiveTimer { + t := &adaptiveTimer{ + timerConfig: config, + logger: logger, + router: router, + onTriggered: onTriggered, + } + if config.policyMode == policyModeMemoryLimit || config.policyMode == policyModeNetworkExtension { + t.limitThresholds = computeLimitThresholds(config.memoryLimit, config.safetyMargin) + } + return t +} + +func (t *adaptiveTimer) start() { + t.access.Lock() + defer t.access.Unlock() + t.startLocked() +} + +func (t *adaptiveTimer) notifyPressure() { + t.access.Lock() + t.startLocked() + t.forceMinInterval = true + t.pendingPressureBaseline = true + t.access.Unlock() + t.poll() +} + +func (t *adaptiveTimer) startLocked() { + if t.timer != nil { + return + } + t.state = pressureStateNormal + t.forceMinInterval = false + t.timer = time.AfterFunc(t.minInterval, t.poll) +} + +func (t *adaptiveTimer) stop() { + t.access.Lock() + defer t.access.Unlock() + if t.timer != nil { + t.timer.Stop() + t.timer = nil + } +} + +func (t *adaptiveTimer) poll() { + var triggered bool + var rateTriggered bool + sample := readMemorySample(t.policyMode) + + t.access.Lock() + if t.timer == nil { + t.access.Unlock() + return + } + if t.pendingPressureBaseline { + t.pressureBaseline = sample + t.pressureBaselineTime = time.Now() + t.pendingPressureBaseline = false + } + previousState := t.state + t.state = t.nextState(sample) + if t.state == pressureStateNormal { + t.forceMinInterval = false + t.pressureBaselineTime = time.Time{} + } + t.timer.Reset(t.intervalForState()) + triggered = previousState != pressureStateTriggered && t.state == pressureStateTriggered + if !triggered && !t.pressureBaselineTime.IsZero() && t.memoryLimit > 0 && + sample.usage > t.pressureBaseline.usage && sample.usage < t.memoryLimit { + elapsed := time.Since(t.pressureBaselineTime) + if elapsed >= t.minInterval/2 { + growth := sample.usage - t.pressureBaseline.usage + ratePerSecond := float64(growth) / elapsed.Seconds() + headroom := t.memoryLimit - sample.usage + timeToLimit := time.Duration(float64(headroom)/ratePerSecond) * time.Second + if timeToLimit < t.minInterval { + triggered = true + rateTriggered = true + t.state = pressureStateTriggered + } + } + } + t.access.Unlock() + + if !triggered { + return + } + if rateTriggered { + if t.killerDisabled { + t.logger.Warn("memory growth rate critical (report only), usage: ", byteformats.FormatMemoryBytes(sample.usage), t.logDetails(sample)) + } else { + t.logger.Error("memory growth rate critical, usage: ", byteformats.FormatMemoryBytes(sample.usage), t.logDetails(sample), ", resetting network") + t.router.ResetNetwork() + } + } else { + if t.killerDisabled { + t.logger.Warn("memory threshold reached (report only), usage: ", byteformats.FormatMemoryBytes(sample.usage), t.logDetails(sample)) + } else { + t.logger.Error("memory threshold reached, usage: ", byteformats.FormatMemoryBytes(sample.usage), t.logDetails(sample), ", resetting network") + t.router.ResetNetwork() + } + } + t.onTriggered(sample.usage) + runtimeDebug.FreeOSMemory() +} + +func (t *adaptiveTimer) nextState(sample memorySample) pressureState { + switch t.policyMode { + case policyModeMemoryLimit, policyModeNetworkExtension: + return nextPressureState(t.state, + sample.usage >= t.limitThresholds.trigger, + sample.usage >= t.limitThresholds.armed, + sample.usage >= t.limitThresholds.resume, + ) + case policyModeAvailable: + if !sample.availableKnown { + return pressureStateNormal + } + thresholds := t.availableThresholds(sample) + return nextPressureState(t.state, + sample.available <= thresholds.trigger, + sample.available <= thresholds.armed, + sample.available <= thresholds.resume, + ) + default: + return pressureStateNormal + } +} + +func computeLimitThresholds(memoryLimit uint64, safetyMargin uint64) pressureThresholds { + triggerMargin := min(safetyMargin, memoryLimit) + armedMargin := min(triggerMargin*2, memoryLimit) + resumeMargin := min(triggerMargin*4, memoryLimit) + return pressureThresholds{ + trigger: memoryLimit - triggerMargin, + armed: memoryLimit - armedMargin, + resume: memoryLimit - resumeMargin, + } +} + +func (t *adaptiveTimer) availableThresholds(sample memorySample) pressureThresholds { + var triggerMargin uint64 + if t.hasSafetyMargin { + triggerMargin = t.safetyMargin + } else if sample.usage == 0 { + triggerMargin = defaultAvailableTriggerMarginMin + } else { + triggerMargin = max(defaultAvailableTriggerMarginMin, min(sample.usage/4, defaultAvailableTriggerMarginMax)) + } + return pressureThresholds{ + trigger: triggerMargin, + armed: triggerMargin * 2, + resume: triggerMargin * 4, + } +} + +func (t *adaptiveTimer) intervalForState() time.Duration { + if t.state == pressureStateNormal { + return t.maxInterval + } + if t.forceMinInterval || t.state == pressureStateTriggered { + return t.minInterval + } + return t.armedInterval +} + +func (t *adaptiveTimer) logDetails(sample memorySample) string { + switch t.policyMode { + case policyModeMemoryLimit, policyModeNetworkExtension: + headroom := uint64(0) + if sample.usage < t.memoryLimit { + headroom = t.memoryLimit - sample.usage + } + return ", limit: " + byteformats.FormatMemoryBytes(t.memoryLimit) + ", headroom: " + byteformats.FormatMemoryBytes(headroom) + case policyModeAvailable: + if sample.availableKnown { + return ", available: " + byteformats.FormatMemoryBytes(sample.available) + } + } + return "" +} + +func nextPressureState(current pressureState, shouldTrigger, shouldArm, shouldStayTriggered bool) pressureState { + if current == pressureStateTriggered { + if shouldStayTriggered { + return pressureStateTriggered + } + return pressureStateNormal + } + if shouldTrigger { + return pressureStateTriggered + } + if shouldArm { + return pressureStateArmed + } + return pressureStateNormal +} + +func readMemorySample(mode policyMode) memorySample { + sample := memorySample{ + usage: memory.Total(), + } + if mode == policyModeAvailable { + sample.availableKnown = true + sample.available = memory.Available() + } + return sample +} From ff94634b525cac71b373076d065adafb66c63a82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 7 Apr 2026 13:43:10 +0800 Subject: [PATCH 08/93] Also enable certificate store by default on Apple platforms `SecTrustEvaluateWithError` is serial --- box.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/box.go b/box.go index 67feadc2d4..2242aa01bc 100644 --- a/box.go +++ b/box.go @@ -170,10 +170,7 @@ func New(options Options) (*Box, error) { var internalServices []adapter.LifecycleService certificateOptions := common.PtrValueOrDefault(options.Certificate) - if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem || - len(certificateOptions.Certificate) > 0 || - len(certificateOptions.CertificatePath) > 0 || - len(certificateOptions.CertificateDirectoryPath) > 0 { + if C.IsAndroid || C.IsDarwin || certificateOptions.Store != "" { certificateStore, err := certificate.NewStore(ctx, logFactory.NewLogger("certificate"), certificateOptions) if err != nil { return nil, err From e1a7ab3df3851370c87ea24f50e915c87ea5fa76 Mon Sep 17 00:00:00 2001 From: nekohasekai Date: Tue, 7 Apr 2026 20:02:32 +0800 Subject: [PATCH 09/93] Add evaluate DNS rule action and related rule items --- adapter/dns.go | 9 +- adapter/inbound.go | 65 +- adapter/inbound_test.go | 45 + adapter/router.go | 12 +- adapter/rule.go | 7 +- box.go | 3 +- constant/dns.go | 25 +- constant/rule.go | 2 + dns/client.go | 33 +- dns/repro_test.go | 111 + dns/router.go | 788 ++++- dns/router_test.go | 2547 +++++++++++++++++ dns/transport_adapter.go | 23 - dns/transport_dialer.go | 99 +- docs/changelog.md | 12 +- docs/configuration/dns/fakeip.md | 8 +- docs/configuration/dns/fakeip.zh.md | 6 +- docs/configuration/dns/index.md | 4 +- docs/configuration/dns/index.zh.md | 2 +- docs/configuration/dns/rule.md | 99 +- docs/configuration/dns/rule.zh.md | 102 +- docs/configuration/dns/rule_action.md | 75 +- docs/configuration/dns/rule_action.zh.md | 73 +- docs/configuration/dns/server/index.md | 2 +- docs/configuration/dns/server/index.zh.md | 2 +- docs/configuration/dns/server/legacy.md | 10 +- docs/configuration/dns/server/legacy.zh.md | 6 +- docs/configuration/experimental/cache-file.md | 2 +- .../experimental/cache-file.zh.md | 2 +- docs/configuration/route/index.md | 4 +- docs/configuration/route/rule_action.md | 2 +- docs/deprecated.md | 33 +- docs/deprecated.zh.md | 30 +- docs/migration.md | 105 + docs/migration.zh.md | 105 + experimental/deprecated/constants.go | 60 +- .../libbox/internal/oomprofile/linkname.go | 1 - .../internal/oomprofile/mapping_darwin.go | 1 - option/dns.go | 272 +- option/dns_record.go | 25 +- option/dns_record_test.go | 40 + option/dns_test.go | 54 + option/rule.go | 77 +- option/rule_action.go | 14 + option/rule_action_test.go | 29 + option/rule_dns.go | 64 +- option/rule_nested.go | 133 + option/rule_nested_test.go | 68 + route/router.go | 4 + route/rule/rule_abstract.go | 1 - route/rule/rule_action.go | 55 +- route/rule/rule_default.go | 4 + route/rule/rule_dns.go | 179 +- route/rule/rule_item_cidr.go | 19 +- route/rule/rule_item_ip_accept_any.go | 3 + route/rule/rule_item_ip_is_private.go | 23 +- route/rule/rule_item_response_rcode.go | 26 + route/rule/rule_item_response_record.go | 63 + route/rule/rule_item_rule_set.go | 11 + route/rule/rule_item_rule_set_test.go | 138 + route/rule/rule_nested_action.go | 71 + route/rule/rule_nested_action_test.go | 88 + route/rule/rule_set.go | 22 + route/rule/rule_set_local.go | 9 +- route/rule/rule_set_remote.go | 9 +- route/rule/rule_set_semantics_test.go | 427 ++- route/rule/rule_set_update_validation_test.go | 111 + 67 files changed, 5882 insertions(+), 672 deletions(-) create mode 100644 adapter/inbound_test.go create mode 100644 dns/repro_test.go create mode 100644 dns/router_test.go create mode 100644 option/dns_record_test.go create mode 100644 option/dns_test.go create mode 100644 option/rule_action_test.go create mode 100644 option/rule_nested.go create mode 100644 option/rule_nested_test.go create mode 100644 route/rule/rule_item_response_rcode.go create mode 100644 route/rule/rule_item_response_record.go create mode 100644 route/rule/rule_item_rule_set_test.go create mode 100644 route/rule/rule_nested_action.go create mode 100644 route/rule/rule_nested_action_test.go create mode 100644 route/rule/rule_set_update_validation_test.go diff --git a/adapter/dns.go b/adapter/dns.go index 23fbc9def4..f527e7ccd3 100644 --- a/adapter/dns.go +++ b/adapter/dns.go @@ -25,8 +25,8 @@ type DNSRouter interface { type DNSClient interface { Start() - Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error) - Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) + Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(response *dns.Msg) bool) (*dns.Msg, error) + Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) ClearCache() } @@ -74,11 +74,6 @@ type DNSTransport interface { Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error) } -type LegacyDNSTransport interface { - LegacyStrategy() C.DomainStrategy - LegacyClientSubnet() netip.Prefix -} - type DNSTransportRegistry interface { option.DNSTransportOptionsRegistry CreateDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) (DNSTransport, error) diff --git a/adapter/inbound.go b/adapter/inbound.go index 52af336e5b..6f53b1222e 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -10,6 +10,8 @@ import ( "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" M "github.com/sagernet/sing/common/metadata" + + "github.com/miekg/dns" ) type Inbound interface { @@ -79,14 +81,16 @@ type InboundContext struct { FallbackNetworkType []C.InterfaceType FallbackDelay time.Duration - DestinationAddresses []netip.Addr - SourceGeoIPCode string - GeoIPCode string - ProcessInfo *ConnectionOwner - SourceMACAddress net.HardwareAddr - SourceHostname string - QueryType uint16 - FakeIP bool + DestinationAddresses []netip.Addr + DNSResponse *dns.Msg + DestinationAddressMatchFromResponse bool + SourceGeoIPCode string + GeoIPCode string + ProcessInfo *ConnectionOwner + SourceMACAddress net.HardwareAddr + SourceHostname string + QueryType uint16 + FakeIP bool // rule cache @@ -115,6 +119,51 @@ func (c *InboundContext) ResetRuleMatchCache() { c.DidMatch = false } +func (c *InboundContext) DNSResponseAddressesForMatch() []netip.Addr { + return DNSResponseAddresses(c.DNSResponse) +} + +func DNSResponseAddresses(response *dns.Msg) []netip.Addr { + if response == nil || response.Rcode != dns.RcodeSuccess { + return nil + } + addresses := make([]netip.Addr, 0, len(response.Answer)) + for _, rawRecord := range response.Answer { + switch record := rawRecord.(type) { + case *dns.A: + addr := M.AddrFromIP(record.A) + if addr.IsValid() { + addresses = append(addresses, addr) + } + case *dns.AAAA: + addr := M.AddrFromIP(record.AAAA) + if addr.IsValid() { + addresses = append(addresses, addr) + } + case *dns.HTTPS: + for _, value := range record.SVCB.Value { + switch hint := value.(type) { + case *dns.SVCBIPv4Hint: + for _, ip := range hint.Hint { + addr := M.AddrFromIP(ip).Unmap() + if addr.IsValid() { + addresses = append(addresses, addr) + } + } + case *dns.SVCBIPv6Hint: + for _, ip := range hint.Hint { + addr := M.AddrFromIP(ip) + if addr.IsValid() { + addresses = append(addresses, addr) + } + } + } + } + } + } + return addresses +} + type inboundContextKey struct{} func WithContext(ctx context.Context, inboundContext *InboundContext) context.Context { diff --git a/adapter/inbound_test.go b/adapter/inbound_test.go new file mode 100644 index 0000000000..ec8c31289c --- /dev/null +++ b/adapter/inbound_test.go @@ -0,0 +1,45 @@ +package adapter + +import ( + "net" + "net/netip" + "testing" + + "github.com/miekg/dns" + "github.com/stretchr/testify/require" +) + +func TestDNSResponseAddressesUnmapsHTTPSIPv4Hints(t *testing.T) { + t.Parallel() + + ipv4Hint := net.ParseIP("1.1.1.1") + require.NotNil(t, ipv4Hint) + + response := &dns.Msg{ + MsgHdr: dns.MsgHdr{ + Response: true, + Rcode: dns.RcodeSuccess, + }, + Answer: []dns.RR{ + &dns.HTTPS{ + SVCB: dns.SVCB{ + Hdr: dns.RR_Header{ + Name: dns.Fqdn("example.com"), + Rrtype: dns.TypeHTTPS, + Class: dns.ClassINET, + Ttl: 60, + }, + Priority: 1, + Target: ".", + Value: []dns.SVCBKeyValue{ + &dns.SVCBIPv4Hint{Hint: []net.IP{ipv4Hint}}, + }, + }, + }, + }, + } + + addresses := DNSResponseAddresses(response) + require.Equal(t, []netip.Addr{netip.MustParseAddr("1.1.1.1")}, addresses) + require.True(t, addresses[0].Is4()) +} diff --git a/adapter/router.go b/adapter/router.go index 82e6881a60..f1e3da9a0c 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -66,10 +66,16 @@ type RuleSet interface { type RuleSetUpdateCallback func(it RuleSet) +type DNSRuleSetUpdateValidator interface { + ValidateRuleSetMetadataUpdate(tag string, metadata RuleSetMetadata) error +} + +// ip_version is not a headless-rule item, so ContainsIPVersionRule is intentionally absent. type RuleSetMetadata struct { - ContainsProcessRule bool - ContainsWIFIRule bool - ContainsIPCIDRRule bool + ContainsProcessRule bool + ContainsWIFIRule bool + ContainsIPCIDRRule bool + ContainsDNSQueryTypeRule bool } type HTTPStartContext struct { ctx context.Context diff --git a/adapter/rule.go b/adapter/rule.go index f8ee797d44..2117ba45a6 100644 --- a/adapter/rule.go +++ b/adapter/rule.go @@ -2,6 +2,8 @@ package adapter import ( C "github.com/sagernet/sing-box/constant" + + "github.com/miekg/dns" ) type HeadlessRule interface { @@ -18,8 +20,9 @@ type Rule interface { type DNSRule interface { Rule + LegacyPreMatch(metadata *InboundContext) bool WithAddressLimit() bool - MatchAddressLimit(metadata *InboundContext) bool + MatchAddressLimit(metadata *InboundContext, response *dns.Msg) bool } type RuleAction interface { @@ -29,7 +32,7 @@ type RuleAction interface { func IsFinalAction(action RuleAction) bool { switch action.Type() { - case C.RuleActionTypeSniff, C.RuleActionTypeResolve: + case C.RuleActionTypeSniff, C.RuleActionTypeResolve, C.RuleActionTypeEvaluate: return false default: return true diff --git a/box.go b/box.go index 2242aa01bc..d21ab29a44 100644 --- a/box.go +++ b/box.go @@ -195,6 +195,7 @@ func New(options Options) (*Box, error) { service.MustRegister[adapter.CertificateProviderManager](ctx, certificateProviderManager) dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions) service.MustRegister[adapter.DNSRouter](ctx, dnsRouter) + service.MustRegister[adapter.DNSRuleSetUpdateValidator](ctx, dnsRouter) networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions) if err != nil { return nil, E.Cause(err, "initialize network manager") @@ -483,7 +484,7 @@ func (s *Box) preStart() error { if err != nil { return err } - err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router) + err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.network, s.connection, s.router, s.dnsRouter) if err != nil { return err } diff --git a/constant/dns.go b/constant/dns.go index 15d6096c78..c7cd0d0374 100644 --- a/constant/dns.go +++ b/constant/dns.go @@ -15,19 +15,18 @@ const ( ) const ( - DNSTypeLegacy = "legacy" - DNSTypeLegacyRcode = "legacy_rcode" - DNSTypeUDP = "udp" - DNSTypeTCP = "tcp" - DNSTypeTLS = "tls" - DNSTypeHTTPS = "https" - DNSTypeQUIC = "quic" - DNSTypeHTTP3 = "h3" - DNSTypeLocal = "local" - DNSTypeHosts = "hosts" - DNSTypeFakeIP = "fakeip" - DNSTypeDHCP = "dhcp" - DNSTypeTailscale = "tailscale" + DNSTypeLegacy = "legacy" + DNSTypeUDP = "udp" + DNSTypeTCP = "tcp" + DNSTypeTLS = "tls" + DNSTypeHTTPS = "https" + DNSTypeQUIC = "quic" + DNSTypeHTTP3 = "h3" + DNSTypeLocal = "local" + DNSTypeHosts = "hosts" + DNSTypeFakeIP = "fakeip" + DNSTypeDHCP = "dhcp" + DNSTypeTailscale = "tailscale" ) const ( diff --git a/constant/rule.go b/constant/rule.go index 55cad2e137..15d71c5301 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -29,6 +29,8 @@ const ( const ( RuleActionTypeRoute = "route" RuleActionTypeRouteOptions = "route-options" + RuleActionTypeEvaluate = "evaluate" + RuleActionTypeRespond = "respond" RuleActionTypeDirect = "direct" RuleActionTypeBypass = "bypass" RuleActionTypeReject = "reject" diff --git a/dns/client.go b/dns/client.go index 1a2ee8f8c3..08468b352a 100644 --- a/dns/client.go +++ b/dns/client.go @@ -5,7 +5,6 @@ import ( "errors" "net" "net/netip" - "strings" "time" "github.com/sagernet/sing-box/adapter" @@ -14,7 +13,6 @@ import ( "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" "github.com/sagernet/sing/common/task" "github.com/sagernet/sing/contrab/freelru" "github.com/sagernet/sing/contrab/maphash" @@ -109,7 +107,7 @@ func extractNegativeTTL(response *dns.Msg) (uint32, bool) { return 0, false } -func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error) { +func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) (*dns.Msg, error) { if len(message.Question) == 0 { if c.logger != nil { c.logger.WarnContext(ctx, "bad question size: ", len(message.Question)) @@ -239,13 +237,10 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m disableCache = disableCache || (response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError) if responseChecker != nil { var rejected bool - // TODO: add accept_any rule and support to check response instead of addresses if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError { rejected = true - } else if len(response.Answer) == 0 { - rejected = !responseChecker(nil) } else { - rejected = !responseChecker(MessageToAddresses(response)) + rejected = !responseChecker(response) } if rejected { if !disableCache && c.rdrc != nil { @@ -321,7 +316,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m return response, nil } -func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) { +func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) { domain = FqdnToDomain(domain) dnsName := dns.Fqdn(domain) var strategy C.DomainStrategy @@ -406,7 +401,7 @@ func (c *Client) storeCache(transport adapter.DNSTransport, question dns.Questio } } -func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTransport, name string, qType uint16, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) { +func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTransport, name string, qType uint16, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) { question := dns.Question{ Name: name, Qtype: qType, @@ -530,25 +525,7 @@ func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransp } func MessageToAddresses(response *dns.Msg) []netip.Addr { - if response == nil || response.Rcode != dns.RcodeSuccess { - return nil - } - addresses := make([]netip.Addr, 0, len(response.Answer)) - for _, rawAnswer := range response.Answer { - switch answer := rawAnswer.(type) { - case *dns.A: - addresses = append(addresses, M.AddrFromIP(answer.A)) - case *dns.AAAA: - addresses = append(addresses, M.AddrFromIP(answer.AAAA)) - case *dns.HTTPS: - for _, value := range answer.SVCB.Value { - if value.Key() == dns.SVCB_IPV4HINT || value.Key() == dns.SVCB_IPV6HINT { - addresses = append(addresses, common.Map(strings.Split(value.String(), ","), M.ParseAddr)...) - } - } - } - } - return addresses + return adapter.DNSResponseAddresses(response) } func wrapError(err error) error { diff --git a/dns/repro_test.go b/dns/repro_test.go new file mode 100644 index 0000000000..113f7c49b9 --- /dev/null +++ b/dns/repro_test.go @@ -0,0 +1,111 @@ +package dns + +import ( + "context" + "net/netip" + "testing" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json/badoption" + + mDNS "github.com/miekg/dns" + "github.com/stretchr/testify/require" +) + +func TestReproLookupWithRulesUsesRequestStrategy(t *testing.T) { + t.Parallel() + + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + var qTypes []uint16 + router := newTestRouter(t, nil, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + }, + }, &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + qTypes = append(qTypes, message.Question[0].Qtype) + if message.Question[0].Qtype == mDNS.TypeA { + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2.2.2.2")}, 60), nil + } + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2001:db8::1")}, 60), nil + }, + }) + + addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{ + Strategy: C.DomainStrategyIPv4Only, + }) + require.NoError(t, err) + require.Equal(t, []uint16{mDNS.TypeA}, qTypes) + require.Equal(t, []netip.Addr{netip.MustParseAddr("2.2.2.2")}, addresses) +} + +func TestReproLogicalMatchResponseIPCIDR(t *testing.T) { + t.Parallel() + + transportManager := &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + }, + } + client := &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + switch transport.Tag() { + case "upstream": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil + case "selected": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil + default: + return nil, E.New("unexpected transport") + } + }, + } + rules := []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "upstream"}, + }, + }, + }, + { + Type: C.RuleTypeLogical, + LogicalOptions: option.LogicalDNSRule{ + RawLogicalDNSRule: option.RawLogicalDNSRule{ + Mode: C.LogicalTypeOr, + Rules: []option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + IPCIDR: badoption.Listable[string]{"1.1.1.0/24"}, + }, + }, + }}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + } + router := newTestRouter(t, rules, transportManager, client) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, + }, adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("8.8.8.8")}, MessageToAddresses(response)) +} diff --git a/dns/router.go b/dns/router.go index 4f18959b7c..8392da9113 100644 --- a/dns/router.go +++ b/dns/router.go @@ -5,11 +5,13 @@ import ( "errors" "net/netip" "strings" + "sync" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/taskmonitor" C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/experimental/deprecated" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" R "github.com/sagernet/sing-box/route/rule" @@ -19,6 +21,7 @@ import ( F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/task" "github.com/sagernet/sing/contrab/freelru" "github.com/sagernet/sing/contrab/maphash" "github.com/sagernet/sing/service" @@ -26,7 +29,10 @@ import ( mDNS "github.com/miekg/dns" ) -var _ adapter.DNSRouter = (*Router)(nil) +var ( + _ adapter.DNSRouter = (*Router)(nil) + _ adapter.DNSRuleSetUpdateValidator = (*Router)(nil) +) type Router struct { ctx context.Context @@ -34,10 +40,15 @@ type Router struct { transport adapter.DNSTransportManager outbound adapter.OutboundManager client adapter.DNSClient + rawRules []option.DNSRule rules []adapter.DNSRule defaultDomainStrategy C.DomainStrategy dnsReverseMapping freelru.Cache[netip.Addr, string] platformInterface adapter.PlatformInterface + legacyDNSMode bool + rulesAccess sync.RWMutex + started bool + closing bool } func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOptions) *Router { @@ -46,6 +57,7 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOp logger: logFactory.NewLogger("dns"), transport: service.FromContext[adapter.DNSTransportManager](ctx), outbound: service.FromContext[adapter.OutboundManager](ctx), + rawRules: make([]option.DNSRule, 0, len(options.Rules)), rules: make([]adapter.DNSRule, 0, len(options.Rules)), defaultDomainStrategy: C.DomainStrategy(options.Strategy), } @@ -74,13 +86,12 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOp } func (r *Router) Initialize(rules []option.DNSRule) error { - for i, ruleOptions := range rules { - dnsRule, err := R.NewDNSRule(r.ctx, r.logger, ruleOptions, true) - if err != nil { - return E.Cause(err, "parse dns rule[", i, "]") - } - r.rules = append(r.rules, dnsRule) + r.rawRules = append(r.rawRules[:0], rules...) + newRules, _, _, err := r.buildRules(false) + if err != nil { + return err } + closeRules(newRules) return nil } @@ -92,32 +103,146 @@ func (r *Router) Start(stage adapter.StartStage) error { r.client.Start() monitor.Finish() - for i, rule := range r.rules { - monitor.Start("initialize DNS rule[", i, "]") - err := rule.Start() - monitor.Finish() - if err != nil { - return E.Cause(err, "initialize DNS rule[", i, "]") - } + monitor.Start("initialize DNS rules") + newRules, legacyDNSMode, modeFlags, err := r.buildRules(true) + monitor.Finish() + if err != nil { + return err + } + r.rulesAccess.Lock() + if r.closing { + r.rulesAccess.Unlock() + closeRules(newRules) + return nil + } + r.rules = newRules + r.legacyDNSMode = legacyDNSMode + r.started = true + r.rulesAccess.Unlock() + if legacyDNSMode && common.Any(newRules, func(rule adapter.DNSRule) bool { return rule.WithAddressLimit() }) { + deprecated.Report(r.ctx, deprecated.OptionLegacyDNSAddressFilter) + } + if legacyDNSMode && modeFlags.neededFromStrategy { + deprecated.Report(r.ctx, deprecated.OptionLegacyDNSRuleStrategy) } } return nil } func (r *Router) Close() error { - monitor := taskmonitor.New(r.logger, C.StopTimeout) - var err error - for i, rule := range r.rules { - monitor.Start("close dns rule[", i, "]") - err = E.Append(err, rule.Close(), func(err error) error { - return E.Cause(err, "close dns rule[", i, "]") - }) - monitor.Finish() + r.rulesAccess.Lock() + if r.closing { + r.rulesAccess.Unlock() + return nil + } + r.closing = true + runtimeRules := r.rules + r.rules = nil + r.rulesAccess.Unlock() + closeRules(runtimeRules) + return nil +} + +func (r *Router) buildRules(startRules bool) ([]adapter.DNSRule, bool, dnsRuleModeFlags, error) { + for i, ruleOptions := range r.rawRules { + err := R.ValidateNoNestedDNSRuleActions(ruleOptions) + if err != nil { + return nil, false, dnsRuleModeFlags{}, E.Cause(err, "parse dns rule[", i, "]") + } + } + router := service.FromContext[adapter.Router](r.ctx) + legacyDNSMode, modeFlags, err := resolveLegacyDNSMode(router, r.rawRules, nil) + if err != nil { + return nil, false, dnsRuleModeFlags{}, err + } + if !legacyDNSMode { + err = validateLegacyDNSModeDisabledRules(r.rawRules) + if err != nil { + return nil, false, dnsRuleModeFlags{}, err + } + } + err = validateEvaluateFakeIPRules(r.rawRules, r.transport) + if err != nil { + return nil, false, dnsRuleModeFlags{}, err + } + newRules := make([]adapter.DNSRule, 0, len(r.rawRules)) + for i, ruleOptions := range r.rawRules { + var dnsRule adapter.DNSRule + dnsRule, err = R.NewDNSRule(r.ctx, r.logger, ruleOptions, true, legacyDNSMode) + if err != nil { + closeRules(newRules) + return nil, false, dnsRuleModeFlags{}, E.Cause(err, "parse dns rule[", i, "]") + } + newRules = append(newRules, dnsRule) + } + if startRules { + for i, rule := range newRules { + err = rule.Start() + if err != nil { + closeRules(newRules) + return nil, false, dnsRuleModeFlags{}, E.Cause(err, "initialize DNS rule[", i, "]") + } + } + } + return newRules, legacyDNSMode, modeFlags, nil +} + +func closeRules(rules []adapter.DNSRule) { + for _, rule := range rules { + _ = rule.Close() + } +} + +func (r *Router) ValidateRuleSetMetadataUpdate(tag string, metadata adapter.RuleSetMetadata) error { + if len(r.rawRules) == 0 { + return nil + } + router := service.FromContext[adapter.Router](r.ctx) + if router == nil { + return E.New("router service not found") + } + overrides := map[string]adapter.RuleSetMetadata{ + tag: metadata, + } + r.rulesAccess.RLock() + started := r.started + legacyDNSMode := r.legacyDNSMode + closing := r.closing + r.rulesAccess.RUnlock() + if closing { + return nil + } + if !started { + candidateLegacyDNSMode, _, err := resolveLegacyDNSMode(router, r.rawRules, overrides) + if err != nil { + return err + } + if !candidateLegacyDNSMode { + return validateLegacyDNSModeDisabledRules(r.rawRules) + } + return nil + } + candidateLegacyDNSMode, flags, err := resolveLegacyDNSMode(router, r.rawRules, overrides) + if err != nil { + return err + } + if legacyDNSMode { + if !candidateLegacyDNSMode && flags.disabled { + err := validateLegacyDNSModeDisabledRules(r.rawRules) + if err != nil { + return err + } + return E.New(deprecated.OptionLegacyDNSAddressFilter.MessageWithLink()) + } + return nil + } + if candidateLegacyDNSMode { + return E.New(deprecated.OptionLegacyDNSAddressFilter.MessageWithLink()) } - return err + return nil } -func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int, isAddressQuery bool, options *adapter.DNSQueryOptions) (adapter.DNSTransport, adapter.DNSRule, int) { +func (r *Router) matchDNS(ctx context.Context, rules []adapter.DNSRule, allowFakeIP bool, ruleIndex int, isAddressQuery bool, options *adapter.DNSQueryOptions) (adapter.DNSTransport, adapter.DNSRule, int) { metadata := adapter.ContextFrom(ctx) if metadata == nil { panic("no context") @@ -126,22 +251,18 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int, if ruleIndex != -1 { currentRuleIndex = ruleIndex + 1 } - for ; currentRuleIndex < len(r.rules); currentRuleIndex++ { - currentRule := r.rules[currentRuleIndex] + for ; currentRuleIndex < len(rules); currentRuleIndex++ { + currentRule := rules[currentRuleIndex] if currentRule.WithAddressLimit() && !isAddressQuery { continue } metadata.ResetRuleCache() - if currentRule.Match(metadata) { - displayRuleIndex := currentRuleIndex - if displayRuleIndex != -1 { - displayRuleIndex += displayRuleIndex + 1 - } - ruleDescription := currentRule.String() - if ruleDescription != "" { - r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] ", currentRule, " => ", currentRule.Action()) + metadata.DestinationAddressMatchFromResponse = false + if currentRule.LegacyPreMatch(metadata) { + if ruleDescription := currentRule.String(); ruleDescription != "" { + r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] ", currentRule, " => ", currentRule.Action()) } else { - r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action()) + r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] => ", currentRule.Action()) } switch action := currentRule.Action().(type) { case *R.RuleActionDNSRoute: @@ -166,14 +287,6 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int, if action.ClientSubnet.IsValid() { options.ClientSubnet = action.ClientSubnet } - if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy { - if options.Strategy == C.DomainStrategyAsIS { - options.Strategy = legacyTransport.LegacyStrategy() - } - if !options.ClientSubnet.IsValid() { - options.ClientSubnet = legacyTransport.LegacyClientSubnet() - } - } return transport, currentRule, currentRuleIndex case *R.RuleActionDNSRouteOptions: if action.Strategy != C.DomainStrategyAsIS { @@ -196,15 +309,270 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int, } } transport := r.transport.Default() - if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy { - if options.Strategy == C.DomainStrategyAsIS { - options.Strategy = legacyTransport.LegacyStrategy() + return transport, nil, -1 +} + +func (r *Router) applyDNSRouteOptions(options *adapter.DNSQueryOptions, routeOptions R.RuleActionDNSRouteOptions) { + // Strategy is intentionally skipped here. A non-default DNS rule action strategy + // forces legacy mode via resolveLegacyDNSMode, so this path is only reachable + // when strategy remains at its default value. + if routeOptions.DisableCache { + options.DisableCache = true + } + if routeOptions.RewriteTTL != nil { + options.RewriteTTL = routeOptions.RewriteTTL + } + if routeOptions.ClientSubnet.IsValid() { + options.ClientSubnet = routeOptions.ClientSubnet + } +} + +type dnsRouteStatus uint8 + +const ( + dnsRouteStatusMissing dnsRouteStatus = iota + dnsRouteStatusSkipped + dnsRouteStatusResolved +) + +func (r *Router) resolveDNSRoute(server string, routeOptions R.RuleActionDNSRouteOptions, allowFakeIP bool, options *adapter.DNSQueryOptions) (adapter.DNSTransport, dnsRouteStatus) { + transport, loaded := r.transport.Transport(server) + if !loaded { + return nil, dnsRouteStatusMissing + } + isFakeIP := transport.Type() == C.DNSTypeFakeIP + if isFakeIP && !allowFakeIP { + return transport, dnsRouteStatusSkipped + } + r.applyDNSRouteOptions(options, routeOptions) + if isFakeIP { + options.DisableCache = true + } + return transport, dnsRouteStatusResolved +} + +func (r *Router) logRuleMatch(ctx context.Context, ruleIndex int, currentRule adapter.DNSRule) { + if ruleDescription := currentRule.String(); ruleDescription != "" { + r.logger.DebugContext(ctx, "match[", ruleIndex, "] ", currentRule, " => ", currentRule.Action()) + } else { + r.logger.DebugContext(ctx, "match[", ruleIndex, "] => ", currentRule.Action()) + } +} + +type exchangeWithRulesResult struct { + response *mDNS.Msg + transport adapter.DNSTransport + rejectAction *R.RuleActionReject + err error +} + +const dnsRespondMissingResponseMessage = "respond action requires an evaluated response from a preceding evaluate action" + +func (r *Router) exchangeWithRules(ctx context.Context, rules []adapter.DNSRule, message *mDNS.Msg, options adapter.DNSQueryOptions, allowFakeIP bool) exchangeWithRulesResult { + metadata := adapter.ContextFrom(ctx) + if metadata == nil { + panic("no context") + } + effectiveOptions := options + var evaluatedResponse *mDNS.Msg + var evaluatedTransport adapter.DNSTransport + for currentRuleIndex, currentRule := range rules { + metadata.ResetRuleCache() + metadata.DNSResponse = evaluatedResponse + metadata.DestinationAddressMatchFromResponse = false + if !currentRule.Match(metadata) { + continue } - if !options.ClientSubnet.IsValid() { - options.ClientSubnet = legacyTransport.LegacyClientSubnet() + r.logRuleMatch(ctx, currentRuleIndex, currentRule) + switch action := currentRule.Action().(type) { + case *R.RuleActionDNSRouteOptions: + r.applyDNSRouteOptions(&effectiveOptions, *action) + case *R.RuleActionEvaluate: + queryOptions := effectiveOptions + transport, loaded := r.transport.Transport(action.Server) + if !loaded { + r.logger.ErrorContext(ctx, "transport not found: ", action.Server) + evaluatedResponse = nil + evaluatedTransport = nil + continue + } + r.applyDNSRouteOptions(&queryOptions, action.RuleActionDNSRouteOptions) + exchangeOptions := queryOptions + if exchangeOptions.Strategy == C.DomainStrategyAsIS { + exchangeOptions.Strategy = r.defaultDomainStrategy + } + response, err := r.client.Exchange(adapter.OverrideContext(ctx), transport, message, exchangeOptions, nil) + if err != nil { + r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String()))) + evaluatedResponse = nil + evaluatedTransport = nil + continue + } + evaluatedResponse = response + evaluatedTransport = transport + case *R.RuleActionRespond: + if evaluatedResponse == nil { + return exchangeWithRulesResult{ + err: E.New(dnsRespondMissingResponseMessage), + } + } + return exchangeWithRulesResult{ + response: evaluatedResponse, + transport: evaluatedTransport, + } + case *R.RuleActionDNSRoute: + queryOptions := effectiveOptions + transport, status := r.resolveDNSRoute(action.Server, action.RuleActionDNSRouteOptions, allowFakeIP, &queryOptions) + switch status { + case dnsRouteStatusMissing: + r.logger.ErrorContext(ctx, "transport not found: ", action.Server) + continue + case dnsRouteStatusSkipped: + continue + } + exchangeOptions := queryOptions + if exchangeOptions.Strategy == C.DomainStrategyAsIS { + exchangeOptions.Strategy = r.defaultDomainStrategy + } + response, err := r.client.Exchange(adapter.OverrideContext(ctx), transport, message, exchangeOptions, nil) + return exchangeWithRulesResult{ + response: response, + transport: transport, + err: err, + } + case *R.RuleActionReject: + switch action.Method { + case C.RuleActionRejectMethodDefault: + return exchangeWithRulesResult{ + response: &mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + Id: message.Id, + Rcode: mDNS.RcodeRefused, + Response: true, + }, + Question: []mDNS.Question{message.Question[0]}, + }, + rejectAction: action, + } + case C.RuleActionRejectMethodDrop: + return exchangeWithRulesResult{ + rejectAction: action, + err: tun.ErrDrop, + } + } + case *R.RuleActionPredefined: + return exchangeWithRulesResult{ + response: action.Response(message), + } } } - return transport, nil, -1 + transport := r.transport.Default() + exchangeOptions := effectiveOptions + if exchangeOptions.Strategy == C.DomainStrategyAsIS { + exchangeOptions.Strategy = r.defaultDomainStrategy + } + response, err := r.client.Exchange(adapter.OverrideContext(ctx), transport, message, exchangeOptions, nil) + return exchangeWithRulesResult{ + response: response, + transport: transport, + err: err, + } +} + +func (r *Router) resolveLookupStrategy(options adapter.DNSQueryOptions) C.DomainStrategy { + if options.LookupStrategy != C.DomainStrategyAsIS { + return options.LookupStrategy + } + if options.Strategy != C.DomainStrategyAsIS { + return options.Strategy + } + return r.defaultDomainStrategy +} + +func withLookupQueryMetadata(ctx context.Context, qType uint16) context.Context { + ctx, metadata := adapter.ExtendContext(ctx) + metadata.QueryType = qType + metadata.IPVersion = 0 + switch qType { + case mDNS.TypeA: + metadata.IPVersion = 4 + case mDNS.TypeAAAA: + metadata.IPVersion = 6 + } + return ctx +} + +func filterAddressesByQueryType(addresses []netip.Addr, qType uint16) []netip.Addr { + switch qType { + case mDNS.TypeA: + return common.Filter(addresses, func(address netip.Addr) bool { + return address.Is4() + }) + case mDNS.TypeAAAA: + return common.Filter(addresses, func(address netip.Addr) bool { + return address.Is6() + }) + default: + return addresses + } +} + +func (r *Router) lookupWithRules(ctx context.Context, rules []adapter.DNSRule, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) { + strategy := r.resolveLookupStrategy(options) + lookupOptions := options + if strategy != C.DomainStrategyAsIS { + lookupOptions.Strategy = strategy + } + if strategy == C.DomainStrategyIPv4Only { + return r.lookupWithRulesType(ctx, rules, domain, mDNS.TypeA, lookupOptions) + } + if strategy == C.DomainStrategyIPv6Only { + return r.lookupWithRulesType(ctx, rules, domain, mDNS.TypeAAAA, lookupOptions) + } + var ( + response4 []netip.Addr + response6 []netip.Addr + ) + var group task.Group + group.Append("exchange4", func(ctx context.Context) error { + result, err := r.lookupWithRulesType(ctx, rules, domain, mDNS.TypeA, lookupOptions) + response4 = result + return err + }) + group.Append("exchange6", func(ctx context.Context) error { + result, err := r.lookupWithRulesType(ctx, rules, domain, mDNS.TypeAAAA, lookupOptions) + response6 = result + return err + }) + err := group.Run(ctx) + if len(response4) == 0 && len(response6) == 0 { + return nil, err + } + return sortAddresses(response4, response6, strategy), nil +} + +func (r *Router) lookupWithRulesType(ctx context.Context, rules []adapter.DNSRule, domain string, qType uint16, options adapter.DNSQueryOptions) ([]netip.Addr, error) { + request := &mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + RecursionDesired: true, + }, + Question: []mDNS.Question{{ + Name: mDNS.Fqdn(domain), + Qtype: qType, + Qclass: mDNS.ClassINET, + }}, + } + exchangeResult := r.exchangeWithRules(withLookupQueryMetadata(ctx, qType), rules, request, options, false) + if exchangeResult.rejectAction != nil { + return nil, exchangeResult.rejectAction.Error(ctx) + } + if exchangeResult.err != nil { + return nil, exchangeResult.err + } + if exchangeResult.response.Rcode != mDNS.RcodeSuccess { + return nil, RcodeError(exchangeResult.response.Rcode) + } + return filterAddressesByQueryType(MessageToAddresses(exchangeResult.response), qType), nil } func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapter.DNSQueryOptions) (*mDNS.Msg, error) { @@ -220,6 +588,13 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte } return &responseMessage, nil } + r.rulesAccess.RLock() + defer r.rulesAccess.RUnlock() + if r.closing { + return nil, E.New("dns router closed") + } + rules := r.rules + legacyDNSMode := r.legacyDNSMode r.logger.DebugContext(ctx, "exchange ", FormatQuestion(message.Question[0].String())) var ( response *mDNS.Msg @@ -230,6 +605,8 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte ctx, metadata = adapter.ExtendContext(ctx) metadata.Destination = M.Socksaddr{} metadata.QueryType = message.Question[0].Qtype + metadata.DNSResponse = nil + metadata.DestinationAddressMatchFromResponse = false switch metadata.QueryType { case mDNS.TypeA: metadata.IPVersion = 4 @@ -239,18 +616,13 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte metadata.Domain = FqdnToDomain(message.Question[0].Name) if options.Transport != nil { transport = options.Transport - if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy { - if options.Strategy == C.DomainStrategyAsIS { - options.Strategy = legacyTransport.LegacyStrategy() - } - if !options.ClientSubnet.IsValid() { - options.ClientSubnet = legacyTransport.LegacyClientSubnet() - } - } if options.Strategy == C.DomainStrategyAsIS { options.Strategy = r.defaultDomainStrategy } response, err = r.client.Exchange(ctx, transport, message, options, nil) + } else if !legacyDNSMode { + exchangeResult := r.exchangeWithRules(ctx, rules, message, options, true) + response, transport, err = exchangeResult.response, exchangeResult.transport, exchangeResult.err } else { var ( rule adapter.DNSRule @@ -260,7 +632,7 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte for { dnsCtx := adapter.OverrideContext(ctx) dnsOptions := options - transport, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message), &dnsOptions) + transport, rule, ruleIndex = r.matchDNS(ctx, rules, true, ruleIndex, isAddressQuery(message), &dnsOptions) if rule != nil { switch action := rule.Action().(type) { case *R.RuleActionReject: @@ -278,7 +650,9 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte return nil, tun.ErrDrop } case *R.RuleActionPredefined: - return action.Response(message), nil + err = nil + response = action.Response(message) + goto done } } responseCheck := addressLimitResponseCheck(rule, metadata) @@ -306,6 +680,7 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte break } } +done: if err != nil { return nil, err } @@ -325,6 +700,13 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte } func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) { + r.rulesAccess.RLock() + defer r.rulesAccess.RUnlock() + if r.closing { + return nil, E.New("dns router closed") + } + rules := r.rules + legacyDNSMode := r.legacyDNSMode var ( responseAddrs []netip.Addr err error @@ -338,6 +720,8 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ r.logger.DebugContext(ctx, "response rejected for ", domain, " (cached)") } else if errors.Is(err, ErrResponseRejected) { r.logger.DebugContext(ctx, "response rejected for ", domain) + } else if R.IsRejected(err) { + r.logger.DebugContext(ctx, "lookup rejected for ", domain) } else { r.logger.ErrorContext(ctx, E.Cause(err, "lookup failed for ", domain)) } @@ -350,20 +734,16 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ ctx, metadata := adapter.ExtendContext(ctx) metadata.Destination = M.Socksaddr{} metadata.Domain = FqdnToDomain(domain) + metadata.DNSResponse = nil + metadata.DestinationAddressMatchFromResponse = false if options.Transport != nil { transport := options.Transport - if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy { - if options.Strategy == C.DomainStrategyAsIS { - options.Strategy = legacyTransport.LegacyStrategy() - } - if !options.ClientSubnet.IsValid() { - options.ClientSubnet = legacyTransport.LegacyClientSubnet() - } - } if options.Strategy == C.DomainStrategyAsIS { options.Strategy = r.defaultDomainStrategy } responseAddrs, err = r.client.Lookup(ctx, transport, domain, options, nil) + } else if !legacyDNSMode { + responseAddrs, err = r.lookupWithRules(ctx, rules, domain, options) } else { var ( transport adapter.DNSTransport @@ -374,7 +754,7 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ for { dnsCtx := adapter.OverrideContext(ctx) dnsOptions := options - transport, rule, ruleIndex = r.matchDNS(ctx, false, ruleIndex, true, &dnsOptions) + transport, rule, ruleIndex = r.matchDNS(ctx, rules, false, ruleIndex, true, &dnsOptions) if rule != nil { switch action := rule.Action().(type) { case *R.RuleActionReject: @@ -425,15 +805,14 @@ func isAddressQuery(message *mDNS.Msg) bool { return false } -func addressLimitResponseCheck(rule adapter.DNSRule, metadata *adapter.InboundContext) func(responseAddrs []netip.Addr) bool { +func addressLimitResponseCheck(rule adapter.DNSRule, metadata *adapter.InboundContext) func(response *mDNS.Msg) bool { if rule == nil || !rule.WithAddressLimit() { return nil } responseMetadata := *metadata - return func(responseAddrs []netip.Addr) bool { + return func(response *mDNS.Msg) bool { checkMetadata := responseMetadata - checkMetadata.DestinationAddresses = responseAddrs - return rule.MatchAddressLimit(&checkMetadata) + return rule.MatchAddressLimit(&checkMetadata, response) } } @@ -458,3 +837,268 @@ func (r *Router) ResetNetwork() { transport.Reset() } } + +func defaultRuleNeedsLegacyDNSModeFromAddressFilter(rule option.DefaultDNSRule) bool { + if rule.IPAcceptAny || rule.RuleSetIPCIDRAcceptEmpty { //nolint:staticcheck + return true + } + return !rule.MatchResponse && (len(rule.IPCIDR) > 0 || rule.IPIsPrivate) +} + +func hasResponseMatchFields(rule option.DefaultDNSRule) bool { + return rule.ResponseRcode != nil || + len(rule.ResponseAnswer) > 0 || + len(rule.ResponseNs) > 0 || + len(rule.ResponseExtra) > 0 +} + +func defaultRuleDisablesLegacyDNSMode(rule option.DefaultDNSRule) bool { + return rule.MatchResponse || + hasResponseMatchFields(rule) || + rule.Action == C.RuleActionTypeEvaluate || + rule.Action == C.RuleActionTypeRespond || + rule.IPVersion > 0 || + len(rule.QueryType) > 0 +} + +type dnsRuleModeFlags struct { + disabled bool + needed bool + neededFromStrategy bool +} + +func (f *dnsRuleModeFlags) merge(other dnsRuleModeFlags) { + f.disabled = f.disabled || other.disabled + f.needed = f.needed || other.needed + f.neededFromStrategy = f.neededFromStrategy || other.neededFromStrategy +} + +func resolveLegacyDNSMode(router adapter.Router, rules []option.DNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) (bool, dnsRuleModeFlags, error) { + flags, err := dnsRuleModeRequirements(router, rules, metadataOverrides) + if err != nil { + return false, flags, err + } + if flags.disabled && flags.neededFromStrategy { + return false, flags, E.New(deprecated.OptionLegacyDNSRuleStrategy.MessageWithLink()) + } + if flags.disabled { + return false, flags, nil + } + return flags.needed, flags, nil +} + +func dnsRuleModeRequirements(router adapter.Router, rules []option.DNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) (dnsRuleModeFlags, error) { + var flags dnsRuleModeFlags + for i, rule := range rules { + ruleFlags, err := dnsRuleModeRequirementsInRule(router, rule, metadataOverrides) + if err != nil { + return dnsRuleModeFlags{}, E.Cause(err, "dns rule[", i, "]") + } + flags.merge(ruleFlags) + } + return flags, nil +} + +func dnsRuleModeRequirementsInRule(router adapter.Router, rule option.DNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) (dnsRuleModeFlags, error) { + switch rule.Type { + case "", C.RuleTypeDefault: + return dnsRuleModeRequirementsInDefaultRule(router, rule.DefaultOptions, metadataOverrides) + case C.RuleTypeLogical: + flags := dnsRuleModeFlags{ + disabled: dnsRuleActionType(rule) == C.RuleActionTypeEvaluate || dnsRuleActionType(rule) == C.RuleActionTypeRespond, + neededFromStrategy: dnsRuleActionHasStrategy(rule.LogicalOptions.DNSRuleAction), + } + flags.needed = flags.neededFromStrategy + for i, subRule := range rule.LogicalOptions.Rules { + subFlags, err := dnsRuleModeRequirementsInRule(router, subRule, metadataOverrides) + if err != nil { + return dnsRuleModeFlags{}, E.Cause(err, "sub rule[", i, "]") + } + flags.merge(subFlags) + } + return flags, nil + default: + return dnsRuleModeFlags{}, nil + } +} + +func dnsRuleModeRequirementsInDefaultRule(router adapter.Router, rule option.DefaultDNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) (dnsRuleModeFlags, error) { + flags := dnsRuleModeFlags{ + disabled: defaultRuleDisablesLegacyDNSMode(rule), + neededFromStrategy: dnsRuleActionHasStrategy(rule.DNSRuleAction), + } + flags.needed = defaultRuleNeedsLegacyDNSModeFromAddressFilter(rule) || flags.neededFromStrategy + if len(rule.RuleSet) == 0 { + return flags, nil + } + if router == nil { + return dnsRuleModeFlags{}, E.New("router service not found") + } + for _, tag := range rule.RuleSet { + metadata, err := lookupDNSRuleSetMetadata(router, tag, metadataOverrides) + if err != nil { + return dnsRuleModeFlags{}, err + } + // ip_version is not a headless-rule item, so ContainsIPVersionRule is intentionally absent. + flags.disabled = flags.disabled || metadata.ContainsDNSQueryTypeRule + if !rule.RuleSetIPCIDRMatchSource && metadata.ContainsIPCIDRRule { + flags.needed = true + } + } + return flags, nil +} + +func lookupDNSRuleSetMetadata(router adapter.Router, tag string, metadataOverrides map[string]adapter.RuleSetMetadata) (adapter.RuleSetMetadata, error) { + if metadataOverrides != nil { + if metadata, loaded := metadataOverrides[tag]; loaded { + return metadata, nil + } + } + ruleSet, loaded := router.RuleSet(tag) + if !loaded { + return adapter.RuleSetMetadata{}, E.New("rule-set not found: ", tag) + } + return ruleSet.Metadata(), nil +} + +func referencedDNSRuleSetTags(rules []option.DNSRule) []string { + tagMap := make(map[string]bool) + var walkRule func(rule option.DNSRule) + walkRule = func(rule option.DNSRule) { + switch rule.Type { + case "", C.RuleTypeDefault: + for _, tag := range rule.DefaultOptions.RuleSet { + tagMap[tag] = true + } + case C.RuleTypeLogical: + for _, subRule := range rule.LogicalOptions.Rules { + walkRule(subRule) + } + } + } + for _, rule := range rules { + walkRule(rule) + } + tags := make([]string, 0, len(tagMap)) + for tag := range tagMap { + if tag != "" { + tags = append(tags, tag) + } + } + return tags +} + +func validateLegacyDNSModeDisabledRules(rules []option.DNSRule) error { + var seenEvaluate bool + for i, rule := range rules { + requiresPriorEvaluate, err := validateLegacyDNSModeDisabledRuleTree(rule) + if err != nil { + return E.Cause(err, "validate dns rule[", i, "]") + } + if requiresPriorEvaluate && !seenEvaluate { + return E.New("dns rule[", i, "]: response-based matching requires a preceding evaluate action") + } + if dnsRuleActionType(rule) == C.RuleActionTypeEvaluate { + seenEvaluate = true + } + } + return nil +} + +func validateEvaluateFakeIPRules(rules []option.DNSRule, transportManager adapter.DNSTransportManager) error { + if transportManager == nil { + return nil + } + for i, rule := range rules { + if dnsRuleActionType(rule) != C.RuleActionTypeEvaluate { + continue + } + server := dnsRuleActionServer(rule) + if server == "" { + continue + } + transport, loaded := transportManager.Transport(server) + if !loaded || transport.Type() != C.DNSTypeFakeIP { + continue + } + return E.New("dns rule[", i, "]: evaluate action cannot use fakeip server: ", server) + } + return nil +} + +func validateLegacyDNSModeDisabledRuleTree(rule option.DNSRule) (bool, error) { + switch rule.Type { + case "", C.RuleTypeDefault: + return validateLegacyDNSModeDisabledDefaultRule(rule.DefaultOptions) + case C.RuleTypeLogical: + requiresPriorEvaluate := dnsRuleActionType(rule) == C.RuleActionTypeRespond + for i, subRule := range rule.LogicalOptions.Rules { + subRequiresPriorEvaluate, err := validateLegacyDNSModeDisabledRuleTree(subRule) + if err != nil { + return false, E.Cause(err, "sub rule[", i, "]") + } + requiresPriorEvaluate = requiresPriorEvaluate || subRequiresPriorEvaluate + } + return requiresPriorEvaluate, nil + default: + return false, nil + } +} + +func validateLegacyDNSModeDisabledDefaultRule(rule option.DefaultDNSRule) (bool, error) { + hasResponseRecords := hasResponseMatchFields(rule) + if (hasResponseRecords || len(rule.IPCIDR) > 0 || rule.IPIsPrivate) && !rule.MatchResponse { + return false, E.New("Response Match Fields (ip_cidr, ip_is_private, response_rcode, response_answer, response_ns, response_extra) require match_response to be enabled") + } + // Intentionally do not reject rule_set here. A referenced rule set may mix + // destination-IP predicates with pre-response predicates such as domain items. + // When match_response is false, those destination-IP branches fail closed during + // pre-response evaluation instead of consuming DNS response state, while sibling + // non-response branches remain matchable. + if rule.IPAcceptAny { //nolint:staticcheck + return false, E.New(deprecated.OptionIPAcceptAny.MessageWithLink()) + } + if rule.RuleSetIPCIDRAcceptEmpty { //nolint:staticcheck + return false, E.New(deprecated.OptionRuleSetIPCIDRAcceptEmpty.MessageWithLink()) + } + return rule.MatchResponse || rule.Action == C.RuleActionTypeRespond, nil +} + +func dnsRuleActionHasStrategy(action option.DNSRuleAction) bool { + switch action.Action { + case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate: + return C.DomainStrategy(action.RouteOptions.Strategy) != C.DomainStrategyAsIS + case C.RuleActionTypeRouteOptions: + return C.DomainStrategy(action.RouteOptionsOptions.Strategy) != C.DomainStrategyAsIS + default: + return false + } +} + +func dnsRuleActionType(rule option.DNSRule) string { + switch rule.Type { + case "", C.RuleTypeDefault: + if rule.DefaultOptions.Action == "" { + return C.RuleActionTypeRoute + } + return rule.DefaultOptions.Action + case C.RuleTypeLogical: + if rule.LogicalOptions.Action == "" { + return C.RuleActionTypeRoute + } + return rule.LogicalOptions.Action + default: + return "" + } +} + +func dnsRuleActionServer(rule option.DNSRule) string { + switch rule.Type { + case "", C.RuleTypeDefault: + return rule.DefaultOptions.RouteOptions.Server + case C.RuleTypeLogical: + return rule.LogicalOptions.RouteOptions.Server + default: + return "" + } +} diff --git a/dns/router_test.go b/dns/router_test.go new file mode 100644 index 0000000000..54213b23c3 --- /dev/null +++ b/dns/router_test.go @@ -0,0 +1,2547 @@ +package dns + +import ( + "context" + "net" + "net/netip" + "strings" + "sync" + "testing" + "time" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/experimental/deprecated" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + rulepkg "github.com/sagernet/sing-box/route/rule" + "github.com/sagernet/sing-tun" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json/badoption" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/x/list" + "github.com/sagernet/sing/service" + + mDNS "github.com/miekg/dns" + "github.com/stretchr/testify/require" + "go4.org/netipx" +) + +type fakeDNSTransport struct { + tag string + transportType string +} + +func (t *fakeDNSTransport) Start(adapter.StartStage) error { return nil } +func (t *fakeDNSTransport) Close() error { return nil } +func (t *fakeDNSTransport) Type() string { return t.transportType } +func (t *fakeDNSTransport) Tag() string { return t.tag } +func (t *fakeDNSTransport) Dependencies() []string { return nil } +func (t *fakeDNSTransport) Reset() {} +func (t *fakeDNSTransport) Exchange(context.Context, *mDNS.Msg) (*mDNS.Msg, error) { + return nil, E.New("unused transport exchange") +} + +type fakeDNSTransportManager struct { + defaultTransport adapter.DNSTransport + transports map[string]adapter.DNSTransport +} + +func (m *fakeDNSTransportManager) Start(adapter.StartStage) error { return nil } +func (m *fakeDNSTransportManager) Close() error { return nil } +func (m *fakeDNSTransportManager) Transports() []adapter.DNSTransport { + transports := make([]adapter.DNSTransport, 0, len(m.transports)) + for _, transport := range m.transports { + transports = append(transports, transport) + } + return transports +} + +func (m *fakeDNSTransportManager) Transport(tag string) (adapter.DNSTransport, bool) { + transport, loaded := m.transports[tag] + return transport, loaded +} +func (m *fakeDNSTransportManager) Default() adapter.DNSTransport { return m.defaultTransport } +func (m *fakeDNSTransportManager) FakeIP() adapter.FakeIPTransport { + return nil +} +func (m *fakeDNSTransportManager) Remove(string) error { return nil } +func (m *fakeDNSTransportManager) Create(context.Context, log.ContextLogger, string, string, any) error { + return E.New("unsupported") +} + +type fakeDNSClient struct { + beforeExchange func(ctx context.Context, transport adapter.DNSTransport, message *mDNS.Msg) + exchange func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) + lookupWithCtx func(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) + lookup func(transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) +} + +type fakeDeprecatedManager struct { + features []deprecated.Note +} + +type fakeRouter struct { + access sync.RWMutex + ruleSets map[string]adapter.RuleSet +} + +func (r *fakeRouter) Start(adapter.StartStage) error { return nil } +func (r *fakeRouter) Close() error { return nil } +func (r *fakeRouter) PreMatch(metadata adapter.InboundContext, _ tun.DirectRouteContext, _ time.Duration, _ bool) (tun.DirectRouteDestination, error) { + return nil, nil +} + +func (r *fakeRouter) RouteConnection(context.Context, net.Conn, adapter.InboundContext) error { + return nil +} + +func (r *fakeRouter) RoutePacketConnection(context.Context, N.PacketConn, adapter.InboundContext) error { + return nil +} + +func (r *fakeRouter) RouteConnectionEx(context.Context, net.Conn, adapter.InboundContext, N.CloseHandlerFunc) { +} + +func (r *fakeRouter) RoutePacketConnectionEx(context.Context, N.PacketConn, adapter.InboundContext, N.CloseHandlerFunc) { +} + +func (r *fakeRouter) RuleSet(tag string) (adapter.RuleSet, bool) { + r.access.RLock() + defer r.access.RUnlock() + ruleSet, loaded := r.ruleSets[tag] + return ruleSet, loaded +} + +func (r *fakeRouter) setRuleSet(tag string, ruleSet adapter.RuleSet) { + r.access.Lock() + defer r.access.Unlock() + if r.ruleSets == nil { + r.ruleSets = make(map[string]adapter.RuleSet) + } + r.ruleSets[tag] = ruleSet +} +func (r *fakeRouter) Rules() []adapter.Rule { return nil } +func (r *fakeRouter) NeedFindProcess() bool { return false } +func (r *fakeRouter) NeedFindNeighbor() bool { return false } +func (r *fakeRouter) NeighborResolver() adapter.NeighborResolver { return nil } +func (r *fakeRouter) AppendTracker(adapter.ConnectionTracker) {} +func (r *fakeRouter) ResetNetwork() {} + +type fakeRuleSet struct { + access sync.Mutex + metadata adapter.RuleSetMetadata + metadataRead func(adapter.RuleSetMetadata) adapter.RuleSetMetadata + match func(*adapter.InboundContext) bool + callbacks list.List[adapter.RuleSetUpdateCallback] + refs int + afterIncrementReference func() + beforeDecrementReference func() +} + +func (s *fakeRuleSet) Name() string { return "fake-rule-set" } +func (s *fakeRuleSet) StartContext(context.Context, *adapter.HTTPStartContext) error { return nil } +func (s *fakeRuleSet) PostStart() error { return nil } +func (s *fakeRuleSet) Metadata() adapter.RuleSetMetadata { + s.access.Lock() + metadata := s.metadata + metadataRead := s.metadataRead + s.access.Unlock() + if metadataRead != nil { + return metadataRead(metadata) + } + return metadata +} +func (s *fakeRuleSet) ExtractIPSet() []*netipx.IPSet { return nil } +func (s *fakeRuleSet) IncRef() { + s.access.Lock() + s.refs++ + afterIncrementReference := s.afterIncrementReference + s.access.Unlock() + if afterIncrementReference != nil { + afterIncrementReference() + } +} + +func (s *fakeRuleSet) DecRef() { + s.access.Lock() + beforeDecrementReference := s.beforeDecrementReference + s.access.Unlock() + if beforeDecrementReference != nil { + beforeDecrementReference() + } + s.access.Lock() + defer s.access.Unlock() + s.refs-- + if s.refs < 0 { + panic("rule-set: negative refs") + } +} +func (s *fakeRuleSet) Cleanup() {} +func (s *fakeRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] { + s.access.Lock() + defer s.access.Unlock() + return s.callbacks.PushBack(callback) +} + +func (s *fakeRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) { + s.access.Lock() + defer s.access.Unlock() + s.callbacks.Remove(element) +} +func (s *fakeRuleSet) Close() error { return nil } +func (s *fakeRuleSet) Match(metadata *adapter.InboundContext) bool { + s.access.Lock() + match := s.match + s.access.Unlock() + if match != nil { + return match(metadata) + } + return true +} +func (s *fakeRuleSet) String() string { return "fake-rule-set" } +func (s *fakeRuleSet) updateMetadata(metadata adapter.RuleSetMetadata) { + s.access.Lock() + s.metadata = metadata + callbacks := s.callbacks.Array() + s.access.Unlock() + for _, callback := range callbacks { + callback(s) + } +} + +func (s *fakeRuleSet) snapshotCallbacks() []adapter.RuleSetUpdateCallback { + s.access.Lock() + defer s.access.Unlock() + return s.callbacks.Array() +} + +func (s *fakeRuleSet) refCount() int { + s.access.Lock() + defer s.access.Unlock() + return s.refs +} + +func (m *fakeDeprecatedManager) ReportDeprecated(feature deprecated.Note) { + m.features = append(m.features, feature) +} + +func (c *fakeDNSClient) Start() {} + +func (c *fakeDNSClient) Exchange(ctx context.Context, transport adapter.DNSTransport, message *mDNS.Msg, _ adapter.DNSQueryOptions, _ func(*mDNS.Msg) bool) (*mDNS.Msg, error) { + if c.beforeExchange != nil { + c.beforeExchange(ctx, transport, message) + } + if c.exchange == nil { + if len(message.Question) != 1 { + return nil, E.New("unused client exchange") + } + var ( + addresses []netip.Addr + response *mDNS.Msg + err error + ) + if c.lookupWithCtx != nil { + addresses, response, err = c.lookupWithCtx(ctx, transport, FqdnToDomain(message.Question[0].Name), adapter.DNSQueryOptions{}) + } else if c.lookup != nil { + addresses, response, err = c.lookup(transport, FqdnToDomain(message.Question[0].Name), adapter.DNSQueryOptions{}) + } else { + return nil, E.New("unused client exchange") + } + if err != nil { + return nil, err + } + if response != nil { + return response, nil + } + return FixedResponse(0, message.Question[0], addresses, 60), nil + } + return c.exchange(transport, message) +} + +func (c *fakeDNSClient) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(*mDNS.Msg) bool) ([]netip.Addr, error) { + if c.lookup == nil && c.lookupWithCtx == nil { + return nil, E.New("unused client lookup") + } + var ( + addresses []netip.Addr + response *mDNS.Msg + err error + ) + if c.lookupWithCtx != nil { + addresses, response, err = c.lookupWithCtx(ctx, transport, domain, options) + } else { + addresses, response, err = c.lookup(transport, domain, options) + } + if err != nil { + return nil, err + } + if response == nil { + response = FixedResponse(0, fixedQuestion(domain, mDNS.TypeA), addresses, 60) + } + if responseChecker != nil && !responseChecker(response) { + return nil, ErrResponseRejected + } + if addresses != nil { + return addresses, nil + } + return MessageToAddresses(response), nil +} + +func (c *fakeDNSClient) ClearCache() {} + +func newTestRouter(t *testing.T, rules []option.DNSRule, transportManager *fakeDNSTransportManager, client *fakeDNSClient) *Router { + router := newTestRouterWithContext(t, context.Background(), rules, transportManager, client) + t.Cleanup(func() { + router.Close() + }) + return router +} + +func newTestRouterWithContext(t *testing.T, ctx context.Context, rules []option.DNSRule, transportManager *fakeDNSTransportManager, client *fakeDNSClient) *Router { + return newTestRouterWithContextAndLogger(t, ctx, rules, transportManager, client, log.NewNOPFactory().NewLogger("dns")) +} + +func newTestRouterWithContextAndLogger(t *testing.T, ctx context.Context, rules []option.DNSRule, transportManager *fakeDNSTransportManager, client *fakeDNSClient, dnsLogger log.ContextLogger) *Router { + t.Helper() + router := &Router{ + ctx: ctx, + logger: dnsLogger, + transport: transportManager, + client: client, + rawRules: make([]option.DNSRule, 0, len(rules)), + rules: make([]adapter.DNSRule, 0, len(rules)), + defaultDomainStrategy: C.DomainStrategyAsIS, + } + if rules != nil { + err := router.Initialize(rules) + require.NoError(t, err) + err = router.Start(adapter.StartStateStart) + require.NoError(t, err) + } + return router +} + +func waitForLogMessageContaining(t *testing.T, entries <-chan log.Entry, done <-chan struct{}, substring string) log.Entry { + t.Helper() + timeout := time.After(time.Second) + for { + select { + case entry, ok := <-entries: + if !ok { + t.Fatal("log subscription closed") + } + if strings.Contains(entry.Message, substring) { + return entry + } + case <-done: + t.Fatal("log subscription closed") + case <-timeout: + t.Fatalf("timed out waiting for log message containing %q", substring) + } + } +} + +func fixedQuestion(name string, qType uint16) mDNS.Question { + return mDNS.Question{ + Name: mDNS.Fqdn(name), + Qtype: qType, + Qclass: mDNS.ClassINET, + } +} + +func mustRecord(t *testing.T, record string) option.DNSRecordOptions { + t.Helper() + var value option.DNSRecordOptions + require.NoError(t, value.UnmarshalJSON([]byte(`"`+record+`"`))) + return value +} + +func TestInitializeRejectsDirectLegacyRuleWhenRuleSetForcesNew(t *testing.T) { + t.Parallel() + + ctx := context.Background() + ruleSet, err := rulepkg.NewRuleSet(ctx, log.NewNOPFactory().NewLogger("router"), option.RuleSet{ + Type: C.RuleSetTypeInline, + Tag: "query-set", + InlineOptions: option.PlainRuleSet{ + Rules: []option.HeadlessRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultHeadlessRule{ + QueryType: badoption.Listable[option.DNSQueryType]{option.DNSQueryType(mDNS.TypeA)}, + }, + }}, + }, + }) + require.NoError(t, err) + ctx = service.ContextWith[adapter.Router](ctx, &fakeRouter{ + ruleSets: map[string]adapter.RuleSet{ + "query-set": ruleSet, + }, + }) + + router := &Router{ + ctx: ctx, + logger: log.NewNOPFactory().NewLogger("dns"), + transport: &fakeDNSTransportManager{}, + client: &fakeDNSClient{}, + rawRules: make([]option.DNSRule, 0, 2), + defaultDomainStrategy: C.DomainStrategyAsIS, + } + err = router.Initialize([]option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + RuleSet: badoption.Listable[string]{"query-set"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "default"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + IPIsPrivate: true, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "private"}, + }, + }, + }, + }) + require.ErrorContains(t, err, "Response Match Fields") + require.ErrorContains(t, err, "require match_response") +} + +func TestLookupLegacyDNSModeDefersRuleSetDestinationIPMatch(t *testing.T) { + t.Parallel() + + ctx := context.Background() + ruleSet, err := rulepkg.NewRuleSet(ctx, log.NewNOPFactory().NewLogger("router"), option.RuleSet{ + Type: C.RuleSetTypeInline, + Tag: "legacy-ipcidr-set", + InlineOptions: option.PlainRuleSet{ + Rules: []option.HeadlessRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultHeadlessRule{ + IPCIDR: badoption.Listable[string]{"10.0.0.0/8"}, + }, + }}, + }, + }) + require.NoError(t, err) + ctx = service.ContextWith[adapter.Router](ctx, &fakeRouter{ + ruleSets: map[string]adapter.RuleSet{ + "legacy-ipcidr-set": ruleSet, + }, + }) + + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + privateTransport := &fakeDNSTransport{tag: "private", transportType: C.DNSTypeUDP} + router := newTestRouterWithContext(t, ctx, []option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + RuleSet: badoption.Listable[string]{"legacy-ipcidr-set"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "private"}, + }, + }, + }}, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + "private": privateTransport, + }, + }, &fakeDNSClient{ + lookup: func(transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) { + require.Equal(t, "example.com", domain) + require.Equal(t, "private", transport.Tag()) + response := FixedResponse(0, fixedQuestion(domain, mDNS.TypeA), []netip.Addr{netip.MustParseAddr("10.0.0.1")}, 60) + return MessageToAddresses(response), response, nil + }, + }) + + require.True(t, router.legacyDNSMode) + + addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{ + LookupStrategy: C.DomainStrategyIPv4Only, + }) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("10.0.0.1")}, addresses) +} + +func TestRuleSetUpdateReleasesOldRuleSetRefs(t *testing.T) { + t.Parallel() + + fakeSet := &fakeRuleSet{} + ctx := service.ContextWith[adapter.Router](context.Background(), &fakeRouter{ + ruleSets: map[string]adapter.RuleSet{ + "dynamic-set": fakeSet, + }, + }) + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + router := newTestRouterWithContext(t, ctx, []option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + RuleSet: badoption.Listable[string]{"dynamic-set"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "default"}, + }, + }, + }}, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + }, + }, &fakeDNSClient{}) + + require.Equal(t, 1, fakeSet.refCount()) + + fakeSet.updateMetadata(adapter.RuleSetMetadata{}) + require.Equal(t, 1, fakeSet.refCount()) + + fakeSet.updateMetadata(adapter.RuleSetMetadata{}) + require.Equal(t, 1, fakeSet.refCount()) + + require.NoError(t, router.Close()) + require.Zero(t, fakeSet.refCount()) +} + +func TestValidateRuleSetMetadataUpdateRejectsRuleSetThatWouldDisableLegacyDNSMode(t *testing.T) { + t.Parallel() + + fakeSet := &fakeRuleSet{ + metadata: adapter.RuleSetMetadata{ + ContainsIPCIDRRule: true, + }, + } + routerService := &fakeRouter{ + ruleSets: map[string]adapter.RuleSet{ + "dynamic-set": fakeSet, + }, + } + ctx := service.ContextWith[adapter.Router](context.Background(), routerService) + router := newTestRouterWithContext(t, ctx, []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + RuleSet: badoption.Listable[string]{"dynamic-set"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + IPIsPrivate: true, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + }, &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + }, + }, &fakeDNSClient{ + lookup: func(adapter.DNSTransport, string, adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) { + return []netip.Addr{netip.MustParseAddr("10.0.0.1")}, nil, nil + }, + }) + require.True(t, router.legacyDNSMode) + + err := router.ValidateRuleSetMetadataUpdate("dynamic-set", adapter.RuleSetMetadata{ + ContainsDNSQueryTypeRule: true, + }) + require.ErrorContains(t, err, "require match_response") +} + +func TestValidateRuleSetMetadataUpdateRejectsRuleSetOnlyLegacyModeSwitchToNew(t *testing.T) { + t.Parallel() + + fakeSet := &fakeRuleSet{ + metadata: adapter.RuleSetMetadata{ + ContainsIPCIDRRule: true, + }, + } + routerService := &fakeRouter{ + ruleSets: map[string]adapter.RuleSet{ + "dynamic-set": fakeSet, + }, + } + ctx := service.ContextWith[adapter.Router](context.Background(), routerService) + router := newTestRouterWithContext(t, ctx, []option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + RuleSet: badoption.Listable[string]{"dynamic-set"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }}, &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + }, + }, &fakeDNSClient{ + lookup: func(adapter.DNSTransport, string, adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) { + return []netip.Addr{netip.MustParseAddr("10.0.0.1")}, nil, nil + }, + }) + require.True(t, router.legacyDNSMode) + + err := router.ValidateRuleSetMetadataUpdate("dynamic-set", adapter.RuleSetMetadata{ + ContainsIPCIDRRule: true, + ContainsDNSQueryTypeRule: true, + }) + require.ErrorContains(t, err, "Address Filter Fields") +} + +func TestValidateRuleSetMetadataUpdateBeforeStartUsesStartupValidation(t *testing.T) { + t.Parallel() + + fakeSet := &fakeRuleSet{} + routerService := &fakeRouter{ + ruleSets: map[string]adapter.RuleSet{ + "dynamic-set": fakeSet, + }, + } + ctx := service.ContextWith[adapter.Router](context.Background(), routerService) + router := &Router{ + ctx: ctx, + logger: log.NewNOPFactory().NewLogger("dns"), + transport: &fakeDNSTransportManager{}, + client: &fakeDNSClient{}, + rawRules: make([]option.DNSRule, 0, 2), + rules: make([]adapter.DNSRule, 0, 2), + defaultDomainStrategy: C.DomainStrategyAsIS, + } + err := router.Initialize([]option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + RuleSet: badoption.Listable[string]{"dynamic-set"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + IPIsPrivate: true, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + }) + require.NoError(t, err) + require.False(t, router.started) + + err = router.ValidateRuleSetMetadataUpdate("dynamic-set", adapter.RuleSetMetadata{ + ContainsDNSQueryTypeRule: true, + }) + require.ErrorContains(t, err, "require match_response") +} + +func TestValidateRuleSetMetadataUpdateRejectsRuleSetThatWouldRequireLegacyDNSMode(t *testing.T) { + t.Parallel() + + fakeSet := &fakeRuleSet{} + routerService := &fakeRouter{ + ruleSets: map[string]adapter.RuleSet{ + "dynamic-set": fakeSet, + }, + } + ctx := service.ContextWith[adapter.Router](context.Background(), routerService) + router := newTestRouterWithContext(t, ctx, []option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + RuleSet: badoption.Listable[string]{"dynamic-set"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }}, &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + }, + }, &fakeDNSClient{ + lookup: func(adapter.DNSTransport, string, adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) { + return []netip.Addr{netip.MustParseAddr("1.1.1.1")}, nil, nil + }, + }) + require.False(t, router.legacyDNSMode) + + err := router.ValidateRuleSetMetadataUpdate("dynamic-set", adapter.RuleSetMetadata{ + ContainsIPCIDRRule: true, + }) + require.ErrorContains(t, err, "Address Filter Fields") +} + +func TestValidateRuleSetMetadataUpdateAllowsRuleSetThatKeepsNonLegacyDNSMode(t *testing.T) { + t.Parallel() + + fakeSet := &fakeRuleSet{} + routerService := &fakeRouter{ + ruleSets: map[string]adapter.RuleSet{ + "dynamic-set": fakeSet, + }, + } + ctx := service.ContextWith[adapter.Router](context.Background(), routerService) + router := newTestRouterWithContext(t, ctx, []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "upstream"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + RuleSet: badoption.Listable[string]{"dynamic-set"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + }, &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, + }, + }, &fakeDNSClient{}) + require.False(t, router.legacyDNSMode) + + err := router.ValidateRuleSetMetadataUpdate("dynamic-set", adapter.RuleSetMetadata{ + ContainsIPCIDRRule: true, + }) + require.NoError(t, err) +} + +func TestValidateRuleSetMetadataUpdateAllowsRelaxingLegacyRequirement(t *testing.T) { + t.Parallel() + + fakeSet := &fakeRuleSet{ + metadata: adapter.RuleSetMetadata{ + ContainsIPCIDRRule: true, + }, + } + routerService := &fakeRouter{ + ruleSets: map[string]adapter.RuleSet{ + "dynamic-set": fakeSet, + }, + } + ctx := service.ContextWith[adapter.Router](context.Background(), routerService) + router := newTestRouterWithContext(t, ctx, []option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + RuleSet: badoption.Listable[string]{"dynamic-set"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }}, &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + }, + }, &fakeDNSClient{ + lookup: func(adapter.DNSTransport, string, adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) { + return []netip.Addr{netip.MustParseAddr("10.0.0.1")}, nil, nil + }, + }) + require.True(t, router.legacyDNSMode) + + err := router.ValidateRuleSetMetadataUpdate("dynamic-set", adapter.RuleSetMetadata{}) + require.NoError(t, err) +} + +func TestCloseWaitsForInFlightLookupUntilContextCancellation(t *testing.T) { + t.Parallel() + + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + selectedTransport := &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP} + lookupStarted := make(chan struct{}) + var lookupStartedOnce sync.Once + router := newTestRouter(t, []option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }}, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + "selected": selectedTransport, + }, + }, &fakeDNSClient{ + lookupWithCtx: func(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) { + require.Equal(t, "selected", transport.Tag()) + require.Equal(t, "example.com", domain) + lookupStartedOnce.Do(func() { + close(lookupStarted) + }) + <-ctx.Done() + return nil, nil, ctx.Err() + }, + }) + + lookupCtx, cancelLookup := context.WithCancel(context.Background()) + defer cancelLookup() + var ( + lookupErr error + closeErr error + ) + lookupDone := make(chan struct{}) + go func() { + _, lookupErr = router.Lookup(lookupCtx, "example.com", adapter.DNSQueryOptions{}) + close(lookupDone) + }() + + select { + case <-lookupStarted: + case <-time.After(time.Second): + t.Fatal("lookup did not reach DNS client") + } + + closeDone := make(chan struct{}) + go func() { + closeErr = router.Close() + close(closeDone) + }() + + select { + case <-closeDone: + t.Fatal("close finished before lookup context cancellation") + default: + } + + cancelLookup() + + select { + case <-lookupDone: + case <-time.After(time.Second): + t.Fatal("lookup did not finish after cancellation") + } + select { + case <-closeDone: + case <-time.After(time.Second): + t.Fatal("close did not finish after lookup cancellation") + } + + require.ErrorIs(t, lookupErr, context.Canceled) + require.NoError(t, closeErr) +} + +func TestLookupLegacyDNSModeDefersDirectDestinationIPMatch(t *testing.T) { + t.Parallel() + + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + privateTransport := &fakeDNSTransport{tag: "private", transportType: C.DNSTypeUDP} + client := &fakeDNSClient{ + lookup: func(transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) { + require.Equal(t, "example.com", domain) + require.Equal(t, C.DomainStrategyIPv4Only, options.LookupStrategy) + switch transport.Tag() { + case "private": + response := FixedResponse(0, fixedQuestion(domain, mDNS.TypeA), []netip.Addr{netip.MustParseAddr("10.0.0.1")}, 60) + return MessageToAddresses(response), response, nil + case "default": + t.Fatal("default transport should not be used when legacy rule matches after response") + } + return nil, nil, E.New("unexpected transport") + }, + } + router := newTestRouter(t, []option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + IPIsPrivate: true, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "private"}, + }, + }, + }}, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + "private": privateTransport, + }, + }, client) + + require.True(t, router.legacyDNSMode) + + addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{ + LookupStrategy: C.DomainStrategyIPv4Only, + }) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("10.0.0.1")}, addresses) +} + +func TestLookupLegacyDNSModeFallsBackAfterRejectedAddressLimitResponse(t *testing.T) { + t.Parallel() + + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + privateTransport := &fakeDNSTransport{tag: "private", transportType: C.DNSTypeUDP} + var lookupAccess sync.Mutex + var lookupTags []string + recordLookup := func(tag string) { + lookupAccess.Lock() + lookupTags = append(lookupTags, tag) + lookupAccess.Unlock() + } + currentLookupTags := func() []string { + lookupAccess.Lock() + defer lookupAccess.Unlock() + return append([]string(nil), lookupTags...) + } + client := &fakeDNSClient{ + lookup: func(transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) { + require.Equal(t, "example.com", domain) + require.Equal(t, C.DomainStrategyIPv4Only, options.LookupStrategy) + recordLookup(transport.Tag()) + switch transport.Tag() { + case "private": + response := FixedResponse(0, fixedQuestion(domain, mDNS.TypeA), []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60) + return MessageToAddresses(response), response, nil + case "default": + response := FixedResponse(0, fixedQuestion(domain, mDNS.TypeA), []netip.Addr{netip.MustParseAddr("9.9.9.9")}, 60) + return MessageToAddresses(response), response, nil + } + return nil, nil, E.New("unexpected transport") + }, + } + router := newTestRouter(t, []option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + IPIsPrivate: true, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "private"}, + }, + }, + }}, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + "private": privateTransport, + }, + }, client) + + addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{ + LookupStrategy: C.DomainStrategyIPv4Only, + }) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("9.9.9.9")}, addresses) + require.Equal(t, []string{"private", "default"}, currentLookupTags()) +} + +func TestLookupLegacyDNSModeRuleSetAcceptEmptyDoesNotTreatMismatchAsEmpty(t *testing.T) { + t.Parallel() + + ctx := context.Background() + ruleSet, err := rulepkg.NewRuleSet(ctx, log.NewNOPFactory().NewLogger("router"), option.RuleSet{ + Type: C.RuleSetTypeInline, + Tag: "legacy-ipcidr-set", + InlineOptions: option.PlainRuleSet{ + Rules: []option.HeadlessRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultHeadlessRule{ + IPCIDR: badoption.Listable[string]{"10.0.0.0/8"}, + }, + }}, + }, + }) + require.NoError(t, err) + ctx = service.ContextWith[adapter.Router](ctx, &fakeRouter{ + ruleSets: map[string]adapter.RuleSet{ + "legacy-ipcidr-set": ruleSet, + }, + }) + + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + privateTransport := &fakeDNSTransport{tag: "private", transportType: C.DNSTypeUDP} + var lookupAccess sync.Mutex + var lookupTags []string + recordLookup := func(tag string) { + lookupAccess.Lock() + lookupTags = append(lookupTags, tag) + lookupAccess.Unlock() + } + currentLookupTags := func() []string { + lookupAccess.Lock() + defer lookupAccess.Unlock() + return append([]string(nil), lookupTags...) + } + router := newTestRouterWithContext(t, ctx, []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + RuleSet: badoption.Listable[string]{"legacy-ipcidr-set"}, + RuleSetIPCIDRAcceptEmpty: true, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "private"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "default"}, + }, + }, + }, + }, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + "private": privateTransport, + }, + }, &fakeDNSClient{ + lookup: func(transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, *mDNS.Msg, error) { + require.Equal(t, "example.com", domain) + require.Equal(t, C.DomainStrategyIPv4Only, options.LookupStrategy) + recordLookup(transport.Tag()) + switch transport.Tag() { + case "private": + response := FixedResponse(0, fixedQuestion(domain, mDNS.TypeA), []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60) + return MessageToAddresses(response), response, nil + case "default": + response := FixedResponse(0, fixedQuestion(domain, mDNS.TypeA), []netip.Addr{netip.MustParseAddr("9.9.9.9")}, 60) + return MessageToAddresses(response), response, nil + } + return nil, nil, E.New("unexpected transport") + }, + }) + + require.True(t, router.legacyDNSMode) + + addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{ + LookupStrategy: C.DomainStrategyIPv4Only, + }) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("9.9.9.9")}, addresses) + require.Equal(t, []string{"private", "default"}, currentLookupTags()) +} + +func TestExchangeLegacyDNSModeDisabledEvaluateMatchResponseRoute(t *testing.T) { + t.Parallel() + + transportManager := &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + }, + } + client := &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + switch transport.Tag() { + case "upstream": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil + case "selected": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil + default: + return nil, E.New("unexpected transport") + } + }, + } + rules := []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "upstream"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + ResponseAnswer: badoption.Listable[option.DNSRecordOptions]{mustRecord(t, "example.com. IN A 1.1.1.1")}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + } + router := newTestRouter(t, rules, transportManager, client) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, + }, adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("8.8.8.8")}, MessageToAddresses(response)) +} + +func TestExchangeLegacyDNSModeDisabledEvaluateMatchResponseRcodeRoute(t *testing.T) { + t.Parallel() + + transportManager := &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + }, + } + client := &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + switch transport.Tag() { + case "upstream": + return &mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + Response: true, + Rcode: mDNS.RcodeNameError, + }, + Question: []mDNS.Question{message.Question[0]}, + }, nil + case "selected": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil + default: + return nil, E.New("unexpected transport") + } + }, + } + rcode := option.DNSRCode(mDNS.RcodeNameError) + rules := []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "upstream"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + ResponseRcode: &rcode, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + } + router := newTestRouter(t, rules, transportManager, client) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, + }, adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("8.8.8.8")}, MessageToAddresses(response)) +} + +func TestExchangeLegacyDNSModeDisabledEvaluateMatchResponseNsRoute(t *testing.T) { + t.Parallel() + + transportManager := &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + }, + } + nsRecord := mustRecord(t, "example.com. IN NS ns1.example.com.") + client := &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + switch transport.Tag() { + case "upstream": + return &mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + Response: true, + Rcode: mDNS.RcodeSuccess, + }, + Question: []mDNS.Question{message.Question[0]}, + Ns: []mDNS.RR{nsRecord.Build()}, + }, nil + case "selected": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil + default: + return nil, E.New("unexpected transport") + } + }, + } + rules := []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "upstream"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + ResponseNs: badoption.Listable[option.DNSRecordOptions]{nsRecord}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + } + router := newTestRouter(t, rules, transportManager, client) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, + }, adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("8.8.8.8")}, MessageToAddresses(response)) +} + +func TestExchangeLegacyDNSModeDisabledEvaluateMatchResponseExtraRoute(t *testing.T) { + t.Parallel() + + transportManager := &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + }, + } + extraRecord := mustRecord(t, "ns1.example.com. IN A 192.0.2.53") + client := &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + switch transport.Tag() { + case "upstream": + return &mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + Response: true, + Rcode: mDNS.RcodeSuccess, + }, + Question: []mDNS.Question{message.Question[0]}, + Extra: []mDNS.RR{extraRecord.Build()}, + }, nil + case "selected": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil + default: + return nil, E.New("unexpected transport") + } + }, + } + rules := []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "upstream"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + ResponseExtra: badoption.Listable[option.DNSRecordOptions]{extraRecord}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + } + router := newTestRouter(t, rules, transportManager, client) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, + }, adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("8.8.8.8")}, MessageToAddresses(response)) +} + +func TestExchangeLegacyDNSModeDisabledEvaluateDoesNotLeakAddressesToNextQuery(t *testing.T) { + t.Parallel() + + transportManager := &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + }, + } + var inspectedSelected bool + client := &fakeDNSClient{ + beforeExchange: func(ctx context.Context, transport adapter.DNSTransport, message *mDNS.Msg) { + if transport.Tag() != "selected" { + return + } + inspectedSelected = true + metadata := adapter.ContextFrom(ctx) + require.NotNil(t, metadata) + require.Empty(t, metadata.DestinationAddresses) + require.NotNil(t, metadata.DNSResponse) + }, + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + switch transport.Tag() { + case "upstream": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil + case "selected": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil + default: + return nil, E.New("unexpected transport") + } + }, + } + rules := []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "upstream"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + ResponseAnswer: badoption.Listable[option.DNSRecordOptions]{mustRecord(t, "example.com. IN A 1.1.1.1")}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + } + router := newTestRouter(t, rules, transportManager, client) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, + }, adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.True(t, inspectedSelected) + require.Equal(t, []netip.Addr{netip.MustParseAddr("8.8.8.8")}, MessageToAddresses(response)) +} + +func TestExchangeLegacyDNSModeDisabledEvaluateRouteResolutionFailureClearsResponse(t *testing.T) { + t.Parallel() + + transportManager := &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + }, + } + client := &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + switch transport.Tag() { + case "upstream": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil + case "selected": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil + case "default": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("4.4.4.4")}, 60), nil + default: + return nil, E.New("unexpected transport") + } + }, + } + rules := []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "upstream"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "missing"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + ResponseAnswer: badoption.Listable[option.DNSRecordOptions]{mustRecord(t, "example.com. IN A 1.1.1.1")}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + } + router := newTestRouter(t, rules, transportManager, client) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, + }, adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("4.4.4.4")}, MessageToAddresses(response)) +} + +func TestExchangeLegacyDNSModeDisabledSecondEvaluateOverwritesFirstResponse(t *testing.T) { + t.Parallel() + + transportManager := &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + "first-upstream": &fakeDNSTransport{tag: "first-upstream", transportType: C.DNSTypeUDP}, + "second-upstream": &fakeDNSTransport{tag: "second-upstream", transportType: C.DNSTypeUDP}, + "first-match": &fakeDNSTransport{tag: "first-match", transportType: C.DNSTypeUDP}, + "second-match": &fakeDNSTransport{tag: "second-match", transportType: C.DNSTypeUDP}, + }, + } + client := &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + switch transport.Tag() { + case "first-upstream": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil + case "second-upstream": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2.2.2.2")}, 60), nil + case "first-match": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("7.7.7.7")}, 60), nil + case "second-match": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil + case "default": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("4.4.4.4")}, 60), nil + default: + return nil, E.New("unexpected transport") + } + }, + } + rules := []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "first-upstream"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "second-upstream"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + ResponseAnswer: badoption.Listable[option.DNSRecordOptions]{mustRecord(t, "example.com. IN A 1.1.1.1")}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "first-match"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + ResponseAnswer: badoption.Listable[option.DNSRecordOptions]{mustRecord(t, "example.com. IN A 2.2.2.2")}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "second-match"}, + }, + }, + }, + } + router := newTestRouter(t, rules, transportManager, client) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, + }, adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("8.8.8.8")}, MessageToAddresses(response)) +} + +func TestExchangeLegacyDNSModeDisabledEvaluateExchangeFailureUsesMatchResponseBooleanSemantics(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + invert bool + expectedAddr netip.Addr + }{ + { + name: "plain match_response rule stays false", + expectedAddr: netip.MustParseAddr("4.4.4.4"), + }, + { + name: "invert match_response rule becomes true", + invert: true, + expectedAddr: netip.MustParseAddr("8.8.8.8"), + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + transportManager := &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + }, + } + client := &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + switch transport.Tag() { + case "upstream": + return nil, E.New("upstream exchange failed") + case "selected": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil + case "default": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("4.4.4.4")}, 60), nil + default: + return nil, E.New("unexpected transport") + } + }, + } + rules := []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "upstream"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + Invert: testCase.invert, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + } + router := newTestRouter(t, rules, transportManager, client) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, + }, adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{testCase.expectedAddr}, MessageToAddresses(response)) + }) + } +} + +func TestExchangeLegacyDNSModeDisabledRespondReturnsEvaluatedResponse(t *testing.T) { + t.Parallel() + + var exchanges []string + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + router := newTestRouter(t, []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "upstream"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRespond, + }, + }, + }, + }, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, + }, + }, &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + exchanges = append(exchanges, transport.Tag()) + require.Equal(t, "upstream", transport.Tag()) + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil + }, + }) + require.False(t, router.legacyDNSMode) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, + }, adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []string{"upstream"}, exchanges) + require.Equal(t, []netip.Addr{netip.MustParseAddr("1.1.1.1")}, MessageToAddresses(response)) +} + +func TestLookupLegacyDNSModeDisabledRespondReturnsEvaluatedResponse(t *testing.T) { + t.Parallel() + + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + router := newTestRouter(t, []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "upstream"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRespond, + }, + }, + }, + }, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, + }, + }, &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + require.Equal(t, "upstream", transport.Tag()) + switch message.Question[0].Qtype { + case mDNS.TypeA: + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil + case mDNS.TypeAAAA: + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2001:db8::1")}, 60), nil + default: + return nil, E.New("unexpected qtype") + } + }, + }) + require.False(t, router.legacyDNSMode) + + addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{ + netip.MustParseAddr("1.1.1.1"), + netip.MustParseAddr("2001:db8::1"), + }, addresses) +} + +func TestExchangeLegacyDNSModeDisabledRespondWithoutEvaluatedResponseReturnsError(t *testing.T) { + t.Parallel() + + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + router := newTestRouter(t, []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "upstream"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRespond, + }, + }, + }, + }, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, + }, + }, &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, _ *mDNS.Msg) (*mDNS.Msg, error) { + require.Equal(t, "upstream", transport.Tag()) + return nil, E.New("upstream exchange failed") + }, + }) + require.False(t, router.legacyDNSMode) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, + }, adapter.DNSQueryOptions{}) + require.Nil(t, response) + require.ErrorContains(t, err, dnsRespondMissingResponseMessage) +} + +func TestLookupLegacyDNSModeDisabledAllowsPartialSuccessForExchangeFailure(t *testing.T) { + t.Parallel() + + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + router := newTestRouter(t, nil, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + }, + }, &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + require.Equal(t, "default", transport.Tag()) + switch message.Question[0].Qtype { + case mDNS.TypeA: + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil + case mDNS.TypeAAAA: + return nil, E.New("ipv6 failed") + default: + return nil, E.New("unexpected qtype") + } + }, + }) + router.legacyDNSMode = false + + addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("1.1.1.1")}, addresses) +} + +func TestLookupLegacyDNSModeDisabledAllowsPartialSuccessForRcodeError(t *testing.T) { + t.Parallel() + + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + router := newTestRouter(t, nil, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + }, + }, &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + require.Equal(t, "default", transport.Tag()) + switch message.Question[0].Qtype { + case mDNS.TypeA: + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil + case mDNS.TypeAAAA: + return &mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + Response: true, + Rcode: mDNS.RcodeNameError, + }, + Question: []mDNS.Question{message.Question[0]}, + }, nil + default: + return nil, E.New("unexpected qtype") + } + }, + }) + router.legacyDNSMode = false + + addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("1.1.1.1")}, addresses) +} + +func TestLookupLegacyDNSModeDisabledSkipsFakeIPRule(t *testing.T) { + t.Parallel() + + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + router := newTestRouter(t, []option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "fake"}, + }, + }, + }}, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + "fake": &fakeDNSTransport{tag: "fake", transportType: C.DNSTypeFakeIP}, + }, + }, &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + require.Equal(t, "default", transport.Tag()) + if message.Question[0].Qtype == mDNS.TypeA { + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2.2.2.2")}, 60), nil + } + return FixedResponse(0, message.Question[0], nil, 60), nil + }, + }) + router.legacyDNSMode = false + + addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("2.2.2.2")}, addresses) +} + +func TestExchangeLegacyDNSModeDisabledAllowsRouteFakeIPRule(t *testing.T) { + t.Parallel() + + fakeTransport := &fakeDNSTransport{tag: "fake", transportType: C.DNSTypeFakeIP} + router := newTestRouter(t, []option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "fake"}, + }, + }, + }}, &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + "fake": fakeTransport, + }, + }, &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + require.Same(t, fakeTransport, transport) + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("198.18.0.1")}, 60), nil + }, + }) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, + }, adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("198.18.0.1")}, MessageToAddresses(response)) +} + +func TestInitializeRejectsDNSRuleStrategyWhenLegacyDNSModeIsDisabledByEvaluate(t *testing.T) { + t.Parallel() + + router := &Router{ + ctx: context.Background(), + logger: log.NewNOPFactory().NewLogger("dns"), + transport: &fakeDNSTransportManager{}, + client: &fakeDNSClient{}, + rawRules: make([]option.DNSRule, 0, 1), + defaultDomainStrategy: C.DomainStrategyAsIS, + } + err := router.Initialize([]option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{ + Server: "default", + Strategy: option.DomainStrategy(C.DomainStrategyIPv4Only), + }, + }, + }, + }}) + require.ErrorContains(t, err, "strategy") + require.ErrorContains(t, err, "deprecated") +} + +func TestInitializeRejectsEvaluateFakeIPServerInDefaultRule(t *testing.T) { + t.Parallel() + + router := &Router{ + ctx: context.Background(), + logger: log.NewNOPFactory().NewLogger("dns"), + transport: &fakeDNSTransportManager{transports: map[string]adapter.DNSTransport{"fake": &fakeDNSTransport{tag: "fake", transportType: C.DNSTypeFakeIP}}}, + client: &fakeDNSClient{}, + rawRules: make([]option.DNSRule, 0, 1), + defaultDomainStrategy: C.DomainStrategyAsIS, + } + err := router.Initialize([]option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "fake"}, + }, + }, + }}) + require.ErrorContains(t, err, "evaluate action cannot use fakeip server") + require.ErrorContains(t, err, "fake") +} + +func TestInitializeRejectsEvaluateFakeIPServerInLogicalRule(t *testing.T) { + t.Parallel() + + router := &Router{ + ctx: context.Background(), + logger: log.NewNOPFactory().NewLogger("dns"), + transport: &fakeDNSTransportManager{transports: map[string]adapter.DNSTransport{"fake": &fakeDNSTransport{tag: "fake", transportType: C.DNSTypeFakeIP}}}, + client: &fakeDNSClient{}, + rawRules: make([]option.DNSRule, 0, 1), + defaultDomainStrategy: C.DomainStrategyAsIS, + } + err := router.Initialize([]option.DNSRule{{ + Type: C.RuleTypeLogical, + LogicalOptions: option.LogicalDNSRule{ + RawLogicalDNSRule: option.RawLogicalDNSRule{ + Mode: C.LogicalTypeOr, + Rules: []option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + }, + }}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "fake"}, + }, + }, + }}) + require.ErrorContains(t, err, "evaluate action cannot use fakeip server") + require.ErrorContains(t, err, "fake") +} + +func TestInitializeRejectsDNSRuleStrategyWhenLegacyDNSModeIsDisabledByMatchResponse(t *testing.T) { + t.Parallel() + + router := &Router{ + ctx: context.Background(), + logger: log.NewNOPFactory().NewLogger("dns"), + transport: &fakeDNSTransportManager{}, + client: &fakeDNSClient{}, + rawRules: make([]option.DNSRule, 0, 1), + defaultDomainStrategy: C.DomainStrategyAsIS, + } + err := router.Initialize([]option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRouteOptions, + RouteOptionsOptions: option.DNSRouteOptionsActionOptions{ + Strategy: option.DomainStrategy(C.DomainStrategyIPv4Only), + }, + }, + }, + }}) + require.ErrorContains(t, err, "strategy") + require.ErrorContains(t, err, "deprecated") +} + +func TestInitializeRejectsDNSMatchResponseWithoutPrecedingEvaluate(t *testing.T) { + t.Parallel() + + router := &Router{ + ctx: context.Background(), + logger: log.NewNOPFactory().NewLogger("dns"), + transport: &fakeDNSTransportManager{}, + client: &fakeDNSClient{}, + rawRules: make([]option.DNSRule, 0, 1), + defaultDomainStrategy: C.DomainStrategyAsIS, + } + err := router.Initialize([]option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + ResponseAnswer: badoption.Listable[option.DNSRecordOptions]{mustRecord(t, "example.com. IN A 1.1.1.1")}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "default"}, + }, + }, + }}) + require.ErrorContains(t, err, "preceding evaluate action") +} + +func TestInitializeRejectsDNSRespondWithoutPrecedingEvaluate(t *testing.T) { + t.Parallel() + + router := &Router{ + ctx: context.Background(), + logger: log.NewNOPFactory().NewLogger("dns"), + transport: &fakeDNSTransportManager{}, + client: &fakeDNSClient{}, + rawRules: make([]option.DNSRule, 0, 1), + defaultDomainStrategy: C.DomainStrategyAsIS, + } + err := router.Initialize([]option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRespond, + }, + }, + }}) + require.ErrorContains(t, err, "preceding evaluate action") +} + +func TestInitializeRejectsLogicalDNSRespondWithoutPrecedingEvaluate(t *testing.T) { + t.Parallel() + + router := &Router{ + ctx: context.Background(), + logger: log.NewNOPFactory().NewLogger("dns"), + transport: &fakeDNSTransportManager{}, + client: &fakeDNSClient{}, + rawRules: make([]option.DNSRule, 0, 1), + defaultDomainStrategy: C.DomainStrategyAsIS, + } + err := router.Initialize([]option.DNSRule{{ + Type: C.RuleTypeLogical, + LogicalOptions: option.LogicalDNSRule{ + RawLogicalDNSRule: option.RawLogicalDNSRule{ + Mode: C.LogicalTypeOr, + Rules: []option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + }, + }}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRespond, + }, + }, + }}) + require.ErrorContains(t, err, "preceding evaluate action") +} + +func TestInitializeRejectsEvaluateRuleWithResponseMatchWithoutPrecedingEvaluate(t *testing.T) { + t.Parallel() + + router := &Router{ + ctx: context.Background(), + logger: log.NewNOPFactory().NewLogger("dns"), + transport: &fakeDNSTransportManager{}, + client: &fakeDNSClient{}, + rawRules: make([]option.DNSRule, 0, 1), + defaultDomainStrategy: C.DomainStrategyAsIS, + } + err := router.Initialize([]option.DNSRule{{ + Type: C.RuleTypeLogical, + LogicalOptions: option.LogicalDNSRule{ + RawLogicalDNSRule: option.RawLogicalDNSRule{ + Mode: C.LogicalTypeOr, + Rules: []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + ResponseAnswer: badoption.Listable[option.DNSRecordOptions]{mustRecord(t, "example.com. IN A 1.1.1.1")}, + }, + }, + }, + }, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "default"}, + }, + }, + }}) + require.ErrorContains(t, err, "preceding evaluate action") +} + +func TestInitializeAllowsEvaluateRuleWithResponseMatchAfterPrecedingEvaluate(t *testing.T) { + t.Parallel() + + router := &Router{ + ctx: context.Background(), + logger: log.NewNOPFactory().NewLogger("dns"), + transport: &fakeDNSTransportManager{}, + client: &fakeDNSClient{}, + rawRules: make([]option.DNSRule, 0, 2), + defaultDomainStrategy: C.DomainStrategyAsIS, + } + err := router.Initialize([]option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"bootstrap.example"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "bootstrap"}, + }, + }, + }, + { + Type: C.RuleTypeLogical, + LogicalOptions: option.LogicalDNSRule{ + RawLogicalDNSRule: option.RawLogicalDNSRule{ + Mode: C.LogicalTypeOr, + Rules: []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + ResponseAnswer: badoption.Listable[option.DNSRecordOptions]{mustRecord(t, "example.com. IN A 1.1.1.1")}, + }, + }, + }, + }, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "default"}, + }, + }, + }, + }) + require.NoError(t, err) +} + +func TestLookupLegacyDNSModeDisabledReturnsRejectedErrorForRejectAction(t *testing.T) { + t.Parallel() + + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + router := newTestRouter(t, []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeReject, + RejectOptions: option.RejectActionOptions{ + Method: C.RuleActionRejectMethodDefault, + }, + }, + }, + }, + }, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + }, + }, &fakeDNSClient{}) + require.False(t, router.legacyDNSMode) + + addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{}) + require.Nil(t, addresses) + require.Error(t, err) + require.True(t, rulepkg.IsRejected(err)) +} + +func TestExchangeLegacyDNSModeDisabledReturnsRefusedResponseForRejectAction(t *testing.T) { + t.Parallel() + + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + router := newTestRouter(t, []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeReject, + RejectOptions: option.RejectActionOptions{ + Method: C.RuleActionRejectMethodDefault, + }, + }, + }, + }, + }, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + }, + }, &fakeDNSClient{}) + require.False(t, router.legacyDNSMode) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, + }, adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, mDNS.RcodeRefused, response.Rcode) + require.Equal(t, []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, response.Question) +} + +func TestExchangeLegacyDNSModeDisabledReturnsDropErrorForRejectDropAction(t *testing.T) { + t.Parallel() + + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + router := newTestRouter(t, []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeReject, + RejectOptions: option.RejectActionOptions{ + Method: C.RuleActionRejectMethodDrop, + }, + }, + }, + }, + }, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + }, + }, &fakeDNSClient{}) + require.False(t, router.legacyDNSMode) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, + }, adapter.DNSQueryOptions{}) + require.Nil(t, response) + require.ErrorIs(t, err, tun.ErrDrop) +} + +func TestLookupLegacyDNSModeDisabledFiltersPerQueryTypeAddressesBeforeMerging(t *testing.T) { + t.Parallel() + + defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP} + router := newTestRouter(t, []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypePredefined, + PredefinedOptions: option.DNSRouteActionPredefined{ + Answer: badoption.Listable[option.DNSRecordOptions]{ + mustRecord(t, "example.com. IN A 1.1.1.1"), + mustRecord(t, "example.com. IN AAAA 2001:db8::1"), + }, + }, + }, + }, + }, + }, &fakeDNSTransportManager{ + defaultTransport: defaultTransport, + transports: map[string]adapter.DNSTransport{ + "default": defaultTransport, + }, + }, &fakeDNSClient{}) + require.False(t, router.legacyDNSMode) + + addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{ + netip.MustParseAddr("1.1.1.1"), + netip.MustParseAddr("2001:db8::1"), + }, addresses) +} + +func TestExchangeLegacyDNSModeDisabledLogicalMatchResponseIPCIDRFallsThrough(t *testing.T) { + t.Parallel() + + transportManager := &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + }, + } + client := &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + switch transport.Tag() { + case "upstream": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("9.9.9.9")}, 60), nil + case "selected": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil + case "default": + return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("4.4.4.4")}, 60), nil + default: + return nil, E.New("unexpected transport") + } + }, + } + rules := []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "upstream"}, + }, + }, + }, + { + Type: C.RuleTypeLogical, + LogicalOptions: option.LogicalDNSRule{ + RawLogicalDNSRule: option.RawLogicalDNSRule{ + Mode: C.LogicalTypeOr, + Rules: []option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + IPCIDR: badoption.Listable[string]{"1.1.1.0/24"}, + }, + }, + }}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + } + router := newTestRouter(t, rules, transportManager, client) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)}, + }, adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("4.4.4.4")}, MessageToAddresses(response)) +} + +func TestLegacyDNSModeReportsLegacyAddressFilterDeprecation(t *testing.T) { + t.Parallel() + + manager := &fakeDeprecatedManager{} + ctx := service.ContextWith[deprecated.Manager](context.Background(), manager) + router := &Router{ + ctx: ctx, + logger: log.NewNOPFactory().NewLogger("dns"), + client: &fakeDNSClient{}, + defaultDomainStrategy: C.DomainStrategyAsIS, + } + err := router.Initialize([]option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + IPCIDR: badoption.Listable[string]{"1.1.1.0/24"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "default"}, + }, + }, + }}) + require.NoError(t, err) + + err = router.Start(adapter.StartStateStart) + require.NoError(t, err) + require.Len(t, manager.features, 1) + require.Equal(t, deprecated.OptionLegacyDNSAddressFilter.Name, manager.features[0].Name) +} + +func TestLegacyDNSModeReportsDNSRuleStrategyDeprecation(t *testing.T) { + t.Parallel() + + manager := &fakeDeprecatedManager{} + ctx := service.ContextWith[deprecated.Manager](context.Background(), manager) + router := &Router{ + ctx: ctx, + logger: log.NewNOPFactory().NewLogger("dns"), + client: &fakeDNSClient{}, + defaultDomainStrategy: C.DomainStrategyAsIS, + } + err := router.Initialize([]option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{ + Server: "default", + Strategy: option.DomainStrategy(C.DomainStrategyIPv4Only), + }, + }, + }, + }}) + require.NoError(t, err) + + err = router.Start(adapter.StartStateStart) + require.NoError(t, err) + require.Len(t, manager.features, 1) + require.Equal(t, deprecated.OptionLegacyDNSRuleStrategy.Name, manager.features[0].Name) +} diff --git a/dns/transport_adapter.go b/dns/transport_adapter.go index 4734570978..1e6620f25d 100644 --- a/dns/transport_adapter.go +++ b/dns/transport_adapter.go @@ -1,21 +1,13 @@ package dns import ( - "net/netip" - - "github.com/sagernet/sing-box/adapter" - C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" ) -var _ adapter.LegacyDNSTransport = (*TransportAdapter)(nil) - type TransportAdapter struct { transportType string transportTag string dependencies []string - strategy C.DomainStrategy - clientSubnet netip.Prefix } func NewTransportAdapter(transportType string, transportTag string, dependencies []string) TransportAdapter { @@ -35,8 +27,6 @@ func NewTransportAdapterWithLocalOptions(transportType string, transportTag stri transportType: transportType, transportTag: transportTag, dependencies: dependencies, - strategy: C.DomainStrategy(localOptions.LegacyStrategy), - clientSubnet: localOptions.LegacyClientSubnet, } } @@ -45,15 +35,10 @@ func NewTransportAdapterWithRemoteOptions(transportType string, transportTag str if remoteOptions.DomainResolver != nil && remoteOptions.DomainResolver.Server != "" { dependencies = append(dependencies, remoteOptions.DomainResolver.Server) } - if remoteOptions.LegacyAddressResolver != "" { - dependencies = append(dependencies, remoteOptions.LegacyAddressResolver) - } return TransportAdapter{ transportType: transportType, transportTag: transportTag, dependencies: dependencies, - strategy: C.DomainStrategy(remoteOptions.LegacyStrategy), - clientSubnet: remoteOptions.LegacyClientSubnet, } } @@ -68,11 +53,3 @@ func (a *TransportAdapter) Tag() string { func (a *TransportAdapter) Dependencies() []string { return a.dependencies } - -func (a *TransportAdapter) LegacyStrategy() C.DomainStrategy { - return a.strategy -} - -func (a *TransportAdapter) LegacyClientSubnet() netip.Prefix { - return a.clientSubnet -} diff --git a/dns/transport_dialer.go b/dns/transport_dialer.go index b3ee8082ab..971002ac40 100644 --- a/dns/transport_dialer.go +++ b/dns/transport_dialer.go @@ -2,104 +2,25 @@ package dns import ( "context" - "net" - "time" - "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/dialer" - C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" - E "github.com/sagernet/sing/common/exceptions" - M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" - "github.com/sagernet/sing/service" ) func NewLocalDialer(ctx context.Context, options option.LocalDNSServerOptions) (N.Dialer, error) { - if options.LegacyDefaultDialer { - return dialer.NewDefaultOutbound(ctx), nil - } else { - return dialer.NewWithOptions(dialer.Options{ - Context: ctx, - Options: options.DialerOptions, - DirectResolver: true, - LegacyDNSDialer: options.Legacy, - }) - } -} - -func NewRemoteDialer(ctx context.Context, options option.RemoteDNSServerOptions) (N.Dialer, error) { - if options.LegacyDefaultDialer { - transportDialer := dialer.NewDefaultOutbound(ctx) - if options.LegacyAddressResolver != "" { - transport := service.FromContext[adapter.DNSTransportManager](ctx) - resolverTransport, loaded := transport.Transport(options.LegacyAddressResolver) - if !loaded { - return nil, E.New("address resolver not found: ", options.LegacyAddressResolver) - } - transportDialer = newTransportDialer(transportDialer, service.FromContext[adapter.DNSRouter](ctx), resolverTransport, C.DomainStrategy(options.LegacyAddressStrategy), time.Duration(options.LegacyAddressFallbackDelay)) - } else if options.ServerIsDomain() { - return nil, E.New("missing address resolver for server: ", options.Server) - } - return transportDialer, nil - } else { - return dialer.NewWithOptions(dialer.Options{ - Context: ctx, - Options: options.DialerOptions, - RemoteIsDomain: options.ServerIsDomain(), - DirectResolver: true, - LegacyDNSDialer: options.Legacy, - }) - } -} - -type legacyTransportDialer struct { - dialer N.Dialer - dnsRouter adapter.DNSRouter - transport adapter.DNSTransport - strategy C.DomainStrategy - fallbackDelay time.Duration -} - -func newTransportDialer(dialer N.Dialer, dnsRouter adapter.DNSRouter, transport adapter.DNSTransport, strategy C.DomainStrategy, fallbackDelay time.Duration) *legacyTransportDialer { - return &legacyTransportDialer{ - dialer, - dnsRouter, - transport, - strategy, - fallbackDelay, - } -} - -func (d *legacyTransportDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { - if destination.IsIP() { - return d.dialer.DialContext(ctx, network, destination) - } - addresses, err := d.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{ - Transport: d.transport, - Strategy: d.strategy, + return dialer.NewWithOptions(dialer.Options{ + Context: ctx, + Options: options.DialerOptions, + DirectResolver: true, }) - if err != nil { - return nil, err - } - return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.strategy == C.DomainStrategyPreferIPv6, d.fallbackDelay) } -func (d *legacyTransportDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { - if destination.IsIP() { - return d.dialer.ListenPacket(ctx, destination) - } - addresses, err := d.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{ - Transport: d.transport, - Strategy: d.strategy, +func NewRemoteDialer(ctx context.Context, options option.RemoteDNSServerOptions) (N.Dialer, error) { + return dialer.NewWithOptions(dialer.Options{ + Context: ctx, + Options: options.DialerOptions, + RemoteIsDomain: options.ServerIsDomain(), + DirectResolver: true, }) - if err != nil { - return nil, err - } - conn, _, err := N.ListenSerial(ctx, d.dialer, destination, addresses) - return conn, err -} - -func (d *legacyTransportDialer) Upstream() any { - return d.dialer } diff --git a/docs/changelog.md b/docs/changelog.md index f38e84de9e..8cd5296675 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -682,7 +682,7 @@ DNS servers are refactored for better performance and scalability. See [DNS server](/configuration/dns/server/). -For migration, see [Migrate to new DNS server formats](/migration/#migrate-to-new-dns-servers). +For migration, see [Migrate to new DNS server formats](/migration/#migrate-to-new-dns-server-formats). Compatibility for old formats will be removed in sing-box 1.14.0. @@ -1152,7 +1152,7 @@ DNS servers are refactored for better performance and scalability. See [DNS server](/configuration/dns/server/). -For migration, see [Migrate to new DNS server formats](/migration/#migrate-to-new-dns-servers). +For migration, see [Migrate to new DNS server formats](/migration/#migrate-to-new-dns-server-formats). Compatibility for old formats will be removed in sing-box 1.14.0. @@ -1988,7 +1988,7 @@ See [Migration](/migration/#process_path-format-update-on-windows). The new DNS feature allows you to more precisely bypass Chinese websites via **DNS leaks**. Do not use plain local DNS if using this method. -See [Address Filter Fields](/configuration/dns/rule#address-filter-fields). +See [Legacy Address Filter Fields](/configuration/dns/rule#legacy-address-filter-fields). [Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) updated. @@ -2002,7 +2002,7 @@ the [Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users **5**: The new feature allows you to cache the check results of -[Address filter DNS rule items](/configuration/dns/rule/#address-filter-fields) until expiration. +[Legacy Address Filter Fields](/configuration/dns/rule/#legacy-address-filter-fields) until expiration. **6**: @@ -2183,7 +2183,7 @@ See [TUN](/configuration/inbound/tun) inbound. **1**: The new feature allows you to cache the check results of -[Address filter DNS rule items](/configuration/dns/rule/#address-filter-fields) until expiration. +[Legacy Address Filter Fields](/configuration/dns/rule/#legacy-address-filter-fields) until expiration. #### 1.9.0-alpha.7 @@ -2230,7 +2230,7 @@ See [Migration](/migration/#process_path-format-update-on-windows). The new DNS feature allows you to more precisely bypass Chinese websites via **DNS leaks**. Do not use plain local DNS if using this method. -See [Address Filter Fields](/configuration/dns/rule#address-filter-fields). +See [Legacy Address Filter Fields](/configuration/dns/rule#legacy-address-filter-fields). [Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) updated. diff --git a/docs/configuration/dns/fakeip.md b/docs/configuration/dns/fakeip.md index f9204d3452..a0524dc8b0 100644 --- a/docs/configuration/dns/fakeip.md +++ b/docs/configuration/dns/fakeip.md @@ -1,10 +1,10 @@ --- -icon: material/delete-clock +icon: material/note-remove --- -!!! failure "Deprecated in sing-box 1.12.0" +!!! failure "Removed in sing-box 1.14.0" - Legacy fake-ip configuration is deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-servers). + Legacy fake-ip configuration is deprecated in sing-box 1.12.0 and removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-server-formats). ### Structure @@ -26,6 +26,6 @@ Enable FakeIP service. IPv4 address range for FakeIP. -#### inet6_address +#### inet6_range IPv6 address range for FakeIP. diff --git a/docs/configuration/dns/fakeip.zh.md b/docs/configuration/dns/fakeip.zh.md index c8d5dfe301..1e5eca60b6 100644 --- a/docs/configuration/dns/fakeip.zh.md +++ b/docs/configuration/dns/fakeip.zh.md @@ -1,10 +1,10 @@ --- -icon: material/delete-clock +icon: material/note-remove --- -!!! failure "已在 sing-box 1.12.0 废弃" +!!! failure "已在 sing-box 1.14.0 移除" - 旧的 fake-ip 配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。 + 旧的 fake-ip 配置已在 sing-box 1.12.0 废弃且已在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。 ### 结构 diff --git a/docs/configuration/dns/index.md b/docs/configuration/dns/index.md index c6750a01bb..cbb58906f1 100644 --- a/docs/configuration/dns/index.md +++ b/docs/configuration/dns/index.md @@ -39,7 +39,7 @@ icon: material/alert-decagram |----------|---------------------------------| | `server` | List of [DNS Server](./server/) | | `rules` | List of [DNS Rule](./rule/) | -| `fakeip` | [FakeIP](./fakeip/) | +| `fakeip` | :material-note-remove: [FakeIP](./fakeip/) | #### final @@ -88,4 +88,4 @@ Append a `edns0-subnet` OPT extra record with the specified IP prefix to every q If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically. -Can be overrides by `servers.[].client_subnet` or `rules.[].client_subnet`. +Can be overridden by `servers.[].client_subnet` or `rules.[].client_subnet`. diff --git a/docs/configuration/dns/index.zh.md b/docs/configuration/dns/index.zh.md index 68927a5f41..cd2518107c 100644 --- a/docs/configuration/dns/index.zh.md +++ b/docs/configuration/dns/index.zh.md @@ -88,6 +88,6 @@ LRU 缓存容量。 可以被 `servers.[].client_subnet` 或 `rules.[].client_subnet` 覆盖。 -#### fakeip +#### fakeip :material-note-remove: [FakeIP](./fakeip/) 设置。 diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 0b3e56da69..aacdc003fd 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -5,7 +5,14 @@ icon: material/alert-decagram !!! quote "Changes in sing-box 1.14.0" :material-plus: [source_mac_address](#source_mac_address) - :material-plus: [source_hostname](#source_hostname) + :material-plus: [source_hostname](#source_hostname) + :material-plus: [match_response](#match_response) + :material-delete-clock: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty) + :material-delete-clock: [ip_accept_any](#ip_accept_any) + :material-plus: [response_rcode](#response_rcode) + :material-plus: [response_answer](#response_answer) + :material-plus: [response_ns](#response_ns) + :material-plus: [response_extra](#response_extra) !!! quote "Changes in sing-box 1.13.0" @@ -94,12 +101,6 @@ icon: material/alert-decagram "192.168.0.1" ], "source_ip_is_private": false, - "ip_cidr": [ - "10.0.0.0/24", - "192.168.0.1" - ], - "ip_is_private": false, - "ip_accept_any": false, "source_port": [ 12345 ], @@ -171,7 +172,16 @@ icon: material/alert-decagram "geosite-cn" ], "rule_set_ip_cidr_match_source": false, - "rule_set_ip_cidr_accept_empty": false, + "match_response": false, + "ip_cidr": [ + "10.0.0.0/24", + "192.168.0.1" + ], + "ip_is_private": false, + "response_rcode": "", + "response_answer": [], + "response_ns": [], + "response_extra": [], "invert": false, "outbound": [ "direct" @@ -180,7 +190,9 @@ icon: material/alert-decagram "server": "local", // Deprecated - + + "ip_accept_any": false, + "rule_set_ip_cidr_accept_empty": false, "rule_set_ipcidr_match_source": false, "geosite": [ "cn" @@ -477,6 +489,19 @@ Make `ip_cidr` rule items in rule-sets match the source IP. Make `ip_cidr` rule items in rule-sets match the source IP. +#### match_response + +!!! question "Since sing-box 1.14.0" + +Enable response-based matching. When enabled, this rule matches against the evaluated response +(set by a preceding [`evaluate`](/configuration/dns/rule_action/#evaluate) action) +instead of only matching the original query. + +The evaluated response can also be returned directly by a later [`respond`](/configuration/dns/rule_action/#respond) action. + +Required for Response Match Fields (`response_rcode`, `response_answer`, `response_ns`, `response_extra`). +Also required for `ip_cidr` and `ip_is_private` when used with `evaluate` or Response Match Fields. + #### invert Invert match result. @@ -521,7 +546,12 @@ See [DNS Rule Actions](../rule_action/) for details. Moved to [DNS Rule Action](../rule_action#route). -### Address Filter Fields +### Legacy Address Filter Fields + +!!! failure "Deprecated in sing-box 1.14.0" + + Legacy Address Filter Fields are deprecated and will be removed in sing-box 1.16.0, + check [Migration](/migration/#migrate-address-filter-fields-to-response-matching). Only takes effect for address requests (A/AAAA/HTTPS). When the query results do not match the address filtering rule items, the current rule will be skipped. @@ -547,24 +577,73 @@ Match GeoIP with query response. Match IP CIDR with query response. +As a Legacy Address Filter Field, deprecated. Use with `match_response` instead, +check [Migration](/migration/#migrate-address-filter-fields-to-response-matching). + #### ip_is_private !!! question "Since sing-box 1.9.0" Match private IP with query response. +As a Legacy Address Filter Field, deprecated. Use with `match_response` instead, +check [Migration](/migration/#migrate-address-filter-fields-to-response-matching). + #### rule_set_ip_cidr_accept_empty !!! question "Since sing-box 1.10.0" +!!! failure "Deprecated in sing-box 1.14.0" + + `rule_set_ip_cidr_accept_empty` is deprecated and will be removed in sing-box 1.16.0, + check [Migration](/migration/#migrate-address-filter-fields-to-response-matching). + Make `ip_cidr` rules in rule-sets accept empty query response. #### ip_accept_any !!! question "Since sing-box 1.12.0" +!!! failure "Deprecated in sing-box 1.14.0" + + `ip_accept_any` is deprecated and will be removed in sing-box 1.16.0, + check [Migration](/migration/#migrate-address-filter-fields-to-response-matching). + Match any IP with query response. +### Response Match Fields + +!!! question "Since sing-box 1.14.0" + +Match fields for the evaluated response. Require `match_response` to be set to `true` +and a preceding rule with [`evaluate`](/configuration/dns/rule_action/#evaluate) action to populate the response. + +That evaluated response may also be returned directly by a later [`respond`](/configuration/dns/rule_action/#respond) action. + +#### response_rcode + +Match DNS response code. + +Accepted values are the same as in the [predefined action rcode](/configuration/dns/rule_action/#rcode). + +#### response_answer + +Match DNS answer records. + +Record format is the same as in [predefined action answer](/configuration/dns/rule_action/#answer). + +#### response_ns + +Match DNS name server records. + +Record format is the same as in [predefined action ns](/configuration/dns/rule_action/#ns). + +#### response_extra + +Match DNS extra records. + +Record format is the same as in [predefined action extra](/configuration/dns/rule_action/#extra). + ### Logical Fields #### type diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index 82f85648f0..a3633789f6 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -5,7 +5,14 @@ icon: material/alert-decagram !!! quote "sing-box 1.14.0 中的更改" :material-plus: [source_mac_address](#source_mac_address) - :material-plus: [source_hostname](#source_hostname) + :material-plus: [source_hostname](#source_hostname) + :material-plus: [match_response](#match_response) + :material-delete-clock: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty) + :material-delete-clock: [ip_accept_any](#ip_accept_any) + :material-plus: [response_rcode](#response_rcode) + :material-plus: [response_answer](#response_answer) + :material-plus: [response_ns](#response_ns) + :material-plus: [response_extra](#response_extra) !!! quote "sing-box 1.13.0 中的更改" @@ -94,12 +101,6 @@ icon: material/alert-decagram "192.168.0.1" ], "source_ip_is_private": false, - "ip_cidr": [ - "10.0.0.0/24", - "192.168.0.1" - ], - "ip_is_private": false, - "ip_accept_any": false, "source_port": [ 12345 ], @@ -171,7 +172,16 @@ icon: material/alert-decagram "geosite-cn" ], "rule_set_ip_cidr_match_source": false, - "rule_set_ip_cidr_accept_empty": false, + "match_response": false, + "ip_cidr": [ + "10.0.0.0/24", + "192.168.0.1" + ], + "ip_is_private": false, + "response_rcode": "", + "response_answer": [], + "response_ns": [], + "response_extra": [], "invert": false, "outbound": [ "direct" @@ -180,6 +190,9 @@ icon: material/alert-decagram "server": "local", // 已弃用 + + "ip_accept_any": false, + "rule_set_ip_cidr_accept_empty": false, "rule_set_ipcidr_match_source": false, "geosite": [ "cn" @@ -476,6 +489,17 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`. 使规则集中的 `ip_cidr` 规则匹配源 IP。 +#### match_response + +!!! question "自 sing-box 1.14.0 起" + +启用响应匹配。启用后,此规则将匹配已评估的响应(由前序 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作设置),而不仅是匹配原始查询。 + +该已评估的响应也可以被后续的 [`respond`](/zh/configuration/dns/rule_action/#respond) 动作直接返回。 + +响应匹配字段(`response_rcode`、`response_answer`、`response_ns`、`response_extra`)需要此选项。 +当与 `evaluate` 或响应匹配字段一起使用时,`ip_cidr` 和 `ip_is_private` 也需要此选项。 + #### invert 反选匹配结果。 @@ -520,7 +544,12 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`. 已移动到 [DNS 规则动作](../rule_action#route). -### 地址筛选字段 +### 旧版地址筛选字段 + +!!! failure "已在 sing-box 1.14.0 废弃" + + 旧版地址筛选字段已废弃,且将在 sing-box 1.16.0 中被移除, + 参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。 仅对地址请求 (A/AAAA/HTTPS) 生效。 当查询结果与地址筛选规则项不匹配时,将跳过当前规则。 @@ -547,23 +576,72 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`. 与查询响应匹配 IP CIDR。 +作为旧版地址筛选字段已废弃。请改为配合 `match_response` 使用, +参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。 + #### ip_is_private !!! question "自 sing-box 1.9.0 起" 与查询响应匹配非公开 IP。 +作为旧版地址筛选字段已废弃。请改为配合 `match_response` 使用, +参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。 + +#### rule_set_ip_cidr_accept_empty + +!!! question "自 sing-box 1.10.0 起" + +!!! failure "已在 sing-box 1.14.0 废弃" + + `rule_set_ip_cidr_accept_empty` 已废弃且将在 sing-box 1.16.0 中被移除, + 参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。 + +使规则集中的 `ip_cidr` 规则接受空查询响应。 + #### ip_accept_any !!! question "自 sing-box 1.12.0 起" +!!! failure "已在 sing-box 1.14.0 废弃" + + `ip_accept_any` 已废弃且将在 sing-box 1.16.0 中被移除, + 参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。 + 匹配任意 IP。 -#### rule_set_ip_cidr_accept_empty +### 响应匹配字段 -!!! question "自 sing-box 1.10.0 起" +!!! question "自 sing-box 1.14.0 起" -使规则集中的 `ip_cidr` 规则接受空查询响应。 +已评估的响应的匹配字段。需要将 `match_response` 设为 `true`, +且需要前序规则使用 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作来填充响应。 + +该已评估的响应也可以被后续的 [`respond`](/zh/configuration/dns/rule_action/#respond) 动作直接返回。 + +#### response_rcode + +匹配 DNS 响应码。 + +接受的值与 [predefined 动作 rcode](/zh/configuration/dns/rule_action/#rcode) 中相同。 + +#### response_answer + +匹配 DNS 应答记录。 + +记录格式与 [predefined 动作 answer](/zh/configuration/dns/rule_action/#answer) 中相同。 + +#### response_ns + +匹配 DNS 名称服务器记录。 + +记录格式与 [predefined 动作 ns](/zh/configuration/dns/rule_action/#ns) 中相同。 + +#### response_extra + +匹配 DNS 额外记录。 + +记录格式与 [predefined 动作 extra](/zh/configuration/dns/rule_action/#extra) 中相同。 ### 逻辑字段 diff --git a/docs/configuration/dns/rule_action.md b/docs/configuration/dns/rule_action.md index db9033f8b7..e71a28c8a9 100644 --- a/docs/configuration/dns/rule_action.md +++ b/docs/configuration/dns/rule_action.md @@ -2,6 +2,12 @@ icon: material/new-box --- +!!! quote "Changes in sing-box 1.14.0" + + :material-delete-clock: [strategy](#strategy) + :material-plus: [evaluate](#evaluate) + :material-plus: [respond](#respond) + !!! quote "Changes in sing-box 1.12.0" :material-plus: [strategy](#strategy) @@ -34,7 +40,11 @@ Tag of target server. !!! question "Since sing-box 1.12.0" -Set domain strategy for this query. +!!! failure "Deprecated in sing-box 1.14.0" + + `strategy` is deprecated in sing-box 1.14.0 and will be removed in sing-box 1.16.0. + +Set domain strategy for this query. Deprecated, check [Migration](/migration/#migrate-dns-rule-action-strategy-to-rule-items). One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`. @@ -52,7 +62,68 @@ Append a `edns0-subnet` OPT extra record with the specified IP prefix to every q If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically. -Will overrides `dns.client_subnet`. +Will override `dns.client_subnet`. + +### evaluate + +!!! question "Since sing-box 1.14.0" + +```json +{ + "action": "evaluate", + "server": "", + "disable_cache": false, + "rewrite_ttl": null, + "client_subnet": null +} +``` + +`evaluate` sends a DNS query to the specified server and saves the evaluated response for subsequent rules +to match against using [`match_response`](/configuration/dns/rule/#match_response) and response fields. +Unlike `route`, it does **not** terminate rule evaluation. + +Only allowed on top-level DNS rules (not inside logical sub-rules). +Rules that use [`match_response`](/configuration/dns/rule/#match_response) or Response Match Fields +require a preceding top-level rule with `evaluate` action. A rule's own `evaluate` action +does not satisfy this requirement, because matching happens before the action runs. + +#### server + +==Required== + +Tag of target server. + +#### disable_cache + +Disable cache and save cache in this query. + +#### rewrite_ttl + +Rewrite TTL in DNS responses. + +#### client_subnet + +Append a `edns0-subnet` OPT extra record with the specified IP prefix to every query by default. + +If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically. + +Will override `dns.client_subnet`. + +### respond + +!!! question "Since sing-box 1.14.0" + +```json +{ + "action": "respond" +} +``` + +`respond` terminates rule evaluation and returns the evaluated response from a preceding [`evaluate`](/configuration/dns/rule_action/#evaluate) action. + +This action does not send a new DNS query and has no extra options. + +Only allowed after a preceding top-level `evaluate` rule. If the action is reached without an evaluated response at runtime, the request fails with an error instead of falling through to later rules. ### route-options diff --git a/docs/configuration/dns/rule_action.zh.md b/docs/configuration/dns/rule_action.zh.md index 9e59c6bd2b..f11bb58920 100644 --- a/docs/configuration/dns/rule_action.zh.md +++ b/docs/configuration/dns/rule_action.zh.md @@ -2,6 +2,12 @@ icon: material/new-box --- +!!! quote "sing-box 1.14.0 中的更改" + + :material-delete-clock: [strategy](#strategy) + :material-plus: [evaluate](#evaluate) + :material-plus: [respond](#respond) + !!! quote "sing-box 1.12.0 中的更改" :material-plus: [strategy](#strategy) @@ -34,7 +40,11 @@ icon: material/new-box !!! question "自 sing-box 1.12.0 起" -为此查询设置域名策略。 +!!! failure "已在 sing-box 1.14.0 废弃" + + `strategy` 已在 sing-box 1.14.0 废弃,且将在 sing-box 1.16.0 中被移除。 + +为此查询设置域名策略。已废弃,参阅[迁移指南](/zh/migration/#迁移-dns-规则动作-strategy-到规则项)。 可选项:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。 @@ -54,6 +64,65 @@ icon: material/new-box 将覆盖 `dns.client_subnet`. +### evaluate + +!!! question "自 sing-box 1.14.0 起" + +```json +{ + "action": "evaluate", + "server": "", + "disable_cache": false, + "rewrite_ttl": null, + "client_subnet": null +} +``` + +`evaluate` 向指定服务器发送 DNS 查询并保存已评估的响应,供后续规则通过 [`match_response`](/zh/configuration/dns/rule/#match_response) 和响应字段进行匹配。与 `route` 不同,它**不会**终止规则评估。 + +仅允许在顶层 DNS 规则中使用(不可在逻辑子规则内部使用)。 +使用 [`match_response`](/zh/configuration/dns/rule/#match_response) 或响应匹配字段的规则, +需要位于更早的顶层 `evaluate` 规则之后。规则自身的 `evaluate` 动作不能满足这个条件, +因为匹配发生在动作执行之前。 + +#### server + +==必填== + +目标 DNS 服务器的标签。 + +#### disable_cache + +在此查询中禁用缓存。 + +#### rewrite_ttl + +重写 DNS 回应中的 TTL。 + +#### client_subnet + +默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。 + +如果值是 IP 地址而不是前缀,则会自动附加 `/32` 或 `/128`。 + +将覆盖 `dns.client_subnet`. + +### respond + +!!! question "自 sing-box 1.14.0 起" + +```json +{ + "action": "respond" +} +``` + +`respond` 会终止规则评估,并直接返回前序 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作保存的已评估的响应。 + +此动作不会发起新的 DNS 查询,也没有额外选项。 + +只能用于前面已有顶层 `evaluate` 规则的场景。如果运行时命中该动作时没有已评估的响应,则请求会直接返回错误,而不是继续匹配后续规则。 + ### route-options ```json @@ -84,7 +153,7 @@ icon: material/new-box - `default`: 返回 REFUSED。 - `drop`: 丢弃请求。 -默认使用 `defualt`。 +默认使用 `default`。 #### no_drop diff --git a/docs/configuration/dns/server/index.md b/docs/configuration/dns/server/index.md index 4f10948e58..b610cf5b02 100644 --- a/docs/configuration/dns/server/index.md +++ b/docs/configuration/dns/server/index.md @@ -29,7 +29,7 @@ The type of the DNS server. | Type | Format | |-----------------|---------------------------| -| empty (default) | [Legacy](./legacy/) | +| empty (default) | :material-note-remove: [Legacy](./legacy/) | | `local` | [Local](./local/) | | `hosts` | [Hosts](./hosts/) | | `tcp` | [TCP](./tcp/) | diff --git a/docs/configuration/dns/server/index.zh.md b/docs/configuration/dns/server/index.zh.md index d6deef5a33..d1a4dc3c40 100644 --- a/docs/configuration/dns/server/index.zh.md +++ b/docs/configuration/dns/server/index.zh.md @@ -29,7 +29,7 @@ DNS 服务器的类型。 | 类型 | 格式 | |-----------------|---------------------------| -| empty (default) | [Legacy](./legacy/) | +| empty (default) | :material-note-remove: [Legacy](./legacy/) | | `local` | [Local](./local/) | | `hosts` | [Hosts](./hosts/) | | `tcp` | [TCP](./tcp/) | diff --git a/docs/configuration/dns/server/legacy.md b/docs/configuration/dns/server/legacy.md index 387d76ec26..e27b19cbfd 100644 --- a/docs/configuration/dns/server/legacy.md +++ b/docs/configuration/dns/server/legacy.md @@ -1,10 +1,10 @@ --- -icon: material/delete-clock +icon: material/note-remove --- -!!! failure "Deprecated in sing-box 1.12.0" +!!! failure "Removed in sing-box 1.14.0" - Legacy DNS servers is deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-servers). + Legacy DNS servers are deprecated in sing-box 1.12.0 and removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-server-formats). !!! quote "Changes in sing-box 1.9.0" @@ -108,6 +108,6 @@ Append a `edns0-subnet` OPT extra record with the specified IP prefix to every q If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically. -Can be overrides by `rules.[].client_subnet`. +Can be overridden by `rules.[].client_subnet`. -Will overrides `dns.client_subnet`. +Will override `dns.client_subnet`. diff --git a/docs/configuration/dns/server/legacy.zh.md b/docs/configuration/dns/server/legacy.zh.md index 906db47c77..2ad36839f8 100644 --- a/docs/configuration/dns/server/legacy.zh.md +++ b/docs/configuration/dns/server/legacy.zh.md @@ -1,10 +1,10 @@ --- -icon: material/delete-clock +icon: material/note-remove --- -!!! failure "Deprecated in sing-box 1.12.0" +!!! failure "已在 sing-box 1.14.0 移除" - 旧的 DNS 服务器配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。 + 旧的 DNS 服务器配置已在 sing-box 1.12.0 废弃且已在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。 !!! quote "sing-box 1.9.0 中的更改" diff --git a/docs/configuration/experimental/cache-file.md b/docs/configuration/experimental/cache-file.md index 4ad0361c86..f91ee50fde 100644 --- a/docs/configuration/experimental/cache-file.md +++ b/docs/configuration/experimental/cache-file.md @@ -44,7 +44,7 @@ Store fakeip in the cache file Store rejected DNS response cache in the cache file -The check results of [Address filter DNS rule items](/configuration/dns/rule/#address-filter-fields) +The check results of [Legacy Address Filter Fields](/configuration/dns/rule/#legacy-address-filter-fields) will be cached until expiration. #### rdrc_timeout diff --git a/docs/configuration/experimental/cache-file.zh.md b/docs/configuration/experimental/cache-file.zh.md index 309e13a1ea..a998aa7736 100644 --- a/docs/configuration/experimental/cache-file.zh.md +++ b/docs/configuration/experimental/cache-file.zh.md @@ -42,7 +42,7 @@ 将拒绝的 DNS 响应缓存存储在缓存文件中。 -[地址筛选 DNS 规则项](/zh/configuration/dns/rule/#地址筛选字段) 的检查结果将被缓存至过期。 +[旧版地址筛选字段](/zh/configuration/dns/rule/#旧版地址筛选字段) 的检查结果将被缓存至过期。 #### rdrc_timeout diff --git a/docs/configuration/route/index.md b/docs/configuration/route/index.md index 40104b619e..6c59f85079 100644 --- a/docs/configuration/route/index.md +++ b/docs/configuration/route/index.md @@ -153,7 +153,7 @@ Automatically detected from common DHCP servers (dnsmasq, odhcpd, ISC dhcpd, Kea See [Dial Fields](/configuration/shared/dial/#domain_resolver) for details. -Can be overrides by `outbound.domain_resolver`. +Can be overridden by `outbound.domain_resolver`. #### default_network_strategy @@ -163,7 +163,7 @@ See [Dial Fields](/configuration/shared/dial/#network_strategy) for details. Takes no effect if `outbound.bind_interface`, `outbound.inet4_bind_address` or `outbound.inet6_bind_address` is set. -Can be overrides by `outbound.network_strategy`. +Can be overridden by `outbound.network_strategy`. Conflicts with `default_interface`. diff --git a/docs/configuration/route/rule_action.md b/docs/configuration/route/rule_action.md index 523ffec206..4f2a35cbd6 100644 --- a/docs/configuration/route/rule_action.md +++ b/docs/configuration/route/rule_action.md @@ -316,4 +316,4 @@ Append a `edns0-subnet` OPT extra record with the specified IP prefix to every q If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically. -Will overrides `dns.client_subnet`. +Will override `dns.client_subnet`. diff --git a/docs/deprecated.md b/docs/deprecated.md index 3faf986e08..70084b6df9 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -14,14 +14,43 @@ check [Migration](../migration/#migrate-inline-acme-to-certificate-provider). Old fields will be removed in sing-box 1.16.0. +#### Legacy `strategy` DNS rule action option + +Legacy `strategy` DNS rule action option is deprecated, +check [Migration](../migration/#migrate-dns-rule-action-strategy-to-rule-items). + +Old fields will be removed in sing-box 1.16.0. + +#### Legacy `ip_accept_any` DNS rule item + +Legacy `ip_accept_any` DNS rule item is deprecated, +check [Migration](../migration/#migrate-address-filter-fields-to-response-matching). + +Old fields will be removed in sing-box 1.16.0. + +#### Legacy `rule_set_ip_cidr_accept_empty` DNS rule item + +Legacy `rule_set_ip_cidr_accept_empty` DNS rule item is deprecated, +check [Migration](../migration/#migrate-address-filter-fields-to-response-matching). + +Old fields will be removed in sing-box 1.16.0. + +#### Legacy Address Filter Fields in DNS rules + +Legacy Address Filter Fields (`ip_cidr`, `ip_is_private` without `match_response`) +in DNS rules are deprecated, +check [Migration](../migration/#migrate-address-filter-fields-to-response-matching). + +Old behavior will be removed in sing-box 1.16.0. + ## 1.12.0 #### Legacy DNS server formats DNS servers are refactored, -check [Migration](../migration/#migrate-to-new-dns-servers). +check [Migration](../migration/#migrate-to-new-dns-server-formats). -Compatibility for old formats will be removed in sing-box 1.14.0. +Old formats were removed in sing-box 1.14.0. #### `outbound` DNS rule item diff --git a/docs/deprecated.zh.md b/docs/deprecated.zh.md index e710e78ce7..f98b0c010a 100644 --- a/docs/deprecated.zh.md +++ b/docs/deprecated.zh.md @@ -14,6 +14,34 @@ TLS 中的内联 ACME 选项(`tls.acme`)已废弃, 旧字段将在 sing-box 1.16.0 中被移除。 +#### 旧版 DNS 规则动作 `strategy` 选项 + +旧版 DNS 规则动作 `strategy` 选项已废弃, +参阅[迁移指南](/zh/migration/#迁移-dns-规则动作-strategy-到规则项)。 + +旧字段将在 sing-box 1.16.0 中被移除。 + +#### 旧版 `ip_accept_any` DNS 规则项 + +旧版 `ip_accept_any` DNS 规则项已废弃, +参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。 + +旧字段将在 sing-box 1.16.0 中被移除。 + +#### 旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项 + +旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项已废弃, +参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。 + +旧字段将在 sing-box 1.16.0 中被移除。 + +#### 旧版地址筛选字段 (DNS 规则) + +旧版地址筛选字段(不使用 `match_response` 的 `ip_cidr`、`ip_is_private`)已废弃, +参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。 + +旧行为将在 sing-box 1.16.0 中被移除。 + ## 1.12.0 #### 旧的 DNS 服务器格式 @@ -21,7 +49,7 @@ TLS 中的内联 ACME 选项(`tls.acme`)已废弃, DNS 服务器已重构, 参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式). -对旧格式的兼容性将在 sing-box 1.14.0 中被移除。 +旧格式已在 sing-box 1.14.0 中被移除。 #### `outbound` DNS 规则项 diff --git a/docs/migration.md b/docs/migration.md index 810bae190a..91e771babd 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -79,6 +79,111 @@ See [ACME](/configuration/shared/certificate-provider/acme/) for fields newly ad } ``` +### Migrate DNS rule action strategy to rule items + +Legacy `strategy` DNS rule action option is deprecated. + +In sing-box 1.14.0, internal domain resolution (Lookup) now splits A and AAAA queries +at the rule level, so each query type is evaluated independently through the full rule chain. +Use `ip_version` or `query_type` rule items to control which query types a rule matches. + +!!! info "References" + + [DNS Rule](/configuration/dns/rule/) / + [DNS Rule Action](/configuration/dns/rule_action/) + +=== ":material-card-remove: Deprecated" + + ```json + { + "dns": { + "rules": [ + { + "domain_suffix": ".cn", + "action": "route", + "server": "local", + "strategy": "ipv4_only" + } + ] + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "dns": { + "rules": [ + { + "domain_suffix": ".cn", + "ip_version": 4, + "action": "route", + "server": "local" + } + ] + } + } + ``` + +### Migrate address filter fields to response matching + +Legacy Address Filter Fields (`ip_cidr`, `ip_is_private` without `match_response`) in DNS rules are deprecated, +along with Legacy `ip_accept_any` and Legacy `rule_set_ip_cidr_accept_empty` DNS rule items. + +In sing-box 1.14.0, use the [`evaluate`](/configuration/dns/rule_action/#evaluate) action +to fetch a DNS response, then match against it explicitly with `match_response`. + +!!! info "References" + + [DNS Rule](/configuration/dns/rule/) / + [DNS Rule Action](/configuration/dns/rule_action/#evaluate) + +=== ":material-card-remove: Deprecated" + + ```json + { + "dns": { + "rules": [ + { + "rule_set": "geoip-cn", + "action": "route", + "server": "local" + }, + { + "action": "route", + "server": "remote" + } + ] + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "dns": { + "rules": [ + { + "action": "evaluate", + "server": "remote" + }, + { + "match_response": true, + "rule_set": "geoip-cn", + "action": "route", + "server": "local" + }, + { + "action": "route", + "server": "remote" + } + ] + } + } + ``` + ## 1.12.0 ### Migrate to new DNS server formats diff --git a/docs/migration.zh.md b/docs/migration.zh.md index 18e2872613..3f12740553 100644 --- a/docs/migration.zh.md +++ b/docs/migration.zh.md @@ -79,6 +79,111 @@ sing-box 1.14.0 新增字段参阅 [ACME](/zh/configuration/shared/certificate-p } ``` +### 迁移 DNS 规则动作 strategy 到规则项 + +旧版 DNS 规则动作 `strategy` 选项已废弃。 + +在 sing-box 1.14.0 中,内部域名解析(Lookup)现在在规则层拆分 A 和 AAAA 查询, +每种查询类型独立通过完整的规则链评估。 +请使用 `ip_version` 或 `query_type` 规则项来控制规则匹配的查询类型。 + +!!! info "参考" + + [DNS 规则](/zh/configuration/dns/rule/) / + [DNS 规则动作](/zh/configuration/dns/rule_action/) + +=== ":material-card-remove: 弃用的" + + ```json + { + "dns": { + "rules": [ + { + "domain_suffix": ".cn", + "action": "route", + "server": "local", + "strategy": "ipv4_only" + } + ] + } + } + ``` + +=== ":material-card-multiple: 新的" + + ```json + { + "dns": { + "rules": [ + { + "domain_suffix": ".cn", + "ip_version": 4, + "action": "route", + "server": "local" + } + ] + } + } + ``` + +### 迁移地址筛选字段到响应匹配 + +旧版地址筛选字段(不使用 `match_response` 的 `ip_cidr`、`ip_is_private`)已废弃, +旧版 `ip_accept_any` 和旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项也已废弃。 + +在 sing-box 1.14.0 中,请使用 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作 +获取 DNS 响应,然后通过 `match_response` 显式匹配。 + +!!! info "参考" + + [DNS 规则](/zh/configuration/dns/rule/) / + [DNS 规则动作](/zh/configuration/dns/rule_action/#evaluate) + +=== ":material-card-remove: 弃用的" + + ```json + { + "dns": { + "rules": [ + { + "rule_set": "geoip-cn", + "action": "route", + "server": "local" + }, + { + "action": "route", + "server": "remote" + } + ] + } + } + ``` + +=== ":material-card-multiple: 新的" + + ```json + { + "dns": { + "rules": [ + { + "action": "evaluate", + "server": "remote" + }, + { + "match_response": true, + "rule_set": "geoip-cn", + "action": "route", + "server": "local" + }, + { + "action": "route", + "server": "remote" + } + ] + } + } + ``` + ## 1.12.0 ### 迁移到新的 DNS 服务器格式 diff --git a/experimental/deprecated/constants.go b/experimental/deprecated/constants.go index 3526cda831..543a10bb6c 100644 --- a/experimental/deprecated/constants.go +++ b/experimental/deprecated/constants.go @@ -57,24 +57,6 @@ func (n Note) MessageWithLink() string { } } -var OptionLegacyDNSTransport = Note{ - Name: "legacy-dns-transport", - Description: "legacy DNS servers", - DeprecatedVersion: "1.12.0", - ScheduledVersion: "1.14.0", - EnvName: "LEGACY_DNS_SERVERS", - MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-to-new-dns-server-formats", -} - -var OptionLegacyDNSFakeIPOptions = Note{ - Name: "legacy-dns-fakeip-options", - Description: "legacy DNS fakeip options", - DeprecatedVersion: "1.12.0", - ScheduledVersion: "1.14.0", - EnvName: "LEGACY_DNS_FAKEIP_OPTIONS", - MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-to-new-dns-server-formats", -} - var OptionOutboundDNSRuleItem = Note{ Name: "outbound-dns-rule-item", Description: "outbound DNS rule item", @@ -111,11 +93,49 @@ var OptionInlineACME = Note{ MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-inline-acme-to-certificate-provider", } +var OptionIPAcceptAny = Note{ + Name: "dns-rule-ip-accept-any", + Description: "Legacy `ip_accept_any` DNS rule item", + DeprecatedVersion: "1.14.0", + ScheduledVersion: "1.16.0", + EnvName: "DNS_RULE_IP_ACCEPT_ANY", + MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-address-filter-fields-to-response-matching", +} + +var OptionRuleSetIPCIDRAcceptEmpty = Note{ + Name: "dns-rule-rule-set-ip-cidr-accept-empty", + Description: "Legacy `rule_set_ip_cidr_accept_empty` DNS rule item", + DeprecatedVersion: "1.14.0", + ScheduledVersion: "1.16.0", + EnvName: "DNS_RULE_RULE_SET_IP_CIDR_ACCEPT_EMPTY", + MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-address-filter-fields-to-response-matching", +} + +var OptionLegacyDNSAddressFilter = Note{ + Name: "legacy-dns-address-filter", + Description: "Legacy Address Filter Fields in DNS rules", + DeprecatedVersion: "1.14.0", + ScheduledVersion: "1.16.0", + EnvName: "LEGACY_DNS_ADDRESS_FILTER", + MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-address-filter-fields-to-response-matching", +} + +var OptionLegacyDNSRuleStrategy = Note{ + Name: "legacy-dns-rule-strategy", + Description: "Legacy `strategy` DNS rule action option", + DeprecatedVersion: "1.14.0", + ScheduledVersion: "1.16.0", + EnvName: "LEGACY_DNS_RULE_STRATEGY", + MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-dns-rule-action-strategy-to-rule-items", +} + var Options = []Note{ - OptionLegacyDNSTransport, - OptionLegacyDNSFakeIPOptions, OptionOutboundDNSRuleItem, OptionMissingDomainResolver, OptionLegacyDomainStrategyOptions, OptionInlineACME, + OptionIPAcceptAny, + OptionRuleSetIPCIDRAcceptEmpty, + OptionLegacyDNSAddressFilter, + OptionLegacyDNSRuleStrategy, } diff --git a/experimental/libbox/internal/oomprofile/linkname.go b/experimental/libbox/internal/oomprofile/linkname.go index 2a5e10ed10..f7ab271798 100644 --- a/experimental/libbox/internal/oomprofile/linkname.go +++ b/experimental/libbox/internal/oomprofile/linkname.go @@ -6,7 +6,6 @@ import ( "runtime" _ "runtime/pprof" "unsafe" - _ "unsafe" ) diff --git a/experimental/libbox/internal/oomprofile/mapping_darwin.go b/experimental/libbox/internal/oomprofile/mapping_darwin.go index 8d5d854029..e273000569 100644 --- a/experimental/libbox/internal/oomprofile/mapping_darwin.go +++ b/experimental/libbox/internal/oomprofile/mapping_darwin.go @@ -6,7 +6,6 @@ import ( "encoding/binary" "os" "unsafe" - _ "unsafe" ) diff --git a/option/dns.go b/option/dns.go index b5ccf20804..ee29ce096f 100644 --- a/option/dns.go +++ b/option/dns.go @@ -3,19 +3,14 @@ package option import ( "context" "net/netip" - "net/url" C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/experimental/deprecated" - "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json/badjson" "github.com/sagernet/sing/common/json/badoption" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/service" - - "github.com/miekg/dns" ) type RawDNSOptions struct { @@ -26,80 +21,29 @@ type RawDNSOptions struct { DNSClientOptions } -type LegacyDNSOptions struct { - FakeIP *LegacyDNSFakeIPOptions `json:"fakeip,omitempty"` -} - type DNSOptions struct { RawDNSOptions - LegacyDNSOptions } -type contextKeyDontUpgrade struct{} - -func ContextWithDontUpgrade(ctx context.Context) context.Context { - return context.WithValue(ctx, (*contextKeyDontUpgrade)(nil), true) -} +const ( + legacyDNSFakeIPRemovedMessage = "legacy DNS fakeip options are deprecated in sing-box 1.12.0 and removed in sing-box 1.14.0, checkout migration: https://sing-box.sagernet.org/migration/#migrate-to-new-dns-server-formats" + legacyDNSServerRemovedMessage = "legacy DNS server formats are deprecated in sing-box 1.12.0 and removed in sing-box 1.14.0, checkout migration: https://sing-box.sagernet.org/migration/#migrate-to-new-dns-server-formats" +) -func dontUpgradeFromContext(ctx context.Context) bool { - return ctx.Value((*contextKeyDontUpgrade)(nil)) == true +type removedLegacyDNSOptions struct { + FakeIP json.RawMessage `json:"fakeip,omitempty"` } func (o *DNSOptions) UnmarshalJSONContext(ctx context.Context, content []byte) error { - err := json.UnmarshalContext(ctx, content, &o.LegacyDNSOptions) + var legacyOptions removedLegacyDNSOptions + err := json.UnmarshalContext(ctx, content, &legacyOptions) if err != nil { return err } - dontUpgrade := dontUpgradeFromContext(ctx) - legacyOptions := o.LegacyDNSOptions - if !dontUpgrade { - if o.FakeIP != nil && o.FakeIP.Enabled { - deprecated.Report(ctx, deprecated.OptionLegacyDNSFakeIPOptions) - ctx = context.WithValue(ctx, (*LegacyDNSFakeIPOptions)(nil), o.FakeIP) - } - o.LegacyDNSOptions = LegacyDNSOptions{} - } - err = badjson.UnmarshallExcludedContext(ctx, content, legacyOptions, &o.RawDNSOptions) - if err != nil { - return err - } - if !dontUpgrade { - rcodeMap := make(map[string]int) - o.Servers = common.Filter(o.Servers, func(it DNSServerOptions) bool { - if it.Type == C.DNSTypeLegacyRcode { - rcodeMap[it.Tag] = it.Options.(int) - return false - } - return true - }) - if len(rcodeMap) > 0 { - for i := 0; i < len(o.Rules); i++ { - rewriteRcode(rcodeMap, &o.Rules[i]) - } - } - } - return nil -} - -func rewriteRcode(rcodeMap map[string]int, rule *DNSRule) { - switch rule.Type { - case C.RuleTypeDefault: - rewriteRcodeAction(rcodeMap, &rule.DefaultOptions.DNSRuleAction) - case C.RuleTypeLogical: - rewriteRcodeAction(rcodeMap, &rule.LogicalOptions.DNSRuleAction) - } -} - -func rewriteRcodeAction(rcodeMap map[string]int, ruleAction *DNSRuleAction) { - if ruleAction.Action != C.RuleActionTypeRoute { - return - } - rcode, loaded := rcodeMap[ruleAction.RouteOptions.Server] - if !loaded { - return + if len(legacyOptions.FakeIP) != 0 { + return E.New(legacyDNSFakeIPRemovedMessage) } - ruleAction.Action = C.RuleActionTypePredefined - ruleAction.PredefinedOptions.Rcode = common.Ptr(DNSRCode(rcode)) + return badjson.UnmarshallExcludedContext(ctx, content, legacyOptions, &o.RawDNSOptions) } type DNSClientOptions struct { @@ -111,12 +55,6 @@ type DNSClientOptions struct { ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` } -type LegacyDNSFakeIPOptions struct { - Enabled bool `json:"enabled,omitempty"` - Inet4Range *badoption.Prefix `json:"inet4_range,omitempty"` - Inet6Range *badoption.Prefix `json:"inet6_range,omitempty"` -} - type DNSTransportOptionsRegistry interface { CreateOptions(transportType string) (any, bool) } @@ -129,10 +67,6 @@ type _DNSServerOptions struct { type DNSServerOptions _DNSServerOptions func (o *DNSServerOptions) MarshalJSONContext(ctx context.Context) ([]byte, error) { - switch o.Type { - case C.DNSTypeLegacy: - o.Type = "" - } return badjson.MarshallObjectsContext(ctx, (*_DNSServerOptions)(o), o.Options) } @@ -148,9 +82,7 @@ func (o *DNSServerOptions) UnmarshalJSONContext(ctx context.Context, content []b var options any switch o.Type { case "", C.DNSTypeLegacy: - o.Type = C.DNSTypeLegacy - options = new(LegacyDNSServerOptions) - deprecated.Report(ctx, deprecated.OptionLegacyDNSTransport) + return E.New(legacyDNSServerRemovedMessage) default: var loaded bool options, loaded = registry.CreateOptions(o.Type) @@ -163,169 +95,6 @@ func (o *DNSServerOptions) UnmarshalJSONContext(ctx context.Context, content []b return err } o.Options = options - if o.Type == C.DNSTypeLegacy && !dontUpgradeFromContext(ctx) { - err = o.Upgrade(ctx) - if err != nil { - return err - } - } - return nil -} - -func (o *DNSServerOptions) Upgrade(ctx context.Context) error { - if o.Type != C.DNSTypeLegacy { - return nil - } - options := o.Options.(*LegacyDNSServerOptions) - serverURL, _ := url.Parse(options.Address) - var serverType string - if serverURL != nil && serverURL.Scheme != "" { - serverType = serverURL.Scheme - } else { - switch options.Address { - case "local", "fakeip": - serverType = options.Address - default: - serverType = C.DNSTypeUDP - } - } - remoteOptions := RemoteDNSServerOptions{ - RawLocalDNSServerOptions: RawLocalDNSServerOptions{ - DialerOptions: DialerOptions{ - Detour: options.Detour, - DomainResolver: &DomainResolveOptions{ - Server: options.AddressResolver, - Strategy: options.AddressStrategy, - }, - FallbackDelay: options.AddressFallbackDelay, - }, - Legacy: true, - LegacyStrategy: options.Strategy, - LegacyDefaultDialer: options.Detour == "", - LegacyClientSubnet: options.ClientSubnet.Build(netip.Prefix{}), - }, - LegacyAddressResolver: options.AddressResolver, - LegacyAddressStrategy: options.AddressStrategy, - LegacyAddressFallbackDelay: options.AddressFallbackDelay, - } - switch serverType { - case C.DNSTypeLocal: - o.Type = C.DNSTypeLocal - o.Options = &LocalDNSServerOptions{ - RawLocalDNSServerOptions: remoteOptions.RawLocalDNSServerOptions, - } - case C.DNSTypeUDP: - o.Type = C.DNSTypeUDP - o.Options = &remoteOptions - var serverAddr M.Socksaddr - if serverURL == nil || serverURL.Scheme == "" { - serverAddr = M.ParseSocksaddr(options.Address) - } else { - serverAddr = M.ParseSocksaddr(serverURL.Host) - } - if !serverAddr.IsValid() { - return E.New("invalid server address") - } - remoteOptions.Server = serverAddr.AddrString() - if serverAddr.Port != 0 && serverAddr.Port != 53 { - remoteOptions.ServerPort = serverAddr.Port - } - case C.DNSTypeTCP: - o.Type = C.DNSTypeTCP - o.Options = &remoteOptions - if serverURL == nil { - return E.New("invalid server address") - } - serverAddr := M.ParseSocksaddr(serverURL.Host) - if !serverAddr.IsValid() { - return E.New("invalid server address") - } - remoteOptions.Server = serverAddr.AddrString() - if serverAddr.Port != 0 && serverAddr.Port != 53 { - remoteOptions.ServerPort = serverAddr.Port - } - case C.DNSTypeTLS, C.DNSTypeQUIC: - o.Type = serverType - if serverURL == nil { - return E.New("invalid server address") - } - serverAddr := M.ParseSocksaddr(serverURL.Host) - if !serverAddr.IsValid() { - return E.New("invalid server address") - } - remoteOptions.Server = serverAddr.AddrString() - if serverAddr.Port != 0 && serverAddr.Port != 853 { - remoteOptions.ServerPort = serverAddr.Port - } - o.Options = &RemoteTLSDNSServerOptions{ - RemoteDNSServerOptions: remoteOptions, - } - case C.DNSTypeHTTPS, C.DNSTypeHTTP3: - o.Type = serverType - httpsOptions := RemoteHTTPSDNSServerOptions{ - RemoteTLSDNSServerOptions: RemoteTLSDNSServerOptions{ - RemoteDNSServerOptions: remoteOptions, - }, - } - o.Options = &httpsOptions - if serverURL == nil { - return E.New("invalid server address") - } - serverAddr := M.ParseSocksaddr(serverURL.Host) - if !serverAddr.IsValid() { - return E.New("invalid server address") - } - httpsOptions.Server = serverAddr.AddrString() - if serverAddr.Port != 0 && serverAddr.Port != 443 { - httpsOptions.ServerPort = serverAddr.Port - } - if serverURL.Path != "/dns-query" { - httpsOptions.Path = serverURL.Path - } - case "rcode": - var rcode int - if serverURL == nil { - return E.New("invalid server address") - } - switch serverURL.Host { - case "success": - rcode = dns.RcodeSuccess - case "format_error": - rcode = dns.RcodeFormatError - case "server_failure": - rcode = dns.RcodeServerFailure - case "name_error": - rcode = dns.RcodeNameError - case "not_implemented": - rcode = dns.RcodeNotImplemented - case "refused": - rcode = dns.RcodeRefused - default: - return E.New("unknown rcode: ", serverURL.Host) - } - o.Type = C.DNSTypeLegacyRcode - o.Options = rcode - case C.DNSTypeDHCP: - o.Type = C.DNSTypeDHCP - dhcpOptions := DHCPDNSServerOptions{} - if serverURL == nil { - return E.New("invalid server address") - } - if serverURL.Host != "" && serverURL.Host != "auto" { - dhcpOptions.Interface = serverURL.Host - } - o.Options = &dhcpOptions - case C.DNSTypeFakeIP: - o.Type = C.DNSTypeFakeIP - fakeipOptions := FakeIPDNSServerOptions{} - if legacyOptions, loaded := ctx.Value((*LegacyDNSFakeIPOptions)(nil)).(*LegacyDNSFakeIPOptions); loaded { - fakeipOptions.Inet4Range = legacyOptions.Inet4Range - fakeipOptions.Inet6Range = legacyOptions.Inet6Range - } - o.Options = &fakeipOptions - default: - return E.New("unsupported DNS server scheme: ", serverType) - } return nil } @@ -350,16 +119,6 @@ func (o *DNSServerAddressOptions) ReplaceServerOptions(options ServerOptions) { *o = DNSServerAddressOptions(options) } -type LegacyDNSServerOptions struct { - Address string `json:"address"` - AddressResolver string `json:"address_resolver,omitempty"` - AddressStrategy DomainStrategy `json:"address_strategy,omitempty"` - AddressFallbackDelay badoption.Duration `json:"address_fallback_delay,omitempty"` - Strategy DomainStrategy `json:"strategy,omitempty"` - Detour string `json:"detour,omitempty"` - ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` -} - type HostsDNSServerOptions struct { Path badoption.Listable[string] `json:"path,omitempty"` Predefined *badjson.TypedMap[string, badoption.Listable[netip.Addr]] `json:"predefined,omitempty"` @@ -367,10 +126,6 @@ type HostsDNSServerOptions struct { type RawLocalDNSServerOptions struct { DialerOptions - Legacy bool `json:"-"` - LegacyStrategy DomainStrategy `json:"-"` - LegacyDefaultDialer bool `json:"-"` - LegacyClientSubnet netip.Prefix `json:"-"` } type LocalDNSServerOptions struct { @@ -381,9 +136,6 @@ type LocalDNSServerOptions struct { type RemoteDNSServerOptions struct { RawLocalDNSServerOptions DNSServerAddressOptions - LegacyAddressResolver string `json:"-"` - LegacyAddressStrategy DomainStrategy `json:"-"` - LegacyAddressFallbackDelay badoption.Duration `json:"-"` } type RemoteTLSDNSServerOptions struct { diff --git a/option/dns_record.go b/option/dns_record.go index fa72b61b73..f10e03d9b6 100644 --- a/option/dns_record.go +++ b/option/dns_record.go @@ -2,6 +2,7 @@ package option import ( "encoding/base64" + "strings" "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" @@ -11,6 +12,8 @@ import ( "github.com/miekg/dns" ) +const defaultDNSRecordTTL uint32 = 3600 + type DNSRCode int func (r DNSRCode) MarshalJSON() ([]byte, error) { @@ -76,10 +79,13 @@ func (o *DNSRecordOptions) UnmarshalJSON(data []byte) error { if err == nil { return o.unmarshalBase64(binary) } - record, err := dns.NewRR(stringValue) + record, err := parseDNSRecord(stringValue) if err != nil { return err } + if record == nil { + return E.New("empty DNS record") + } if a, isA := record.(*dns.A); isA { a.A = M.AddrFromIP(a.A).Unmap().AsSlice() } @@ -87,6 +93,16 @@ func (o *DNSRecordOptions) UnmarshalJSON(data []byte) error { return nil } +func parseDNSRecord(stringValue string) (dns.RR, error) { + if len(stringValue) > 0 && stringValue[len(stringValue)-1] != '\n' { + stringValue += "\n" + } + parser := dns.NewZoneParser(strings.NewReader(stringValue), "", "") + parser.SetDefaultTTL(defaultDNSRecordTTL) + record, _ := parser.Next() + return record, parser.Err() +} + func (o *DNSRecordOptions) unmarshalBase64(binary []byte) error { record, _, err := dns.UnpackRR(binary, 0) if err != nil { @@ -100,3 +116,10 @@ func (o *DNSRecordOptions) unmarshalBase64(binary []byte) error { func (o DNSRecordOptions) Build() dns.RR { return o.RR } + +func (o DNSRecordOptions) Match(record dns.RR) bool { + if o.RR == nil || record == nil { + return false + } + return dns.IsDuplicate(o.RR, record) +} diff --git a/option/dns_record_test.go b/option/dns_record_test.go new file mode 100644 index 0000000000..759ef5fc5a --- /dev/null +++ b/option/dns_record_test.go @@ -0,0 +1,40 @@ +package option + +import ( + "testing" + + "github.com/miekg/dns" + "github.com/stretchr/testify/require" +) + +func mustRecordOptions(t *testing.T, record string) DNSRecordOptions { + t.Helper() + var value DNSRecordOptions + require.NoError(t, value.UnmarshalJSON([]byte(`"`+record+`"`))) + return value +} + +func TestDNSRecordOptionsUnmarshalJSONRejectsRelativeNames(t *testing.T) { + t.Parallel() + + for _, record := range []string{ + "@ IN A 1.1.1.1", + "www IN CNAME example.com.", + "example.com. IN CNAME @", + "example.com. IN CNAME www", + } { + var value DNSRecordOptions + err := value.UnmarshalJSON([]byte(`"` + record + `"`)) + require.Error(t, err) + } +} + +func TestDNSRecordOptionsMatchIgnoresTTL(t *testing.T) { + t.Parallel() + + expected := mustRecordOptions(t, "example.com. 600 IN A 1.1.1.1") + record, err := dns.NewRR("example.com. 60 IN A 1.1.1.1") + require.NoError(t, err) + + require.True(t, expected.Match(record)) +} diff --git a/option/dns_test.go b/option/dns_test.go new file mode 100644 index 0000000000..4e7bf9a92b --- /dev/null +++ b/option/dns_test.go @@ -0,0 +1,54 @@ +package option + +import ( + "context" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/service" + + "github.com/stretchr/testify/require" +) + +type stubDNSTransportOptionsRegistry struct{} + +func (stubDNSTransportOptionsRegistry) CreateOptions(transportType string) (any, bool) { + switch transportType { + case C.DNSTypeUDP: + return new(RemoteDNSServerOptions), true + case C.DNSTypeFakeIP: + return new(FakeIPDNSServerOptions), true + default: + return nil, false + } +} + +func TestDNSOptionsRejectsLegacyFakeIPOptions(t *testing.T) { + t.Parallel() + + ctx := service.ContextWith[DNSTransportOptionsRegistry](context.Background(), stubDNSTransportOptionsRegistry{}) + var options DNSOptions + err := json.UnmarshalContext(ctx, []byte(`{ + "fakeip": { + "enabled": true, + "inet4_range": "198.18.0.0/15" + } + }`), &options) + require.EqualError(t, err, legacyDNSFakeIPRemovedMessage) +} + +func TestDNSServerOptionsRejectsLegacyFormats(t *testing.T) { + t.Parallel() + + ctx := service.ContextWith[DNSTransportOptionsRegistry](context.Background(), stubDNSTransportOptionsRegistry{}) + testCases := []string{ + `{"address":"1.1.1.1"}`, + `{"type":"legacy","address":"1.1.1.1"}`, + } + for _, content := range testCases { + var options DNSServerOptions + err := json.UnmarshalContext(ctx, []byte(content), &options) + require.EqualError(t, err, legacyDNSServerRemovedMessage) + } +} diff --git a/option/rule.go b/option/rule.go index b792ccf4b2..9fd9437973 100644 --- a/option/rule.go +++ b/option/rule.go @@ -1,6 +1,7 @@ package option import ( + "context" "reflect" C "github.com/sagernet/sing-box/constant" @@ -33,26 +34,24 @@ func (r Rule) MarshalJSON() ([]byte, error) { return badjson.MarshallObjects((_Rule)(r), v) } -func (r *Rule) UnmarshalJSON(bytes []byte) error { - err := json.Unmarshal(bytes, (*_Rule)(r)) +func (r *Rule) UnmarshalJSONContext(ctx context.Context, bytes []byte) error { + err := json.UnmarshalContext(ctx, bytes, (*_Rule)(r)) + if err != nil { + return err + } + payload, err := rulePayloadWithoutType(ctx, bytes) if err != nil { return err } - var v any switch r.Type { case "", C.RuleTypeDefault: r.Type = C.RuleTypeDefault - v = &r.DefaultOptions + return unmarshalDefaultRuleContext(ctx, payload, &r.DefaultOptions) case C.RuleTypeLogical: - v = &r.LogicalOptions + return unmarshalLogicalRuleContext(ctx, payload, &r.LogicalOptions) default: return E.New("unknown rule type: " + r.Type) } - err = badjson.UnmarshallExcluded(bytes, (*_Rule)(r), v) - if err != nil { - return err - } - return nil } func (r Rule) IsValid() bool { @@ -160,6 +159,64 @@ func (r *LogicalRule) UnmarshalJSON(data []byte) error { return badjson.UnmarshallExcluded(data, &r.RawLogicalRule, &r.RuleAction) } +func rulePayloadWithoutType(ctx context.Context, data []byte) ([]byte, error) { + var content badjson.JSONObject + err := content.UnmarshalJSONContext(ctx, data) + if err != nil { + return nil, err + } + content.Remove("type") + return content.MarshalJSONContext(ctx) +} + +func unmarshalDefaultRuleContext(ctx context.Context, data []byte, rule *DefaultRule) error { + rawAction, routeOptions, err := inspectRouteRuleAction(ctx, data) + if err != nil { + return err + } + err = rejectNestedRouteRuleAction(ctx, data) + if err != nil { + return err + } + depth := nestedRuleDepth(ctx) + err = json.UnmarshalContext(ctx, data, &rule.RawDefaultRule) + if err != nil { + return err + } + err = badjson.UnmarshallExcludedContext(ctx, data, &rule.RawDefaultRule, &rule.RuleAction) + if err != nil { + return err + } + if depth > 0 && rawAction == "" && routeOptions == (RouteActionOptions{}) { + rule.RuleAction = RuleAction{} + } + return nil +} + +func unmarshalLogicalRuleContext(ctx context.Context, data []byte, rule *LogicalRule) error { + rawAction, routeOptions, err := inspectRouteRuleAction(ctx, data) + if err != nil { + return err + } + err = rejectNestedRouteRuleAction(ctx, data) + if err != nil { + return err + } + depth := nestedRuleDepth(ctx) + err = json.UnmarshalContext(nestedRuleChildContext(ctx), data, &rule.RawLogicalRule) + if err != nil { + return err + } + err = badjson.UnmarshallExcludedContext(ctx, data, &rule.RawLogicalRule, &rule.RuleAction) + if err != nil { + return err + } + if depth > 0 && rawAction == "" && routeOptions == (RouteActionOptions{}) { + rule.RuleAction = RuleAction{} + } + return nil +} + func (r *LogicalRule) IsValid() bool { return len(r.Rules) > 0 && common.All(r.Rules, Rule.IsValid) } diff --git a/option/rule_action.go b/option/rule_action.go index 4310825520..212396b7b9 100644 --- a/option/rule_action.go +++ b/option/rule_action.go @@ -115,6 +115,10 @@ func (r DNSRuleAction) MarshalJSON() ([]byte, error) { case C.RuleActionTypeRoute: r.Action = "" v = r.RouteOptions + case C.RuleActionTypeEvaluate: + v = r.RouteOptions + case C.RuleActionTypeRespond: + v = nil case C.RuleActionTypeRouteOptions: v = r.RouteOptionsOptions case C.RuleActionTypeReject: @@ -124,6 +128,9 @@ func (r DNSRuleAction) MarshalJSON() ([]byte, error) { default: return nil, E.New("unknown DNS rule action: " + r.Action) } + if v == nil { + return badjson.MarshallObjects((_DNSRuleAction)(r)) + } return badjson.MarshallObjects((_DNSRuleAction)(r), v) } @@ -137,6 +144,10 @@ func (r *DNSRuleAction) UnmarshalJSONContext(ctx context.Context, data []byte) e case "", C.RuleActionTypeRoute: r.Action = C.RuleActionTypeRoute v = &r.RouteOptions + case C.RuleActionTypeEvaluate: + v = &r.RouteOptions + case C.RuleActionTypeRespond: + v = nil case C.RuleActionTypeRouteOptions: v = &r.RouteOptionsOptions case C.RuleActionTypeReject: @@ -146,6 +157,9 @@ func (r *DNSRuleAction) UnmarshalJSONContext(ctx context.Context, data []byte) e default: return E.New("unknown DNS rule action: " + r.Action) } + if v == nil { + return json.UnmarshalDisallowUnknownFields(data, &_DNSRuleAction{}) + } return badjson.UnmarshallExcludedContext(ctx, data, (*_DNSRuleAction)(r), v) } diff --git a/option/rule_action_test.go b/option/rule_action_test.go new file mode 100644 index 0000000000..0007cd36ed --- /dev/null +++ b/option/rule_action_test.go @@ -0,0 +1,29 @@ +package option + +import ( + "context" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common/json" + + "github.com/stretchr/testify/require" +) + +func TestDNSRuleActionRespondUnmarshalJSON(t *testing.T) { + t.Parallel() + + var action DNSRuleAction + err := json.UnmarshalContext(context.Background(), []byte(`{"action":"respond"}`), &action) + require.NoError(t, err) + require.Equal(t, C.RuleActionTypeRespond, action.Action) + require.Equal(t, DNSRouteActionOptions{}, action.RouteOptions) +} + +func TestDNSRuleActionRespondRejectsUnknownFields(t *testing.T) { + t.Parallel() + + var action DNSRuleAction + err := json.UnmarshalContext(context.Background(), []byte(`{"action":"respond","disable_cache":true}`), &action) + require.ErrorContains(t, err, "unknown field") +} diff --git a/option/rule_dns.go b/option/rule_dns.go index 880b96ac54..d1298635b8 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -35,7 +35,7 @@ func (r DNSRule) MarshalJSON() ([]byte, error) { } func (r *DNSRule) UnmarshalJSONContext(ctx context.Context, bytes []byte) error { - err := json.Unmarshal(bytes, (*_DNSRule)(r)) + err := json.UnmarshalContext(ctx, bytes, (*_DNSRule)(r)) if err != nil { return err } @@ -78,12 +78,6 @@ type RawDefaultDNSRule struct { DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"` DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"` DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"` - Geosite badoption.Listable[string] `json:"geosite,omitempty"` - SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"` - GeoIP badoption.Listable[string] `json:"geoip,omitempty"` - IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` - IPIsPrivate bool `json:"ip_is_private,omitempty"` - IPAcceptAny bool `json:"ip_accept_any,omitempty"` SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"` SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"` @@ -110,9 +104,23 @@ type RawDefaultDNSRule struct { SourceHostname badoption.Listable[string] `json:"source_hostname,omitempty"` RuleSet badoption.Listable[string] `json:"rule_set,omitempty"` RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"` - RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"` + MatchResponse bool `json:"match_response,omitempty"` + IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` + IPIsPrivate bool `json:"ip_is_private,omitempty"` + ResponseRcode *DNSRCode `json:"response_rcode,omitempty"` + ResponseAnswer badoption.Listable[DNSRecordOptions] `json:"response_answer,omitempty"` + ResponseNs badoption.Listable[DNSRecordOptions] `json:"response_ns,omitempty"` + ResponseExtra badoption.Listable[DNSRecordOptions] `json:"response_extra,omitempty"` Invert bool `json:"invert,omitempty"` + // Deprecated: removed in sing-box 1.12.0 + Geosite badoption.Listable[string] `json:"geosite,omitempty"` + SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"` + GeoIP badoption.Listable[string] `json:"geoip,omitempty"` + // Deprecated: use match_response with response items + IPAcceptAny bool `json:"ip_accept_any,omitempty"` + // Deprecated: removed in sing-box 1.11.0 + RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"` // Deprecated: renamed to rule_set_ip_cidr_match_source Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` } @@ -127,11 +135,27 @@ func (r DefaultDNSRule) MarshalJSON() ([]byte, error) { } func (r *DefaultDNSRule) UnmarshalJSONContext(ctx context.Context, data []byte) error { - err := json.UnmarshalContext(ctx, data, &r.RawDefaultDNSRule) + rawAction, routeOptions, err := inspectDNSRuleAction(ctx, data) + if err != nil { + return err + } + err = rejectNestedDNSRuleAction(ctx, data) + if err != nil { + return err + } + depth := nestedRuleDepth(ctx) + err = json.UnmarshalContext(ctx, data, &r.RawDefaultDNSRule) + if err != nil { + return err + } + err = badjson.UnmarshallExcludedContext(ctx, data, &r.RawDefaultDNSRule, &r.DNSRuleAction) if err != nil { return err } - return badjson.UnmarshallExcludedContext(ctx, data, &r.RawDefaultDNSRule, &r.DNSRuleAction) + if depth > 0 && rawAction == "" && routeOptions == (DNSRouteActionOptions{}) { + r.DNSRuleAction = DNSRuleAction{} + } + return nil } func (r DefaultDNSRule) IsValid() bool { @@ -156,11 +180,27 @@ func (r LogicalDNSRule) MarshalJSON() ([]byte, error) { } func (r *LogicalDNSRule) UnmarshalJSONContext(ctx context.Context, data []byte) error { - err := json.Unmarshal(data, &r.RawLogicalDNSRule) + rawAction, routeOptions, err := inspectDNSRuleAction(ctx, data) if err != nil { return err } - return badjson.UnmarshallExcludedContext(ctx, data, &r.RawLogicalDNSRule, &r.DNSRuleAction) + err = rejectNestedDNSRuleAction(ctx, data) + if err != nil { + return err + } + depth := nestedRuleDepth(ctx) + err = json.UnmarshalContext(nestedRuleChildContext(ctx), data, &r.RawLogicalDNSRule) + if err != nil { + return err + } + err = badjson.UnmarshallExcludedContext(ctx, data, &r.RawLogicalDNSRule, &r.DNSRuleAction) + if err != nil { + return err + } + if depth > 0 && rawAction == "" && routeOptions == (DNSRouteActionOptions{}) { + r.DNSRuleAction = DNSRuleAction{} + } + return nil } func (r *LogicalDNSRule) IsValid() bool { diff --git a/option/rule_nested.go b/option/rule_nested.go new file mode 100644 index 0000000000..172165729a --- /dev/null +++ b/option/rule_nested.go @@ -0,0 +1,133 @@ +package option + +import ( + "context" + "reflect" + "strings" + + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" +) + +type nestedRuleDepthContextKey struct{} + +const ( + RouteRuleActionNestedUnsupportedMessage = "rule action is not supported in nested rules" + DNSRuleActionNestedUnsupportedMessage = "DNS rule action is not supported in nested rules" +) + +var ( + routeRuleActionKeys = jsonFieldNames(reflect.TypeFor[_RuleAction](), reflect.TypeFor[RouteActionOptions]()) + dnsRuleActionKeys = jsonFieldNames(reflect.TypeFor[_DNSRuleAction](), reflect.TypeFor[DNSRouteActionOptions]()) +) + +func nestedRuleChildContext(ctx context.Context) context.Context { + return context.WithValue(ctx, nestedRuleDepthContextKey{}, nestedRuleDepth(ctx)+1) +} + +func rejectNestedRouteRuleAction(ctx context.Context, content []byte) error { + return rejectNestedRuleAction(ctx, content, routeRuleActionKeys, RouteRuleActionNestedUnsupportedMessage) +} + +func rejectNestedDNSRuleAction(ctx context.Context, content []byte) error { + return rejectNestedRuleAction(ctx, content, dnsRuleActionKeys, DNSRuleActionNestedUnsupportedMessage) +} + +func nestedRuleDepth(ctx context.Context) int { + depth, _ := ctx.Value(nestedRuleDepthContextKey{}).(int) + return depth +} + +func rejectNestedRuleAction(ctx context.Context, content []byte, keys []string, message string) error { + if nestedRuleDepth(ctx) == 0 { + return nil + } + hasActionKey, err := hasAnyJSONKey(ctx, content, keys...) + if err != nil { + return err + } + if hasActionKey { + return E.New(message) + } + return nil +} + +func hasAnyJSONKey(ctx context.Context, content []byte, keys ...string) (bool, error) { + var object badjson.JSONObject + err := object.UnmarshalJSONContext(ctx, content) + if err != nil { + return false, err + } + for _, key := range keys { + if object.ContainsKey(key) { + return true, nil + } + } + return false, nil +} + +func inspectRouteRuleAction(ctx context.Context, content []byte) (string, RouteActionOptions, error) { + var rawAction _RuleAction + err := json.UnmarshalContext(ctx, content, &rawAction) + if err != nil { + return "", RouteActionOptions{}, err + } + var routeOptions RouteActionOptions + err = json.UnmarshalContext(ctx, content, &routeOptions) + if err != nil { + return "", RouteActionOptions{}, err + } + return rawAction.Action, routeOptions, nil +} + +func inspectDNSRuleAction(ctx context.Context, content []byte) (string, DNSRouteActionOptions, error) { + var rawAction _DNSRuleAction + err := json.UnmarshalContext(ctx, content, &rawAction) + if err != nil { + return "", DNSRouteActionOptions{}, err + } + var routeOptions DNSRouteActionOptions + err = json.UnmarshalContext(ctx, content, &routeOptions) + if err != nil { + return "", DNSRouteActionOptions{}, err + } + return rawAction.Action, routeOptions, nil +} + +func jsonFieldNames(types ...reflect.Type) []string { + fieldMap := make(map[string]struct{}) + for _, fieldType := range types { + appendJSONFieldNames(fieldMap, fieldType) + } + fieldNames := make([]string, 0, len(fieldMap)) + for fieldName := range fieldMap { + fieldNames = append(fieldNames, fieldName) + } + return fieldNames +} + +func appendJSONFieldNames(fieldMap map[string]struct{}, fieldType reflect.Type) { + for fieldType.Kind() == reflect.Pointer { + fieldType = fieldType.Elem() + } + if fieldType.Kind() != reflect.Struct { + return + } + for i := range fieldType.NumField() { + field := fieldType.Field(i) + tagValue := field.Tag.Get("json") + tagName, _, _ := strings.Cut(tagValue, ",") + if tagName == "-" { + continue + } + if field.Anonymous && tagName == "" { + appendJSONFieldNames(fieldMap, field.Type) + continue + } + if tagName == "" { + tagName = field.Name + } + fieldMap[tagName] = struct{}{} + } +} diff --git a/option/rule_nested_test.go b/option/rule_nested_test.go new file mode 100644 index 0000000000..3b2ef2e5f0 --- /dev/null +++ b/option/rule_nested_test.go @@ -0,0 +1,68 @@ +package option + +import ( + "context" + "testing" + + "github.com/sagernet/sing/common/json" + + "github.com/stretchr/testify/require" +) + +func TestRuleRejectsNestedDefaultRuleAction(t *testing.T) { + t.Parallel() + + var rule Rule + err := json.UnmarshalContext(context.Background(), []byte(`{ + "type": "logical", + "mode": "and", + "rules": [ + {"domain": "example.com", "outbound": "direct"} + ] + }`), &rule) + require.ErrorContains(t, err, RouteRuleActionNestedUnsupportedMessage) +} + +func TestRuleLeavesUnknownNestedKeysToNormalValidation(t *testing.T) { + t.Parallel() + + var rule Rule + err := json.UnmarshalContext(context.Background(), []byte(`{ + "type": "logical", + "mode": "and", + "rules": [ + {"domain": "example.com", "foo": "bar"} + ] + }`), &rule) + require.ErrorContains(t, err, "unknown field") + require.NotContains(t, err.Error(), RouteRuleActionNestedUnsupportedMessage) +} + +func TestDNSRuleRejectsNestedDefaultRuleAction(t *testing.T) { + t.Parallel() + + var rule DNSRule + err := json.UnmarshalContext(context.Background(), []byte(`{ + "type": "logical", + "mode": "and", + "rules": [ + {"domain": "example.com", "server": "default"} + ] + }`), &rule) + require.ErrorContains(t, err, DNSRuleActionNestedUnsupportedMessage) +} + +func TestDNSRuleLeavesUnknownNestedKeysToNormalValidation(t *testing.T) { + t.Parallel() + + var rule DNSRule + err := json.UnmarshalContext(context.Background(), []byte(`{ + "type": "logical", + "mode": "and", + "rules": [ + {"domain": "example.com", "foo": "bar"} + ] + }`), &rule) + require.ErrorContains(t, err, "unknown field") + require.NotContains(t, err.Error(), DNSRuleActionNestedUnsupportedMessage) +} diff --git a/route/router.go b/route/router.go index 2815d5095b..03546b2a7e 100644 --- a/route/router.go +++ b/route/router.go @@ -70,6 +70,10 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.Route func (r *Router) Initialize(rules []option.Rule, ruleSets []option.RuleSet) error { for i, options := range rules { + err := R.ValidateNoNestedRuleActions(options) + if err != nil { + return E.Cause(err, "parse rule[", i, "]") + } rule, err := R.NewRule(r.ctx, r.logger, options, false) if err != nil { return E.Cause(err, "parse rule[", i, "]") diff --git a/route/rule/rule_abstract.go b/route/rule/rule_abstract.go index 8a95fa6d2a..d7b844adbb 100644 --- a/route/rule/rule_abstract.go +++ b/route/rule/rule_abstract.go @@ -156,7 +156,6 @@ func (r *abstractDefaultRule) matchStatesWithBase(metadata *adapter.InboundConte return r.invertedFailure(inheritedBase) } if r.invert { - // DNS pre-lookup defers destination address-limit checks until the response phase. if metadata.IgnoreDestinationIPCIDRMatch && stateSet == emptyRuleMatchState() && !metadata.DidMatch && len(r.destinationIPCIDRItems) > 0 { return emptyRuleMatchState().withBase(inheritedBase) } diff --git a/route/rule/rule_action.go b/route/rule/rule_action.go index cac814e765..2fe6ba98a4 100644 --- a/route/rule/rule_action.go +++ b/route/rule/rule_action.go @@ -132,6 +132,18 @@ func NewDNSRuleAction(logger logger.ContextLogger, action option.DNSRuleAction) ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptions.ClientSubnet)), }, } + case C.RuleActionTypeEvaluate: + return &RuleActionEvaluate{ + Server: action.RouteOptions.Server, + RuleActionDNSRouteOptions: RuleActionDNSRouteOptions{ + Strategy: C.DomainStrategy(action.RouteOptions.Strategy), + DisableCache: action.RouteOptions.DisableCache, + RewriteTTL: action.RouteOptions.RewriteTTL, + ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptions.ClientSubnet)), + }, + } + case C.RuleActionTypeRespond: + return &RuleActionRespond{} case C.RuleActionTypeRouteOptions: return &RuleActionDNSRouteOptions{ Strategy: C.DomainStrategy(action.RouteOptionsOptions.Strategy), @@ -230,7 +242,7 @@ func (r *RuleActionRouteOptions) Descriptions() []string { descriptions = append(descriptions, F.ToString("network-type=", strings.Join(common.Map(r.NetworkType, C.InterfaceType.String), ","))) } if r.FallbackNetworkType != nil { - descriptions = append(descriptions, F.ToString("fallback-network-type="+strings.Join(common.Map(r.NetworkType, C.InterfaceType.String), ","))) + descriptions = append(descriptions, F.ToString("fallback-network-type=", strings.Join(common.Map(r.FallbackNetworkType, C.InterfaceType.String), ","))) } if r.FallbackDelay > 0 { descriptions = append(descriptions, F.ToString("fallback-delay=", r.FallbackDelay.String())) @@ -266,18 +278,45 @@ func (r *RuleActionDNSRoute) Type() string { } func (r *RuleActionDNSRoute) String() string { + return formatDNSRouteAction("route", r.Server, r.RuleActionDNSRouteOptions) +} + +type RuleActionEvaluate struct { + Server string + RuleActionDNSRouteOptions +} + +func (r *RuleActionEvaluate) Type() string { + return C.RuleActionTypeEvaluate +} + +func (r *RuleActionEvaluate) String() string { + return formatDNSRouteAction("evaluate", r.Server, r.RuleActionDNSRouteOptions) +} + +type RuleActionRespond struct{} + +func (r *RuleActionRespond) Type() string { + return C.RuleActionTypeRespond +} + +func (r *RuleActionRespond) String() string { + return "respond" +} + +func formatDNSRouteAction(action string, server string, options RuleActionDNSRouteOptions) string { var descriptions []string - descriptions = append(descriptions, r.Server) - if r.DisableCache { + descriptions = append(descriptions, server) + if options.DisableCache { descriptions = append(descriptions, "disable-cache") } - if r.RewriteTTL != nil { - descriptions = append(descriptions, F.ToString("rewrite-ttl=", *r.RewriteTTL)) + if options.RewriteTTL != nil { + descriptions = append(descriptions, F.ToString("rewrite-ttl=", *options.RewriteTTL)) } - if r.ClientSubnet.IsValid() { - descriptions = append(descriptions, F.ToString("client-subnet=", r.ClientSubnet)) + if options.ClientSubnet.IsValid() { + descriptions = append(descriptions, F.ToString("client-subnet=", options.ClientSubnet)) } - return F.ToString("route(", strings.Join(descriptions, ","), ")") + return F.ToString(action, "(", strings.Join(descriptions, ","), ")") } type RuleActionDNSRouteOptions struct { diff --git a/route/rule/rule_default.go b/route/rule/rule_default.go index 5ce1f87d4a..d4de6ff7ae 100644 --- a/route/rule/rule_default.go +++ b/route/rule/rule_default.go @@ -326,6 +326,10 @@ func NewLogicalRule(ctx context.Context, logger log.ContextLogger, options optio return nil, E.New("unknown logical mode: ", options.Mode) } for i, subOptions := range options.Rules { + err = validateNoNestedRuleActions(subOptions, true) + if err != nil { + return nil, E.Cause(err, "sub rule[", i, "]") + } subRule, err := NewRule(ctx, logger, subOptions, false) if err != nil { return nil, E.Cause(err, "sub rule[", i, "]") diff --git a/route/rule/rule_dns.go b/route/rule/rule_dns.go index f33d6096ae..20fb195f13 100644 --- a/route/rule/rule_dns.go +++ b/route/rule/rule_dns.go @@ -5,58 +5,84 @@ import ( "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/experimental/deprecated" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/service" + + "github.com/miekg/dns" ) -func NewDNSRule(ctx context.Context, logger log.ContextLogger, options option.DNSRule, checkServer bool) (adapter.DNSRule, error) { +func NewDNSRule(ctx context.Context, logger log.ContextLogger, options option.DNSRule, checkServer bool, legacyDNSMode bool) (adapter.DNSRule, error) { switch options.Type { case "", C.RuleTypeDefault: if !options.DefaultOptions.IsValid() { return nil, E.New("missing conditions") } + if !checkServer && options.DefaultOptions.Action == C.RuleActionTypeEvaluate { + return nil, E.New(options.DefaultOptions.Action, " is only allowed on top-level DNS rules") + } + err := validateDNSRuleAction(options.DefaultOptions.DNSRuleAction) + if err != nil { + return nil, err + } switch options.DefaultOptions.Action { - case "", C.RuleActionTypeRoute: + case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate: if options.DefaultOptions.RouteOptions.Server == "" && checkServer { return nil, E.New("missing server field") } } - return NewDefaultDNSRule(ctx, logger, options.DefaultOptions) + return NewDefaultDNSRule(ctx, logger, options.DefaultOptions, legacyDNSMode) case C.RuleTypeLogical: if !options.LogicalOptions.IsValid() { return nil, E.New("missing conditions") } + if !checkServer && options.LogicalOptions.Action == C.RuleActionTypeEvaluate { + return nil, E.New(options.LogicalOptions.Action, " is only allowed on top-level DNS rules") + } + err := validateDNSRuleAction(options.LogicalOptions.DNSRuleAction) + if err != nil { + return nil, err + } switch options.LogicalOptions.Action { - case "", C.RuleActionTypeRoute: + case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate: if options.LogicalOptions.RouteOptions.Server == "" && checkServer { return nil, E.New("missing server field") } } - return NewLogicalDNSRule(ctx, logger, options.LogicalOptions) + return NewLogicalDNSRule(ctx, logger, options.LogicalOptions, legacyDNSMode) default: return nil, E.New("unknown rule type: ", options.Type) } } +func validateDNSRuleAction(action option.DNSRuleAction) error { + if action.Action == C.RuleActionTypeReject && action.RejectOptions.Method == C.RuleActionRejectMethodReply { + return E.New("reject method `reply` is not supported for DNS rules") + } + return nil +} + var _ adapter.DNSRule = (*DefaultDNSRule)(nil) type DefaultDNSRule struct { abstractDefaultRule + matchResponse bool } func (r *DefaultDNSRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { return r.abstractDefaultRule.matchStates(metadata) } -func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options option.DefaultDNSRule) (*DefaultDNSRule, error) { +func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options option.DefaultDNSRule, legacyDNSMode bool) (*DefaultDNSRule, error) { rule := &DefaultDNSRule{ abstractDefaultRule: abstractDefaultRule{ invert: options.Invert, action: NewDNSRuleAction(logger, options.DNSRuleAction), }, + matchResponse: options.MatchResponse, } if len(options.Inbound) > 0 { item := NewInboundRule(options.Inbound) @@ -116,7 +142,7 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op rule.destinationAddressItems = append(rule.destinationAddressItems, item) rule.allItems = append(rule.allItems, item) } - if len(options.Geosite) > 0 { + if len(options.Geosite) > 0 { //nolint:staticcheck return nil, E.New("geosite database is deprecated in sing-box 1.8.0 and removed in sing-box 1.12.0") } if len(options.SourceGeoIP) > 0 { @@ -151,11 +177,36 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) rule.allItems = append(rule.allItems, item) } - if options.IPAcceptAny { + if options.IPAcceptAny { //nolint:staticcheck + if legacyDNSMode { + deprecated.Report(ctx, deprecated.OptionIPAcceptAny) + } else { + return nil, E.New(deprecated.OptionIPAcceptAny.MessageWithLink()) + } item := NewIPAcceptAnyItem() rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) rule.allItems = append(rule.allItems, item) } + if options.ResponseRcode != nil { + item := NewDNSResponseRCodeItem(int(*options.ResponseRcode)) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.ResponseAnswer) > 0 { + item := NewDNSResponseRecordItem("response_answer", options.ResponseAnswer, dnsResponseAnswers) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.ResponseNs) > 0 { + item := NewDNSResponseRecordItem("response_ns", options.ResponseNs, dnsResponseNS) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.ResponseExtra) > 0 { + item := NewDNSResponseRecordItem("response_extra", options.ResponseExtra, dnsResponseExtra) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } if len(options.SourcePort) > 0 { item := NewPortItem(true, options.SourcePort) rule.sourcePortItems = append(rule.sourcePortItems, item) @@ -275,6 +326,13 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if options.RuleSetIPCIDRAcceptEmpty { //nolint:staticcheck + if legacyDNSMode { + deprecated.Report(ctx, deprecated.OptionRuleSetIPCIDRAcceptEmpty) + } else { + return nil, E.New(deprecated.OptionRuleSetIPCIDRAcceptEmpty.MessageWithLink()) + } + } if len(options.RuleSet) > 0 { //nolint:staticcheck if options.Deprecated_RulesetIPCIDRMatchSource { @@ -284,7 +342,7 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op if options.RuleSetIPCIDRMatchSource { matchSource = true } - item := NewRuleSetItem(router, options.RuleSet, matchSource, options.RuleSetIPCIDRAcceptEmpty) + item := NewRuleSetItem(router, options.RuleSet, matchSource, options.RuleSetIPCIDRAcceptEmpty) //nolint:staticcheck rule.ruleSetItem = item rule.allItems = append(rule.allItems, item) } @@ -309,15 +367,35 @@ func (r *DefaultDNSRule) WithAddressLimit() bool { } func (r *DefaultDNSRule) Match(metadata *adapter.InboundContext) bool { + return !r.matchStatesForMatch(metadata).isEmpty() +} + +func (r *DefaultDNSRule) LegacyPreMatch(metadata *adapter.InboundContext) bool { + if r.matchResponse { + return false + } metadata.IgnoreDestinationIPCIDRMatch = true - defer func() { - metadata.IgnoreDestinationIPCIDRMatch = false - }() - return !r.matchStates(metadata).isEmpty() + defer func() { metadata.IgnoreDestinationIPCIDRMatch = false }() + return !r.abstractDefaultRule.matchStates(metadata).isEmpty() } -func (r *DefaultDNSRule) MatchAddressLimit(metadata *adapter.InboundContext) bool { - return !r.matchStates(metadata).isEmpty() +func (r *DefaultDNSRule) matchStatesForMatch(metadata *adapter.InboundContext) ruleMatchStateSet { + if r.matchResponse { + if metadata.DNSResponse == nil { + return r.abstractDefaultRule.invertedFailure(0) + } + matchMetadata := *metadata + matchMetadata.DestinationAddressMatchFromResponse = true + return r.abstractDefaultRule.matchStates(&matchMetadata) + } + return r.abstractDefaultRule.matchStates(metadata) +} + +func (r *DefaultDNSRule) MatchAddressLimit(metadata *adapter.InboundContext, response *dns.Msg) bool { + matchMetadata := *metadata + matchMetadata.DNSResponse = response + matchMetadata.DestinationAddressMatchFromResponse = true + return !r.abstractDefaultRule.matchStates(&matchMetadata).isEmpty() } var _ adapter.DNSRule = (*LogicalDNSRule)(nil) @@ -330,7 +408,53 @@ func (r *LogicalDNSRule) matchStates(metadata *adapter.InboundContext) ruleMatch return r.abstractLogicalRule.matchStates(metadata) } -func NewLogicalDNSRule(ctx context.Context, logger log.ContextLogger, options option.LogicalDNSRule) (*LogicalDNSRule, error) { +func matchDNSHeadlessRuleStatesForMatch(rule adapter.HeadlessRule, metadata *adapter.InboundContext) ruleMatchStateSet { + switch typedRule := rule.(type) { + case *DefaultDNSRule: + return typedRule.matchStatesForMatch(metadata) + case *LogicalDNSRule: + return typedRule.matchStatesForMatch(metadata) + default: + return matchHeadlessRuleStates(typedRule, metadata) + } +} + +func (r *LogicalDNSRule) matchStatesForMatch(metadata *adapter.InboundContext) ruleMatchStateSet { + var stateSet ruleMatchStateSet + if r.mode == C.LogicalTypeAnd { + stateSet = emptyRuleMatchState() + for _, rule := range r.rules { + nestedMetadata := *metadata + nestedMetadata.ResetRuleCache() + nestedStateSet := matchDNSHeadlessRuleStatesForMatch(rule, &nestedMetadata) + if nestedStateSet.isEmpty() { + if r.invert { + return emptyRuleMatchState() + } + return 0 + } + stateSet = stateSet.combine(nestedStateSet) + } + } else { + for _, rule := range r.rules { + nestedMetadata := *metadata + nestedMetadata.ResetRuleCache() + stateSet = stateSet.merge(matchDNSHeadlessRuleStatesForMatch(rule, &nestedMetadata)) + } + if stateSet.isEmpty() { + if r.invert { + return emptyRuleMatchState() + } + return 0 + } + } + if r.invert { + return 0 + } + return stateSet +} + +func NewLogicalDNSRule(ctx context.Context, logger log.ContextLogger, options option.LogicalDNSRule, legacyDNSMode bool) (*LogicalDNSRule, error) { r := &LogicalDNSRule{ abstractLogicalRule: abstractLogicalRule{ rules: make([]adapter.HeadlessRule, len(options.Rules)), @@ -347,7 +471,11 @@ func NewLogicalDNSRule(ctx context.Context, logger log.ContextLogger, options op return nil, E.New("unknown logical mode: ", options.Mode) } for i, subRule := range options.Rules { - rule, err := NewDNSRule(ctx, logger, subRule, false) + err := validateNoNestedDNSRuleActions(subRule, true) + if err != nil { + return nil, E.Cause(err, "sub rule[", i, "]") + } + rule, err := NewDNSRule(ctx, logger, subRule, false, legacyDNSMode) if err != nil { return nil, E.Cause(err, "sub rule[", i, "]") } @@ -377,13 +505,18 @@ func (r *LogicalDNSRule) WithAddressLimit() bool { } func (r *LogicalDNSRule) Match(metadata *adapter.InboundContext) bool { + return !r.matchStatesForMatch(metadata).isEmpty() +} + +func (r *LogicalDNSRule) LegacyPreMatch(metadata *adapter.InboundContext) bool { metadata.IgnoreDestinationIPCIDRMatch = true - defer func() { - metadata.IgnoreDestinationIPCIDRMatch = false - }() - return !r.matchStates(metadata).isEmpty() + defer func() { metadata.IgnoreDestinationIPCIDRMatch = false }() + return !r.abstractLogicalRule.matchStates(metadata).isEmpty() } -func (r *LogicalDNSRule) MatchAddressLimit(metadata *adapter.InboundContext) bool { - return !r.matchStates(metadata).isEmpty() +func (r *LogicalDNSRule) MatchAddressLimit(metadata *adapter.InboundContext, response *dns.Msg) bool { + matchMetadata := *metadata + matchMetadata.DNSResponse = response + matchMetadata.DestinationAddressMatchFromResponse = true + return !r.abstractLogicalRule.matchStates(&matchMetadata).isEmpty() } diff --git a/route/rule/rule_item_cidr.go b/route/rule/rule_item_cidr.go index c823dcf30a..28f74161f1 100644 --- a/route/rule/rule_item_cidr.go +++ b/route/rule/rule_item_cidr.go @@ -76,11 +76,26 @@ func (r *IPCIDRItem) Match(metadata *adapter.InboundContext) bool { if r.isSource || metadata.IPCIDRMatchSource { return r.ipSet.Contains(metadata.Source.Addr) } + if metadata.DestinationAddressMatchFromResponse { + addresses := metadata.DNSResponseAddressesForMatch() + if len(addresses) == 0 { + // Legacy rule_set_ip_cidr_accept_empty only applies when the DNS response + // does not expose any address answers for matching. + return metadata.IPCIDRAcceptEmpty + } + for _, address := range addresses { + if r.ipSet.Contains(address) { + return true + } + } + return false + } if metadata.Destination.IsIP() { return r.ipSet.Contains(metadata.Destination.Addr) } - if len(metadata.DestinationAddresses) > 0 { - for _, address := range metadata.DestinationAddresses { + addresses := metadata.DestinationAddresses + if len(addresses) > 0 { + for _, address := range addresses { if r.ipSet.Contains(address) { return true } diff --git a/route/rule/rule_item_ip_accept_any.go b/route/rule/rule_item_ip_accept_any.go index 1ca7125735..fceebc1860 100644 --- a/route/rule/rule_item_ip_accept_any.go +++ b/route/rule/rule_item_ip_accept_any.go @@ -13,6 +13,9 @@ func NewIPAcceptAnyItem() *IPAcceptAnyItem { } func (r *IPAcceptAnyItem) Match(metadata *adapter.InboundContext) bool { + if metadata.DestinationAddressMatchFromResponse { + return len(metadata.DNSResponseAddressesForMatch()) > 0 + } return len(metadata.DestinationAddresses) > 0 } diff --git a/route/rule/rule_item_ip_is_private.go b/route/rule/rule_item_ip_is_private.go index e185db1db4..c968877395 100644 --- a/route/rule/rule_item_ip_is_private.go +++ b/route/rule/rule_item_ip_is_private.go @@ -1,8 +1,6 @@ package rule import ( - "net/netip" - "github.com/sagernet/sing-box/adapter" N "github.com/sagernet/sing/common/network" ) @@ -18,21 +16,24 @@ func NewIPIsPrivateItem(isSource bool) *IPIsPrivateItem { } func (r *IPIsPrivateItem) Match(metadata *adapter.InboundContext) bool { - var destination netip.Addr if r.isSource { - destination = metadata.Source.Addr - } else { - destination = metadata.Destination.Addr - } - if destination.IsValid() { - return !N.IsPublicAddr(destination) + return !N.IsPublicAddr(metadata.Source.Addr) } - if !r.isSource { - for _, destinationAddress := range metadata.DestinationAddresses { + if metadata.DestinationAddressMatchFromResponse { + for _, destinationAddress := range metadata.DNSResponseAddressesForMatch() { if !N.IsPublicAddr(destinationAddress) { return true } } + return false + } + if metadata.Destination.Addr.IsValid() { + return !N.IsPublicAddr(metadata.Destination.Addr) + } + for _, destinationAddress := range metadata.DestinationAddresses { + if !N.IsPublicAddr(destinationAddress) { + return true + } } return false } diff --git a/route/rule/rule_item_response_rcode.go b/route/rule/rule_item_response_rcode.go new file mode 100644 index 0000000000..cac75e8034 --- /dev/null +++ b/route/rule/rule_item_response_rcode.go @@ -0,0 +1,26 @@ +package rule + +import ( + "github.com/sagernet/sing-box/adapter" + F "github.com/sagernet/sing/common/format" + + "github.com/miekg/dns" +) + +var _ RuleItem = (*DNSResponseRCodeItem)(nil) + +type DNSResponseRCodeItem struct { + rcode int +} + +func NewDNSResponseRCodeItem(rcode int) *DNSResponseRCodeItem { + return &DNSResponseRCodeItem{rcode: rcode} +} + +func (r *DNSResponseRCodeItem) Match(metadata *adapter.InboundContext) bool { + return metadata.DNSResponse != nil && metadata.DNSResponse.Rcode == r.rcode +} + +func (r *DNSResponseRCodeItem) String() string { + return F.ToString("response_rcode=", dns.RcodeToString[r.rcode]) +} diff --git a/route/rule/rule_item_response_record.go b/route/rule/rule_item_response_record.go new file mode 100644 index 0000000000..3a2c889beb --- /dev/null +++ b/route/rule/rule_item_response_record.go @@ -0,0 +1,63 @@ +package rule + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/option" + + "github.com/miekg/dns" +) + +var _ RuleItem = (*DNSResponseRecordItem)(nil) + +type DNSResponseRecordItem struct { + field string + records []option.DNSRecordOptions + selector func(*dns.Msg) []dns.RR +} + +func NewDNSResponseRecordItem(field string, records []option.DNSRecordOptions, selector func(*dns.Msg) []dns.RR) *DNSResponseRecordItem { + return &DNSResponseRecordItem{ + field: field, + records: records, + selector: selector, + } +} + +func (r *DNSResponseRecordItem) Match(metadata *adapter.InboundContext) bool { + if metadata.DNSResponse == nil { + return false + } + records := r.selector(metadata.DNSResponse) + for _, expected := range r.records { + for _, record := range records { + if expected.Match(record) { + return true + } + } + } + return false +} + +func (r *DNSResponseRecordItem) String() string { + descriptions := make([]string, 0, len(r.records)) + for _, record := range r.records { + if record.RR != nil { + descriptions = append(descriptions, record.RR.String()) + } + } + return r.field + "=[" + strings.Join(descriptions, " ") + "]" +} + +func dnsResponseAnswers(message *dns.Msg) []dns.RR { + return message.Answer +} + +func dnsResponseNS(message *dns.Msg) []dns.RR { + return message.Ns +} + +func dnsResponseExtra(message *dns.Msg) []dns.RR { + return message.Extra +} diff --git a/route/rule/rule_item_rule_set.go b/route/rule/rule_item_rule_set.go index 3467843ba1..0136494353 100644 --- a/route/rule/rule_item_rule_set.go +++ b/route/rule/rule_item_rule_set.go @@ -29,9 +29,11 @@ func NewRuleSetItem(router adapter.Router, tagList []string, ipCIDRMatchSource b } func (r *RuleSetItem) Start() error { + _ = r.Close() for _, tag := range r.tagList { ruleSet, loaded := r.router.RuleSet(tag) if !loaded { + _ = r.Close() return E.New("rule-set not found: ", tag) } ruleSet.IncRef() @@ -40,6 +42,15 @@ func (r *RuleSetItem) Start() error { return nil } +func (r *RuleSetItem) Close() error { + for _, ruleSet := range r.setList { + ruleSet.DecRef() + } + clear(r.setList) + r.setList = nil + return nil +} + func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool { return !r.matchStates(metadata).isEmpty() } diff --git a/route/rule/rule_item_rule_set_test.go b/route/rule/rule_item_rule_set_test.go new file mode 100644 index 0000000000..21d2070d9b --- /dev/null +++ b/route/rule/rule_item_rule_set_test.go @@ -0,0 +1,138 @@ +package rule + +import ( + "context" + "net" + "sync/atomic" + "testing" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-tun" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/x/list" + + "github.com/stretchr/testify/require" + "go4.org/netipx" +) + +type ruleSetItemTestRouter struct { + ruleSets map[string]adapter.RuleSet +} + +func (r *ruleSetItemTestRouter) Start(adapter.StartStage) error { return nil } +func (r *ruleSetItemTestRouter) Close() error { return nil } +func (r *ruleSetItemTestRouter) PreMatch(adapter.InboundContext, tun.DirectRouteContext, time.Duration, bool) (tun.DirectRouteDestination, error) { + return nil, nil +} + +func (r *ruleSetItemTestRouter) RouteConnection(context.Context, net.Conn, adapter.InboundContext) error { + return nil +} + +func (r *ruleSetItemTestRouter) RoutePacketConnection(context.Context, N.PacketConn, adapter.InboundContext) error { + return nil +} + +func (r *ruleSetItemTestRouter) RouteConnectionEx(context.Context, net.Conn, adapter.InboundContext, N.CloseHandlerFunc) { +} + +func (r *ruleSetItemTestRouter) RoutePacketConnectionEx(context.Context, N.PacketConn, adapter.InboundContext, N.CloseHandlerFunc) { +} + +func (r *ruleSetItemTestRouter) RuleSet(tag string) (adapter.RuleSet, bool) { + ruleSet, loaded := r.ruleSets[tag] + return ruleSet, loaded +} +func (r *ruleSetItemTestRouter) Rules() []adapter.Rule { return nil } +func (r *ruleSetItemTestRouter) NeedFindProcess() bool { return false } +func (r *ruleSetItemTestRouter) NeedFindNeighbor() bool { return false } +func (r *ruleSetItemTestRouter) NeighborResolver() adapter.NeighborResolver { return nil } +func (r *ruleSetItemTestRouter) AppendTracker(adapter.ConnectionTracker) {} +func (r *ruleSetItemTestRouter) ResetNetwork() {} + +type countingRuleSet struct { + name string + refs atomic.Int32 +} + +func (s *countingRuleSet) Name() string { return s.name } +func (s *countingRuleSet) StartContext(context.Context, *adapter.HTTPStartContext) error { return nil } +func (s *countingRuleSet) PostStart() error { return nil } +func (s *countingRuleSet) Metadata() adapter.RuleSetMetadata { return adapter.RuleSetMetadata{} } +func (s *countingRuleSet) ExtractIPSet() []*netipx.IPSet { return nil } +func (s *countingRuleSet) IncRef() { s.refs.Add(1) } +func (s *countingRuleSet) DecRef() { + if s.refs.Add(-1) < 0 { + panic("rule-set: negative refs") + } +} +func (s *countingRuleSet) Cleanup() {} +func (s *countingRuleSet) RegisterCallback(adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] { + return nil +} +func (s *countingRuleSet) UnregisterCallback(*list.Element[adapter.RuleSetUpdateCallback]) {} +func (s *countingRuleSet) Close() error { return nil } +func (s *countingRuleSet) Match(*adapter.InboundContext) bool { return true } +func (s *countingRuleSet) String() string { return s.name } +func (s *countingRuleSet) RefCount() int32 { return s.refs.Load() } + +func TestRuleSetItemCloseReleasesRefs(t *testing.T) { + t.Parallel() + + firstSet := &countingRuleSet{name: "first"} + secondSet := &countingRuleSet{name: "second"} + item := NewRuleSetItem(&ruleSetItemTestRouter{ + ruleSets: map[string]adapter.RuleSet{ + "first": firstSet, + "second": secondSet, + }, + }, []string{"first", "second"}, false, false) + + require.NoError(t, item.Start()) + require.EqualValues(t, 1, firstSet.RefCount()) + require.EqualValues(t, 1, secondSet.RefCount()) + + require.NoError(t, item.Close()) + require.Zero(t, firstSet.RefCount()) + require.Zero(t, secondSet.RefCount()) + + require.NoError(t, item.Close()) + require.Zero(t, firstSet.RefCount()) + require.Zero(t, secondSet.RefCount()) +} + +func TestRuleSetItemStartRollbackOnFailure(t *testing.T) { + t.Parallel() + + firstSet := &countingRuleSet{name: "first"} + item := NewRuleSetItem(&ruleSetItemTestRouter{ + ruleSets: map[string]adapter.RuleSet{ + "first": firstSet, + }, + }, []string{"first", "missing"}, false, false) + + err := item.Start() + require.ErrorContains(t, err, "rule-set not found: missing") + require.Zero(t, firstSet.RefCount()) +} + +func TestRuleSetItemRestartKeepsBalancedRefs(t *testing.T) { + t.Parallel() + + firstSet := &countingRuleSet{name: "first"} + item := NewRuleSetItem(&ruleSetItemTestRouter{ + ruleSets: map[string]adapter.RuleSet{ + "first": firstSet, + }, + }, []string{"first"}, false, false) + + require.NoError(t, item.Start()) + require.EqualValues(t, 1, firstSet.RefCount()) + + require.NoError(t, item.Start()) + require.EqualValues(t, 1, firstSet.RefCount()) + + require.NoError(t, item.Close()) + require.Zero(t, firstSet.RefCount()) +} diff --git a/route/rule/rule_nested_action.go b/route/rule/rule_nested_action.go new file mode 100644 index 0000000000..44e58839b5 --- /dev/null +++ b/route/rule/rule_nested_action.go @@ -0,0 +1,71 @@ +package rule + +import ( + "reflect" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +func ValidateNoNestedRuleActions(rule option.Rule) error { + return validateNoNestedRuleActions(rule, false) +} + +func ValidateNoNestedDNSRuleActions(rule option.DNSRule) error { + return validateNoNestedDNSRuleActions(rule, false) +} + +func validateNoNestedRuleActions(rule option.Rule, nested bool) error { + if nested && ruleHasConfiguredAction(rule) { + return E.New(option.RouteRuleActionNestedUnsupportedMessage) + } + if rule.Type != C.RuleTypeLogical { + return nil + } + for i, subRule := range rule.LogicalOptions.Rules { + err := validateNoNestedRuleActions(subRule, true) + if err != nil { + return E.Cause(err, "sub rule[", i, "]") + } + } + return nil +} + +func validateNoNestedDNSRuleActions(rule option.DNSRule, nested bool) error { + if nested && dnsRuleHasConfiguredAction(rule) { + return E.New(option.DNSRuleActionNestedUnsupportedMessage) + } + if rule.Type != C.RuleTypeLogical { + return nil + } + for i, subRule := range rule.LogicalOptions.Rules { + err := validateNoNestedDNSRuleActions(subRule, true) + if err != nil { + return E.Cause(err, "sub rule[", i, "]") + } + } + return nil +} + +func ruleHasConfiguredAction(rule option.Rule) bool { + switch rule.Type { + case "", C.RuleTypeDefault: + return !reflect.DeepEqual(rule.DefaultOptions.RuleAction, option.RuleAction{}) + case C.RuleTypeLogical: + return !reflect.DeepEqual(rule.LogicalOptions.RuleAction, option.RuleAction{}) + default: + return false + } +} + +func dnsRuleHasConfiguredAction(rule option.DNSRule) bool { + switch rule.Type { + case "", C.RuleTypeDefault: + return !reflect.DeepEqual(rule.DefaultOptions.DNSRuleAction, option.DNSRuleAction{}) + case C.RuleTypeLogical: + return !reflect.DeepEqual(rule.LogicalOptions.DNSRuleAction, option.DNSRuleAction{}) + default: + return false + } +} diff --git a/route/rule/rule_nested_action_test.go b/route/rule/rule_nested_action_test.go new file mode 100644 index 0000000000..f895b89282 --- /dev/null +++ b/route/rule/rule_nested_action_test.go @@ -0,0 +1,88 @@ +package rule + +import ( + "context" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + + "github.com/stretchr/testify/require" +) + +func TestNewRuleRejectsNestedRuleAction(t *testing.T) { + t.Parallel() + + _, err := NewRule(context.Background(), log.NewNOPFactory().NewLogger("router"), option.Rule{ + Type: C.RuleTypeLogical, + LogicalOptions: option.LogicalRule{ + RawLogicalRule: option.RawLogicalRule{ + Mode: C.LogicalTypeAnd, + Rules: []option.Rule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.RouteActionOptions{ + Outbound: "direct", + }, + }, + }, + }}, + }, + }, + }, false) + require.ErrorContains(t, err, option.RouteRuleActionNestedUnsupportedMessage) +} + +func TestNewDNSRuleRejectsNestedRuleAction(t *testing.T) { + t.Parallel() + + _, err := NewDNSRule(context.Background(), log.NewNOPFactory().NewLogger("dns"), option.DNSRule{ + Type: C.RuleTypeLogical, + LogicalOptions: option.LogicalDNSRule{ + RawLogicalDNSRule: option.RawLogicalDNSRule{ + Mode: C.LogicalTypeAnd, + Rules: []option.DNSRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{ + Server: "default", + }, + }, + }, + }}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{ + Server: "default", + }, + }, + }, + }, true, false) + require.ErrorContains(t, err, option.DNSRuleActionNestedUnsupportedMessage) +} + +func TestNewDNSRuleRejectsReplyRejectMethod(t *testing.T) { + t.Parallel() + + _, err := NewDNSRule(context.Background(), log.NewNOPFactory().NewLogger("dns"), option.DNSRule{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: []string{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeReject, + RejectOptions: option.RejectActionOptions{ + Method: C.RuleActionRejectMethodReply, + }, + }, + }, + }, false, false) + require.ErrorContains(t, err, "reject method `reply` is not supported for DNS rules") +} diff --git a/route/rule/rule_set.go b/route/rule/rule_set.go index 39068dbf35..d286a7941d 100644 --- a/route/rule/rule_set.go +++ b/route/rule/rule_set.go @@ -9,6 +9,7 @@ import ( "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/service" "go4.org/netipx" ) @@ -69,3 +70,24 @@ func isWIFIHeadlessRule(rule option.DefaultHeadlessRule) bool { func isIPCIDRHeadlessRule(rule option.DefaultHeadlessRule) bool { return len(rule.IPCIDR) > 0 || rule.IPSet != nil } + +func isDNSQueryTypeHeadlessRule(rule option.DefaultHeadlessRule) bool { + return len(rule.QueryType) > 0 +} + +func buildRuleSetMetadata(headlessRules []option.HeadlessRule) adapter.RuleSetMetadata { + return adapter.RuleSetMetadata{ + ContainsProcessRule: HasHeadlessRule(headlessRules, isProcessHeadlessRule), + ContainsWIFIRule: HasHeadlessRule(headlessRules, isWIFIHeadlessRule), + ContainsIPCIDRRule: HasHeadlessRule(headlessRules, isIPCIDRHeadlessRule), + ContainsDNSQueryTypeRule: HasHeadlessRule(headlessRules, isDNSQueryTypeHeadlessRule), + } +} + +func validateRuleSetMetadataUpdate(ctx context.Context, tag string, metadata adapter.RuleSetMetadata) error { + validator := service.FromContext[adapter.DNSRuleSetUpdateValidator](ctx) + if validator == nil { + return nil + } + return validator.ValidateRuleSetMetadataUpdate(tag, metadata) +} diff --git a/route/rule/rule_set_local.go b/route/rule/rule_set_local.go index ed873d7069..5408615fc0 100644 --- a/route/rule/rule_set_local.go +++ b/route/rule/rule_set_local.go @@ -137,10 +137,11 @@ func (s *LocalRuleSet) reloadRules(headlessRules []option.HeadlessRule) error { return E.Cause(err, "parse rule_set.rules.[", i, "]") } } - var metadata adapter.RuleSetMetadata - metadata.ContainsProcessRule = HasHeadlessRule(headlessRules, isProcessHeadlessRule) - metadata.ContainsWIFIRule = HasHeadlessRule(headlessRules, isWIFIHeadlessRule) - metadata.ContainsIPCIDRRule = HasHeadlessRule(headlessRules, isIPCIDRHeadlessRule) + metadata := buildRuleSetMetadata(headlessRules) + err = validateRuleSetMetadataUpdate(s.ctx, s.tag, metadata) + if err != nil { + return err + } s.access.Lock() s.rules = rules s.metadata = metadata diff --git a/route/rule/rule_set_remote.go b/route/rule/rule_set_remote.go index bda6e23f1e..53d353b3c1 100644 --- a/route/rule/rule_set_remote.go +++ b/route/rule/rule_set_remote.go @@ -189,10 +189,13 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error { return E.Cause(err, "parse rule_set.rules.[", i, "]") } } + metadata := buildRuleSetMetadata(plainRuleSet.Rules) + err = validateRuleSetMetadataUpdate(s.ctx, s.options.Tag, metadata) + if err != nil { + return err + } s.access.Lock() - s.metadata.ContainsProcessRule = HasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule) - s.metadata.ContainsWIFIRule = HasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule) - s.metadata.ContainsIPCIDRRule = HasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule) + s.metadata = metadata s.rules = rules callbacks := s.callbacks.Array() s.access.Unlock() diff --git a/route/rule/rule_set_semantics_test.go b/route/rule/rule_set_semantics_test.go index a01defe6e6..2fc559d204 100644 --- a/route/rule/rule_set_semantics_test.go +++ b/route/rule/rule_set_semantics_test.go @@ -2,6 +2,7 @@ package rule import ( "context" + "net" "net/netip" "strings" "testing" @@ -14,6 +15,7 @@ import ( M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" + mDNS "github.com/miekg/dns" "github.com/stretchr/testify/require" ) @@ -581,7 +583,7 @@ func TestDNSRuleSetSemantics(t *testing.T) { addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) }) - require.True(t, rule.MatchAddressLimit(&metadata)) + require.True(t, rule.MatchAddressLimit(&metadata, dnsResponseForTest(netip.MustParseAddr("203.0.113.1")))) }) t.Run("dns keeps ruleset or semantics", func(t *testing.T) { t.Parallel() @@ -596,7 +598,7 @@ func TestDNSRuleSetSemantics(t *testing.T) { addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{emptyStateSet, destinationStateSet}}) addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) }) - require.True(t, rule.MatchAddressLimit(&metadata)) + require.True(t, rule.MatchAddressLimit(&metadata, dnsResponseForTest(netip.MustParseAddr("203.0.113.1")))) }) t.Run("ruleset ip cidr flags stay scoped", func(t *testing.T) { t.Parallel() @@ -610,10 +612,384 @@ func TestDNSRuleSetSemantics(t *testing.T) { ipCidrAcceptEmpty: true, }) }) - require.True(t, rule.MatchAddressLimit(&metadata)) + require.True(t, rule.MatchAddressLimit(&metadata, dnsResponseForTest(netip.MustParseAddr("203.0.113.1")))) + require.False(t, rule.MatchAddressLimit(&metadata, dnsResponseForTest(netip.MustParseAddr("8.8.8.8")))) + require.True(t, rule.MatchAddressLimit(&metadata, dnsResponseForTest())) require.False(t, metadata.IPCIDRMatchSource) require.False(t, metadata.IPCIDRAcceptEmpty) }) + t.Run("pre lookup ruleset only deferred fields fail closed", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("lookup.example") + ruleSet := newLocalRuleSetForTest("dns-prelookup-ipcidr", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + })) + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + // This is accepted without match_response so mixed rule_set deployments keep + // working; the destination-IP-only branch simply cannot match before a DNS + // response is available. + require.False(t, rule.Match(&metadata)) + }) + t.Run("pre lookup ruleset destination cidr does not fall back to other predicates", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("lookup.example") + ruleSet := newLocalRuleSetForTest("dns-prelookup-network-and-ipcidr", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP})) + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + })) + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + require.False(t, rule.Match(&metadata)) + }) + t.Run("pre lookup mixed ruleset still matches non response branch", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest( + "dns-prelookup-mixed", + headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP})) + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }), + headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + }), + ) + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + // Destination-IP predicates inside rule_set fail closed before the DNS response, + // but they must not force validation errors or suppress sibling non-response + // branches. + require.True(t, rule.Match(&metadata)) + }) +} + +func TestDNSMatchResponseRuleSetDestinationCIDRUsesDNSResponse(t *testing.T) { + t.Parallel() + + ruleSet := newLocalRuleSetForTest("dns-response-ipcidr", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + })) + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + rule.matchResponse = true + + matchedMetadata := testMetadata("lookup.example") + matchedMetadata.DNSResponse = dnsResponseForTest(netip.MustParseAddr("203.0.113.1")) + require.True(t, rule.Match(&matchedMetadata)) + require.Empty(t, matchedMetadata.DestinationAddresses) + + unmatchedMetadata := testMetadata("lookup.example") + unmatchedMetadata.DNSResponse = dnsResponseForTest(netip.MustParseAddr("8.8.8.8")) + require.False(t, rule.Match(&unmatchedMetadata)) +} + +func TestDNSMatchResponseMissingResponseUsesBooleanSemantics(t *testing.T) { + t.Parallel() + + t.Run("plain rule remains false", func(t *testing.T) { + t.Parallel() + + rule := dnsRuleForTest(func(rule *abstractDefaultRule) {}) + rule.matchResponse = true + + metadata := testMetadata("lookup.example") + require.False(t, rule.Match(&metadata)) + }) + + t.Run("invert rule becomes true", func(t *testing.T) { + t.Parallel() + + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + rule.invert = true + }) + rule.matchResponse = true + + metadata := testMetadata("lookup.example") + require.True(t, rule.Match(&metadata)) + }) + + t.Run("logical wrapper respects inverted child", func(t *testing.T) { + t.Parallel() + + nestedRule := dnsRuleForTest(func(rule *abstractDefaultRule) { + rule.invert = true + }) + nestedRule.matchResponse = true + + logicalRule := &LogicalDNSRule{ + abstractLogicalRule: abstractLogicalRule{ + rules: []adapter.HeadlessRule{nestedRule}, + mode: C.LogicalTypeAnd, + }, + } + + metadata := testMetadata("lookup.example") + require.True(t, logicalRule.Match(&metadata)) + }) +} + +func TestDNSAddressLimitIgnoresDestinationAddresses(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + build func(*testing.T, *abstractDefaultRule) + matchedResponse *mDNS.Msg + unmatchedResponse *mDNS.Msg + }{ + { + name: "ip_cidr", + build: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }, + matchedResponse: dnsResponseForTest(netip.MustParseAddr("203.0.113.1")), + unmatchedResponse: dnsResponseForTest(netip.MustParseAddr("8.8.8.8")), + }, + { + name: "ip_is_private", + build: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationIPIsPrivateItem(rule) + }, + matchedResponse: dnsResponseForTest(netip.MustParseAddr("10.0.0.1")), + unmatchedResponse: dnsResponseForTest(netip.MustParseAddr("8.8.8.8")), + }, + { + name: "ip_accept_any", + build: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationIPAcceptAnyItem(rule) + }, + matchedResponse: dnsResponseForTest(netip.MustParseAddr("203.0.113.1")), + unmatchedResponse: dnsResponseForTest(), + }, + } + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + testCase.build(t, rule) + }) + + mismatchMetadata := testMetadata("lookup.example") + mismatchMetadata.DestinationAddresses = []netip.Addr{netip.MustParseAddr("203.0.113.1")} + require.False(t, rule.MatchAddressLimit(&mismatchMetadata, testCase.unmatchedResponse)) + + matchMetadata := testMetadata("lookup.example") + matchMetadata.DestinationAddresses = []netip.Addr{netip.MustParseAddr("8.8.8.8")} + require.True(t, rule.MatchAddressLimit(&matchMetadata, testCase.matchedResponse)) + }) + } +} + +func TestDNSLegacyAddressLimitPreLookupDefersDirectRules(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + build func(*testing.T, *abstractDefaultRule) + matchedResponse *mDNS.Msg + unmatchedResponse *mDNS.Msg + }{ + { + name: "ip_cidr", + build: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }, + matchedResponse: dnsResponseForTest(netip.MustParseAddr("203.0.113.1")), + unmatchedResponse: dnsResponseForTest(netip.MustParseAddr("8.8.8.8")), + }, + { + name: "ip_is_private", + build: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationIPIsPrivateItem(rule) + }, + matchedResponse: dnsResponseForTest(netip.MustParseAddr("10.0.0.1")), + unmatchedResponse: dnsResponseForTest(netip.MustParseAddr("8.8.8.8")), + }, + { + name: "ip_accept_any", + build: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationIPAcceptAnyItem(rule) + }, + matchedResponse: dnsResponseForTest(netip.MustParseAddr("203.0.113.1")), + unmatchedResponse: dnsResponseForTest(), + }, + } + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + testCase.build(t, rule) + }) + + preLookupMetadata := testMetadata("lookup.example") + require.True(t, rule.LegacyPreMatch(&preLookupMetadata)) + + matchedMetadata := testMetadata("lookup.example") + require.True(t, rule.MatchAddressLimit(&matchedMetadata, testCase.matchedResponse)) + + unmatchedMetadata := testMetadata("lookup.example") + require.False(t, rule.MatchAddressLimit(&unmatchedMetadata, testCase.unmatchedResponse)) + }) + } +} + +func TestDNSLegacyAddressLimitPreLookupDefersRuleSetDestinationCIDR(t *testing.T) { + t.Parallel() + + ruleSet := newLocalRuleSetForTest("dns-legacy-ipcidr", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + })) + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + + preLookupMetadata := testMetadata("lookup.example") + require.True(t, rule.LegacyPreMatch(&preLookupMetadata)) + + matchedMetadata := testMetadata("lookup.example") + require.True(t, rule.MatchAddressLimit(&matchedMetadata, dnsResponseForTest(netip.MustParseAddr("203.0.113.1")))) + + unmatchedMetadata := testMetadata("lookup.example") + require.False(t, rule.MatchAddressLimit(&unmatchedMetadata, dnsResponseForTest(netip.MustParseAddr("8.8.8.8")))) +} + +func TestDNSLegacyLogicalAddressLimitPreLookupDefersNestedRules(t *testing.T) { + t.Parallel() + + nestedRule := dnsRuleForTest(func(rule *abstractDefaultRule) { + addDestinationIPIsPrivateItem(rule) + }) + logicalRule := &LogicalDNSRule{ + abstractLogicalRule: abstractLogicalRule{ + rules: []adapter.HeadlessRule{nestedRule}, + mode: C.LogicalTypeAnd, + }, + } + + preLookupMetadata := testMetadata("lookup.example") + require.True(t, logicalRule.LegacyPreMatch(&preLookupMetadata)) + + matchedMetadata := testMetadata("lookup.example") + require.True(t, logicalRule.MatchAddressLimit(&matchedMetadata, dnsResponseForTest(netip.MustParseAddr("10.0.0.1")))) + + unmatchedMetadata := testMetadata("lookup.example") + require.False(t, logicalRule.MatchAddressLimit(&unmatchedMetadata, dnsResponseForTest(netip.MustParseAddr("8.8.8.8")))) +} + +func TestDNSLegacyInvertAddressLimitPreLookupRegression(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + build func(*testing.T, *abstractDefaultRule) + matchedAddrs []netip.Addr + unmatchedAddrs []netip.Addr + }{ + { + name: "ip_cidr", + build: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }, + matchedAddrs: []netip.Addr{netip.MustParseAddr("203.0.113.1")}, + unmatchedAddrs: []netip.Addr{netip.MustParseAddr("8.8.8.8")}, + }, + { + name: "ip_is_private", + build: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationIPIsPrivateItem(rule) + }, + matchedAddrs: []netip.Addr{netip.MustParseAddr("10.0.0.1")}, + unmatchedAddrs: []netip.Addr{netip.MustParseAddr("8.8.8.8")}, + }, + { + name: "ip_accept_any", + build: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationIPAcceptAnyItem(rule) + }, + matchedAddrs: []netip.Addr{netip.MustParseAddr("203.0.113.1")}, + }, + } + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + rule.invert = true + testCase.build(t, rule) + }) + + preLookupMetadata := testMetadata("lookup.example") + require.True(t, rule.LegacyPreMatch(&preLookupMetadata)) + + matchedMetadata := testMetadata("lookup.example") + require.False(t, rule.MatchAddressLimit(&matchedMetadata, dnsResponseForTest(testCase.matchedAddrs...))) + + unmatchedMetadata := testMetadata("lookup.example") + require.True(t, rule.MatchAddressLimit(&unmatchedMetadata, dnsResponseForTest(testCase.unmatchedAddrs...))) + }) + } +} + +func TestDNSLegacyInvertLogicalAddressLimitPreLookupRegression(t *testing.T) { + t.Parallel() + + t.Run("inverted deferred child does not suppress branch", func(t *testing.T) { + t.Parallel() + + logicalRule := &LogicalDNSRule{ + abstractLogicalRule: abstractLogicalRule{ + rules: []adapter.HeadlessRule{ + dnsRuleForTest(func(rule *abstractDefaultRule) { + rule.invert = true + addDestinationIPIsPrivateItem(rule) + }), + }, + mode: C.LogicalTypeAnd, + }, + } + + preLookupMetadata := testMetadata("lookup.example") + require.True(t, logicalRule.LegacyPreMatch(&preLookupMetadata)) + }) +} + +func TestDNSLegacyInvertRuleSetAddressLimitPreLookupRegression(t *testing.T) { + t.Parallel() + + ruleSet := newLocalRuleSetForTest("dns-legacy-invert-ipcidr", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + rule.invert = true + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + })) + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + + preLookupMetadata := testMetadata("lookup.example") + require.True(t, rule.LegacyPreMatch(&preLookupMetadata)) + + matchedMetadata := testMetadata("lookup.example") + require.False(t, rule.MatchAddressLimit(&matchedMetadata, dnsResponseForTest(netip.MustParseAddr("203.0.113.1")))) + + unmatchedMetadata := testMetadata("lookup.example") + require.True(t, rule.MatchAddressLimit(&unmatchedMetadata, dnsResponseForTest(netip.MustParseAddr("8.8.8.8")))) } func TestDNSInvertAddressLimitPreLookupRegression(t *testing.T) { @@ -665,14 +1041,14 @@ func TestDNSInvertAddressLimitPreLookupRegression(t *testing.T) { matchedMetadata := testMetadata("lookup.example") matchedMetadata.DestinationAddresses = testCase.matchedAddrs - require.False(t, rule.MatchAddressLimit(&matchedMetadata)) + require.False(t, rule.MatchAddressLimit(&matchedMetadata, dnsResponseForTest(testCase.matchedAddrs...))) unmatchedMetadata := testMetadata("lookup.example") unmatchedMetadata.DestinationAddresses = testCase.unmatchedAddrs - require.True(t, rule.MatchAddressLimit(&unmatchedMetadata)) + require.True(t, rule.MatchAddressLimit(&unmatchedMetadata, dnsResponseForTest(testCase.unmatchedAddrs...))) }) } - t.Run("mixed resolved and deferred fields keep old pre lookup false", func(t *testing.T) { + t.Run("mixed resolved and deferred fields invert matches pre lookup", func(t *testing.T) { t.Parallel() metadata := testMetadata("lookup.example") rule := dnsRuleForTest(func(rule *abstractDefaultRule) { @@ -680,9 +1056,9 @@ func TestDNSInvertAddressLimitPreLookupRegression(t *testing.T) { addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP})) addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) }) - require.False(t, rule.Match(&metadata)) + require.True(t, rule.Match(&metadata)) }) - t.Run("ruleset only deferred fields keep old pre lookup false", func(t *testing.T) { + t.Run("ruleset only deferred fields invert matches pre lookup", func(t *testing.T) { t.Parallel() metadata := testMetadata("lookup.example") ruleSet := newLocalRuleSetForTest("dns-ruleset-ipcidr", headlessDefaultRule(t, func(rule *abstractDefaultRule) { @@ -692,7 +1068,7 @@ func TestDNSInvertAddressLimitPreLookupRegression(t *testing.T) { rule.invert = true addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) }) - require.False(t, rule.Match(&metadata)) + require.True(t, rule.Match(&metadata)) }) } @@ -763,6 +1139,39 @@ func testMetadata(domain string) adapter.InboundContext { } } +func dnsResponseForTest(addresses ...netip.Addr) *mDNS.Msg { + response := &mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + Response: true, + Rcode: mDNS.RcodeSuccess, + }, + } + for _, address := range addresses { + if address.Is4() { + response.Answer = append(response.Answer, &mDNS.A{ + Hdr: mDNS.RR_Header{ + Name: mDNS.Fqdn("lookup.example"), + Rrtype: mDNS.TypeA, + Class: mDNS.ClassINET, + Ttl: 60, + }, + A: net.IP(append([]byte(nil), address.AsSlice()...)), + }) + } else { + response.Answer = append(response.Answer, &mDNS.AAAA{ + Hdr: mDNS.RR_Header{ + Name: mDNS.Fqdn("lookup.example"), + Rrtype: mDNS.TypeAAAA, + Class: mDNS.ClassINET, + Ttl: 60, + }, + AAAA: net.IP(append([]byte(nil), address.AsSlice()...)), + }) + } + } + return response +} + func addRuleSetItem(rule *abstractDefaultRule, item *RuleSetItem) { rule.ruleSetItem = item rule.allItems = append(rule.allItems, item) diff --git a/route/rule/rule_set_update_validation_test.go b/route/rule/rule_set_update_validation_test.go new file mode 100644 index 0000000000..0583d7bb62 --- /dev/null +++ b/route/rule/rule_set_update_validation_test.go @@ -0,0 +1,111 @@ +package rule + +import ( + "context" + "sync/atomic" + "testing" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json/badoption" + "github.com/sagernet/sing/common/x/list" + "github.com/sagernet/sing/service" + + "github.com/stretchr/testify/require" +) + +type fakeDNSRuleSetUpdateValidator struct { + validate func(tag string, metadata adapter.RuleSetMetadata) error +} + +func (v *fakeDNSRuleSetUpdateValidator) ValidateRuleSetMetadataUpdate(tag string, metadata adapter.RuleSetMetadata) error { + if v.validate == nil { + return nil + } + return v.validate(tag, metadata) +} + +func TestLocalRuleSetReloadRulesRejectsInvalidUpdateBeforeCommit(t *testing.T) { + t.Parallel() + + var callbackCount atomic.Int32 + ctx := service.ContextWith[adapter.DNSRuleSetUpdateValidator](context.Background(), &fakeDNSRuleSetUpdateValidator{ + validate: func(tag string, metadata adapter.RuleSetMetadata) error { + require.Equal(t, "dynamic-set", tag) + if metadata.ContainsDNSQueryTypeRule { + return E.New("dns conflict") + } + return nil + }, + }) + ruleSet := &LocalRuleSet{ + ctx: ctx, + tag: "dynamic-set", + fileFormat: C.RuleSetFormatSource, + } + _ = ruleSet.callbacks.PushBack(func(adapter.RuleSet) { + callbackCount.Add(1) + }) + + err := ruleSet.reloadRules([]option.HeadlessRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultHeadlessRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + }}) + require.NoError(t, err) + require.Equal(t, int32(1), callbackCount.Load()) + require.False(t, ruleSet.metadata.ContainsDNSQueryTypeRule) + require.True(t, ruleSet.Match(&adapter.InboundContext{Domain: "example.com"})) + + err = ruleSet.reloadRules([]option.HeadlessRule{{ + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultHeadlessRule{ + QueryType: badoption.Listable[option.DNSQueryType]{option.DNSQueryType(1)}, + }, + }}) + require.ErrorContains(t, err, "dns conflict") + require.Equal(t, int32(1), callbackCount.Load()) + require.False(t, ruleSet.metadata.ContainsDNSQueryTypeRule) + require.True(t, ruleSet.Match(&adapter.InboundContext{Domain: "example.com"})) +} + +func TestRemoteRuleSetLoadBytesRejectsInvalidUpdateBeforeCommit(t *testing.T) { + t.Parallel() + + var callbackCount atomic.Int32 + ctx := service.ContextWith[adapter.DNSRuleSetUpdateValidator](context.Background(), &fakeDNSRuleSetUpdateValidator{ + validate: func(tag string, metadata adapter.RuleSetMetadata) error { + require.Equal(t, "dynamic-set", tag) + if metadata.ContainsDNSQueryTypeRule { + return E.New("dns conflict") + } + return nil + }, + }) + ruleSet := &RemoteRuleSet{ + ctx: ctx, + options: option.RuleSet{ + Tag: "dynamic-set", + Format: C.RuleSetFormatSource, + }, + callbacks: list.List[adapter.RuleSetUpdateCallback]{}, + } + _ = ruleSet.callbacks.PushBack(func(adapter.RuleSet) { + callbackCount.Add(1) + }) + + err := ruleSet.loadBytes([]byte(`{"version":4,"rules":[{"domain":["example.com"]}]}`)) + require.NoError(t, err) + require.Equal(t, int32(1), callbackCount.Load()) + require.False(t, ruleSet.metadata.ContainsDNSQueryTypeRule) + require.True(t, ruleSet.Match(&adapter.InboundContext{Domain: "example.com"})) + + err = ruleSet.loadBytes([]byte(`{"version":4,"rules":[{"query_type":["A"]}]}`)) + require.ErrorContains(t, err, "dns conflict") + require.Equal(t, int32(1), callbackCount.Load()) + require.False(t, ruleSet.metadata.ContainsDNSQueryTypeRule) + require.True(t, ruleSet.Match(&adapter.InboundContext{Domain: "example.com"})) +} From 9e8f13c0ac15be9576fbd6f6d7bb75fa276c9e31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 7 Apr 2026 20:52:06 +0800 Subject: [PATCH 10/93] platform: Fix set local --- experimental/libbox/setup.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/experimental/libbox/setup.go b/experimental/libbox/setup.go index 5b4b375d88..01a4540442 100644 --- a/experimental/libbox/setup.go +++ b/experimental/libbox/setup.go @@ -6,6 +6,7 @@ import ( "path/filepath" "runtime" "runtime/debug" + "strings" "time" C "github.com/sagernet/sing-box/constant" @@ -13,6 +14,7 @@ import ( "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/service/oomkiller" "github.com/sagernet/sing/common/byteformats" + E "github.com/sagernet/sing/common/exceptions" ) var ( @@ -97,8 +99,14 @@ func Setup(options *SetupOptions) error { return redirectStderr(filepath.Join(sWorkingPath, "CrashReport-"+sCrashReportSource+".log")) } -func SetLocale(localeId string) { - locale.Set(localeId) +func SetLocale(localeId string) error { + if strings.Contains(localeId, "@") { + localeId = strings.Split(localeId, "@")[0] + } + if !locale.Set(localeId) { + return E.New("unsupported locale: ", localeId) + } + return nil } func Version() string { From 0f6d110dad1ba3b9e1a731696ff4a44576e33bbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 7 Apr 2026 20:53:53 +0800 Subject: [PATCH 11/93] Fix deprecated warning double-formatting on localized clients --- daemon/started_service.go | 9 ++++-- daemon/started_service.pb.go | 43 ++++++++++++++++++++++----- daemon/started_service.proto | 3 ++ experimental/libbox/command_client.go | 6 ++-- 4 files changed, 48 insertions(+), 13 deletions(-) diff --git a/daemon/started_service.go b/daemon/started_service.go index 9622d88b40..bdae10f7d5 100644 --- a/daemon/started_service.go +++ b/daemon/started_service.go @@ -1063,9 +1063,12 @@ func (s *StartedService) GetDeprecatedWarnings(ctx context.Context, empty *empty return &DeprecatedWarnings{ Warnings: common.Map(notes, func(it deprecated.Note) *DeprecatedWarning { return &DeprecatedWarning{ - Message: it.Message(), - Impending: it.Impending(), - MigrationLink: it.MigrationLink, + Message: it.Message(), + Impending: it.Impending(), + MigrationLink: it.MigrationLink, + Description: it.Description, + DeprecatedVersion: it.DeprecatedVersion, + ScheduledVersion: it.ScheduledVersion, } }), }, nil diff --git a/daemon/started_service.pb.go b/daemon/started_service.pb.go index 403ba66050..271f80a114 100644 --- a/daemon/started_service.pb.go +++ b/daemon/started_service.pb.go @@ -1709,12 +1709,15 @@ func (x *DeprecatedWarnings) GetWarnings() []*DeprecatedWarning { } type DeprecatedWarning struct { - state protoimpl.MessageState `protogen:"open.v1"` - Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` - Impending bool `protobuf:"varint,2,opt,name=impending,proto3" json:"impending,omitempty"` - MigrationLink string `protobuf:"bytes,3,opt,name=migrationLink,proto3" json:"migrationLink,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + Impending bool `protobuf:"varint,2,opt,name=impending,proto3" json:"impending,omitempty"` + MigrationLink string `protobuf:"bytes,3,opt,name=migrationLink,proto3" json:"migrationLink,omitempty"` + Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` + DeprecatedVersion string `protobuf:"bytes,5,opt,name=deprecatedVersion,proto3" json:"deprecatedVersion,omitempty"` + ScheduledVersion string `protobuf:"bytes,6,opt,name=scheduledVersion,proto3" json:"scheduledVersion,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *DeprecatedWarning) Reset() { @@ -1768,6 +1771,27 @@ func (x *DeprecatedWarning) GetMigrationLink() string { return "" } +func (x *DeprecatedWarning) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *DeprecatedWarning) GetDeprecatedVersion() string { + if x != nil { + return x.DeprecatedVersion + } + return "" +} + +func (x *DeprecatedWarning) GetScheduledVersion() string { + if x != nil { + return x.ScheduledVersion + } + return "" +} + type StartedAt struct { state protoimpl.MessageState `protogen:"open.v1"` StartedAt int64 `protobuf:"varint,1,opt,name=startedAt,proto3" json:"startedAt,omitempty"` @@ -1990,11 +2014,14 @@ const file_daemon_started_service_proto_rawDesc = "" + "\x16CloseConnectionRequest\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\"K\n" + "\x12DeprecatedWarnings\x125\n" + - "\bwarnings\x18\x01 \x03(\v2\x19.daemon.DeprecatedWarningR\bwarnings\"q\n" + + "\bwarnings\x18\x01 \x03(\v2\x19.daemon.DeprecatedWarningR\bwarnings\"\xed\x01\n" + "\x11DeprecatedWarning\x12\x18\n" + "\amessage\x18\x01 \x01(\tR\amessage\x12\x1c\n" + "\timpending\x18\x02 \x01(\bR\timpending\x12$\n" + - "\rmigrationLink\x18\x03 \x01(\tR\rmigrationLink\")\n" + + "\rmigrationLink\x18\x03 \x01(\tR\rmigrationLink\x12 \n" + + "\vdescription\x18\x04 \x01(\tR\vdescription\x12,\n" + + "\x11deprecatedVersion\x18\x05 \x01(\tR\x11deprecatedVersion\x12*\n" + + "\x10scheduledVersion\x18\x06 \x01(\tR\x10scheduledVersion\")\n" + "\tStartedAt\x12\x1c\n" + "\tstartedAt\x18\x01 \x01(\x03R\tstartedAt*U\n" + "\bLogLevel\x12\t\n" + diff --git a/daemon/started_service.proto b/daemon/started_service.proto index 3434c3f19d..27f8667fbf 100644 --- a/daemon/started_service.proto +++ b/daemon/started_service.proto @@ -221,6 +221,9 @@ message DeprecatedWarning { string message = 1; bool impending = 2; string migrationLink = 3; + string description = 4; + string deprecatedVersion = 5; + string scheduledVersion = 6; } message StartedAt { diff --git a/experimental/libbox/command_client.go b/experimental/libbox/command_client.go index 114198a146..a915e64fa0 100644 --- a/experimental/libbox/command_client.go +++ b/experimental/libbox/command_client.go @@ -574,8 +574,10 @@ func (c *CommandClient) GetDeprecatedNotes() (DeprecatedNoteIterator, error) { var notes []*DeprecatedNote for _, warning := range warnings.Warnings { notes = append(notes, &DeprecatedNote{ - Description: warning.Message, - MigrationLink: warning.MigrationLink, + Description: warning.Description, + DeprecatedVersion: warning.DeprecatedVersion, + ScheduledVersion: warning.ScheduledVersion, + MigrationLink: warning.MigrationLink, }) } return newIterator(notes), nil From 5ee373c549483cd5767f2f95e0b8928cd3fd4132 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 7 Apr 2026 21:19:40 +0800 Subject: [PATCH 12/93] oom-killer: Free memory on pressure notification and use gradual interval backoff --- service/oomkiller/service_darwin.go | 2 ++ service/oomkiller/timer.go | 23 ++++++++++++++++------- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/service/oomkiller/service_darwin.go b/service/oomkiller/service_darwin.go index 7c957dcefb..1d51c1b480 100644 --- a/service/oomkiller/service_darwin.go +++ b/service/oomkiller/service_darwin.go @@ -33,6 +33,7 @@ static void stopMemoryPressureMonitor() { import "C" import ( + runtimeDebug "runtime/debug" "sync" "github.com/sagernet/sing-box/adapter" @@ -88,6 +89,7 @@ func (s *Service) Close() error { //export goMemoryPressureCallback func goMemoryPressureCallback(status C.ulong) { + runtimeDebug.FreeOSMemory() globalAccess.Lock() services := make([]*Service, len(globalServices)) copy(services, globalServices) diff --git a/service/oomkiller/timer.go b/service/oomkiller/timer.go index 146ecc3547..6f13d825ae 100644 --- a/service/oomkiller/timer.go +++ b/service/oomkiller/timer.go @@ -107,6 +107,7 @@ type adaptiveTimer struct { access sync.Mutex timer *time.Timer state pressureState + currentInterval time.Duration forceMinInterval bool pendingPressureBaseline bool pressureBaseline memorySample @@ -178,7 +179,9 @@ func (t *adaptiveTimer) poll() { t.state = t.nextState(sample) if t.state == pressureStateNormal { t.forceMinInterval = false - t.pressureBaselineTime = time.Time{} + if !t.pressureBaselineTime.IsZero() && time.Since(t.pressureBaselineTime) > t.maxInterval { + t.pressureBaselineTime = time.Time{} + } } t.timer.Reset(t.intervalForState()) triggered = previousState != pressureStateTriggered && t.state == pressureStateTriggered @@ -272,13 +275,19 @@ func (t *adaptiveTimer) availableThresholds(sample memorySample) pressureThresho } func (t *adaptiveTimer) intervalForState() time.Duration { - if t.state == pressureStateNormal { - return t.maxInterval - } - if t.forceMinInterval || t.state == pressureStateTriggered { - return t.minInterval + switch { + case t.forceMinInterval || t.state == pressureStateTriggered: + t.currentInterval = t.minInterval + case t.state == pressureStateArmed: + t.currentInterval = t.armedInterval + default: + if t.currentInterval == 0 { + t.currentInterval = t.maxInterval + } else { + t.currentInterval = min(t.currentInterval*2, t.maxInterval) + } } - return t.armedInterval + return t.currentInterval } func (t *adaptiveTimer) logDetails(sample memorySample) string { From 0b8f38081726c15bb8282124fad2c060d17130a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 8 Apr 2026 14:44:14 +0800 Subject: [PATCH 13/93] tools: Network Quality & STUN --- cmd/sing-box/cmd_tools_networkquality.go | 121 ++ cmd/sing-box/cmd_tools_stun.go | 79 ++ common/networkquality/http.go | 142 +++ common/networkquality/http3.go | 55 + common/networkquality/http3_stub.go | 12 + common/networkquality/networkquality.go | 1413 +++++++++++++++++++++ common/stun/stun.go | 607 +++++++++ daemon/started_service.go | 209 ++- daemon/started_service.pb.go | 591 ++++++++- daemon/started_service.proto | 49 + daemon/started_service_grpc.pb.go | 211 ++- experimental/libbox/command.go | 1 + experimental/libbox/command_client.go | 117 ++ experimental/libbox/command_types.go | 16 + experimental/libbox/command_types_nq.go | 51 + experimental/libbox/command_types_stun.go | 35 + experimental/libbox/networkquality.go | 74 ++ experimental/libbox/setup.go | 52 + experimental/libbox/stun.go | 50 + 19 files changed, 3799 insertions(+), 86 deletions(-) create mode 100644 cmd/sing-box/cmd_tools_networkquality.go create mode 100644 cmd/sing-box/cmd_tools_stun.go create mode 100644 common/networkquality/http.go create mode 100644 common/networkquality/http3.go create mode 100644 common/networkquality/http3_stub.go create mode 100644 common/networkquality/networkquality.go create mode 100644 common/stun/stun.go create mode 100644 experimental/libbox/command_types_nq.go create mode 100644 experimental/libbox/command_types_stun.go create mode 100644 experimental/libbox/networkquality.go create mode 100644 experimental/libbox/stun.go diff --git a/cmd/sing-box/cmd_tools_networkquality.go b/cmd/sing-box/cmd_tools_networkquality.go new file mode 100644 index 0000000000..5f63571de7 --- /dev/null +++ b/cmd/sing-box/cmd_tools_networkquality.go @@ -0,0 +1,121 @@ +package main + +import ( + "fmt" + "os" + "strings" + "time" + + "github.com/sagernet/sing-box/common/networkquality" + "github.com/sagernet/sing-box/log" + + "github.com/spf13/cobra" +) + +var ( + commandNetworkQualityFlagConfigURL string + commandNetworkQualityFlagSerial bool + commandNetworkQualityFlagMaxRuntime int + commandNetworkQualityFlagHTTP3 bool +) + +var commandNetworkQuality = &cobra.Command{ + Use: "networkquality", + Short: "Run a network quality test", + Run: func(cmd *cobra.Command, args []string) { + err := runNetworkQuality() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandNetworkQuality.Flags().StringVar( + &commandNetworkQualityFlagConfigURL, + "config-url", "", + "Network quality test config URL (default: Apple mensura)", + ) + commandNetworkQuality.Flags().BoolVar( + &commandNetworkQualityFlagSerial, + "serial", false, + "Run download and upload tests sequentially instead of in parallel", + ) + commandNetworkQuality.Flags().IntVar( + &commandNetworkQualityFlagMaxRuntime, + "max-runtime", int(networkquality.DefaultMaxRuntime/time.Second), + "Network quality maximum runtime in seconds", + ) + commandNetworkQuality.Flags().BoolVar( + &commandNetworkQualityFlagHTTP3, + "http3", false, + "Use HTTP/3 (QUIC) for measurement traffic", + ) + commandTools.AddCommand(commandNetworkQuality) +} + +func runNetworkQuality() error { + instance, err := createPreStartedClient() + if err != nil { + return err + } + defer instance.Close() + + dialer, err := createDialer(instance, commandToolsFlagOutbound) + if err != nil { + return err + } + + httpClient := networkquality.NewHTTPClient(dialer) + defer httpClient.CloseIdleConnections() + + measurementClientFactory, err := networkquality.NewOptionalHTTP3Factory(dialer, commandNetworkQualityFlagHTTP3) + if err != nil { + return err + } + + fmt.Fprintln(os.Stderr, "==== NETWORK QUALITY TEST ====") + + result, err := networkquality.Run(networkquality.Options{ + ConfigURL: commandNetworkQualityFlagConfigURL, + HTTPClient: httpClient, + NewMeasurementClient: measurementClientFactory, + Serial: commandNetworkQualityFlagSerial, + MaxRuntime: time.Duration(commandNetworkQualityFlagMaxRuntime) * time.Second, + Context: globalCtx, + OnProgress: func(p networkquality.Progress) { + if !commandNetworkQualityFlagSerial && p.Phase != networkquality.PhaseIdle { + fmt.Fprintf(os.Stderr, "\rDownload: %s RPM: %d Upload: %s RPM: %d", + networkquality.FormatBitrate(p.DownloadCapacity), p.DownloadRPM, + networkquality.FormatBitrate(p.UploadCapacity), p.UploadRPM) + return + } + switch networkquality.Phase(p.Phase) { + case networkquality.PhaseIdle: + if p.IdleLatencyMs > 0 { + fmt.Fprintf(os.Stderr, "\rIdle Latency: %d ms", p.IdleLatencyMs) + } else { + fmt.Fprint(os.Stderr, "\rMeasuring idle latency...") + } + case networkquality.PhaseDownload: + fmt.Fprintf(os.Stderr, "\rDownload: %s RPM: %d", + networkquality.FormatBitrate(p.DownloadCapacity), p.DownloadRPM) + case networkquality.PhaseUpload: + fmt.Fprintf(os.Stderr, "\rUpload: %s RPM: %d", + networkquality.FormatBitrate(p.UploadCapacity), p.UploadRPM) + } + }, + }) + if err != nil { + return err + } + + fmt.Fprintln(os.Stderr) + fmt.Fprintln(os.Stderr, strings.Repeat("-", 40)) + fmt.Fprintf(os.Stderr, "Idle Latency: %d ms\n", result.IdleLatencyMs) + fmt.Fprintf(os.Stderr, "Download Capacity: %-20s Accuracy: %s\n", networkquality.FormatBitrate(result.DownloadCapacity), result.DownloadCapacityAccuracy) + fmt.Fprintf(os.Stderr, "Upload Capacity: %-20s Accuracy: %s\n", networkquality.FormatBitrate(result.UploadCapacity), result.UploadCapacityAccuracy) + fmt.Fprintf(os.Stderr, "Download Responsiveness: %-20s Accuracy: %s\n", fmt.Sprintf("%d RPM", result.DownloadRPM), result.DownloadRPMAccuracy) + fmt.Fprintf(os.Stderr, "Upload Responsiveness: %-20s Accuracy: %s\n", fmt.Sprintf("%d RPM", result.UploadRPM), result.UploadRPMAccuracy) + return nil +} diff --git a/cmd/sing-box/cmd_tools_stun.go b/cmd/sing-box/cmd_tools_stun.go new file mode 100644 index 0000000000..f13086caaa --- /dev/null +++ b/cmd/sing-box/cmd_tools_stun.go @@ -0,0 +1,79 @@ +package main + +import ( + "fmt" + "os" + + "github.com/sagernet/sing-box/common/stun" + "github.com/sagernet/sing-box/log" + + "github.com/spf13/cobra" +) + +var commandSTUNFlagServer string + +var commandSTUN = &cobra.Command{ + Use: "stun", + Short: "Run a STUN test", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + err := runSTUN() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandSTUN.Flags().StringVarP(&commandSTUNFlagServer, "server", "s", stun.DefaultServer, "STUN server address") + commandTools.AddCommand(commandSTUN) +} + +func runSTUN() error { + instance, err := createPreStartedClient() + if err != nil { + return err + } + defer instance.Close() + + dialer, err := createDialer(instance, commandToolsFlagOutbound) + if err != nil { + return err + } + + fmt.Fprintln(os.Stderr, "==== STUN TEST ====") + + result, err := stun.Run(stun.Options{ + Server: commandSTUNFlagServer, + Dialer: dialer, + Context: globalCtx, + OnProgress: func(p stun.Progress) { + switch p.Phase { + case stun.PhaseBinding: + if p.ExternalAddr != "" { + fmt.Fprintf(os.Stderr, "\rExternal Address: %s (%d ms)", p.ExternalAddr, p.LatencyMs) + } else { + fmt.Fprint(os.Stderr, "\rSending binding request...") + } + case stun.PhaseNATMapping: + fmt.Fprint(os.Stderr, "\rDetecting NAT mapping behavior...") + case stun.PhaseNATFiltering: + fmt.Fprint(os.Stderr, "\rDetecting NAT filtering behavior...") + } + }, + }) + if err != nil { + return err + } + + fmt.Fprintln(os.Stderr) + fmt.Fprintf(os.Stderr, "External Address: %s\n", result.ExternalAddr) + fmt.Fprintf(os.Stderr, "Latency: %d ms\n", result.LatencyMs) + if result.NATTypeSupported { + fmt.Fprintf(os.Stderr, "NAT Mapping: %s\n", result.NATMapping) + fmt.Fprintf(os.Stderr, "NAT Filtering: %s\n", result.NATFiltering) + } else { + fmt.Fprintln(os.Stderr, "NAT Type Detection: not supported by server") + } + return nil +} diff --git a/common/networkquality/http.go b/common/networkquality/http.go new file mode 100644 index 0000000000..f9ff2a4a5b --- /dev/null +++ b/common/networkquality/http.go @@ -0,0 +1,142 @@ +package networkquality + +import ( + "context" + "fmt" + "net" + "net/http" + "strings" + + C "github.com/sagernet/sing-box/constant" + sBufio "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func FormatBitrate(bps int64) string { + switch { + case bps >= 1_000_000_000: + return fmt.Sprintf("%.1f Gbps", float64(bps)/1_000_000_000) + case bps >= 1_000_000: + return fmt.Sprintf("%.1f Mbps", float64(bps)/1_000_000) + case bps >= 1_000: + return fmt.Sprintf("%.1f Kbps", float64(bps)/1_000) + default: + return fmt.Sprintf("%d bps", bps) + } +} + +func NewHTTPClient(dialer N.Dialer) *http.Client { + transport := &http.Transport{ + ForceAttemptHTTP2: true, + TLSHandshakeTimeout: C.TCPTimeout, + } + if dialer != nil { + transport.DialContext = func(ctx context.Context, network string, addr string) (net.Conn, error) { + return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + } + } + return &http.Client{Transport: transport} +} + +func baseTransportFromClient(client *http.Client) (*http.Transport, error) { + if client == nil { + return nil, E.New("http client is nil") + } + if client.Transport == nil { + return http.DefaultTransport.(*http.Transport).Clone(), nil + } + transport, ok := client.Transport.(*http.Transport) + if !ok { + return nil, E.New("http client transport must be *http.Transport") + } + return transport.Clone(), nil +} + +func newMeasurementClient( + baseClient *http.Client, + connectEndpoint string, + singleConnection bool, + disableKeepAlives bool, + readCounters []N.CountFunc, + writeCounters []N.CountFunc, +) (*http.Client, error) { + transport, err := baseTransportFromClient(baseClient) + if err != nil { + return nil, err + } + transport.DisableCompression = true + transport.DisableKeepAlives = disableKeepAlives + if singleConnection { + transport.MaxConnsPerHost = 1 + transport.MaxIdleConnsPerHost = 1 + transport.MaxIdleConns = 1 + } + + baseDialContext := transport.DialContext + if baseDialContext == nil { + dialer := &net.Dialer{} + baseDialContext = dialer.DialContext + } + transport.DialContext = func(ctx context.Context, network string, addr string) (net.Conn, error) { + dialAddr := addr + if connectEndpoint != "" { + dialAddr = rewriteDialAddress(addr, connectEndpoint) + } + conn, dialErr := baseDialContext(ctx, network, dialAddr) + if dialErr != nil { + return nil, dialErr + } + if len(readCounters) > 0 || len(writeCounters) > 0 { + return sBufio.NewCounterConn(conn, readCounters, writeCounters), nil + } + return conn, nil + } + + return &http.Client{ + Transport: transport, + CheckRedirect: baseClient.CheckRedirect, + Jar: baseClient.Jar, + Timeout: baseClient.Timeout, + }, nil +} + +type MeasurementClientFactory func( + connectEndpoint string, + singleConnection bool, + disableKeepAlives bool, + readCounters []N.CountFunc, + writeCounters []N.CountFunc, +) (*http.Client, error) + +func defaultMeasurementClientFactory(baseClient *http.Client) MeasurementClientFactory { + return func(connectEndpoint string, singleConnection, disableKeepAlives bool, readCounters, writeCounters []N.CountFunc) (*http.Client, error) { + return newMeasurementClient(baseClient, connectEndpoint, singleConnection, disableKeepAlives, readCounters, writeCounters) + } +} + +func NewOptionalHTTP3Factory(dialer N.Dialer, useHTTP3 bool) (MeasurementClientFactory, error) { + if !useHTTP3 { + return nil, nil + } + return NewHTTP3MeasurementClientFactory(dialer) +} + +func rewriteDialAddress(addr string, connectEndpoint string) string { + connectEndpoint = strings.TrimSpace(connectEndpoint) + host, port, err := net.SplitHostPort(addr) + if err != nil { + return addr + } + endpointHost, endpointPort, err := net.SplitHostPort(connectEndpoint) + if err == nil { + host = endpointHost + if endpointPort != "" { + port = endpointPort + } + } else if connectEndpoint != "" { + host = connectEndpoint + } + return net.JoinHostPort(host, port) +} diff --git a/common/networkquality/http3.go b/common/networkquality/http3.go new file mode 100644 index 0000000000..5e28d9fd68 --- /dev/null +++ b/common/networkquality/http3.go @@ -0,0 +1,55 @@ +//go:build with_quic + +package networkquality + +import ( + "context" + "crypto/tls" + "net" + "net/http" + + "github.com/sagernet/quic-go" + "github.com/sagernet/quic-go/http3" + sBufio "github.com/sagernet/sing/common/bufio" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func NewHTTP3MeasurementClientFactory(dialer N.Dialer) (MeasurementClientFactory, error) { + // singleConnection and disableKeepAlives are not applied: + // HTTP/3 multiplexes streams over a single QUIC connection by default. + return func(connectEndpoint string, _, _ bool, readCounters, writeCounters []N.CountFunc) (*http.Client, error) { + transport := &http3.Transport{ + Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) { + dialAddr := addr + if connectEndpoint != "" { + dialAddr = rewriteDialAddress(addr, connectEndpoint) + } + destination := M.ParseSocksaddr(dialAddr) + var udpConn net.Conn + var dialErr error + if dialer != nil { + udpConn, dialErr = dialer.DialContext(ctx, N.NetworkUDP, destination) + } else { + var netDialer net.Dialer + udpConn, dialErr = netDialer.DialContext(ctx, N.NetworkUDP, destination.String()) + } + if dialErr != nil { + return nil, dialErr + } + var wrappedConn net.Conn = udpConn + if len(readCounters) > 0 || len(writeCounters) > 0 { + wrappedConn = sBufio.NewCounterConn(udpConn, readCounters, writeCounters) + } + packetConn := sBufio.NewUnbindPacketConn(wrappedConn) + quicConn, dialErr := quic.DialEarly(ctx, packetConn, udpConn.RemoteAddr(), tlsCfg, cfg) + if dialErr != nil { + udpConn.Close() + return nil, dialErr + } + return quicConn, nil + }, + } + return &http.Client{Transport: transport}, nil + }, nil +} diff --git a/common/networkquality/http3_stub.go b/common/networkquality/http3_stub.go new file mode 100644 index 0000000000..632837e68d --- /dev/null +++ b/common/networkquality/http3_stub.go @@ -0,0 +1,12 @@ +//go:build !with_quic + +package networkquality + +import ( + C "github.com/sagernet/sing-box/constant" + N "github.com/sagernet/sing/common/network" +) + +func NewHTTP3MeasurementClientFactory(dialer N.Dialer) (MeasurementClientFactory, error) { + return nil, C.ErrQUICNotIncluded +} diff --git a/common/networkquality/networkquality.go b/common/networkquality/networkquality.go new file mode 100644 index 0000000000..a4c73472cb --- /dev/null +++ b/common/networkquality/networkquality.go @@ -0,0 +1,1413 @@ +package networkquality + +import ( + "context" + "crypto/tls" + "encoding/json" + "io" + "math" + "math/rand" + "net/http" + "net/http/httptrace" + "net/url" + "sort" + "strings" + "sync" + "sync/atomic" + "time" + + sBufio "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" +) + +const DefaultConfigURL = "https://mensura.cdn-apple.com/api/v1/gm/config" + +type Config struct { + Version int `json:"version"` + TestEndpoint string `json:"test_endpoint"` + URLs URLs `json:"urls"` +} + +type URLs struct { + SmallHTTPSDownloadURL string `json:"small_https_download_url"` + LargeHTTPSDownloadURL string `json:"large_https_download_url"` + HTTPSUploadURL string `json:"https_upload_url"` + SmallDownloadURL string `json:"small_download_url"` + LargeDownloadURL string `json:"large_download_url"` + UploadURL string `json:"upload_url"` +} + +func (u *URLs) smallDownloadURL() string { + if u.SmallHTTPSDownloadURL != "" { + return u.SmallHTTPSDownloadURL + } + return u.SmallDownloadURL +} + +func (u *URLs) largeDownloadURL() string { + if u.LargeHTTPSDownloadURL != "" { + return u.LargeHTTPSDownloadURL + } + return u.LargeDownloadURL +} + +func (u *URLs) uploadURL() string { + if u.HTTPSUploadURL != "" { + return u.HTTPSUploadURL + } + return u.UploadURL +} + +type Accuracy int32 + +const ( + AccuracyLow Accuracy = 0 + AccuracyMedium Accuracy = 1 + AccuracyHigh Accuracy = 2 +) + +func (a Accuracy) String() string { + switch a { + case AccuracyHigh: + return "High" + case AccuracyMedium: + return "Medium" + default: + return "Low" + } +} + +type Result struct { + DownloadCapacity int64 + UploadCapacity int64 + DownloadRPM int32 + UploadRPM int32 + IdleLatencyMs int32 + DownloadCapacityAccuracy Accuracy + UploadCapacityAccuracy Accuracy + DownloadRPMAccuracy Accuracy + UploadRPMAccuracy Accuracy +} + +type Progress struct { + Phase Phase + DownloadCapacity int64 + UploadCapacity int64 + DownloadRPM int32 + UploadRPM int32 + IdleLatencyMs int32 + ElapsedMs int64 + DownloadCapacityAccuracy Accuracy + UploadCapacityAccuracy Accuracy + DownloadRPMAccuracy Accuracy + UploadRPMAccuracy Accuracy +} + +type Phase int32 + +const ( + PhaseIdle Phase = 0 + PhaseDownload Phase = 1 + PhaseUpload Phase = 2 + PhaseDone Phase = 3 +) + +type Options struct { + ConfigURL string + HTTPClient *http.Client + NewMeasurementClient MeasurementClientFactory + Serial bool + MaxRuntime time.Duration + OnProgress func(Progress) + Context context.Context +} + +const DefaultMaxRuntime = 20 * time.Second + +type measurementSettings struct { + idleProbeCount int + testTimeout time.Duration + stabilityInterval time.Duration + sampleInterval time.Duration + progressInterval time.Duration + maxProbesPerSecond int + initialConnections int + maxConnections int + movingAvgDistance int + trimPercent int + stdDevTolerancePct float64 + maxProbeCapacityPct float64 +} + +var settings = measurementSettings{ + idleProbeCount: 5, + testTimeout: DefaultMaxRuntime, + stabilityInterval: time.Second, + sampleInterval: 250 * time.Millisecond, + progressInterval: 500 * time.Millisecond, + maxProbesPerSecond: 100, + initialConnections: 1, + maxConnections: 16, + movingAvgDistance: 4, + trimPercent: 5, + stdDevTolerancePct: 5, + maxProbeCapacityPct: 0.05, +} + +type resolvedConfig struct { + smallURL *url.URL + largeURL *url.URL + uploadURL *url.URL + connectEndpoint string +} + +type directionPlan struct { + dataURL *url.URL + probeURL *url.URL + connectEndpoint string + isUpload bool +} + +type probeTrace struct { + reused bool + connectStart time.Time + connectDone time.Time + tlsStart time.Time + tlsDone time.Time + tlsVersion uint16 + gotConn time.Time + wroteRequest time.Time + firstResponseByte time.Time +} + +type probeMeasurement struct { + total time.Duration + tcp time.Duration + tls time.Duration + httpFirst time.Duration + httpLoaded time.Duration + bytes int64 + reused bool +} + +type probeRound struct { + interval int + tcp time.Duration + tls time.Duration + httpFirst time.Duration + httpLoaded time.Duration +} + +func (p probeRound) responsivenessLatency() float64 { + var foreignSamples []float64 + if p.tcp > 0 { + foreignSamples = append(foreignSamples, durationMillis(p.tcp)) + } + if p.tls > 0 { + foreignSamples = append(foreignSamples, durationMillis(p.tls)) + } + if p.httpFirst > 0 { + foreignSamples = append(foreignSamples, durationMillis(p.httpFirst)) + } + if len(foreignSamples) == 0 || p.httpLoaded <= 0 { + return 0 + } + return (meanFloat64s(foreignSamples) + durationMillis(p.httpLoaded)) / 2 +} + +const maxConsecutiveErrors = 3 + +type loadConnection struct { + client *http.Client + dataURL *url.URL + isUpload bool + active atomic.Bool + ready atomic.Bool +} + +func (c *loadConnection) run(ctx context.Context, onError func(error)) { + defer c.client.CloseIdleConnections() + markActive := func() { + c.ready.Store(true) + c.active.Store(true) + } + var consecutiveErrors int + for { + select { + case <-ctx.Done(): + return + default: + } + var err error + if c.isUpload { + err = runUploadRequest(ctx, c.client, c.dataURL.String(), markActive) + } else { + err = runDownloadRequest(ctx, c.client, c.dataURL.String(), markActive) + } + c.active.Store(false) + if err != nil { + if ctx.Err() != nil { + return + } + consecutiveErrors++ + if consecutiveErrors > maxConsecutiveErrors { + onError(err) + return + } + c.client.CloseIdleConnections() + continue + } + consecutiveErrors = 0 + } +} + +type intervalThroughput struct { + interval int + bps float64 +} + +type intervalWindow struct { + lower int + upper int +} + +type stabilityTracker struct { + window int + stdDevTolerancePct float64 + instantaneous []float64 + movingAverages []float64 +} + +func (s *stabilityTracker) add(value float64) bool { + if value <= 0 || math.IsNaN(value) || math.IsInf(value, 0) { + return false + } + s.instantaneous = append(s.instantaneous, value) + if len(s.instantaneous) > s.window { + s.instantaneous = s.instantaneous[len(s.instantaneous)-s.window:] + } + s.movingAverages = append(s.movingAverages, meanFloat64s(s.instantaneous)) + if len(s.movingAverages) > s.window { + s.movingAverages = s.movingAverages[len(s.movingAverages)-s.window:] + } + return s.stable() +} + +func (s *stabilityTracker) ready() bool { + return len(s.movingAverages) >= s.window +} + +func (s *stabilityTracker) accuracy() Accuracy { + if s.stable() { + return AccuracyHigh + } + if s.ready() { + return AccuracyMedium + } + return AccuracyLow +} + +func (s *stabilityTracker) stable() bool { + if len(s.movingAverages) < s.window { + return false + } + currentAverage := s.movingAverages[len(s.movingAverages)-1] + if currentAverage <= 0 { + return false + } + return stdDevFloat64s(s.movingAverages) <= currentAverage*(s.stdDevTolerancePct/100) +} + +type directionMeasurement struct { + capacity int64 + rpm int32 + capacityAccuracy Accuracy + rpmAccuracy Accuracy +} + +type directionRunner struct { + factory MeasurementClientFactory + plan directionPlan + probeBytes int64 + + errCh chan error + errOnce sync.Once + wg sync.WaitGroup + + totalBytes atomic.Int64 + currentCapacity atomic.Int64 + currentRPM atomic.Int32 + currentInterval atomic.Int64 + + connMu sync.Mutex + connections []*loadConnection + + probeMu sync.Mutex + probeRounds []probeRound + intervalProbeValues []float64 + responsivenessWindow *intervalWindow + throughputs []intervalThroughput + throughputWindow *intervalWindow +} + +func newDirectionRunner(factory MeasurementClientFactory, plan directionPlan, probeBytes int64) *directionRunner { + return &directionRunner{ + factory: factory, + plan: plan, + probeBytes: probeBytes, + errCh: make(chan error, 1), + } +} + +func (r *directionRunner) fail(err error) { + if err == nil { + return + } + r.errOnce.Do(func() { + select { + case r.errCh <- err: + default: + } + }) +} + +func (r *directionRunner) onConnectionFailed(err error) { + r.connMu.Lock() + activeCount := 0 + for _, conn := range r.connections { + if conn.active.Load() { + activeCount++ + } + } + r.connMu.Unlock() + if activeCount == 0 { + r.fail(err) + } +} + +func (r *directionRunner) addConnection(ctx context.Context) error { + counter := N.CountFunc(func(n int64) { r.totalBytes.Add(n) }) + var readCounters, writeCounters []N.CountFunc + if r.plan.isUpload { + writeCounters = []N.CountFunc{counter} + } else { + readCounters = []N.CountFunc{counter} + } + client, err := r.factory(r.plan.connectEndpoint, true, false, readCounters, writeCounters) + if err != nil { + return err + } + conn := &loadConnection{ + client: client, + dataURL: r.plan.dataURL, + isUpload: r.plan.isUpload, + } + r.connMu.Lock() + r.connections = append(r.connections, conn) + r.connMu.Unlock() + r.wg.Add(1) + go func() { + defer r.wg.Done() + conn.run(ctx, r.onConnectionFailed) + }() + return nil +} + +func (r *directionRunner) connectionCount() int { + r.connMu.Lock() + defer r.connMu.Unlock() + return len(r.connections) +} + +func (r *directionRunner) pickReadyConnection() *loadConnection { + r.connMu.Lock() + defer r.connMu.Unlock() + var ready []*loadConnection + for _, conn := range r.connections { + if conn.ready.Load() && conn.active.Load() { + ready = append(ready, conn) + } + } + if len(ready) == 0 { + return nil + } + return ready[rand.Intn(len(ready))] +} + +func (r *directionRunner) startProber(ctx context.Context) { + r.wg.Add(1) + go func() { + defer r.wg.Done() + ticker := time.NewTicker(r.probeInterval()) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + } + conn := r.pickReadyConnection() + if conn == nil { + continue + } + go func(selfClient *http.Client) { + foreignClient, err := r.factory(r.plan.connectEndpoint, true, true, nil, nil) + if err != nil { + return + } + round, err := collectProbeRound(ctx, foreignClient, selfClient, r.plan.probeURL.String()) + foreignClient.CloseIdleConnections() + if err != nil { + return + } + r.recordProbeRound(probeRound{ + interval: int(r.currentInterval.Load()), + tcp: round.tcp, + tls: round.tls, + httpFirst: round.httpFirst, + httpLoaded: round.httpLoaded, + }) + }(conn.client) + ticker.Reset(r.probeInterval()) + } + }() +} + +func (r *directionRunner) probeInterval() time.Duration { + interval := time.Second / time.Duration(settings.maxProbesPerSecond) + capacity := r.currentCapacity.Load() + if capacity <= 0 || r.probeBytes <= 0 || settings.maxProbeCapacityPct <= 0 { + return interval + } + bitsPerRound := float64(r.probeBytes*2) * 8 + minSeconds := bitsPerRound / (float64(capacity) * settings.maxProbeCapacityPct) + if minSeconds <= 0 { + return interval + } + capacityInterval := time.Duration(minSeconds * float64(time.Second)) + if capacityInterval > interval { + interval = capacityInterval + } + return interval +} + +func (r *directionRunner) recordProbeRound(round probeRound) { + r.probeMu.Lock() + r.probeRounds = append(r.probeRounds, round) + if latency := round.responsivenessLatency(); latency > 0 { + r.intervalProbeValues = append(r.intervalProbeValues, latency) + } + r.currentRPM.Store(calculateRPM(r.probeRounds)) + r.probeMu.Unlock() +} + +func (r *directionRunner) swapIntervalProbeValues() []float64 { + r.probeMu.Lock() + defer r.probeMu.Unlock() + values := append([]float64(nil), r.intervalProbeValues...) + r.intervalProbeValues = nil + return values +} + +func (r *directionRunner) setResponsivenessWindow(currentInterval int) { + lower := currentInterval - settings.movingAvgDistance + 1 + if lower < 0 { + lower = 0 + } + r.probeMu.Lock() + r.responsivenessWindow = &intervalWindow{lower: lower, upper: currentInterval} + r.probeMu.Unlock() +} + +func (r *directionRunner) recordThroughput(interval int, bps float64) { + r.probeMu.Lock() + r.throughputs = append(r.throughputs, intervalThroughput{interval: interval, bps: bps}) + r.probeMu.Unlock() +} + +func (r *directionRunner) setThroughputWindow(currentInterval int) { + lower := currentInterval - settings.movingAvgDistance + 1 + if lower < 0 { + lower = 0 + } + r.probeMu.Lock() + r.throughputWindow = &intervalWindow{lower: lower, upper: currentInterval} + r.probeMu.Unlock() +} + +func (r *directionRunner) finalRPM() int32 { + r.probeMu.Lock() + defer r.probeMu.Unlock() + if r.responsivenessWindow == nil { + return calculateRPM(r.probeRounds) + } + var rounds []probeRound + for _, round := range r.probeRounds { + if round.interval >= r.responsivenessWindow.lower && round.interval <= r.responsivenessWindow.upper { + rounds = append(rounds, round) + } + } + if len(rounds) == 0 { + rounds = r.probeRounds + } + return calculateRPM(rounds) +} + +func (r *directionRunner) finalCapacity(totalDuration time.Duration) int64 { + r.probeMu.Lock() + defer r.probeMu.Unlock() + var samples []float64 + if r.throughputWindow != nil { + for _, sample := range r.throughputs { + if sample.interval >= r.throughputWindow.lower && sample.interval <= r.throughputWindow.upper { + samples = append(samples, sample.bps) + } + } + } + if len(samples) == 0 { + for _, sample := range r.throughputs { + samples = append(samples, sample.bps) + } + } + if len(samples) > 0 { + return int64(math.Round(upperTrimmedMean(samples, settings.trimPercent))) + } + if totalDuration > 0 { + return int64(float64(r.totalBytes.Load()) * 8 / totalDuration.Seconds()) + } + return 0 +} + +func (r *directionRunner) wait() { + r.wg.Wait() +} + +func Run(options Options) (*Result, error) { + ctx := options.Context + if ctx == nil { + ctx = context.Background() + } + if options.HTTPClient == nil { + return nil, E.New("http client is required") + } + maxRuntime, err := normalizeMaxRuntime(options.MaxRuntime) + if err != nil { + return nil, err + } + configURL := resolveConfigURL(options.ConfigURL) + config, err := fetchConfig(ctx, options.HTTPClient, configURL) + if err != nil { + return nil, E.Cause(err, "fetch config") + } + resolved, err := validateConfig(config) + if err != nil { + return nil, E.Cause(err, "validate config") + } + + start := time.Now() + report := func(progress Progress) { + if options.OnProgress == nil { + return + } + progress.ElapsedMs = time.Since(start).Milliseconds() + options.OnProgress(progress) + } + + factory := options.NewMeasurementClient + if factory == nil { + factory = defaultMeasurementClientFactory(options.HTTPClient) + } + + report(Progress{Phase: PhaseIdle}) + idleLatency, probeBytes, err := measureIdleLatency(ctx, factory, resolved) + if err != nil { + return nil, E.Cause(err, "measure idle latency") + } + report(Progress{Phase: PhaseIdle, IdleLatencyMs: idleLatency}) + + start = time.Now() + + var download, upload *directionMeasurement + if options.Serial { + download, upload, err = measureSerial( + ctx, + factory, + resolved, + idleLatency, + probeBytes, + maxRuntime, + report, + ) + } else { + download, upload, err = measureParallel( + ctx, + factory, + resolved, + idleLatency, + probeBytes, + maxRuntime, + report, + ) + } + if err != nil { + return nil, err + } + + result := &Result{ + DownloadCapacity: download.capacity, + UploadCapacity: upload.capacity, + DownloadRPM: download.rpm, + UploadRPM: upload.rpm, + IdleLatencyMs: idleLatency, + DownloadCapacityAccuracy: download.capacityAccuracy, + UploadCapacityAccuracy: upload.capacityAccuracy, + DownloadRPMAccuracy: download.rpmAccuracy, + UploadRPMAccuracy: upload.rpmAccuracy, + } + report(Progress{ + Phase: PhaseDone, + DownloadCapacity: result.DownloadCapacity, + UploadCapacity: result.UploadCapacity, + DownloadRPM: result.DownloadRPM, + UploadRPM: result.UploadRPM, + IdleLatencyMs: result.IdleLatencyMs, + DownloadCapacityAccuracy: result.DownloadCapacityAccuracy, + UploadCapacityAccuracy: result.UploadCapacityAccuracy, + DownloadRPMAccuracy: result.DownloadRPMAccuracy, + UploadRPMAccuracy: result.UploadRPMAccuracy, + }) + return result, nil +} + +func normalizeMaxRuntime(maxRuntime time.Duration) (time.Duration, error) { + if maxRuntime == 0 { + return settings.testTimeout, nil + } + if maxRuntime < 0 { + return 0, E.New("max runtime must be positive") + } + return maxRuntime, nil +} + +func measureSerial( + ctx context.Context, + factory MeasurementClientFactory, + resolved *resolvedConfig, + idleLatency int32, + probeBytes int64, + maxRuntime time.Duration, + report func(Progress), +) (*directionMeasurement, *directionMeasurement, error) { + downloadRuntime, uploadRuntime := splitRuntimeBudget(maxRuntime, 2) + report(Progress{Phase: PhaseDownload, IdleLatencyMs: idleLatency}) + download, err := measureDirection(ctx, factory, directionPlan{ + dataURL: resolved.largeURL, + probeURL: resolved.smallURL, + connectEndpoint: resolved.connectEndpoint, + }, probeBytes, downloadRuntime, func(capacity int64, rpm int32) { + report(Progress{ + Phase: PhaseDownload, + DownloadCapacity: capacity, + DownloadRPM: rpm, + IdleLatencyMs: idleLatency, + }) + }) + if err != nil { + return nil, nil, E.Cause(err, "measure download") + } + + report(Progress{ + Phase: PhaseUpload, + DownloadCapacity: download.capacity, + DownloadRPM: download.rpm, + IdleLatencyMs: idleLatency, + }) + upload, err := measureDirection(ctx, factory, directionPlan{ + dataURL: resolved.uploadURL, + probeURL: resolved.smallURL, + connectEndpoint: resolved.connectEndpoint, + isUpload: true, + }, probeBytes, uploadRuntime, func(capacity int64, rpm int32) { + report(Progress{ + Phase: PhaseUpload, + DownloadCapacity: download.capacity, + UploadCapacity: capacity, + DownloadRPM: download.rpm, + UploadRPM: rpm, + IdleLatencyMs: idleLatency, + }) + }) + if err != nil { + return nil, nil, E.Cause(err, "measure upload") + } + return download, upload, nil +} + +func measureParallel( + ctx context.Context, + factory MeasurementClientFactory, + resolved *resolvedConfig, + idleLatency int32, + probeBytes int64, + maxRuntime time.Duration, + report func(Progress), +) (*directionMeasurement, *directionMeasurement, error) { + type parallelResult struct { + measurement *directionMeasurement + err error + } + type progressState struct { + sync.Mutex + downloadCapacity int64 + uploadCapacity int64 + downloadRPM int32 + uploadRPM int32 + } + + parallelCtx, cancel := context.WithCancel(ctx) + defer cancel() + + report(Progress{Phase: PhaseDownload, IdleLatencyMs: idleLatency}) + report(Progress{Phase: PhaseUpload, IdleLatencyMs: idleLatency}) + + var state progressState + sendProgress := func(phase Phase, capacity int64, rpm int32) { + state.Lock() + if phase == PhaseDownload { + state.downloadCapacity = capacity + state.downloadRPM = rpm + } else { + state.uploadCapacity = capacity + state.uploadRPM = rpm + } + snapshot := Progress{ + Phase: phase, + DownloadCapacity: state.downloadCapacity, + UploadCapacity: state.uploadCapacity, + DownloadRPM: state.downloadRPM, + UploadRPM: state.uploadRPM, + IdleLatencyMs: idleLatency, + } + state.Unlock() + report(snapshot) + } + + var wg sync.WaitGroup + downloadCh := make(chan parallelResult, 1) + uploadCh := make(chan parallelResult, 1) + wg.Add(2) + go func() { + defer wg.Done() + measurement, err := measureDirection(parallelCtx, factory, directionPlan{ + dataURL: resolved.largeURL, + probeURL: resolved.smallURL, + connectEndpoint: resolved.connectEndpoint, + }, probeBytes, maxRuntime, func(capacity int64, rpm int32) { + sendProgress(PhaseDownload, capacity, rpm) + }) + if err != nil { + cancel() + downloadCh <- parallelResult{err: E.Cause(err, "measure download")} + return + } + downloadCh <- parallelResult{measurement: measurement} + }() + go func() { + defer wg.Done() + measurement, err := measureDirection(parallelCtx, factory, directionPlan{ + dataURL: resolved.uploadURL, + probeURL: resolved.smallURL, + connectEndpoint: resolved.connectEndpoint, + isUpload: true, + }, probeBytes, maxRuntime, func(capacity int64, rpm int32) { + sendProgress(PhaseUpload, capacity, rpm) + }) + if err != nil { + cancel() + uploadCh <- parallelResult{err: E.Cause(err, "measure upload")} + return + } + uploadCh <- parallelResult{measurement: measurement} + }() + + download := <-downloadCh + upload := <-uploadCh + wg.Wait() + if download.err != nil { + return nil, nil, download.err + } + if upload.err != nil { + return nil, nil, upload.err + } + return download.measurement, upload.measurement, nil +} + +func resolveConfigURL(rawURL string) string { + rawURL = strings.TrimSpace(rawURL) + if rawURL == "" { + return DefaultConfigURL + } + if !strings.Contains(rawURL, "://") && !strings.Contains(rawURL, "/") { + return "https://" + rawURL + "/.well-known/nq" + } + parsedURL, err := url.Parse(rawURL) + if err != nil { + return rawURL + } + if parsedURL.Scheme != "" && parsedURL.Host != "" && (parsedURL.Path == "" || parsedURL.Path == "/") { + parsedURL.Path = "/.well-known/nq" + return parsedURL.String() + } + return rawURL +} + +func fetchConfig(ctx context.Context, client *http.Client, configURL string) (*Config, error) { + req, err := newRequest(ctx, http.MethodGet, configURL, nil) + if err != nil { + return nil, err + } + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if err = validateResponse(resp); err != nil { + return nil, err + } + var config Config + if err = json.NewDecoder(resp.Body).Decode(&config); err != nil { + return nil, E.Cause(err, "decode config") + } + return &config, nil +} + +func validateConfig(config *Config) (*resolvedConfig, error) { + if config == nil { + return nil, E.New("config is nil") + } + if config.Version != 1 { + return nil, E.New("unsupported config version: ", config.Version) + } + parseURL := func(name string, rawURL string) (*url.URL, error) { + if rawURL == "" { + return nil, E.New("config missing required URL: ", name) + } + parsedURL, err := url.Parse(rawURL) + if err != nil { + return nil, E.Cause(err, "parse "+name) + } + if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { + return nil, E.New("unsupported URL scheme in ", name, ": ", parsedURL.Scheme) + } + if parsedURL.Host == "" { + return nil, E.New("config missing host in ", name) + } + return parsedURL, nil + } + + smallURL, err := parseURL("small_download_url", config.URLs.smallDownloadURL()) + if err != nil { + return nil, err + } + largeURL, err := parseURL("large_download_url", config.URLs.largeDownloadURL()) + if err != nil { + return nil, err + } + uploadURL, err := parseURL("upload_url", config.URLs.uploadURL()) + if err != nil { + return nil, err + } + + if smallURL.Host != largeURL.Host || smallURL.Host != uploadURL.Host { + return nil, E.New("config URLs must use the same host") + } + + return &resolvedConfig{ + smallURL: smallURL, + largeURL: largeURL, + uploadURL: uploadURL, + connectEndpoint: strings.TrimSpace(config.TestEndpoint), + }, nil +} + +func measureIdleLatency(ctx context.Context, factory MeasurementClientFactory, config *resolvedConfig) (int32, int64, error) { + var latencies []int64 + var maxProbeBytes int64 + for i := 0; i < settings.idleProbeCount; i++ { + select { + case <-ctx.Done(): + return 0, 0, ctx.Err() + default: + } + client, err := factory(config.connectEndpoint, true, true, nil, nil) + if err != nil { + return 0, 0, err + } + measurement, err := runProbe(ctx, client, config.smallURL.String(), false) + client.CloseIdleConnections() + if err != nil { + return 0, 0, err + } + latencies = append(latencies, measurement.total.Milliseconds()) + if measurement.bytes > maxProbeBytes { + maxProbeBytes = measurement.bytes + } + } + sort.Slice(latencies, func(i, j int) bool { return latencies[i] < latencies[j] }) + return int32(latencies[len(latencies)/2]), maxProbeBytes, nil +} + +func measureDirection( + ctx context.Context, + factory MeasurementClientFactory, + plan directionPlan, + probeBytes int64, + maxRuntime time.Duration, + onProgress func(capacity int64, rpm int32), +) (*directionMeasurement, error) { + phaseCtx, phaseCancel := context.WithTimeout(ctx, maxRuntime) + defer phaseCancel() + + runner := newDirectionRunner(factory, plan, probeBytes) + defer runner.wait() + + for i := 0; i < settings.initialConnections; i++ { + err := runner.addConnection(phaseCtx) + if err != nil { + return nil, err + } + } + + runner.startProber(phaseCtx) + + throughputTracker := stabilityTracker{ + window: settings.movingAvgDistance, + stdDevTolerancePct: settings.stdDevTolerancePct, + } + responsivenessTracker := stabilityTracker{ + window: settings.movingAvgDistance, + stdDevTolerancePct: settings.stdDevTolerancePct, + } + + start := time.Now() + sampleTicker := time.NewTicker(settings.sampleInterval) + defer sampleTicker.Stop() + intervalTicker := time.NewTicker(settings.stabilityInterval) + defer intervalTicker.Stop() + progressTicker := time.NewTicker(settings.progressInterval) + defer progressTicker.Stop() + + prevSampleBytes := int64(0) + prevSampleTime := start + prevIntervalBytes := int64(0) + prevIntervalTime := start + var ewmaCapacity float64 + var goodputSaturated bool + var intervalIndex int + + for { + select { + case err := <-runner.errCh: + return nil, err + case now := <-sampleTicker.C: + currentBytes := runner.totalBytes.Load() + elapsed := now.Sub(prevSampleTime).Seconds() + if elapsed > 0 { + instantaneousBps := float64(currentBytes-prevSampleBytes) * 8 / elapsed + if ewmaCapacity == 0 { + ewmaCapacity = instantaneousBps + } else { + ewmaCapacity = 0.3*instantaneousBps + 0.7*ewmaCapacity + } + runner.currentCapacity.Store(int64(ewmaCapacity)) + } + prevSampleBytes = currentBytes + prevSampleTime = now + case <-intervalTicker.C: + now := time.Now() + currentBytes := runner.totalBytes.Load() + elapsed := now.Sub(prevIntervalTime).Seconds() + if elapsed > 0 { + intervalBps := float64(currentBytes-prevIntervalBytes) * 8 / elapsed + runner.recordThroughput(intervalIndex, intervalBps) + throughputStable := throughputTracker.add(intervalBps) + if throughputStable && runner.throughputWindow == nil { + runner.setThroughputWindow(intervalIndex) + } + if !goodputSaturated && (throughputStable || (runner.connectionCount() >= settings.maxConnections && throughputTracker.ready())) { + goodputSaturated = true + } + if runner.connectionCount() < settings.maxConnections { + err := runner.addConnection(phaseCtx) + if err != nil { + return nil, err + } + } + } + if goodputSaturated { + if values := runner.swapIntervalProbeValues(); len(values) > 0 { + if responsivenessTracker.add(upperTrimmedMean(values, settings.trimPercent)) && runner.responsivenessWindow == nil { + runner.setResponsivenessWindow(intervalIndex) + phaseCancel() + } + } + } + prevIntervalBytes = currentBytes + prevIntervalTime = now + intervalIndex++ + runner.currentInterval.Store(int64(intervalIndex)) + case <-progressTicker.C: + if onProgress != nil { + onProgress(int64(ewmaCapacity), runner.currentRPM.Load()) + } + case <-phaseCtx.Done(): + if ctx.Err() != nil { + return nil, ctx.Err() + } + totalDuration := time.Since(start) + return &directionMeasurement{ + capacity: runner.finalCapacity(totalDuration), + rpm: runner.finalRPM(), + capacityAccuracy: throughputTracker.accuracy(), + rpmAccuracy: responsivenessTracker.accuracy(), + }, nil + } + } +} + +func splitRuntimeBudget(total time.Duration, directions int) (time.Duration, time.Duration) { + if directions <= 1 { + return total, total + } + first := total / time.Duration(directions) + second := total - first + return first, second +} + +func collectProbeRound(ctx context.Context, foreignClient *http.Client, selfClient *http.Client, rawURL string) (probeMeasurement, error) { + var foreignResult probeMeasurement + var selfResult probeMeasurement + var foreignErr error + var selfErr error + + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + foreignResult, foreignErr = runProbe(ctx, foreignClient, rawURL, false) + }() + go func() { + defer wg.Done() + selfResult, selfErr = runProbe(ctx, selfClient, rawURL, true) + }() + wg.Wait() + + if foreignErr != nil { + return probeMeasurement{}, E.Cause(foreignErr, "foreign probe") + } + if selfErr != nil { + return probeMeasurement{}, E.Cause(selfErr, "self probe") + } + return probeMeasurement{ + tcp: foreignResult.tcp, + tls: foreignResult.tls, + httpFirst: foreignResult.httpFirst, + httpLoaded: selfResult.httpLoaded, + }, nil +} + +func runProbe(ctx context.Context, client *http.Client, rawURL string, expectReuse bool) (probeMeasurement, error) { + var trace probeTrace + start := time.Now() + req, err := newRequest(httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{ + ConnectStart: func(string, string) { + if trace.connectStart.IsZero() { + trace.connectStart = time.Now() + } + }, + ConnectDone: func(string, string, error) { + if trace.connectDone.IsZero() { + trace.connectDone = time.Now() + } + }, + TLSHandshakeStart: func() { + if trace.tlsStart.IsZero() { + trace.tlsStart = time.Now() + } + }, + TLSHandshakeDone: func(state tls.ConnectionState, _ error) { + if trace.tlsDone.IsZero() { + trace.tlsDone = time.Now() + trace.tlsVersion = state.Version + } + }, + GotConn: func(info httptrace.GotConnInfo) { + trace.reused = info.Reused + trace.gotConn = time.Now() + }, + WroteRequest: func(httptrace.WroteRequestInfo) { + trace.wroteRequest = time.Now() + }, + GotFirstResponseByte: func() { + trace.firstResponseByte = time.Now() + }, + }), http.MethodGet, rawURL, nil) + if err != nil { + return probeMeasurement{}, err + } + if !expectReuse { + req.Close = true + } + resp, err := client.Do(req) + if err != nil { + return probeMeasurement{}, err + } + defer resp.Body.Close() + if err = validateResponse(resp); err != nil { + return probeMeasurement{}, err + } + n, err := io.Copy(io.Discard, resp.Body) + end := time.Now() + if err != nil { + return probeMeasurement{}, err + } + if expectReuse && !trace.reused { + return probeMeasurement{}, E.New("self probe did not reuse an existing connection") + } + + httpStart := trace.wroteRequest + if httpStart.IsZero() { + switch { + case !trace.tlsDone.IsZero(): + httpStart = trace.tlsDone + case !trace.connectDone.IsZero(): + httpStart = trace.connectDone + case !trace.gotConn.IsZero(): + httpStart = trace.gotConn + default: + httpStart = start + } + } + + measurement := probeMeasurement{ + total: end.Sub(start), + bytes: n, + reused: trace.reused, + } + if !trace.connectStart.IsZero() && !trace.connectDone.IsZero() && trace.connectDone.After(trace.connectStart) { + measurement.tcp = trace.connectDone.Sub(trace.connectStart) + } + if !trace.tlsStart.IsZero() && !trace.tlsDone.IsZero() && trace.tlsDone.After(trace.tlsStart) { + measurement.tls = trace.tlsDone.Sub(trace.tlsStart) + if roundTrips := tlsHandshakeRoundTrips(trace.tlsVersion); roundTrips > 1 { + measurement.tls /= time.Duration(roundTrips) + } + } + if !trace.firstResponseByte.IsZero() && trace.firstResponseByte.After(httpStart) { + measurement.httpFirst = trace.firstResponseByte.Sub(httpStart) + } + if end.After(httpStart) { + measurement.httpLoaded = end.Sub(httpStart) + } + return measurement, nil +} + +func runDownloadRequest(ctx context.Context, client *http.Client, rawURL string, onActive func()) error { + req, err := newRequest(ctx, http.MethodGet, rawURL, nil) + if err != nil { + return err + } + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + err = validateResponse(resp) + if err != nil { + return err + } + if onActive != nil { + onActive() + } + _, err = sBufio.Copy(io.Discard, resp.Body) + if ctx.Err() != nil { + return nil + } + return err +} + +func runUploadRequest(ctx context.Context, client *http.Client, rawURL string, onActive func()) error { + body := &uploadBody{ + ctx: ctx, + onActive: onActive, + } + req, err := newRequest(ctx, http.MethodPost, rawURL, body) + if err != nil { + return err + } + req.ContentLength = -1 + req.Header.Set("Content-Type", "application/octet-stream") + resp, err := client.Do(req) + if err != nil { + if ctx.Err() != nil { + return nil + } + return err + } + defer resp.Body.Close() + err = validateResponse(resp) + if err != nil { + return err + } + _, _ = io.Copy(io.Discard, resp.Body) + <-ctx.Done() + return nil +} + +func newRequest(ctx context.Context, method string, rawURL string, body io.Reader) (*http.Request, error) { + req, err := http.NewRequestWithContext(ctx, method, rawURL, body) + if err != nil { + return nil, err + } + req.Header.Set("Accept-Encoding", "identity") + return req, nil +} + +func validateResponse(resp *http.Response) error { + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return E.New("unexpected status: ", resp.Status) + } + if encoding := resp.Header.Get("Content-Encoding"); encoding != "" { + return E.New("unexpected content encoding: ", encoding) + } + return nil +} + +func calculateRPM(rounds []probeRound) int32 { + if len(rounds) == 0 { + return 0 + } + var tcpSamples []float64 + var tlsSamples []float64 + var httpFirstSamples []float64 + var httpLoadedSamples []float64 + for _, round := range rounds { + if round.tcp > 0 { + tcpSamples = append(tcpSamples, durationMillis(round.tcp)) + } + if round.tls > 0 { + tlsSamples = append(tlsSamples, durationMillis(round.tls)) + } + if round.httpFirst > 0 { + httpFirstSamples = append(httpFirstSamples, durationMillis(round.httpFirst)) + } + if round.httpLoaded > 0 { + httpLoadedSamples = append(httpLoadedSamples, durationMillis(round.httpLoaded)) + } + } + httpLoaded := upperTrimmedMean(httpLoadedSamples, settings.trimPercent) + if httpLoaded <= 0 { + return 0 + } + var foreignComponents []float64 + if tcp := upperTrimmedMean(tcpSamples, settings.trimPercent); tcp > 0 { + foreignComponents = append(foreignComponents, tcp) + } + if tls := upperTrimmedMean(tlsSamples, settings.trimPercent); tls > 0 { + foreignComponents = append(foreignComponents, tls) + } + if httpFirst := upperTrimmedMean(httpFirstSamples, settings.trimPercent); httpFirst > 0 { + foreignComponents = append(foreignComponents, httpFirst) + } + if len(foreignComponents) == 0 { + return 0 + } + foreignLatency := meanFloat64s(foreignComponents) + foreignRPM := 60000.0 / foreignLatency + loadedRPM := 60000.0 / httpLoaded + return int32(math.Round((foreignRPM + loadedRPM) / 2)) +} + +func tlsHandshakeRoundTrips(version uint16) int { + switch version { + case tls.VersionTLS12, tls.VersionTLS11, tls.VersionTLS10: + return 2 + default: + return 1 + } +} + +func durationMillis(value time.Duration) float64 { + return float64(value) / float64(time.Millisecond) +} + +func upperTrimmedMean(values []float64, trimPercent int) float64 { + trimmed := upperTrimFloat64s(values, trimPercent) + if len(trimmed) == 0 { + return 0 + } + return meanFloat64s(trimmed) +} + +func upperTrimFloat64s(values []float64, trimPercent int) []float64 { + if len(values) == 0 { + return nil + } + trimmed := append([]float64(nil), values...) + sort.Float64s(trimmed) + if trimPercent <= 0 { + return trimmed + } + trimCount := int(math.Floor(float64(len(trimmed)) * float64(trimPercent) / 100)) + if trimCount <= 0 || trimCount >= len(trimmed) { + return trimmed + } + return trimmed[:len(trimmed)-trimCount] +} + +func meanFloat64s(values []float64) float64 { + if len(values) == 0 { + return 0 + } + var total float64 + for _, value := range values { + total += value + } + return total / float64(len(values)) +} + +func stdDevFloat64s(values []float64) float64 { + if len(values) == 0 { + return 0 + } + mean := meanFloat64s(values) + var total float64 + for _, value := range values { + delta := value - mean + total += delta * delta + } + return math.Sqrt(total / float64(len(values))) +} + +type uploadBody struct { + ctx context.Context + activated atomic.Bool + onActive func() +} + +func (u *uploadBody) Read(p []byte) (int, error) { + if err := u.ctx.Err(); err != nil { + return 0, err + } + clear(p) + n := len(p) + if n > 0 && u.onActive != nil && u.activated.CompareAndSwap(false, true) { + u.onActive() + } + return n, nil +} + +func (u *uploadBody) Close() error { + return nil +} diff --git a/common/stun/stun.go b/common/stun/stun.go new file mode 100644 index 0000000000..b4c2313f02 --- /dev/null +++ b/common/stun/stun.go @@ -0,0 +1,607 @@ +package stun + +import ( + "context" + "crypto/rand" + "encoding/binary" + "fmt" + "net" + "net/netip" + "time" + + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +const ( + DefaultServer = "stun.voipgate.com:3478" + + magicCookie = 0x2112A442 + headerSize = 20 + + bindingRequest = 0x0001 + bindingSuccessResponse = 0x0101 + bindingErrorResponse = 0x0111 + + attrMappedAddress = 0x0001 + attrChangeRequest = 0x0003 + attrErrorCode = 0x0009 + attrXORMappedAddress = 0x0020 + attrOtherAddress = 0x802c + + familyIPv4 = 0x01 + familyIPv6 = 0x02 + + changeIP = 0x04 + changePort = 0x02 + + defaultRTO = 500 * time.Millisecond + minRTO = 250 * time.Millisecond + maxRetransmit = 2 +) + +type Phase int32 + +const ( + PhaseBinding Phase = iota + PhaseNATMapping + PhaseNATFiltering + PhaseDone +) + +type NATMapping int32 + +const ( + NATMappingUnknown NATMapping = iota + _ // reserved + NATMappingEndpointIndependent + NATMappingAddressDependent + NATMappingAddressAndPortDependent +) + +func (m NATMapping) String() string { + switch m { + case NATMappingEndpointIndependent: + return "Endpoint Independent" + case NATMappingAddressDependent: + return "Address Dependent" + case NATMappingAddressAndPortDependent: + return "Address and Port Dependent" + default: + return "Unknown" + } +} + +type NATFiltering int32 + +const ( + NATFilteringUnknown NATFiltering = iota + NATFilteringEndpointIndependent + NATFilteringAddressDependent + NATFilteringAddressAndPortDependent +) + +func (f NATFiltering) String() string { + switch f { + case NATFilteringEndpointIndependent: + return "Endpoint Independent" + case NATFilteringAddressDependent: + return "Address Dependent" + case NATFilteringAddressAndPortDependent: + return "Address and Port Dependent" + default: + return "Unknown" + } +} + +type TransactionID [12]byte + +type Options struct { + Server string + Dialer N.Dialer + Context context.Context + OnProgress func(Progress) +} + +type Progress struct { + Phase Phase + ExternalAddr string + LatencyMs int32 + NATMapping NATMapping + NATFiltering NATFiltering +} + +type Result struct { + ExternalAddr string + LatencyMs int32 + NATMapping NATMapping + NATFiltering NATFiltering + NATTypeSupported bool +} + +type parsedResponse struct { + xorMappedAddr netip.AddrPort + mappedAddr netip.AddrPort + otherAddr netip.AddrPort +} + +func (r *parsedResponse) externalAddr() (netip.AddrPort, bool) { + if r.xorMappedAddr.IsValid() { + return r.xorMappedAddr, true + } + if r.mappedAddr.IsValid() { + return r.mappedAddr, true + } + return netip.AddrPort{}, false +} + +type stunAttribute struct { + typ uint16 + value []byte +} + +func newTransactionID() TransactionID { + var id TransactionID + _, _ = rand.Read(id[:]) + return id +} + +func buildBindingRequest(txID TransactionID, attrs ...stunAttribute) []byte { + attrLen := 0 + for _, attr := range attrs { + attrLen += 4 + len(attr.value) + paddingLen(len(attr.value)) + } + + buf := make([]byte, headerSize+attrLen) + binary.BigEndian.PutUint16(buf[0:2], bindingRequest) + binary.BigEndian.PutUint16(buf[2:4], uint16(attrLen)) + binary.BigEndian.PutUint32(buf[4:8], magicCookie) + copy(buf[8:20], txID[:]) + + offset := headerSize + for _, attr := range attrs { + binary.BigEndian.PutUint16(buf[offset:offset+2], attr.typ) + binary.BigEndian.PutUint16(buf[offset+2:offset+4], uint16(len(attr.value))) + copy(buf[offset+4:offset+4+len(attr.value)], attr.value) + offset += 4 + len(attr.value) + paddingLen(len(attr.value)) + } + + return buf +} + +func changeRequestAttr(flags byte) stunAttribute { + return stunAttribute{ + typ: attrChangeRequest, + value: []byte{0, 0, 0, flags}, + } +} + +func parseResponse(data []byte, expectedTxID TransactionID) (*parsedResponse, error) { + if len(data) < headerSize { + return nil, E.New("response too short") + } + + msgType := binary.BigEndian.Uint16(data[0:2]) + if msgType&0xC000 != 0 { + return nil, E.New("invalid STUN message: top 2 bits not zero") + } + + cookie := binary.BigEndian.Uint32(data[4:8]) + if cookie != magicCookie { + return nil, E.New("invalid magic cookie") + } + + var txID TransactionID + copy(txID[:], data[8:20]) + if txID != expectedTxID { + return nil, E.New("transaction ID mismatch") + } + + msgLen := int(binary.BigEndian.Uint16(data[2:4])) + if msgLen > len(data)-headerSize { + return nil, E.New("message length exceeds data") + } + + attrData := data[headerSize : headerSize+msgLen] + + if msgType == bindingErrorResponse { + return nil, parseErrorResponse(attrData) + } + if msgType != bindingSuccessResponse { + return nil, E.New("unexpected message type: ", fmt.Sprintf("0x%04x", msgType)) + } + + resp := &parsedResponse{} + offset := 0 + for offset+4 <= len(attrData) { + attrType := binary.BigEndian.Uint16(attrData[offset : offset+2]) + attrLen := int(binary.BigEndian.Uint16(attrData[offset+2 : offset+4])) + if offset+4+attrLen > len(attrData) { + break + } + attrValue := attrData[offset+4 : offset+4+attrLen] + + switch attrType { + case attrXORMappedAddress: + addr, err := parseXORMappedAddress(attrValue, txID) + if err == nil { + resp.xorMappedAddr = addr + } + case attrMappedAddress: + addr, err := parseMappedAddress(attrValue) + if err == nil { + resp.mappedAddr = addr + } + case attrOtherAddress: + addr, err := parseMappedAddress(attrValue) + if err == nil { + resp.otherAddr = addr + } + } + + offset += 4 + attrLen + paddingLen(attrLen) + } + + return resp, nil +} + +func parseErrorResponse(data []byte) error { + offset := 0 + for offset+4 <= len(data) { + attrType := binary.BigEndian.Uint16(data[offset : offset+2]) + attrLen := int(binary.BigEndian.Uint16(data[offset+2 : offset+4])) + if offset+4+attrLen > len(data) { + break + } + if attrType == attrErrorCode && attrLen >= 4 { + attrValue := data[offset+4 : offset+4+attrLen] + class := int(attrValue[2] & 0x07) + number := int(attrValue[3]) + code := class*100 + number + if attrLen > 4 { + return E.New("STUN error ", code, ": ", string(attrValue[4:])) + } + return E.New("STUN error ", code) + } + offset += 4 + attrLen + paddingLen(attrLen) + } + return E.New("STUN error response") +} + +func parseXORMappedAddress(data []byte, txID TransactionID) (netip.AddrPort, error) { + if len(data) < 4 { + return netip.AddrPort{}, E.New("XOR-MAPPED-ADDRESS too short") + } + + family := data[1] + xPort := binary.BigEndian.Uint16(data[2:4]) + port := xPort ^ uint16(magicCookie>>16) + + switch family { + case familyIPv4: + if len(data) < 8 { + return netip.AddrPort{}, E.New("XOR-MAPPED-ADDRESS IPv4 too short") + } + var ip [4]byte + binary.BigEndian.PutUint32(ip[:], binary.BigEndian.Uint32(data[4:8])^magicCookie) + return netip.AddrPortFrom(netip.AddrFrom4(ip), port), nil + case familyIPv6: + if len(data) < 20 { + return netip.AddrPort{}, E.New("XOR-MAPPED-ADDRESS IPv6 too short") + } + var ip [16]byte + var xorKey [16]byte + binary.BigEndian.PutUint32(xorKey[0:4], magicCookie) + copy(xorKey[4:16], txID[:]) + for i := range 16 { + ip[i] = data[4+i] ^ xorKey[i] + } + return netip.AddrPortFrom(netip.AddrFrom16(ip), port), nil + default: + return netip.AddrPort{}, E.New("unknown address family: ", family) + } +} + +func parseMappedAddress(data []byte) (netip.AddrPort, error) { + if len(data) < 4 { + return netip.AddrPort{}, E.New("MAPPED-ADDRESS too short") + } + + family := data[1] + port := binary.BigEndian.Uint16(data[2:4]) + + switch family { + case familyIPv4: + if len(data) < 8 { + return netip.AddrPort{}, E.New("MAPPED-ADDRESS IPv4 too short") + } + return netip.AddrPortFrom( + netip.AddrFrom4([4]byte{data[4], data[5], data[6], data[7]}), port, + ), nil + case familyIPv6: + if len(data) < 20 { + return netip.AddrPort{}, E.New("MAPPED-ADDRESS IPv6 too short") + } + var ip [16]byte + copy(ip[:], data[4:20]) + return netip.AddrPortFrom(netip.AddrFrom16(ip), port), nil + default: + return netip.AddrPort{}, E.New("unknown address family: ", family) + } +} + +func roundTrip(conn net.PacketConn, addr net.Addr, txID TransactionID, attrs []stunAttribute, rto time.Duration) (*parsedResponse, time.Duration, error) { + request := buildBindingRequest(txID, attrs...) + currentRTO := rto + retransmitCount := 0 + + sendTime := time.Now() + _, err := conn.WriteTo(request, addr) + if err != nil { + return nil, 0, E.Cause(err, "send STUN request") + } + + buf := make([]byte, 1024) + for { + err = conn.SetReadDeadline(sendTime.Add(currentRTO)) + if err != nil { + return nil, 0, E.Cause(err, "set read deadline") + } + + n, _, readErr := conn.ReadFrom(buf) + if readErr != nil { + if E.IsTimeout(readErr) && retransmitCount < maxRetransmit { + retransmitCount++ + currentRTO *= 2 + sendTime = time.Now() + _, err = conn.WriteTo(request, addr) + if err != nil { + return nil, 0, E.Cause(err, "retransmit STUN request") + } + continue + } + return nil, 0, E.Cause(readErr, "read STUN response") + } + + if n < headerSize || buf[0]&0xC0 != 0 || + binary.BigEndian.Uint32(buf[4:8]) != magicCookie { + continue + } + var receivedTxID TransactionID + copy(receivedTxID[:], buf[8:20]) + if receivedTxID != txID { + continue + } + + latency := time.Since(sendTime) + + resp, parseErr := parseResponse(buf[:n], txID) + if parseErr != nil { + return nil, 0, parseErr + } + + return resp, latency, nil + } +} + +func Run(options Options) (*Result, error) { + ctx := options.Context + if ctx == nil { + ctx = context.Background() + } + + server := options.Server + if server == "" { + server = DefaultServer + } + serverSocksaddr := M.ParseSocksaddr(server) + if serverSocksaddr.Port == 0 { + serverSocksaddr.Port = 3478 + } + + reportProgress := options.OnProgress + if reportProgress == nil { + reportProgress = func(Progress) {} + } + + var ( + packetConn net.PacketConn + serverAddr net.Addr + err error + ) + + if options.Dialer != nil { + packetConn, err = options.Dialer.ListenPacket(ctx, serverSocksaddr) + if err != nil { + return nil, E.Cause(err, "create UDP socket") + } + serverAddr = serverSocksaddr + } else { + serverUDPAddr, resolveErr := net.ResolveUDPAddr("udp", serverSocksaddr.String()) + if resolveErr != nil { + return nil, E.Cause(resolveErr, "resolve STUN server") + } + packetConn, err = net.ListenPacket("udp", "") + if err != nil { + return nil, E.Cause(err, "create UDP socket") + } + serverAddr = serverUDPAddr + } + defer func() { + _ = packetConn.Close() + }() + + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + rto := defaultRTO + + // Phase 1: Binding + reportProgress(Progress{Phase: PhaseBinding}) + + txID := newTransactionID() + resp, latency, err := roundTrip(packetConn, serverAddr, txID, nil, rto) + if err != nil { + return nil, E.Cause(err, "binding request") + } + + rto = max(minRTO, 3*latency) + + externalAddr, ok := resp.externalAddr() + if !ok { + return nil, E.New("no mapped address in response") + } + + result := &Result{ + ExternalAddr: externalAddr.String(), + LatencyMs: int32(latency.Milliseconds()), + } + + reportProgress(Progress{ + Phase: PhaseBinding, + ExternalAddr: result.ExternalAddr, + LatencyMs: result.LatencyMs, + }) + + otherAddr := resp.otherAddr + if !otherAddr.IsValid() { + result.NATTypeSupported = false + reportProgress(Progress{ + Phase: PhaseDone, + ExternalAddr: result.ExternalAddr, + LatencyMs: result.LatencyMs, + }) + return result, nil + } + result.NATTypeSupported = true + + select { + case <-ctx.Done(): + return result, nil + default: + } + + // Phase 2: NAT Mapping Detection (RFC 5780 Section 4.3) + reportProgress(Progress{ + Phase: PhaseNATMapping, + ExternalAddr: result.ExternalAddr, + LatencyMs: result.LatencyMs, + }) + + result.NATMapping = detectNATMapping( + packetConn, serverSocksaddr.Port, externalAddr, otherAddr, rto, + ) + + reportProgress(Progress{ + Phase: PhaseNATMapping, + ExternalAddr: result.ExternalAddr, + LatencyMs: result.LatencyMs, + NATMapping: result.NATMapping, + }) + + select { + case <-ctx.Done(): + return result, nil + default: + } + + // Phase 3: NAT Filtering Detection (RFC 5780 Section 4.4) + reportProgress(Progress{ + Phase: PhaseNATFiltering, + ExternalAddr: result.ExternalAddr, + LatencyMs: result.LatencyMs, + NATMapping: result.NATMapping, + }) + + result.NATFiltering = detectNATFiltering(packetConn, serverAddr, rto) + + reportProgress(Progress{ + Phase: PhaseDone, + ExternalAddr: result.ExternalAddr, + LatencyMs: result.LatencyMs, + NATMapping: result.NATMapping, + NATFiltering: result.NATFiltering, + }) + + return result, nil +} + +func detectNATMapping( + conn net.PacketConn, + serverPort uint16, + externalAddr netip.AddrPort, + otherAddr netip.AddrPort, + rto time.Duration, +) NATMapping { + // Mapping Test II: Send to other_ip:server_port + testIIAddr := net.UDPAddrFromAddrPort( + netip.AddrPortFrom(otherAddr.Addr(), serverPort), + ) + txID2 := newTransactionID() + resp2, _, err := roundTrip(conn, testIIAddr, txID2, nil, rto) + if err != nil { + return NATMappingUnknown + } + + externalAddr2, ok := resp2.externalAddr() + if !ok { + return NATMappingUnknown + } + + if externalAddr == externalAddr2 { + return NATMappingEndpointIndependent + } + + // Mapping Test III: Send to other_ip:other_port + testIIIAddr := net.UDPAddrFromAddrPort(otherAddr) + txID3 := newTransactionID() + resp3, _, err := roundTrip(conn, testIIIAddr, txID3, nil, rto) + if err != nil { + return NATMappingUnknown + } + + externalAddr3, ok := resp3.externalAddr() + if !ok { + return NATMappingUnknown + } + + if externalAddr2 == externalAddr3 { + return NATMappingAddressDependent + } + return NATMappingAddressAndPortDependent +} + +func detectNATFiltering( + conn net.PacketConn, + serverAddr net.Addr, + rto time.Duration, +) NATFiltering { + // Filtering Test II: Request response from different IP and port + txID := newTransactionID() + _, _, err := roundTrip(conn, serverAddr, txID, + []stunAttribute{changeRequestAttr(changeIP | changePort)}, rto) + if err == nil { + return NATFilteringEndpointIndependent + } + + // Filtering Test III: Request response from different port only + txID = newTransactionID() + _, _, err = roundTrip(conn, serverAddr, txID, + []stunAttribute{changeRequestAttr(changePort)}, rto) + if err == nil { + return NATFilteringAddressDependent + } + + return NATFilteringAddressAndPortDependent +} + +func paddingLen(n int) int { + if n%4 == 0 { + return 0 + } + return 4 - n%4 +} diff --git a/daemon/started_service.go b/daemon/started_service.go index bdae10f7d5..82336a7d4f 100644 --- a/daemon/started_service.go +++ b/daemon/started_service.go @@ -8,6 +8,9 @@ import ( "time" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/networkquality" + "github.com/sagernet/sing-box/common/stun" "github.com/sagernet/sing-box/common/urltest" "github.com/sagernet/sing-box/experimental/clashapi" "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" @@ -691,7 +694,7 @@ func (s *StartedService) SetSystemProxyEnabled(ctx context.Context, request *Set if err != nil { return nil, err } - return nil, err + return &emptypb.Empty{}, nil } func (s *StartedService) TriggerDebugCrash(ctx context.Context, request *DebugCrashRequest) (*emptypb.Empty, error) { @@ -1080,6 +1083,210 @@ func (s *StartedService) GetStartedAt(ctx context.Context, empty *emptypb.Empty) return &StartedAt{StartedAt: s.startedAt.UnixMilli()}, nil } +func (s *StartedService) ListOutbounds(ctx context.Context, _ *emptypb.Empty) (*OutboundList, error) { + s.serviceAccess.RLock() + if s.serviceStatus.Status != ServiceStatus_STARTED { + s.serviceAccess.RUnlock() + return nil, os.ErrInvalid + } + boxService := s.instance + s.serviceAccess.RUnlock() + historyStorage := boxService.urlTestHistoryStorage + outbounds := boxService.instance.Outbound().Outbounds() + var list OutboundList + for _, ob := range outbounds { + item := &GroupItem{ + Tag: ob.Tag(), + Type: ob.Type(), + } + if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(ob)); history != nil { + item.UrlTestTime = history.Time.Unix() + item.UrlTestDelay = int32(history.Delay) + } + list.Outbounds = append(list.Outbounds, item) + } + return &list, nil +} + +func (s *StartedService) SubscribeOutbounds(_ *emptypb.Empty, server grpc.ServerStreamingServer[OutboundList]) error { + err := s.waitForStarted(server.Context()) + if err != nil { + return err + } + subscription, done, err := s.urlTestObserver.Subscribe() + if err != nil { + return err + } + defer s.urlTestObserver.UnSubscribe(subscription) + for { + s.serviceAccess.RLock() + if s.serviceStatus.Status != ServiceStatus_STARTED { + s.serviceAccess.RUnlock() + return os.ErrInvalid + } + boxService := s.instance + s.serviceAccess.RUnlock() + historyStorage := boxService.urlTestHistoryStorage + outbounds := boxService.instance.Outbound().Outbounds() + var list OutboundList + for _, ob := range outbounds { + item := &GroupItem{ + Tag: ob.Tag(), + Type: ob.Type(), + } + if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(ob)); history != nil { + item.UrlTestTime = history.Time.Unix() + item.UrlTestDelay = int32(history.Delay) + } + list.Outbounds = append(list.Outbounds, item) + } + err = server.Send(&list) + if err != nil { + return err + } + select { + case <-subscription: + case <-s.ctx.Done(): + return s.ctx.Err() + case <-server.Context().Done(): + return server.Context().Err() + case <-done: + return nil + } + } +} + +func resolveOutbound(instance *Instance, tag string) (adapter.Outbound, error) { + if tag == "" { + return instance.instance.Outbound().Default(), nil + } + outbound, loaded := instance.instance.Outbound().Outbound(tag) + if !loaded { + return nil, E.New("outbound not found: ", tag) + } + return outbound, nil +} + +func (s *StartedService) StartNetworkQualityTest( + request *NetworkQualityTestRequest, + server grpc.ServerStreamingServer[NetworkQualityTestProgress], +) error { + err := s.waitForStarted(server.Context()) + if err != nil { + return err + } + s.serviceAccess.RLock() + boxService := s.instance + s.serviceAccess.RUnlock() + + outbound, err := resolveOutbound(boxService, request.OutboundTag) + if err != nil { + return err + } + + resolvedDialer := dialer.NewResolveDialer(boxService.ctx, outbound, true, "", adapter.DNSQueryOptions{}, 0) + httpClient := networkquality.NewHTTPClient(resolvedDialer) + defer httpClient.CloseIdleConnections() + + measurementClientFactory, err := networkquality.NewOptionalHTTP3Factory(resolvedDialer, request.Http3) + if err != nil { + return err + } + + result, nqErr := networkquality.Run(networkquality.Options{ + ConfigURL: request.ConfigURL, + HTTPClient: httpClient, + NewMeasurementClient: measurementClientFactory, + Serial: request.Serial, + MaxRuntime: time.Duration(request.MaxRuntimeSeconds) * time.Second, + Context: server.Context(), + OnProgress: func(p networkquality.Progress) { + _ = server.Send(&NetworkQualityTestProgress{ + Phase: int32(p.Phase), + DownloadCapacity: p.DownloadCapacity, + UploadCapacity: p.UploadCapacity, + DownloadRPM: p.DownloadRPM, + UploadRPM: p.UploadRPM, + IdleLatencyMs: p.IdleLatencyMs, + ElapsedMs: p.ElapsedMs, + DownloadCapacityAccuracy: int32(p.DownloadCapacityAccuracy), + UploadCapacityAccuracy: int32(p.UploadCapacityAccuracy), + DownloadRPMAccuracy: int32(p.DownloadRPMAccuracy), + UploadRPMAccuracy: int32(p.UploadRPMAccuracy), + }) + }, + }) + if nqErr != nil { + return server.Send(&NetworkQualityTestProgress{ + IsFinal: true, + Error: nqErr.Error(), + }) + } + return server.Send(&NetworkQualityTestProgress{ + Phase: int32(networkquality.PhaseDone), + DownloadCapacity: result.DownloadCapacity, + UploadCapacity: result.UploadCapacity, + DownloadRPM: result.DownloadRPM, + UploadRPM: result.UploadRPM, + IdleLatencyMs: result.IdleLatencyMs, + IsFinal: true, + DownloadCapacityAccuracy: int32(result.DownloadCapacityAccuracy), + UploadCapacityAccuracy: int32(result.UploadCapacityAccuracy), + DownloadRPMAccuracy: int32(result.DownloadRPMAccuracy), + UploadRPMAccuracy: int32(result.UploadRPMAccuracy), + }) +} + +func (s *StartedService) StartSTUNTest( + request *STUNTestRequest, + server grpc.ServerStreamingServer[STUNTestProgress], +) error { + err := s.waitForStarted(server.Context()) + if err != nil { + return err + } + s.serviceAccess.RLock() + boxService := s.instance + s.serviceAccess.RUnlock() + + outbound, err := resolveOutbound(boxService, request.OutboundTag) + if err != nil { + return err + } + + resolvedDialer := dialer.NewResolveDialer(boxService.ctx, outbound, true, "", adapter.DNSQueryOptions{}, 0) + + result, stunErr := stun.Run(stun.Options{ + Server: request.Server, + Dialer: resolvedDialer, + Context: server.Context(), + OnProgress: func(p stun.Progress) { + _ = server.Send(&STUNTestProgress{ + Phase: int32(p.Phase), + ExternalAddr: p.ExternalAddr, + LatencyMs: p.LatencyMs, + NatMapping: int32(p.NATMapping), + NatFiltering: int32(p.NATFiltering), + }) + }, + }) + if stunErr != nil { + return server.Send(&STUNTestProgress{ + IsFinal: true, + Error: stunErr.Error(), + }) + } + return server.Send(&STUNTestProgress{ + Phase: int32(stun.PhaseDone), + ExternalAddr: result.ExternalAddr, + LatencyMs: result.LatencyMs, + NatMapping: int32(result.NATMapping), + NatFiltering: int32(result.NATFiltering), + IsFinal: true, + NatTypeSupported: result.NATTypeSupported, + }) +} + func (s *StartedService) mustEmbedUnimplementedStartedServiceServer() { } diff --git a/daemon/started_service.pb.go b/daemon/started_service.pb.go index 271f80a114..c48ea4fe79 100644 --- a/daemon/started_service.pb.go +++ b/daemon/started_service.pb.go @@ -1836,6 +1836,418 @@ func (x *StartedAt) GetStartedAt() int64 { return 0 } +type OutboundList struct { + state protoimpl.MessageState `protogen:"open.v1"` + Outbounds []*GroupItem `protobuf:"bytes,1,rep,name=outbounds,proto3" json:"outbounds,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OutboundList) Reset() { + *x = OutboundList{} + mi := &file_daemon_started_service_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OutboundList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OutboundList) ProtoMessage() {} + +func (x *OutboundList) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OutboundList.ProtoReflect.Descriptor instead. +func (*OutboundList) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{26} +} + +func (x *OutboundList) GetOutbounds() []*GroupItem { + if x != nil { + return x.Outbounds + } + return nil +} + +type NetworkQualityTestRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConfigURL string `protobuf:"bytes,1,opt,name=configURL,proto3" json:"configURL,omitempty"` + OutboundTag string `protobuf:"bytes,2,opt,name=outboundTag,proto3" json:"outboundTag,omitempty"` + Serial bool `protobuf:"varint,3,opt,name=serial,proto3" json:"serial,omitempty"` + MaxRuntimeSeconds int32 `protobuf:"varint,4,opt,name=maxRuntimeSeconds,proto3" json:"maxRuntimeSeconds,omitempty"` + Http3 bool `protobuf:"varint,5,opt,name=http3,proto3" json:"http3,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkQualityTestRequest) Reset() { + *x = NetworkQualityTestRequest{} + mi := &file_daemon_started_service_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkQualityTestRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkQualityTestRequest) ProtoMessage() {} + +func (x *NetworkQualityTestRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NetworkQualityTestRequest.ProtoReflect.Descriptor instead. +func (*NetworkQualityTestRequest) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{27} +} + +func (x *NetworkQualityTestRequest) GetConfigURL() string { + if x != nil { + return x.ConfigURL + } + return "" +} + +func (x *NetworkQualityTestRequest) GetOutboundTag() string { + if x != nil { + return x.OutboundTag + } + return "" +} + +func (x *NetworkQualityTestRequest) GetSerial() bool { + if x != nil { + return x.Serial + } + return false +} + +func (x *NetworkQualityTestRequest) GetMaxRuntimeSeconds() int32 { + if x != nil { + return x.MaxRuntimeSeconds + } + return 0 +} + +func (x *NetworkQualityTestRequest) GetHttp3() bool { + if x != nil { + return x.Http3 + } + return false +} + +type NetworkQualityTestProgress struct { + state protoimpl.MessageState `protogen:"open.v1"` + Phase int32 `protobuf:"varint,1,opt,name=phase,proto3" json:"phase,omitempty"` + DownloadCapacity int64 `protobuf:"varint,2,opt,name=downloadCapacity,proto3" json:"downloadCapacity,omitempty"` + UploadCapacity int64 `protobuf:"varint,3,opt,name=uploadCapacity,proto3" json:"uploadCapacity,omitempty"` + DownloadRPM int32 `protobuf:"varint,4,opt,name=downloadRPM,proto3" json:"downloadRPM,omitempty"` + UploadRPM int32 `protobuf:"varint,5,opt,name=uploadRPM,proto3" json:"uploadRPM,omitempty"` + IdleLatencyMs int32 `protobuf:"varint,6,opt,name=idleLatencyMs,proto3" json:"idleLatencyMs,omitempty"` + ElapsedMs int64 `protobuf:"varint,7,opt,name=elapsedMs,proto3" json:"elapsedMs,omitempty"` + IsFinal bool `protobuf:"varint,8,opt,name=isFinal,proto3" json:"isFinal,omitempty"` + Error string `protobuf:"bytes,9,opt,name=error,proto3" json:"error,omitempty"` + DownloadCapacityAccuracy int32 `protobuf:"varint,10,opt,name=downloadCapacityAccuracy,proto3" json:"downloadCapacityAccuracy,omitempty"` + UploadCapacityAccuracy int32 `protobuf:"varint,11,opt,name=uploadCapacityAccuracy,proto3" json:"uploadCapacityAccuracy,omitempty"` + DownloadRPMAccuracy int32 `protobuf:"varint,12,opt,name=downloadRPMAccuracy,proto3" json:"downloadRPMAccuracy,omitempty"` + UploadRPMAccuracy int32 `protobuf:"varint,13,opt,name=uploadRPMAccuracy,proto3" json:"uploadRPMAccuracy,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NetworkQualityTestProgress) Reset() { + *x = NetworkQualityTestProgress{} + mi := &file_daemon_started_service_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NetworkQualityTestProgress) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkQualityTestProgress) ProtoMessage() {} + +func (x *NetworkQualityTestProgress) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NetworkQualityTestProgress.ProtoReflect.Descriptor instead. +func (*NetworkQualityTestProgress) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{28} +} + +func (x *NetworkQualityTestProgress) GetPhase() int32 { + if x != nil { + return x.Phase + } + return 0 +} + +func (x *NetworkQualityTestProgress) GetDownloadCapacity() int64 { + if x != nil { + return x.DownloadCapacity + } + return 0 +} + +func (x *NetworkQualityTestProgress) GetUploadCapacity() int64 { + if x != nil { + return x.UploadCapacity + } + return 0 +} + +func (x *NetworkQualityTestProgress) GetDownloadRPM() int32 { + if x != nil { + return x.DownloadRPM + } + return 0 +} + +func (x *NetworkQualityTestProgress) GetUploadRPM() int32 { + if x != nil { + return x.UploadRPM + } + return 0 +} + +func (x *NetworkQualityTestProgress) GetIdleLatencyMs() int32 { + if x != nil { + return x.IdleLatencyMs + } + return 0 +} + +func (x *NetworkQualityTestProgress) GetElapsedMs() int64 { + if x != nil { + return x.ElapsedMs + } + return 0 +} + +func (x *NetworkQualityTestProgress) GetIsFinal() bool { + if x != nil { + return x.IsFinal + } + return false +} + +func (x *NetworkQualityTestProgress) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *NetworkQualityTestProgress) GetDownloadCapacityAccuracy() int32 { + if x != nil { + return x.DownloadCapacityAccuracy + } + return 0 +} + +func (x *NetworkQualityTestProgress) GetUploadCapacityAccuracy() int32 { + if x != nil { + return x.UploadCapacityAccuracy + } + return 0 +} + +func (x *NetworkQualityTestProgress) GetDownloadRPMAccuracy() int32 { + if x != nil { + return x.DownloadRPMAccuracy + } + return 0 +} + +func (x *NetworkQualityTestProgress) GetUploadRPMAccuracy() int32 { + if x != nil { + return x.UploadRPMAccuracy + } + return 0 +} + +type STUNTestRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Server string `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"` + OutboundTag string `protobuf:"bytes,2,opt,name=outboundTag,proto3" json:"outboundTag,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *STUNTestRequest) Reset() { + *x = STUNTestRequest{} + mi := &file_daemon_started_service_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *STUNTestRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*STUNTestRequest) ProtoMessage() {} + +func (x *STUNTestRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use STUNTestRequest.ProtoReflect.Descriptor instead. +func (*STUNTestRequest) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{29} +} + +func (x *STUNTestRequest) GetServer() string { + if x != nil { + return x.Server + } + return "" +} + +func (x *STUNTestRequest) GetOutboundTag() string { + if x != nil { + return x.OutboundTag + } + return "" +} + +type STUNTestProgress struct { + state protoimpl.MessageState `protogen:"open.v1"` + Phase int32 `protobuf:"varint,1,opt,name=phase,proto3" json:"phase,omitempty"` + ExternalAddr string `protobuf:"bytes,2,opt,name=externalAddr,proto3" json:"externalAddr,omitempty"` + LatencyMs int32 `protobuf:"varint,3,opt,name=latencyMs,proto3" json:"latencyMs,omitempty"` + NatMapping int32 `protobuf:"varint,4,opt,name=natMapping,proto3" json:"natMapping,omitempty"` + NatFiltering int32 `protobuf:"varint,5,opt,name=natFiltering,proto3" json:"natFiltering,omitempty"` + IsFinal bool `protobuf:"varint,6,opt,name=isFinal,proto3" json:"isFinal,omitempty"` + Error string `protobuf:"bytes,7,opt,name=error,proto3" json:"error,omitempty"` + NatTypeSupported bool `protobuf:"varint,8,opt,name=natTypeSupported,proto3" json:"natTypeSupported,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *STUNTestProgress) Reset() { + *x = STUNTestProgress{} + mi := &file_daemon_started_service_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *STUNTestProgress) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*STUNTestProgress) ProtoMessage() {} + +func (x *STUNTestProgress) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use STUNTestProgress.ProtoReflect.Descriptor instead. +func (*STUNTestProgress) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{30} +} + +func (x *STUNTestProgress) GetPhase() int32 { + if x != nil { + return x.Phase + } + return 0 +} + +func (x *STUNTestProgress) GetExternalAddr() string { + if x != nil { + return x.ExternalAddr + } + return "" +} + +func (x *STUNTestProgress) GetLatencyMs() int32 { + if x != nil { + return x.LatencyMs + } + return 0 +} + +func (x *STUNTestProgress) GetNatMapping() int32 { + if x != nil { + return x.NatMapping + } + return 0 +} + +func (x *STUNTestProgress) GetNatFiltering() int32 { + if x != nil { + return x.NatFiltering + } + return 0 +} + +func (x *STUNTestProgress) GetIsFinal() bool { + if x != nil { + return x.IsFinal + } + return false +} + +func (x *STUNTestProgress) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *STUNTestProgress) GetNatTypeSupported() bool { + if x != nil { + return x.NatTypeSupported + } + return false +} + type Log_Message struct { state protoimpl.MessageState `protogen:"open.v1"` Level LogLevel `protobuf:"varint,1,opt,name=level,proto3,enum=daemon.LogLevel" json:"level,omitempty"` @@ -1846,7 +2258,7 @@ type Log_Message struct { func (x *Log_Message) Reset() { *x = Log_Message{} - mi := &file_daemon_started_service_proto_msgTypes[26] + mi := &file_daemon_started_service_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1858,7 +2270,7 @@ func (x *Log_Message) String() string { func (*Log_Message) ProtoMessage() {} func (x *Log_Message) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[26] + mi := &file_daemon_started_service_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2023,7 +2435,44 @@ const file_daemon_started_service_proto_rawDesc = "" + "\x11deprecatedVersion\x18\x05 \x01(\tR\x11deprecatedVersion\x12*\n" + "\x10scheduledVersion\x18\x06 \x01(\tR\x10scheduledVersion\")\n" + "\tStartedAt\x12\x1c\n" + - "\tstartedAt\x18\x01 \x01(\x03R\tstartedAt*U\n" + + "\tstartedAt\x18\x01 \x01(\x03R\tstartedAt\"?\n" + + "\fOutboundList\x12/\n" + + "\toutbounds\x18\x01 \x03(\v2\x11.daemon.GroupItemR\toutbounds\"\xb7\x01\n" + + "\x19NetworkQualityTestRequest\x12\x1c\n" + + "\tconfigURL\x18\x01 \x01(\tR\tconfigURL\x12 \n" + + "\voutboundTag\x18\x02 \x01(\tR\voutboundTag\x12\x16\n" + + "\x06serial\x18\x03 \x01(\bR\x06serial\x12,\n" + + "\x11maxRuntimeSeconds\x18\x04 \x01(\x05R\x11maxRuntimeSeconds\x12\x14\n" + + "\x05http3\x18\x05 \x01(\bR\x05http3\"\x8e\x04\n" + + "\x1aNetworkQualityTestProgress\x12\x14\n" + + "\x05phase\x18\x01 \x01(\x05R\x05phase\x12*\n" + + "\x10downloadCapacity\x18\x02 \x01(\x03R\x10downloadCapacity\x12&\n" + + "\x0euploadCapacity\x18\x03 \x01(\x03R\x0euploadCapacity\x12 \n" + + "\vdownloadRPM\x18\x04 \x01(\x05R\vdownloadRPM\x12\x1c\n" + + "\tuploadRPM\x18\x05 \x01(\x05R\tuploadRPM\x12$\n" + + "\ridleLatencyMs\x18\x06 \x01(\x05R\ridleLatencyMs\x12\x1c\n" + + "\telapsedMs\x18\a \x01(\x03R\telapsedMs\x12\x18\n" + + "\aisFinal\x18\b \x01(\bR\aisFinal\x12\x14\n" + + "\x05error\x18\t \x01(\tR\x05error\x12:\n" + + "\x18downloadCapacityAccuracy\x18\n" + + " \x01(\x05R\x18downloadCapacityAccuracy\x126\n" + + "\x16uploadCapacityAccuracy\x18\v \x01(\x05R\x16uploadCapacityAccuracy\x120\n" + + "\x13downloadRPMAccuracy\x18\f \x01(\x05R\x13downloadRPMAccuracy\x12,\n" + + "\x11uploadRPMAccuracy\x18\r \x01(\x05R\x11uploadRPMAccuracy\"K\n" + + "\x0fSTUNTestRequest\x12\x16\n" + + "\x06server\x18\x01 \x01(\tR\x06server\x12 \n" + + "\voutboundTag\x18\x02 \x01(\tR\voutboundTag\"\x8a\x02\n" + + "\x10STUNTestProgress\x12\x14\n" + + "\x05phase\x18\x01 \x01(\x05R\x05phase\x12\"\n" + + "\fexternalAddr\x18\x02 \x01(\tR\fexternalAddr\x12\x1c\n" + + "\tlatencyMs\x18\x03 \x01(\x05R\tlatencyMs\x12\x1e\n" + + "\n" + + "natMapping\x18\x04 \x01(\x05R\n" + + "natMapping\x12\"\n" + + "\fnatFiltering\x18\x05 \x01(\x05R\fnatFiltering\x12\x18\n" + + "\aisFinal\x18\x06 \x01(\bR\aisFinal\x12\x14\n" + + "\x05error\x18\a \x01(\tR\x05error\x12*\n" + + "\x10natTypeSupported\x18\b \x01(\bR\x10natTypeSupported*U\n" + "\bLogLevel\x12\t\n" + "\x05PANIC\x10\x00\x12\t\n" + "\x05FATAL\x10\x01\x12\t\n" + @@ -2035,7 +2484,7 @@ const file_daemon_started_service_proto_rawDesc = "" + "\x13ConnectionEventType\x12\x18\n" + "\x14CONNECTION_EVENT_NEW\x10\x00\x12\x1b\n" + "\x17CONNECTION_EVENT_UPDATE\x10\x01\x12\x1b\n" + - "\x17CONNECTION_EVENT_CLOSED\x10\x022\xf5\f\n" + + "\x17CONNECTION_EVENT_CLOSED\x10\x022\xac\x0f\n" + "\x0eStartedService\x12=\n" + "\vStopService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12?\n" + "\rReloadService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12K\n" + @@ -2059,7 +2508,11 @@ const file_daemon_started_service_proto_rawDesc = "" + "\x0fCloseConnection\x12\x1e.daemon.CloseConnectionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12G\n" + "\x13CloseAllConnections\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12M\n" + "\x15GetDeprecatedWarnings\x12\x16.google.protobuf.Empty\x1a\x1a.daemon.DeprecatedWarnings\"\x00\x12;\n" + - "\fGetStartedAt\x12\x16.google.protobuf.Empty\x1a\x11.daemon.StartedAt\"\x00B%Z#github.com/sagernet/sing-box/daemonb\x06proto3" + "\fGetStartedAt\x12\x16.google.protobuf.Empty\x1a\x11.daemon.StartedAt\"\x00\x12?\n" + + "\rListOutbounds\x12\x16.google.protobuf.Empty\x1a\x14.daemon.OutboundList\"\x00\x12F\n" + + "\x12SubscribeOutbounds\x12\x16.google.protobuf.Empty\x1a\x14.daemon.OutboundList\"\x000\x01\x12d\n" + + "\x17StartNetworkQualityTest\x12!.daemon.NetworkQualityTestRequest\x1a\".daemon.NetworkQualityTestProgress\"\x000\x01\x12F\n" + + "\rStartSTUNTest\x12\x17.daemon.STUNTestRequest\x1a\x18.daemon.STUNTestProgress\"\x000\x01B%Z#github.com/sagernet/sing-box/daemonb\x06proto3" var ( file_daemon_started_service_proto_rawDescOnce sync.Once @@ -2075,7 +2528,7 @@ func file_daemon_started_service_proto_rawDescGZIP() []byte { var ( file_daemon_started_service_proto_enumTypes = make([]protoimpl.EnumInfo, 4) - file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 27) + file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 32) file_daemon_started_service_proto_goTypes = []any{ (LogLevel)(0), // 0: daemon.LogLevel (ConnectionEventType)(0), // 1: daemon.ConnectionEventType @@ -2107,14 +2560,19 @@ var ( (*DeprecatedWarnings)(nil), // 27: daemon.DeprecatedWarnings (*DeprecatedWarning)(nil), // 28: daemon.DeprecatedWarning (*StartedAt)(nil), // 29: daemon.StartedAt - (*Log_Message)(nil), // 30: daemon.Log.Message - (*emptypb.Empty)(nil), // 31: google.protobuf.Empty + (*OutboundList)(nil), // 30: daemon.OutboundList + (*NetworkQualityTestRequest)(nil), // 31: daemon.NetworkQualityTestRequest + (*NetworkQualityTestProgress)(nil), // 32: daemon.NetworkQualityTestProgress + (*STUNTestRequest)(nil), // 33: daemon.STUNTestRequest + (*STUNTestProgress)(nil), // 34: daemon.STUNTestProgress + (*Log_Message)(nil), // 35: daemon.Log.Message + (*emptypb.Empty)(nil), // 36: google.protobuf.Empty } ) var file_daemon_started_service_proto_depIdxs = []int32{ 2, // 0: daemon.ServiceStatus.status:type_name -> daemon.ServiceStatus.Type - 30, // 1: daemon.Log.messages:type_name -> daemon.Log.Message + 35, // 1: daemon.Log.messages:type_name -> daemon.Log.Message 0, // 2: daemon.DefaultLogLevel.level:type_name -> daemon.LogLevel 11, // 3: daemon.Groups.group:type_name -> daemon.Group 12, // 4: daemon.Group.items:type_name -> daemon.GroupItem @@ -2124,58 +2582,67 @@ var file_daemon_started_service_proto_depIdxs = []int32{ 22, // 8: daemon.ConnectionEvents.events:type_name -> daemon.ConnectionEvent 25, // 9: daemon.Connection.processInfo:type_name -> daemon.ProcessInfo 28, // 10: daemon.DeprecatedWarnings.warnings:type_name -> daemon.DeprecatedWarning - 0, // 11: daemon.Log.Message.level:type_name -> daemon.LogLevel - 31, // 12: daemon.StartedService.StopService:input_type -> google.protobuf.Empty - 31, // 13: daemon.StartedService.ReloadService:input_type -> google.protobuf.Empty - 31, // 14: daemon.StartedService.SubscribeServiceStatus:input_type -> google.protobuf.Empty - 31, // 15: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty - 31, // 16: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty - 31, // 17: daemon.StartedService.ClearLogs:input_type -> google.protobuf.Empty - 6, // 18: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest - 31, // 19: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty - 31, // 20: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty - 31, // 21: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty - 16, // 22: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode - 13, // 23: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest - 14, // 24: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest - 15, // 25: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest - 31, // 26: daemon.StartedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty - 19, // 27: daemon.StartedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest - 20, // 28: daemon.StartedService.TriggerDebugCrash:input_type -> daemon.DebugCrashRequest - 31, // 29: daemon.StartedService.TriggerOOMReport:input_type -> google.protobuf.Empty - 21, // 30: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest - 26, // 31: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest - 31, // 32: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty - 31, // 33: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty - 31, // 34: daemon.StartedService.GetStartedAt:input_type -> google.protobuf.Empty - 31, // 35: daemon.StartedService.StopService:output_type -> google.protobuf.Empty - 31, // 36: daemon.StartedService.ReloadService:output_type -> google.protobuf.Empty - 4, // 37: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus - 7, // 38: daemon.StartedService.SubscribeLog:output_type -> daemon.Log - 8, // 39: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel - 31, // 40: daemon.StartedService.ClearLogs:output_type -> google.protobuf.Empty - 9, // 41: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status - 10, // 42: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups - 17, // 43: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus - 16, // 44: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode - 31, // 45: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty - 31, // 46: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty - 31, // 47: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty - 31, // 48: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty - 18, // 49: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus - 31, // 50: daemon.StartedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty - 31, // 51: daemon.StartedService.TriggerDebugCrash:output_type -> google.protobuf.Empty - 31, // 52: daemon.StartedService.TriggerOOMReport:output_type -> google.protobuf.Empty - 23, // 53: daemon.StartedService.SubscribeConnections:output_type -> daemon.ConnectionEvents - 31, // 54: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty - 31, // 55: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty - 27, // 56: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings - 29, // 57: daemon.StartedService.GetStartedAt:output_type -> daemon.StartedAt - 35, // [35:58] is the sub-list for method output_type - 12, // [12:35] is the sub-list for method input_type - 12, // [12:12] is the sub-list for extension type_name - 12, // [12:12] is the sub-list for extension extendee - 0, // [0:12] is the sub-list for field type_name + 12, // 11: daemon.OutboundList.outbounds:type_name -> daemon.GroupItem + 0, // 12: daemon.Log.Message.level:type_name -> daemon.LogLevel + 36, // 13: daemon.StartedService.StopService:input_type -> google.protobuf.Empty + 36, // 14: daemon.StartedService.ReloadService:input_type -> google.protobuf.Empty + 36, // 15: daemon.StartedService.SubscribeServiceStatus:input_type -> google.protobuf.Empty + 36, // 16: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty + 36, // 17: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty + 36, // 18: daemon.StartedService.ClearLogs:input_type -> google.protobuf.Empty + 6, // 19: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest + 36, // 20: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty + 36, // 21: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty + 36, // 22: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty + 16, // 23: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode + 13, // 24: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest + 14, // 25: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest + 15, // 26: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest + 36, // 27: daemon.StartedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty + 19, // 28: daemon.StartedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest + 20, // 29: daemon.StartedService.TriggerDebugCrash:input_type -> daemon.DebugCrashRequest + 36, // 30: daemon.StartedService.TriggerOOMReport:input_type -> google.protobuf.Empty + 21, // 31: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest + 26, // 32: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest + 36, // 33: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty + 36, // 34: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty + 36, // 35: daemon.StartedService.GetStartedAt:input_type -> google.protobuf.Empty + 36, // 36: daemon.StartedService.ListOutbounds:input_type -> google.protobuf.Empty + 36, // 37: daemon.StartedService.SubscribeOutbounds:input_type -> google.protobuf.Empty + 31, // 38: daemon.StartedService.StartNetworkQualityTest:input_type -> daemon.NetworkQualityTestRequest + 33, // 39: daemon.StartedService.StartSTUNTest:input_type -> daemon.STUNTestRequest + 36, // 40: daemon.StartedService.StopService:output_type -> google.protobuf.Empty + 36, // 41: daemon.StartedService.ReloadService:output_type -> google.protobuf.Empty + 4, // 42: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus + 7, // 43: daemon.StartedService.SubscribeLog:output_type -> daemon.Log + 8, // 44: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel + 36, // 45: daemon.StartedService.ClearLogs:output_type -> google.protobuf.Empty + 9, // 46: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status + 10, // 47: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups + 17, // 48: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus + 16, // 49: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode + 36, // 50: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty + 36, // 51: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty + 36, // 52: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty + 36, // 53: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty + 18, // 54: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus + 36, // 55: daemon.StartedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty + 36, // 56: daemon.StartedService.TriggerDebugCrash:output_type -> google.protobuf.Empty + 36, // 57: daemon.StartedService.TriggerOOMReport:output_type -> google.protobuf.Empty + 23, // 58: daemon.StartedService.SubscribeConnections:output_type -> daemon.ConnectionEvents + 36, // 59: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty + 36, // 60: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty + 27, // 61: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings + 29, // 62: daemon.StartedService.GetStartedAt:output_type -> daemon.StartedAt + 30, // 63: daemon.StartedService.ListOutbounds:output_type -> daemon.OutboundList + 30, // 64: daemon.StartedService.SubscribeOutbounds:output_type -> daemon.OutboundList + 32, // 65: daemon.StartedService.StartNetworkQualityTest:output_type -> daemon.NetworkQualityTestProgress + 34, // 66: daemon.StartedService.StartSTUNTest:output_type -> daemon.STUNTestProgress + 40, // [40:67] is the sub-list for method output_type + 13, // [13:40] is the sub-list for method input_type + 13, // [13:13] is the sub-list for extension type_name + 13, // [13:13] is the sub-list for extension extendee + 0, // [0:13] is the sub-list for field type_name } func init() { file_daemon_started_service_proto_init() } @@ -2189,7 +2656,7 @@ func file_daemon_started_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_started_service_proto_rawDesc), len(file_daemon_started_service_proto_rawDesc)), NumEnums: 4, - NumMessages: 27, + NumMessages: 32, NumExtensions: 0, NumServices: 1, }, diff --git a/daemon/started_service.proto b/daemon/started_service.proto index 27f8667fbf..6d30da2f9e 100644 --- a/daemon/started_service.proto +++ b/daemon/started_service.proto @@ -34,6 +34,11 @@ service StartedService { rpc CloseAllConnections(google.protobuf.Empty) returns(google.protobuf.Empty) {} rpc GetDeprecatedWarnings(google.protobuf.Empty) returns(DeprecatedWarnings) {} rpc GetStartedAt(google.protobuf.Empty) returns(StartedAt) {} + + rpc ListOutbounds(google.protobuf.Empty) returns (OutboundList) {} + rpc SubscribeOutbounds(google.protobuf.Empty) returns (stream OutboundList) {} + rpc StartNetworkQualityTest(NetworkQualityTestRequest) returns (stream NetworkQualityTestProgress) {} + rpc StartSTUNTest(STUNTestRequest) returns (stream STUNTestProgress) {} } message ServiceStatus { @@ -229,3 +234,47 @@ message DeprecatedWarning { message StartedAt { int64 startedAt = 1; } + +message OutboundList { + repeated GroupItem outbounds = 1; +} + +message NetworkQualityTestRequest { + string configURL = 1; + string outboundTag = 2; + bool serial = 3; + int32 maxRuntimeSeconds = 4; + bool http3 = 5; +} + +message NetworkQualityTestProgress { + int32 phase = 1; + int64 downloadCapacity = 2; + int64 uploadCapacity = 3; + int32 downloadRPM = 4; + int32 uploadRPM = 5; + int32 idleLatencyMs = 6; + int64 elapsedMs = 7; + bool isFinal = 8; + string error = 9; + int32 downloadCapacityAccuracy = 10; + int32 uploadCapacityAccuracy = 11; + int32 downloadRPMAccuracy = 12; + int32 uploadRPMAccuracy = 13; +} + +message STUNTestRequest { + string server = 1; + string outboundTag = 2; +} + +message STUNTestProgress { + int32 phase = 1; + string externalAddr = 2; + int32 latencyMs = 3; + int32 natMapping = 4; + int32 natFiltering = 5; + bool isFinal = 6; + string error = 7; + bool natTypeSupported = 8; +} diff --git a/daemon/started_service_grpc.pb.go b/daemon/started_service_grpc.pb.go index bdf81e4a64..fec7afa0b7 100644 --- a/daemon/started_service_grpc.pb.go +++ b/daemon/started_service_grpc.pb.go @@ -15,29 +15,33 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - StartedService_StopService_FullMethodName = "/daemon.StartedService/StopService" - StartedService_ReloadService_FullMethodName = "/daemon.StartedService/ReloadService" - StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus" - StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog" - StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel" - StartedService_ClearLogs_FullMethodName = "/daemon.StartedService/ClearLogs" - StartedService_SubscribeStatus_FullMethodName = "/daemon.StartedService/SubscribeStatus" - StartedService_SubscribeGroups_FullMethodName = "/daemon.StartedService/SubscribeGroups" - StartedService_GetClashModeStatus_FullMethodName = "/daemon.StartedService/GetClashModeStatus" - StartedService_SubscribeClashMode_FullMethodName = "/daemon.StartedService/SubscribeClashMode" - StartedService_SetClashMode_FullMethodName = "/daemon.StartedService/SetClashMode" - StartedService_URLTest_FullMethodName = "/daemon.StartedService/URLTest" - StartedService_SelectOutbound_FullMethodName = "/daemon.StartedService/SelectOutbound" - StartedService_SetGroupExpand_FullMethodName = "/daemon.StartedService/SetGroupExpand" - StartedService_GetSystemProxyStatus_FullMethodName = "/daemon.StartedService/GetSystemProxyStatus" - StartedService_SetSystemProxyEnabled_FullMethodName = "/daemon.StartedService/SetSystemProxyEnabled" - StartedService_TriggerDebugCrash_FullMethodName = "/daemon.StartedService/TriggerDebugCrash" - StartedService_TriggerOOMReport_FullMethodName = "/daemon.StartedService/TriggerOOMReport" - StartedService_SubscribeConnections_FullMethodName = "/daemon.StartedService/SubscribeConnections" - StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection" - StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections" - StartedService_GetDeprecatedWarnings_FullMethodName = "/daemon.StartedService/GetDeprecatedWarnings" - StartedService_GetStartedAt_FullMethodName = "/daemon.StartedService/GetStartedAt" + StartedService_StopService_FullMethodName = "/daemon.StartedService/StopService" + StartedService_ReloadService_FullMethodName = "/daemon.StartedService/ReloadService" + StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus" + StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog" + StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel" + StartedService_ClearLogs_FullMethodName = "/daemon.StartedService/ClearLogs" + StartedService_SubscribeStatus_FullMethodName = "/daemon.StartedService/SubscribeStatus" + StartedService_SubscribeGroups_FullMethodName = "/daemon.StartedService/SubscribeGroups" + StartedService_GetClashModeStatus_FullMethodName = "/daemon.StartedService/GetClashModeStatus" + StartedService_SubscribeClashMode_FullMethodName = "/daemon.StartedService/SubscribeClashMode" + StartedService_SetClashMode_FullMethodName = "/daemon.StartedService/SetClashMode" + StartedService_URLTest_FullMethodName = "/daemon.StartedService/URLTest" + StartedService_SelectOutbound_FullMethodName = "/daemon.StartedService/SelectOutbound" + StartedService_SetGroupExpand_FullMethodName = "/daemon.StartedService/SetGroupExpand" + StartedService_GetSystemProxyStatus_FullMethodName = "/daemon.StartedService/GetSystemProxyStatus" + StartedService_SetSystemProxyEnabled_FullMethodName = "/daemon.StartedService/SetSystemProxyEnabled" + StartedService_TriggerDebugCrash_FullMethodName = "/daemon.StartedService/TriggerDebugCrash" + StartedService_TriggerOOMReport_FullMethodName = "/daemon.StartedService/TriggerOOMReport" + StartedService_SubscribeConnections_FullMethodName = "/daemon.StartedService/SubscribeConnections" + StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection" + StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections" + StartedService_GetDeprecatedWarnings_FullMethodName = "/daemon.StartedService/GetDeprecatedWarnings" + StartedService_GetStartedAt_FullMethodName = "/daemon.StartedService/GetStartedAt" + StartedService_ListOutbounds_FullMethodName = "/daemon.StartedService/ListOutbounds" + StartedService_SubscribeOutbounds_FullMethodName = "/daemon.StartedService/SubscribeOutbounds" + StartedService_StartNetworkQualityTest_FullMethodName = "/daemon.StartedService/StartNetworkQualityTest" + StartedService_StartSTUNTest_FullMethodName = "/daemon.StartedService/StartSTUNTest" ) // StartedServiceClient is the client API for StartedService service. @@ -67,6 +71,10 @@ type StartedServiceClient interface { CloseAllConnections(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) GetDeprecatedWarnings(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DeprecatedWarnings, error) GetStartedAt(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StartedAt, error) + ListOutbounds(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*OutboundList, error) + SubscribeOutbounds(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OutboundList], error) + StartNetworkQualityTest(ctx context.Context, in *NetworkQualityTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[NetworkQualityTestProgress], error) + StartSTUNTest(ctx context.Context, in *STUNTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[STUNTestProgress], error) } type startedServiceClient struct { @@ -361,6 +369,73 @@ func (c *startedServiceClient) GetStartedAt(ctx context.Context, in *emptypb.Emp return out, nil } +func (c *startedServiceClient) ListOutbounds(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*OutboundList, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(OutboundList) + err := c.cc.Invoke(ctx, StartedService_ListOutbounds_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *startedServiceClient) SubscribeOutbounds(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OutboundList], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[6], StartedService_SubscribeOutbounds_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[emptypb.Empty, OutboundList]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_SubscribeOutboundsClient = grpc.ServerStreamingClient[OutboundList] + +func (c *startedServiceClient) StartNetworkQualityTest(ctx context.Context, in *NetworkQualityTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[NetworkQualityTestProgress], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[7], StartedService_StartNetworkQualityTest_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[NetworkQualityTestRequest, NetworkQualityTestProgress]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_StartNetworkQualityTestClient = grpc.ServerStreamingClient[NetworkQualityTestProgress] + +func (c *startedServiceClient) StartSTUNTest(ctx context.Context, in *STUNTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[STUNTestProgress], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[8], StartedService_StartSTUNTest_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[STUNTestRequest, STUNTestProgress]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_StartSTUNTestClient = grpc.ServerStreamingClient[STUNTestProgress] + // StartedServiceServer is the server API for StartedService service. // All implementations must embed UnimplementedStartedServiceServer // for forward compatibility. @@ -388,6 +463,10 @@ type StartedServiceServer interface { CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error) GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error) GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, error) + ListOutbounds(context.Context, *emptypb.Empty) (*OutboundList, error) + SubscribeOutbounds(*emptypb.Empty, grpc.ServerStreamingServer[OutboundList]) error + StartNetworkQualityTest(*NetworkQualityTestRequest, grpc.ServerStreamingServer[NetworkQualityTestProgress]) error + StartSTUNTest(*STUNTestRequest, grpc.ServerStreamingServer[STUNTestProgress]) error mustEmbedUnimplementedStartedServiceServer() } @@ -489,6 +568,22 @@ func (UnimplementedStartedServiceServer) GetDeprecatedWarnings(context.Context, func (UnimplementedStartedServiceServer) GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, error) { return nil, status.Error(codes.Unimplemented, "method GetStartedAt not implemented") } + +func (UnimplementedStartedServiceServer) ListOutbounds(context.Context, *emptypb.Empty) (*OutboundList, error) { + return nil, status.Error(codes.Unimplemented, "method ListOutbounds not implemented") +} + +func (UnimplementedStartedServiceServer) SubscribeOutbounds(*emptypb.Empty, grpc.ServerStreamingServer[OutboundList]) error { + return status.Error(codes.Unimplemented, "method SubscribeOutbounds not implemented") +} + +func (UnimplementedStartedServiceServer) StartNetworkQualityTest(*NetworkQualityTestRequest, grpc.ServerStreamingServer[NetworkQualityTestProgress]) error { + return status.Error(codes.Unimplemented, "method StartNetworkQualityTest not implemented") +} + +func (UnimplementedStartedServiceServer) StartSTUNTest(*STUNTestRequest, grpc.ServerStreamingServer[STUNTestProgress]) error { + return status.Error(codes.Unimplemented, "method StartSTUNTest not implemented") +} func (UnimplementedStartedServiceServer) mustEmbedUnimplementedStartedServiceServer() {} func (UnimplementedStartedServiceServer) testEmbeddedByValue() {} @@ -882,6 +977,57 @@ func _StartedService_GetStartedAt_Handler(srv interface{}, ctx context.Context, return interceptor(ctx, in, info, handler) } +func _StartedService_ListOutbounds_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StartedServiceServer).ListOutbounds(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StartedService_ListOutbounds_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StartedServiceServer).ListOutbounds(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _StartedService_SubscribeOutbounds_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(emptypb.Empty) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StartedServiceServer).SubscribeOutbounds(m, &grpc.GenericServerStream[emptypb.Empty, OutboundList]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_SubscribeOutboundsServer = grpc.ServerStreamingServer[OutboundList] + +func _StartedService_StartNetworkQualityTest_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(NetworkQualityTestRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StartedServiceServer).StartNetworkQualityTest(m, &grpc.GenericServerStream[NetworkQualityTestRequest, NetworkQualityTestProgress]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_StartNetworkQualityTestServer = grpc.ServerStreamingServer[NetworkQualityTestProgress] + +func _StartedService_StartSTUNTest_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(STUNTestRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StartedServiceServer).StartSTUNTest(m, &grpc.GenericServerStream[STUNTestRequest, STUNTestProgress]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_StartSTUNTestServer = grpc.ServerStreamingServer[STUNTestProgress] + // StartedService_ServiceDesc is the grpc.ServiceDesc for StartedService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -957,6 +1103,10 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetStartedAt", Handler: _StartedService_GetStartedAt_Handler, }, + { + MethodName: "ListOutbounds", + Handler: _StartedService_ListOutbounds_Handler, + }, }, Streams: []grpc.StreamDesc{ { @@ -989,6 +1139,21 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{ Handler: _StartedService_SubscribeConnections_Handler, ServerStreams: true, }, + { + StreamName: "SubscribeOutbounds", + Handler: _StartedService_SubscribeOutbounds_Handler, + ServerStreams: true, + }, + { + StreamName: "StartNetworkQualityTest", + Handler: _StartedService_StartNetworkQualityTest_Handler, + ServerStreams: true, + }, + { + StreamName: "StartSTUNTest", + Handler: _StartedService_StartSTUNTest_Handler, + ServerStreams: true, + }, }, Metadata: "daemon/started_service.proto", } diff --git a/experimental/libbox/command.go b/experimental/libbox/command.go index e3af6a1961..8a43bc9545 100644 --- a/experimental/libbox/command.go +++ b/experimental/libbox/command.go @@ -6,4 +6,5 @@ const ( CommandGroup CommandClashMode CommandConnections + CommandOutbounds ) diff --git a/experimental/libbox/command_client.go b/experimental/libbox/command_client.go index a915e64fa0..8f511d4644 100644 --- a/experimental/libbox/command_client.go +++ b/experimental/libbox/command_client.go @@ -47,6 +47,7 @@ type CommandClientHandler interface { WriteLogs(messageList LogIterator) WriteStatus(message *StatusMessage) WriteGroups(message OutboundGroupIterator) + WriteOutbounds(message OutboundGroupItemIterator) InitializeClashMode(modeList StringIterator, currentMode string) UpdateClashMode(newMode string) WriteConnectionEvents(events *ConnectionEvents) @@ -243,6 +244,8 @@ func (c *CommandClient) dispatchCommands() error { go c.handleClashModeStream() case CommandConnections: go c.handleConnectionsStream() + case CommandOutbounds: + go c.handleOutboundsStream() default: return E.New("unknown command: ", command) } @@ -456,6 +459,25 @@ func (c *CommandClient) handleConnectionsStream() { } } +func (c *CommandClient) handleOutboundsStream() { + client, ctx := c.getStreamContext() + + stream, err := client.SubscribeOutbounds(ctx, &emptypb.Empty{}) + if err != nil { + c.handler.Disconnected(err.Error()) + return + } + + for { + list, err := stream.Recv() + if err != nil { + c.handler.Disconnected(err.Error()) + return + } + c.handler.WriteOutbounds(outboundGroupItemListFromGRPC(list)) + } +} + func (c *CommandClient) SelectOutbound(groupTag string, outboundTag string) error { _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { return client.SelectOutbound(context.Background(), &daemon.SelectOutboundRequest{ @@ -603,3 +625,98 @@ func (c *CommandClient) SetGroupExpand(groupTag string, isExpand bool) error { }) return err } + +func (c *CommandClient) ListOutbounds() (OutboundGroupItemIterator, error) { + return callWithResult(c, func(client daemon.StartedServiceClient) (OutboundGroupItemIterator, error) { + list, err := client.ListOutbounds(context.Background(), &emptypb.Empty{}) + if err != nil { + return nil, err + } + return outboundGroupItemListFromGRPC(list), nil + }) +} + +func (c *CommandClient) StartNetworkQualityTest(configURL string, outboundTag string, serial bool, maxRuntimeSeconds int32, http3 bool, handler NetworkQualityTestHandler) error { + client, err := c.getClientForCall() + if err != nil { + return err + } + if c.standalone { + defer c.closeConnection() + } + stream, err := client.StartNetworkQualityTest(context.Background(), &daemon.NetworkQualityTestRequest{ + ConfigURL: configURL, + OutboundTag: outboundTag, + Serial: serial, + MaxRuntimeSeconds: maxRuntimeSeconds, + Http3: http3, + }) + if err != nil { + return err + } + for { + event, recvErr := stream.Recv() + if recvErr != nil { + handler.OnError(recvErr.Error()) + return recvErr + } + if event.IsFinal { + if event.Error != "" { + handler.OnError(event.Error) + } else { + handler.OnResult(&NetworkQualityResult{ + DownloadCapacity: event.DownloadCapacity, + UploadCapacity: event.UploadCapacity, + DownloadRPM: event.DownloadRPM, + UploadRPM: event.UploadRPM, + IdleLatencyMs: event.IdleLatencyMs, + DownloadCapacityAccuracy: event.DownloadCapacityAccuracy, + UploadCapacityAccuracy: event.UploadCapacityAccuracy, + DownloadRPMAccuracy: event.DownloadRPMAccuracy, + UploadRPMAccuracy: event.UploadRPMAccuracy, + }) + } + return nil + } + handler.OnProgress(networkQualityProgressFromGRPC(event)) + } +} + +func (c *CommandClient) StartSTUNTest(server string, outboundTag string, handler STUNTestHandler) error { + client, err := c.getClientForCall() + if err != nil { + return err + } + if c.standalone { + defer c.closeConnection() + } + stream, err := client.StartSTUNTest(context.Background(), &daemon.STUNTestRequest{ + Server: server, + OutboundTag: outboundTag, + }) + if err != nil { + return err + } + for { + event, recvErr := stream.Recv() + if recvErr != nil { + handler.OnError(recvErr.Error()) + return recvErr + } + if event.IsFinal { + if event.Error != "" { + handler.OnError(event.Error) + } else { + handler.OnResult(&STUNTestResult{ + ExternalAddr: event.ExternalAddr, + LatencyMs: event.LatencyMs, + NATMapping: event.NatMapping, + NATFiltering: event.NatFiltering, + NATTypeSupported: event.NatTypeSupported, + }) + } + return nil + } + handler.OnProgress(stunTestProgressFromGRPC(event)) + } +} diff --git a/experimental/libbox/command_types.go b/experimental/libbox/command_types.go index c330dd4be1..61634b0132 100644 --- a/experimental/libbox/command_types.go +++ b/experimental/libbox/command_types.go @@ -339,6 +339,22 @@ func outboundGroupIteratorFromGRPC(groups *daemon.Groups) OutboundGroupIterator return newIterator(libboxGroups) } +func outboundGroupItemListFromGRPC(list *daemon.OutboundList) OutboundGroupItemIterator { + if list == nil || len(list.Outbounds) == 0 { + return newIterator([]*OutboundGroupItem{}) + } + var items []*OutboundGroupItem + for _, ob := range list.Outbounds { + items = append(items, &OutboundGroupItem{ + Tag: ob.Tag, + Type: ob.Type, + URLTestTime: ob.UrlTestTime, + URLTestDelay: ob.UrlTestDelay, + }) + } + return newIterator(items) +} + func connectionFromGRPC(conn *daemon.Connection) Connection { var processInfo *ProcessInfo if conn.ProcessInfo != nil { diff --git a/experimental/libbox/command_types_nq.go b/experimental/libbox/command_types_nq.go new file mode 100644 index 0000000000..fc8957e2e5 --- /dev/null +++ b/experimental/libbox/command_types_nq.go @@ -0,0 +1,51 @@ +package libbox + +import "github.com/sagernet/sing-box/daemon" + +type NetworkQualityProgress struct { + Phase int32 + DownloadCapacity int64 + UploadCapacity int64 + DownloadRPM int32 + UploadRPM int32 + IdleLatencyMs int32 + ElapsedMs int64 + DownloadCapacityAccuracy int32 + UploadCapacityAccuracy int32 + DownloadRPMAccuracy int32 + UploadRPMAccuracy int32 +} + +type NetworkQualityResult struct { + DownloadCapacity int64 + UploadCapacity int64 + DownloadRPM int32 + UploadRPM int32 + IdleLatencyMs int32 + DownloadCapacityAccuracy int32 + UploadCapacityAccuracy int32 + DownloadRPMAccuracy int32 + UploadRPMAccuracy int32 +} + +type NetworkQualityTestHandler interface { + OnProgress(progress *NetworkQualityProgress) + OnResult(result *NetworkQualityResult) + OnError(message string) +} + +func networkQualityProgressFromGRPC(event *daemon.NetworkQualityTestProgress) *NetworkQualityProgress { + return &NetworkQualityProgress{ + Phase: event.Phase, + DownloadCapacity: event.DownloadCapacity, + UploadCapacity: event.UploadCapacity, + DownloadRPM: event.DownloadRPM, + UploadRPM: event.UploadRPM, + IdleLatencyMs: event.IdleLatencyMs, + ElapsedMs: event.ElapsedMs, + DownloadCapacityAccuracy: event.DownloadCapacityAccuracy, + UploadCapacityAccuracy: event.UploadCapacityAccuracy, + DownloadRPMAccuracy: event.DownloadRPMAccuracy, + UploadRPMAccuracy: event.UploadRPMAccuracy, + } +} diff --git a/experimental/libbox/command_types_stun.go b/experimental/libbox/command_types_stun.go new file mode 100644 index 0000000000..22846c3272 --- /dev/null +++ b/experimental/libbox/command_types_stun.go @@ -0,0 +1,35 @@ +package libbox + +import "github.com/sagernet/sing-box/daemon" + +type STUNTestProgress struct { + Phase int32 + ExternalAddr string + LatencyMs int32 + NATMapping int32 + NATFiltering int32 +} + +type STUNTestResult struct { + ExternalAddr string + LatencyMs int32 + NATMapping int32 + NATFiltering int32 + NATTypeSupported bool +} + +type STUNTestHandler interface { + OnProgress(progress *STUNTestProgress) + OnResult(result *STUNTestResult) + OnError(message string) +} + +func stunTestProgressFromGRPC(event *daemon.STUNTestProgress) *STUNTestProgress { + return &STUNTestProgress{ + Phase: event.Phase, + ExternalAddr: event.ExternalAddr, + LatencyMs: event.LatencyMs, + NATMapping: event.NatMapping, + NATFiltering: event.NatFiltering, + } +} diff --git a/experimental/libbox/networkquality.go b/experimental/libbox/networkquality.go new file mode 100644 index 0000000000..fcbe6f3a66 --- /dev/null +++ b/experimental/libbox/networkquality.go @@ -0,0 +1,74 @@ +package libbox + +import ( + "context" + "time" + + "github.com/sagernet/sing-box/common/networkquality" +) + +type NetworkQualityTest struct { + ctx context.Context + cancel context.CancelFunc +} + +func NewNetworkQualityTest() *NetworkQualityTest { + ctx, cancel := context.WithCancel(context.Background()) + return &NetworkQualityTest{ctx: ctx, cancel: cancel} +} + +func (t *NetworkQualityTest) Start(configURL string, serial bool, maxRuntimeSeconds int32, http3 bool, handler NetworkQualityTestHandler) { + go func() { + httpClient := networkquality.NewHTTPClient(nil) + defer httpClient.CloseIdleConnections() + + measurementClientFactory, err := networkquality.NewOptionalHTTP3Factory(nil, http3) + if err != nil { + handler.OnError(err.Error()) + return + } + + result, err := networkquality.Run(networkquality.Options{ + ConfigURL: configURL, + HTTPClient: httpClient, + NewMeasurementClient: measurementClientFactory, + Serial: serial, + MaxRuntime: time.Duration(maxRuntimeSeconds) * time.Second, + Context: t.ctx, + OnProgress: func(p networkquality.Progress) { + handler.OnProgress(&NetworkQualityProgress{ + Phase: int32(p.Phase), + DownloadCapacity: p.DownloadCapacity, + UploadCapacity: p.UploadCapacity, + DownloadRPM: p.DownloadRPM, + UploadRPM: p.UploadRPM, + IdleLatencyMs: p.IdleLatencyMs, + ElapsedMs: p.ElapsedMs, + DownloadCapacityAccuracy: int32(p.DownloadCapacityAccuracy), + UploadCapacityAccuracy: int32(p.UploadCapacityAccuracy), + DownloadRPMAccuracy: int32(p.DownloadRPMAccuracy), + UploadRPMAccuracy: int32(p.UploadRPMAccuracy), + }) + }, + }) + if err != nil { + handler.OnError(err.Error()) + return + } + handler.OnResult(&NetworkQualityResult{ + DownloadCapacity: result.DownloadCapacity, + UploadCapacity: result.UploadCapacity, + DownloadRPM: result.DownloadRPM, + UploadRPM: result.UploadRPM, + IdleLatencyMs: result.IdleLatencyMs, + DownloadCapacityAccuracy: int32(result.DownloadCapacityAccuracy), + UploadCapacityAccuracy: int32(result.UploadCapacityAccuracy), + DownloadRPMAccuracy: int32(result.DownloadRPMAccuracy), + UploadRPMAccuracy: int32(result.UploadRPMAccuracy), + }) + }() +} + +func (t *NetworkQualityTest) Cancel() { + t.cancel() +} diff --git a/experimental/libbox/setup.go b/experimental/libbox/setup.go index 01a4540442..ac706e9db6 100644 --- a/experimental/libbox/setup.go +++ b/experimental/libbox/setup.go @@ -9,6 +9,8 @@ import ( "strings" "time" + "github.com/sagernet/sing-box/common/networkquality" + "github.com/sagernet/sing-box/common/stun" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental/locale" "github.com/sagernet/sing-box/log" @@ -129,6 +131,56 @@ func FormatDuration(duration int64) string { return log.FormatDuration(time.Duration(duration) * time.Millisecond) } +func FormatBitrate(bps int64) string { + return networkquality.FormatBitrate(bps) +} + +const NetworkQualityDefaultConfigURL = networkquality.DefaultConfigURL + +const NetworkQualityDefaultMaxRuntimeSeconds = int32(networkquality.DefaultMaxRuntime / time.Second) + +const ( + NetworkQualityAccuracyLow = int32(networkquality.AccuracyLow) + NetworkQualityAccuracyMedium = int32(networkquality.AccuracyMedium) + NetworkQualityAccuracyHigh = int32(networkquality.AccuracyHigh) +) + +const ( + NetworkQualityPhaseIdle = int32(networkquality.PhaseIdle) + NetworkQualityPhaseDownload = int32(networkquality.PhaseDownload) + NetworkQualityPhaseUpload = int32(networkquality.PhaseUpload) + NetworkQualityPhaseDone = int32(networkquality.PhaseDone) +) + +const STUNDefaultServer = stun.DefaultServer + +const ( + STUNPhaseBinding = int32(stun.PhaseBinding) + STUNPhaseNATMapping = int32(stun.PhaseNATMapping) + STUNPhaseNATFiltering = int32(stun.PhaseNATFiltering) + STUNPhaseDone = int32(stun.PhaseDone) +) + +const ( + NATMappingEndpointIndependent = int32(stun.NATMappingEndpointIndependent) + NATMappingAddressDependent = int32(stun.NATMappingAddressDependent) + NATMappingAddressAndPortDependent = int32(stun.NATMappingAddressAndPortDependent) +) + +const ( + NATFilteringEndpointIndependent = int32(stun.NATFilteringEndpointIndependent) + NATFilteringAddressDependent = int32(stun.NATFilteringAddressDependent) + NATFilteringAddressAndPortDependent = int32(stun.NATFilteringAddressAndPortDependent) +) + +func FormatNATMapping(value int32) string { + return stun.NATMapping(value).String() +} + +func FormatNATFiltering(value int32) string { + return stun.NATFiltering(value).String() +} + func ProxyDisplayType(proxyType string) string { return C.ProxyDisplayName(proxyType) } diff --git a/experimental/libbox/stun.go b/experimental/libbox/stun.go new file mode 100644 index 0000000000..3f38815d79 --- /dev/null +++ b/experimental/libbox/stun.go @@ -0,0 +1,50 @@ +package libbox + +import ( + "context" + + "github.com/sagernet/sing-box/common/stun" +) + +type STUNTest struct { + ctx context.Context + cancel context.CancelFunc +} + +func NewSTUNTest() *STUNTest { + ctx, cancel := context.WithCancel(context.Background()) + return &STUNTest{ctx: ctx, cancel: cancel} +} + +func (t *STUNTest) Start(server string, handler STUNTestHandler) { + go func() { + result, err := stun.Run(stun.Options{ + Server: server, + Context: t.ctx, + OnProgress: func(p stun.Progress) { + handler.OnProgress(&STUNTestProgress{ + Phase: int32(p.Phase), + ExternalAddr: p.ExternalAddr, + LatencyMs: p.LatencyMs, + NATMapping: int32(p.NATMapping), + NATFiltering: int32(p.NATFiltering), + }) + }, + }) + if err != nil { + handler.OnError(err.Error()) + return + } + handler.OnResult(&STUNTestResult{ + ExternalAddr: result.ExternalAddr, + LatencyMs: result.LatencyMs, + NATMapping: int32(result.NATMapping), + NATFiltering: int32(result.NATFiltering), + NATTypeSupported: result.NATTypeSupported, + }) + }() +} + +func (t *STUNTest) Cancel() { + t.cancel() +} From ccc2742d2bee665963b293dc817a8c8749f551a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 9 Apr 2026 19:33:45 +0800 Subject: [PATCH 14/93] platform: Fix darwin signal handler --- experimental/libbox/signal_handler_darwin.go | 146 +++++++++++++++++++ experimental/libbox/signal_handler_stub.go | 7 + 2 files changed, 153 insertions(+) create mode 100644 experimental/libbox/signal_handler_darwin.go create mode 100644 experimental/libbox/signal_handler_stub.go diff --git a/experimental/libbox/signal_handler_darwin.go b/experimental/libbox/signal_handler_darwin.go new file mode 100644 index 0000000000..a60ddd90fe --- /dev/null +++ b/experimental/libbox/signal_handler_darwin.go @@ -0,0 +1,146 @@ +//go:build darwin && badlinkname + +package libbox + +/* +#include +#include +#include + +static struct sigaction _go_sa[32]; +static struct sigaction _plcrash_sa[32]; +static int _saved = 0; + +static int _signals[] = {SIGSEGV, SIGBUS, SIGFPE, SIGILL, SIGTRAP}; +static const int _signal_count = sizeof(_signals) / sizeof(_signals[0]); + +static void _save_go_handlers(void) { + if (_saved) return; + for (int i = 0; i < _signal_count; i++) + sigaction(_signals[i], NULL, &_go_sa[_signals[i]]); + _saved = 1; +} + +static void _combined_handler(int sig, siginfo_t *info, void *uap) { + // Step 1: PLCrashReporter writes .plcrash, resets all handlers to SIG_DFL, + // and calls raise(sig) which pends (signal is blocked, no SA_NODEFER). + if ((_plcrash_sa[sig].sa_flags & SA_SIGINFO) && + (uintptr_t)_plcrash_sa[sig].sa_sigaction > 1) + _plcrash_sa[sig].sa_sigaction(sig, info, uap); + + // SIGTRAP does not rely on sigreturn -> sigpanic. Once Go's trap trampoline + // is force-installed, we can chain into it directly after PLCrashReporter. + if (sig == SIGTRAP && + (_go_sa[sig].sa_flags & SA_SIGINFO) && + (uintptr_t)_go_sa[sig].sa_sigaction > 1) { + _go_sa[sig].sa_sigaction(sig, info, uap); + return; + } + + // Step 2: Restore Go's handler via sigaction (overwrites PLCrashReporter's SIG_DFL). + // Do NOT call Go's handler directly — Go's preparePanic only modifies the + // ucontext and returns. The actual crash output is written by sigpanic, which + // only runs when the KERNEL restores the modified ucontext via sigreturn. + // A direct C function call has no sigreturn, so sigpanic would never execute. + sigaction(sig, &_go_sa[sig], NULL); + + // Step 3: Return. The kernel restores the original ucontext and re-executes + // the faulting instruction. Two signals are now pending/imminent: + // a) PLCrashReporter's raise() (SI_USER) — Go's handler ignores it + // (sighandler: sigFromUser() → return). + // b) The re-executed fault (SEGV_MAPERR) — Go's handler processes it: + // preparePanic → kernel sigreturn → sigpanic → crash output written + // via debug.SetCrashOutput. +} + +static void _reinstall_handlers(void) { + if (!_saved) return; + for (int i = 0; i < _signal_count; i++) { + int sig = _signals[i]; + struct sigaction current; + sigaction(sig, NULL, ¤t); + // Only save the handler if it's not one of ours + if (current.sa_sigaction != _combined_handler) { + // If current handler is still Go's, PLCrashReporter wasn't installed + if ((current.sa_flags & SA_SIGINFO) && + (uintptr_t)current.sa_sigaction > 1 && + current.sa_sigaction == _go_sa[sig].sa_sigaction) + memset(&_plcrash_sa[sig], 0, sizeof(_plcrash_sa[sig])); + else + _plcrash_sa[sig] = current; + } + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_sigaction = _combined_handler; + sa.sa_flags = SA_SIGINFO | SA_ONSTACK; + sigemptyset(&sa.sa_mask); + sigaction(sig, &sa, NULL); + } +} +*/ +import "C" + +import ( + "reflect" + _ "unsafe" +) + +const ( + _sigtrap = 5 + _nsig = 32 +) + +//go:linkname runtimeGetsig runtime.getsig +func runtimeGetsig(i uint32) uintptr + +//go:linkname runtimeSetsig runtime.setsig +func runtimeSetsig(i uint32, fn uintptr) + +//go:linkname runtimeCgoSigtramp runtime.cgoSigtramp +func runtimeCgoSigtramp() + +//go:linkname runtimeFwdSig runtime.fwdSig +var runtimeFwdSig [_nsig]uintptr + +//go:linkname runtimeHandlingSig runtime.handlingSig +var runtimeHandlingSig [_nsig]uint32 + +func forceGoSIGTRAPHandler() { + runtimeFwdSig[_sigtrap] = runtimeGetsig(_sigtrap) + runtimeHandlingSig[_sigtrap] = 1 + runtimeSetsig(_sigtrap, reflect.ValueOf(runtimeCgoSigtramp).Pointer()) +} + +// PrepareCrashSignalHandlers captures Go's original synchronous signal handlers. +// +// In gomobile/c-archive embeddings, package init runs on the first Go entry. +// That means a native crash reporter installed before the first Go call would +// otherwise be captured as the "Go" handler and break handler restoration on +// SIGSEGV. Go skips SIGTRAP in c-archive mode, so install its trap trampoline +// before saving handlers. Call this before installing PLCrashReporter. +func PrepareCrashSignalHandlers() { + forceGoSIGTRAPHandler() + C._save_go_handlers() +} + +// ReinstallCrashSignalHandlers installs a combined signal handler that chains +// PLCrashReporter (native crash report) and Go's runtime handler (Go crash log). +// +// Call PrepareCrashSignalHandlers before installing PLCrashReporter, then call +// this after PLCrashReporter has been installed. +// +// Flow on SIGSEGV: +// 1. Combined handler calls PLCrashReporter's saved handler → .plcrash written +// 2. Combined handler restores Go's handler via sigaction +// 3. Combined handler returns — kernel re-executes faulting instruction +// 4. PLCrashReporter's pending raise() (SI_USER) is ignored by Go's handler +// 5. Hardware fault → Go's handler → preparePanic → kernel sigreturn → +// sigpanic → crash output via debug.SetCrashOutput +// +// Flow on SIGTRAP: +// 1. PrepareCrashSignalHandlers force-installs Go's cgo trap trampoline +// 2. Combined handler calls PLCrashReporter's saved handler → .plcrash written +// 3. Combined handler directly calls the saved Go trap trampoline +func ReinstallCrashSignalHandlers() { + C._reinstall_handlers() +} diff --git a/experimental/libbox/signal_handler_stub.go b/experimental/libbox/signal_handler_stub.go new file mode 100644 index 0000000000..2ac68b869d --- /dev/null +++ b/experimental/libbox/signal_handler_stub.go @@ -0,0 +1,7 @@ +//go:build !darwin || !badlinkname + +package libbox + +func PrepareCrashSignalHandlers() {} + +func ReinstallCrashSignalHandlers() {} From ce6d6838a8f5b16082f4092afc4482ba82477493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 9 Apr 2026 19:34:27 +0800 Subject: [PATCH 15/93] tools: Tailscale status --- adapter/tailscale.go | 39 ++ daemon/started_service.go | 140 ++++- daemon/started_service.pb.go | 518 +++++++++++++++--- daemon/started_service.proto | 37 ++ daemon/started_service_grpc.pb.go | 96 +++- experimental/libbox/command_client.go | 22 + .../libbox/command_types_tailscale.go | 132 +++++ experimental/libbox/debug.go | 7 +- experimental/libbox/setup.go | 5 + protocol/tailscale/status.go | 92 ++++ 10 files changed, 988 insertions(+), 100 deletions(-) create mode 100644 adapter/tailscale.go create mode 100644 experimental/libbox/command_types_tailscale.go create mode 100644 protocol/tailscale/status.go diff --git a/adapter/tailscale.go b/adapter/tailscale.go new file mode 100644 index 0000000000..35809a542e --- /dev/null +++ b/adapter/tailscale.go @@ -0,0 +1,39 @@ +package adapter + +import "context" + +type TailscaleStatusProvider interface { + SubscribeTailscaleStatus(ctx context.Context, fn func(*TailscaleEndpointStatus)) error +} + +type TailscaleEndpointStatus struct { + BackendState string + AuthURL string + NetworkName string + MagicDNSSuffix string + Self *TailscalePeer + Users map[int64]*TailscaleUser + Peers []*TailscalePeer +} + +type TailscalePeer struct { + HostName string + DNSName string + OS string + TailscaleIPs []string + Online bool + ExitNode bool + ExitNodeOption bool + Active bool + RxBytes int64 + TxBytes int64 + UserID int64 + KeyExpiry int64 +} + +type TailscaleUser struct { + ID int64 + LoginName string + DisplayName string + ProfilePicURL string +} diff --git a/daemon/started_service.go b/daemon/started_service.go index 82336a7d4f..3b7709ff40 100644 --- a/daemon/started_service.go +++ b/daemon/started_service.go @@ -6,12 +6,14 @@ import ( "runtime" "sync" "time" + "unsafe" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/networkquality" "github.com/sagernet/sing-box/common/stun" "github.com/sagernet/sing-box/common/urltest" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental/clashapi" "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" "github.com/sagernet/sing-box/experimental/deprecated" @@ -707,7 +709,7 @@ func (s *StartedService) TriggerDebugCrash(ctx context.Context, request *DebugCr switch request.Type { case DebugCrashRequest_GO: time.AfterFunc(200*time.Millisecond, func() { - panic("debug go crash") + *(*int)(unsafe.Pointer(uintptr(0))) = 0 }) case DebugCrashRequest_NATIVE: err := s.handler.TriggerNativeCrash() @@ -1287,6 +1289,142 @@ func (s *StartedService) StartSTUNTest( }) } +func (s *StartedService) SubscribeTailscaleStatus( + _ *emptypb.Empty, + server grpc.ServerStreamingServer[TailscaleStatusUpdate], +) error { + err := s.waitForStarted(server.Context()) + if err != nil { + return err + } + s.serviceAccess.RLock() + boxService := s.instance + s.serviceAccess.RUnlock() + + endpointManager := service.FromContext[adapter.EndpointManager](boxService.ctx) + if endpointManager == nil { + return status.Error(codes.FailedPrecondition, "endpoint manager not available") + } + + type tailscaleEndpoint struct { + tag string + provider adapter.TailscaleStatusProvider + } + var endpoints []tailscaleEndpoint + for _, endpoint := range endpointManager.Endpoints() { + if endpoint.Type() != C.TypeTailscale { + continue + } + provider, loaded := endpoint.(adapter.TailscaleStatusProvider) + if !loaded { + continue + } + endpoints = append(endpoints, tailscaleEndpoint{ + tag: endpoint.Tag(), + provider: provider, + }) + } + if len(endpoints) == 0 { + return status.Error(codes.NotFound, "no Tailscale endpoint found") + } + + type taggedStatus struct { + tag string + status *adapter.TailscaleEndpointStatus + } + updates := make(chan taggedStatus, len(endpoints)) + ctx, cancel := context.WithCancel(server.Context()) + defer cancel() + + var waitGroup sync.WaitGroup + for _, endpoint := range endpoints { + waitGroup.Add(1) + go func(tag string, provider adapter.TailscaleStatusProvider) { + defer waitGroup.Done() + _ = provider.SubscribeTailscaleStatus(ctx, func(endpointStatus *adapter.TailscaleEndpointStatus) { + select { + case updates <- taggedStatus{tag: tag, status: endpointStatus}: + case <-ctx.Done(): + } + }) + }(endpoint.tag, endpoint.provider) + } + + go func() { + waitGroup.Wait() + close(updates) + }() + + statuses := make(map[string]*adapter.TailscaleEndpointStatus, len(endpoints)) + for update := range updates { + statuses[update.tag] = update.status + protoEndpoints := make([]*TailscaleEndpointStatus, 0, len(statuses)) + for tag, endpointStatus := range statuses { + protoEndpoints = append(protoEndpoints, tailscaleEndpointStatusToProto(tag, endpointStatus)) + } + sendErr := server.Send(&TailscaleStatusUpdate{ + Endpoints: protoEndpoints, + }) + if sendErr != nil { + return sendErr + } + } + return nil +} + +func tailscaleEndpointStatusToProto(tag string, s *adapter.TailscaleEndpointStatus) *TailscaleEndpointStatus { + userGroupMap := make(map[int64]*TailscaleUserGroup) + for userID, user := range s.Users { + userGroupMap[userID] = &TailscaleUserGroup{ + UserID: userID, + LoginName: user.LoginName, + DisplayName: user.DisplayName, + ProfilePicURL: user.ProfilePicURL, + } + } + for _, peer := range s.Peers { + protoPeer := tailscalePeerToProto(peer) + group, loaded := userGroupMap[peer.UserID] + if !loaded { + group = &TailscaleUserGroup{UserID: peer.UserID} + userGroupMap[peer.UserID] = group + } + group.Peers = append(group.Peers, protoPeer) + } + userGroups := make([]*TailscaleUserGroup, 0, len(userGroupMap)) + for _, group := range userGroupMap { + userGroups = append(userGroups, group) + } + result := &TailscaleEndpointStatus{ + EndpointTag: tag, + BackendState: s.BackendState, + AuthURL: s.AuthURL, + NetworkName: s.NetworkName, + MagicDNSSuffix: s.MagicDNSSuffix, + UserGroups: userGroups, + } + if s.Self != nil { + result.Self = tailscalePeerToProto(s.Self) + } + return result +} + +func tailscalePeerToProto(peer *adapter.TailscalePeer) *TailscalePeer { + return &TailscalePeer{ + HostName: peer.HostName, + DnsName: peer.DNSName, + Os: peer.OS, + TailscaleIPs: peer.TailscaleIPs, + Online: peer.Online, + ExitNode: peer.ExitNode, + ExitNodeOption: peer.ExitNodeOption, + Active: peer.Active, + RxBytes: peer.RxBytes, + TxBytes: peer.TxBytes, + KeyExpiry: peer.KeyExpiry, + } +} + func (s *StartedService) mustEmbedUnimplementedStartedServiceServer() { } diff --git a/daemon/started_service.pb.go b/daemon/started_service.pb.go index c48ea4fe79..402b01013d 100644 --- a/daemon/started_service.pb.go +++ b/daemon/started_service.pb.go @@ -2248,6 +2248,342 @@ func (x *STUNTestProgress) GetNatTypeSupported() bool { return false } +type TailscaleStatusUpdate struct { + state protoimpl.MessageState `protogen:"open.v1"` + Endpoints []*TailscaleEndpointStatus `protobuf:"bytes,1,rep,name=endpoints,proto3" json:"endpoints,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TailscaleStatusUpdate) Reset() { + *x = TailscaleStatusUpdate{} + mi := &file_daemon_started_service_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TailscaleStatusUpdate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TailscaleStatusUpdate) ProtoMessage() {} + +func (x *TailscaleStatusUpdate) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[31] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TailscaleStatusUpdate.ProtoReflect.Descriptor instead. +func (*TailscaleStatusUpdate) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{31} +} + +func (x *TailscaleStatusUpdate) GetEndpoints() []*TailscaleEndpointStatus { + if x != nil { + return x.Endpoints + } + return nil +} + +type TailscaleEndpointStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + EndpointTag string `protobuf:"bytes,1,opt,name=endpointTag,proto3" json:"endpointTag,omitempty"` + BackendState string `protobuf:"bytes,2,opt,name=backendState,proto3" json:"backendState,omitempty"` + AuthURL string `protobuf:"bytes,3,opt,name=authURL,proto3" json:"authURL,omitempty"` + NetworkName string `protobuf:"bytes,4,opt,name=networkName,proto3" json:"networkName,omitempty"` + MagicDNSSuffix string `protobuf:"bytes,5,opt,name=magicDNSSuffix,proto3" json:"magicDNSSuffix,omitempty"` + Self *TailscalePeer `protobuf:"bytes,6,opt,name=self,proto3" json:"self,omitempty"` + UserGroups []*TailscaleUserGroup `protobuf:"bytes,7,rep,name=userGroups,proto3" json:"userGroups,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TailscaleEndpointStatus) Reset() { + *x = TailscaleEndpointStatus{} + mi := &file_daemon_started_service_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TailscaleEndpointStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TailscaleEndpointStatus) ProtoMessage() {} + +func (x *TailscaleEndpointStatus) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[32] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TailscaleEndpointStatus.ProtoReflect.Descriptor instead. +func (*TailscaleEndpointStatus) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{32} +} + +func (x *TailscaleEndpointStatus) GetEndpointTag() string { + if x != nil { + return x.EndpointTag + } + return "" +} + +func (x *TailscaleEndpointStatus) GetBackendState() string { + if x != nil { + return x.BackendState + } + return "" +} + +func (x *TailscaleEndpointStatus) GetAuthURL() string { + if x != nil { + return x.AuthURL + } + return "" +} + +func (x *TailscaleEndpointStatus) GetNetworkName() string { + if x != nil { + return x.NetworkName + } + return "" +} + +func (x *TailscaleEndpointStatus) GetMagicDNSSuffix() string { + if x != nil { + return x.MagicDNSSuffix + } + return "" +} + +func (x *TailscaleEndpointStatus) GetSelf() *TailscalePeer { + if x != nil { + return x.Self + } + return nil +} + +func (x *TailscaleEndpointStatus) GetUserGroups() []*TailscaleUserGroup { + if x != nil { + return x.UserGroups + } + return nil +} + +type TailscaleUserGroup struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserID int64 `protobuf:"varint,1,opt,name=userID,proto3" json:"userID,omitempty"` + LoginName string `protobuf:"bytes,2,opt,name=loginName,proto3" json:"loginName,omitempty"` + DisplayName string `protobuf:"bytes,3,opt,name=displayName,proto3" json:"displayName,omitempty"` + ProfilePicURL string `protobuf:"bytes,4,opt,name=profilePicURL,proto3" json:"profilePicURL,omitempty"` + Peers []*TailscalePeer `protobuf:"bytes,5,rep,name=peers,proto3" json:"peers,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TailscaleUserGroup) Reset() { + *x = TailscaleUserGroup{} + mi := &file_daemon_started_service_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TailscaleUserGroup) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TailscaleUserGroup) ProtoMessage() {} + +func (x *TailscaleUserGroup) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TailscaleUserGroup.ProtoReflect.Descriptor instead. +func (*TailscaleUserGroup) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{33} +} + +func (x *TailscaleUserGroup) GetUserID() int64 { + if x != nil { + return x.UserID + } + return 0 +} + +func (x *TailscaleUserGroup) GetLoginName() string { + if x != nil { + return x.LoginName + } + return "" +} + +func (x *TailscaleUserGroup) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + +func (x *TailscaleUserGroup) GetProfilePicURL() string { + if x != nil { + return x.ProfilePicURL + } + return "" +} + +func (x *TailscaleUserGroup) GetPeers() []*TailscalePeer { + if x != nil { + return x.Peers + } + return nil +} + +type TailscalePeer struct { + state protoimpl.MessageState `protogen:"open.v1"` + HostName string `protobuf:"bytes,1,opt,name=hostName,proto3" json:"hostName,omitempty"` + DnsName string `protobuf:"bytes,2,opt,name=dnsName,proto3" json:"dnsName,omitempty"` + Os string `protobuf:"bytes,3,opt,name=os,proto3" json:"os,omitempty"` + TailscaleIPs []string `protobuf:"bytes,4,rep,name=tailscaleIPs,proto3" json:"tailscaleIPs,omitempty"` + Online bool `protobuf:"varint,5,opt,name=online,proto3" json:"online,omitempty"` + ExitNode bool `protobuf:"varint,6,opt,name=exitNode,proto3" json:"exitNode,omitempty"` + ExitNodeOption bool `protobuf:"varint,7,opt,name=exitNodeOption,proto3" json:"exitNodeOption,omitempty"` + Active bool `protobuf:"varint,8,opt,name=active,proto3" json:"active,omitempty"` + RxBytes int64 `protobuf:"varint,9,opt,name=rxBytes,proto3" json:"rxBytes,omitempty"` + TxBytes int64 `protobuf:"varint,10,opt,name=txBytes,proto3" json:"txBytes,omitempty"` + KeyExpiry int64 `protobuf:"varint,11,opt,name=keyExpiry,proto3" json:"keyExpiry,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TailscalePeer) Reset() { + *x = TailscalePeer{} + mi := &file_daemon_started_service_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TailscalePeer) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TailscalePeer) ProtoMessage() {} + +func (x *TailscalePeer) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TailscalePeer.ProtoReflect.Descriptor instead. +func (*TailscalePeer) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{34} +} + +func (x *TailscalePeer) GetHostName() string { + if x != nil { + return x.HostName + } + return "" +} + +func (x *TailscalePeer) GetDnsName() string { + if x != nil { + return x.DnsName + } + return "" +} + +func (x *TailscalePeer) GetOs() string { + if x != nil { + return x.Os + } + return "" +} + +func (x *TailscalePeer) GetTailscaleIPs() []string { + if x != nil { + return x.TailscaleIPs + } + return nil +} + +func (x *TailscalePeer) GetOnline() bool { + if x != nil { + return x.Online + } + return false +} + +func (x *TailscalePeer) GetExitNode() bool { + if x != nil { + return x.ExitNode + } + return false +} + +func (x *TailscalePeer) GetExitNodeOption() bool { + if x != nil { + return x.ExitNodeOption + } + return false +} + +func (x *TailscalePeer) GetActive() bool { + if x != nil { + return x.Active + } + return false +} + +func (x *TailscalePeer) GetRxBytes() int64 { + if x != nil { + return x.RxBytes + } + return 0 +} + +func (x *TailscalePeer) GetTxBytes() int64 { + if x != nil { + return x.TxBytes + } + return 0 +} + +func (x *TailscalePeer) GetKeyExpiry() int64 { + if x != nil { + return x.KeyExpiry + } + return 0 +} + type Log_Message struct { state protoimpl.MessageState `protogen:"open.v1"` Level LogLevel `protobuf:"varint,1,opt,name=level,proto3,enum=daemon.LogLevel" json:"level,omitempty"` @@ -2258,7 +2594,7 @@ type Log_Message struct { func (x *Log_Message) Reset() { *x = Log_Message{} - mi := &file_daemon_started_service_proto_msgTypes[31] + mi := &file_daemon_started_service_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2270,7 +2606,7 @@ func (x *Log_Message) String() string { func (*Log_Message) ProtoMessage() {} func (x *Log_Message) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[31] + mi := &file_daemon_started_service_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2472,7 +2808,38 @@ const file_daemon_started_service_proto_rawDesc = "" + "\fnatFiltering\x18\x05 \x01(\x05R\fnatFiltering\x12\x18\n" + "\aisFinal\x18\x06 \x01(\bR\aisFinal\x12\x14\n" + "\x05error\x18\a \x01(\tR\x05error\x12*\n" + - "\x10natTypeSupported\x18\b \x01(\bR\x10natTypeSupported*U\n" + + "\x10natTypeSupported\x18\b \x01(\bR\x10natTypeSupported\"V\n" + + "\x15TailscaleStatusUpdate\x12=\n" + + "\tendpoints\x18\x01 \x03(\v2\x1f.daemon.TailscaleEndpointStatusR\tendpoints\"\xaa\x02\n" + + "\x17TailscaleEndpointStatus\x12 \n" + + "\vendpointTag\x18\x01 \x01(\tR\vendpointTag\x12\"\n" + + "\fbackendState\x18\x02 \x01(\tR\fbackendState\x12\x18\n" + + "\aauthURL\x18\x03 \x01(\tR\aauthURL\x12 \n" + + "\vnetworkName\x18\x04 \x01(\tR\vnetworkName\x12&\n" + + "\x0emagicDNSSuffix\x18\x05 \x01(\tR\x0emagicDNSSuffix\x12)\n" + + "\x04self\x18\x06 \x01(\v2\x15.daemon.TailscalePeerR\x04self\x12:\n" + + "\n" + + "userGroups\x18\a \x03(\v2\x1a.daemon.TailscaleUserGroupR\n" + + "userGroups\"\xbf\x01\n" + + "\x12TailscaleUserGroup\x12\x16\n" + + "\x06userID\x18\x01 \x01(\x03R\x06userID\x12\x1c\n" + + "\tloginName\x18\x02 \x01(\tR\tloginName\x12 \n" + + "\vdisplayName\x18\x03 \x01(\tR\vdisplayName\x12$\n" + + "\rprofilePicURL\x18\x04 \x01(\tR\rprofilePicURL\x12+\n" + + "\x05peers\x18\x05 \x03(\v2\x15.daemon.TailscalePeerR\x05peers\"\xbf\x02\n" + + "\rTailscalePeer\x12\x1a\n" + + "\bhostName\x18\x01 \x01(\tR\bhostName\x12\x18\n" + + "\adnsName\x18\x02 \x01(\tR\adnsName\x12\x0e\n" + + "\x02os\x18\x03 \x01(\tR\x02os\x12\"\n" + + "\ftailscaleIPs\x18\x04 \x03(\tR\ftailscaleIPs\x12\x16\n" + + "\x06online\x18\x05 \x01(\bR\x06online\x12\x1a\n" + + "\bexitNode\x18\x06 \x01(\bR\bexitNode\x12&\n" + + "\x0eexitNodeOption\x18\a \x01(\bR\x0eexitNodeOption\x12\x16\n" + + "\x06active\x18\b \x01(\bR\x06active\x12\x18\n" + + "\arxBytes\x18\t \x01(\x03R\arxBytes\x12\x18\n" + + "\atxBytes\x18\n" + + " \x01(\x03R\atxBytes\x12\x1c\n" + + "\tkeyExpiry\x18\v \x01(\x03R\tkeyExpiry*U\n" + "\bLogLevel\x12\t\n" + "\x05PANIC\x10\x00\x12\t\n" + "\x05FATAL\x10\x01\x12\t\n" + @@ -2484,7 +2851,7 @@ const file_daemon_started_service_proto_rawDesc = "" + "\x13ConnectionEventType\x12\x18\n" + "\x14CONNECTION_EVENT_NEW\x10\x00\x12\x1b\n" + "\x17CONNECTION_EVENT_UPDATE\x10\x01\x12\x1b\n" + - "\x17CONNECTION_EVENT_CLOSED\x10\x022\xac\x0f\n" + + "\x17CONNECTION_EVENT_CLOSED\x10\x022\x83\x10\n" + "\x0eStartedService\x12=\n" + "\vStopService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12?\n" + "\rReloadService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12K\n" + @@ -2512,7 +2879,8 @@ const file_daemon_started_service_proto_rawDesc = "" + "\rListOutbounds\x12\x16.google.protobuf.Empty\x1a\x14.daemon.OutboundList\"\x00\x12F\n" + "\x12SubscribeOutbounds\x12\x16.google.protobuf.Empty\x1a\x14.daemon.OutboundList\"\x000\x01\x12d\n" + "\x17StartNetworkQualityTest\x12!.daemon.NetworkQualityTestRequest\x1a\".daemon.NetworkQualityTestProgress\"\x000\x01\x12F\n" + - "\rStartSTUNTest\x12\x17.daemon.STUNTestRequest\x1a\x18.daemon.STUNTestProgress\"\x000\x01B%Z#github.com/sagernet/sing-box/daemonb\x06proto3" + "\rStartSTUNTest\x12\x17.daemon.STUNTestRequest\x1a\x18.daemon.STUNTestProgress\"\x000\x01\x12U\n" + + "\x18SubscribeTailscaleStatus\x12\x16.google.protobuf.Empty\x1a\x1d.daemon.TailscaleStatusUpdate\"\x000\x01B%Z#github.com/sagernet/sing-box/daemonb\x06proto3" var ( file_daemon_started_service_proto_rawDescOnce sync.Once @@ -2528,7 +2896,7 @@ func file_daemon_started_service_proto_rawDescGZIP() []byte { var ( file_daemon_started_service_proto_enumTypes = make([]protoimpl.EnumInfo, 4) - file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 32) + file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 36) file_daemon_started_service_proto_goTypes = []any{ (LogLevel)(0), // 0: daemon.LogLevel (ConnectionEventType)(0), // 1: daemon.ConnectionEventType @@ -2565,14 +2933,18 @@ var ( (*NetworkQualityTestProgress)(nil), // 32: daemon.NetworkQualityTestProgress (*STUNTestRequest)(nil), // 33: daemon.STUNTestRequest (*STUNTestProgress)(nil), // 34: daemon.STUNTestProgress - (*Log_Message)(nil), // 35: daemon.Log.Message - (*emptypb.Empty)(nil), // 36: google.protobuf.Empty + (*TailscaleStatusUpdate)(nil), // 35: daemon.TailscaleStatusUpdate + (*TailscaleEndpointStatus)(nil), // 36: daemon.TailscaleEndpointStatus + (*TailscaleUserGroup)(nil), // 37: daemon.TailscaleUserGroup + (*TailscalePeer)(nil), // 38: daemon.TailscalePeer + (*Log_Message)(nil), // 39: daemon.Log.Message + (*emptypb.Empty)(nil), // 40: google.protobuf.Empty } ) var file_daemon_started_service_proto_depIdxs = []int32{ 2, // 0: daemon.ServiceStatus.status:type_name -> daemon.ServiceStatus.Type - 35, // 1: daemon.Log.messages:type_name -> daemon.Log.Message + 39, // 1: daemon.Log.messages:type_name -> daemon.Log.Message 0, // 2: daemon.DefaultLogLevel.level:type_name -> daemon.LogLevel 11, // 3: daemon.Groups.group:type_name -> daemon.Group 12, // 4: daemon.Group.items:type_name -> daemon.GroupItem @@ -2583,66 +2955,72 @@ var file_daemon_started_service_proto_depIdxs = []int32{ 25, // 9: daemon.Connection.processInfo:type_name -> daemon.ProcessInfo 28, // 10: daemon.DeprecatedWarnings.warnings:type_name -> daemon.DeprecatedWarning 12, // 11: daemon.OutboundList.outbounds:type_name -> daemon.GroupItem - 0, // 12: daemon.Log.Message.level:type_name -> daemon.LogLevel - 36, // 13: daemon.StartedService.StopService:input_type -> google.protobuf.Empty - 36, // 14: daemon.StartedService.ReloadService:input_type -> google.protobuf.Empty - 36, // 15: daemon.StartedService.SubscribeServiceStatus:input_type -> google.protobuf.Empty - 36, // 16: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty - 36, // 17: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty - 36, // 18: daemon.StartedService.ClearLogs:input_type -> google.protobuf.Empty - 6, // 19: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest - 36, // 20: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty - 36, // 21: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty - 36, // 22: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty - 16, // 23: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode - 13, // 24: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest - 14, // 25: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest - 15, // 26: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest - 36, // 27: daemon.StartedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty - 19, // 28: daemon.StartedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest - 20, // 29: daemon.StartedService.TriggerDebugCrash:input_type -> daemon.DebugCrashRequest - 36, // 30: daemon.StartedService.TriggerOOMReport:input_type -> google.protobuf.Empty - 21, // 31: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest - 26, // 32: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest - 36, // 33: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty - 36, // 34: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty - 36, // 35: daemon.StartedService.GetStartedAt:input_type -> google.protobuf.Empty - 36, // 36: daemon.StartedService.ListOutbounds:input_type -> google.protobuf.Empty - 36, // 37: daemon.StartedService.SubscribeOutbounds:input_type -> google.protobuf.Empty - 31, // 38: daemon.StartedService.StartNetworkQualityTest:input_type -> daemon.NetworkQualityTestRequest - 33, // 39: daemon.StartedService.StartSTUNTest:input_type -> daemon.STUNTestRequest - 36, // 40: daemon.StartedService.StopService:output_type -> google.protobuf.Empty - 36, // 41: daemon.StartedService.ReloadService:output_type -> google.protobuf.Empty - 4, // 42: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus - 7, // 43: daemon.StartedService.SubscribeLog:output_type -> daemon.Log - 8, // 44: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel - 36, // 45: daemon.StartedService.ClearLogs:output_type -> google.protobuf.Empty - 9, // 46: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status - 10, // 47: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups - 17, // 48: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus - 16, // 49: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode - 36, // 50: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty - 36, // 51: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty - 36, // 52: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty - 36, // 53: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty - 18, // 54: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus - 36, // 55: daemon.StartedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty - 36, // 56: daemon.StartedService.TriggerDebugCrash:output_type -> google.protobuf.Empty - 36, // 57: daemon.StartedService.TriggerOOMReport:output_type -> google.protobuf.Empty - 23, // 58: daemon.StartedService.SubscribeConnections:output_type -> daemon.ConnectionEvents - 36, // 59: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty - 36, // 60: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty - 27, // 61: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings - 29, // 62: daemon.StartedService.GetStartedAt:output_type -> daemon.StartedAt - 30, // 63: daemon.StartedService.ListOutbounds:output_type -> daemon.OutboundList - 30, // 64: daemon.StartedService.SubscribeOutbounds:output_type -> daemon.OutboundList - 32, // 65: daemon.StartedService.StartNetworkQualityTest:output_type -> daemon.NetworkQualityTestProgress - 34, // 66: daemon.StartedService.StartSTUNTest:output_type -> daemon.STUNTestProgress - 40, // [40:67] is the sub-list for method output_type - 13, // [13:40] is the sub-list for method input_type - 13, // [13:13] is the sub-list for extension type_name - 13, // [13:13] is the sub-list for extension extendee - 0, // [0:13] is the sub-list for field type_name + 36, // 12: daemon.TailscaleStatusUpdate.endpoints:type_name -> daemon.TailscaleEndpointStatus + 38, // 13: daemon.TailscaleEndpointStatus.self:type_name -> daemon.TailscalePeer + 37, // 14: daemon.TailscaleEndpointStatus.userGroups:type_name -> daemon.TailscaleUserGroup + 38, // 15: daemon.TailscaleUserGroup.peers:type_name -> daemon.TailscalePeer + 0, // 16: daemon.Log.Message.level:type_name -> daemon.LogLevel + 40, // 17: daemon.StartedService.StopService:input_type -> google.protobuf.Empty + 40, // 18: daemon.StartedService.ReloadService:input_type -> google.protobuf.Empty + 40, // 19: daemon.StartedService.SubscribeServiceStatus:input_type -> google.protobuf.Empty + 40, // 20: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty + 40, // 21: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty + 40, // 22: daemon.StartedService.ClearLogs:input_type -> google.protobuf.Empty + 6, // 23: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest + 40, // 24: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty + 40, // 25: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty + 40, // 26: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty + 16, // 27: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode + 13, // 28: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest + 14, // 29: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest + 15, // 30: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest + 40, // 31: daemon.StartedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty + 19, // 32: daemon.StartedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest + 20, // 33: daemon.StartedService.TriggerDebugCrash:input_type -> daemon.DebugCrashRequest + 40, // 34: daemon.StartedService.TriggerOOMReport:input_type -> google.protobuf.Empty + 21, // 35: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest + 26, // 36: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest + 40, // 37: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty + 40, // 38: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty + 40, // 39: daemon.StartedService.GetStartedAt:input_type -> google.protobuf.Empty + 40, // 40: daemon.StartedService.ListOutbounds:input_type -> google.protobuf.Empty + 40, // 41: daemon.StartedService.SubscribeOutbounds:input_type -> google.protobuf.Empty + 31, // 42: daemon.StartedService.StartNetworkQualityTest:input_type -> daemon.NetworkQualityTestRequest + 33, // 43: daemon.StartedService.StartSTUNTest:input_type -> daemon.STUNTestRequest + 40, // 44: daemon.StartedService.SubscribeTailscaleStatus:input_type -> google.protobuf.Empty + 40, // 45: daemon.StartedService.StopService:output_type -> google.protobuf.Empty + 40, // 46: daemon.StartedService.ReloadService:output_type -> google.protobuf.Empty + 4, // 47: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus + 7, // 48: daemon.StartedService.SubscribeLog:output_type -> daemon.Log + 8, // 49: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel + 40, // 50: daemon.StartedService.ClearLogs:output_type -> google.protobuf.Empty + 9, // 51: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status + 10, // 52: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups + 17, // 53: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus + 16, // 54: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode + 40, // 55: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty + 40, // 56: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty + 40, // 57: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty + 40, // 58: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty + 18, // 59: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus + 40, // 60: daemon.StartedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty + 40, // 61: daemon.StartedService.TriggerDebugCrash:output_type -> google.protobuf.Empty + 40, // 62: daemon.StartedService.TriggerOOMReport:output_type -> google.protobuf.Empty + 23, // 63: daemon.StartedService.SubscribeConnections:output_type -> daemon.ConnectionEvents + 40, // 64: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty + 40, // 65: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty + 27, // 66: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings + 29, // 67: daemon.StartedService.GetStartedAt:output_type -> daemon.StartedAt + 30, // 68: daemon.StartedService.ListOutbounds:output_type -> daemon.OutboundList + 30, // 69: daemon.StartedService.SubscribeOutbounds:output_type -> daemon.OutboundList + 32, // 70: daemon.StartedService.StartNetworkQualityTest:output_type -> daemon.NetworkQualityTestProgress + 34, // 71: daemon.StartedService.StartSTUNTest:output_type -> daemon.STUNTestProgress + 35, // 72: daemon.StartedService.SubscribeTailscaleStatus:output_type -> daemon.TailscaleStatusUpdate + 45, // [45:73] is the sub-list for method output_type + 17, // [17:45] is the sub-list for method input_type + 17, // [17:17] is the sub-list for extension type_name + 17, // [17:17] is the sub-list for extension extendee + 0, // [0:17] is the sub-list for field type_name } func init() { file_daemon_started_service_proto_init() } @@ -2656,7 +3034,7 @@ func file_daemon_started_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_started_service_proto_rawDesc), len(file_daemon_started_service_proto_rawDesc)), NumEnums: 4, - NumMessages: 32, + NumMessages: 36, NumExtensions: 0, NumServices: 1, }, diff --git a/daemon/started_service.proto b/daemon/started_service.proto index 6d30da2f9e..f2531f08d1 100644 --- a/daemon/started_service.proto +++ b/daemon/started_service.proto @@ -39,6 +39,7 @@ service StartedService { rpc SubscribeOutbounds(google.protobuf.Empty) returns (stream OutboundList) {} rpc StartNetworkQualityTest(NetworkQualityTestRequest) returns (stream NetworkQualityTestProgress) {} rpc StartSTUNTest(STUNTestRequest) returns (stream STUNTestProgress) {} + rpc SubscribeTailscaleStatus(google.protobuf.Empty) returns (stream TailscaleStatusUpdate) {} } message ServiceStatus { @@ -278,3 +279,39 @@ message STUNTestProgress { string error = 7; bool natTypeSupported = 8; } + +message TailscaleStatusUpdate { + repeated TailscaleEndpointStatus endpoints = 1; +} + +message TailscaleEndpointStatus { + string endpointTag = 1; + string backendState = 2; + string authURL = 3; + string networkName = 4; + string magicDNSSuffix = 5; + TailscalePeer self = 6; + repeated TailscaleUserGroup userGroups = 7; +} + +message TailscaleUserGroup { + int64 userID = 1; + string loginName = 2; + string displayName = 3; + string profilePicURL = 4; + repeated TailscalePeer peers = 5; +} + +message TailscalePeer { + string hostName = 1; + string dnsName = 2; + string os = 3; + repeated string tailscaleIPs = 4; + bool online = 5; + bool exitNode = 6; + bool exitNodeOption = 7; + bool active = 8; + int64 rxBytes = 9; + int64 txBytes = 10; + int64 keyExpiry = 11; +} diff --git a/daemon/started_service_grpc.pb.go b/daemon/started_service_grpc.pb.go index fec7afa0b7..af8024035b 100644 --- a/daemon/started_service_grpc.pb.go +++ b/daemon/started_service_grpc.pb.go @@ -15,33 +15,34 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - StartedService_StopService_FullMethodName = "/daemon.StartedService/StopService" - StartedService_ReloadService_FullMethodName = "/daemon.StartedService/ReloadService" - StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus" - StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog" - StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel" - StartedService_ClearLogs_FullMethodName = "/daemon.StartedService/ClearLogs" - StartedService_SubscribeStatus_FullMethodName = "/daemon.StartedService/SubscribeStatus" - StartedService_SubscribeGroups_FullMethodName = "/daemon.StartedService/SubscribeGroups" - StartedService_GetClashModeStatus_FullMethodName = "/daemon.StartedService/GetClashModeStatus" - StartedService_SubscribeClashMode_FullMethodName = "/daemon.StartedService/SubscribeClashMode" - StartedService_SetClashMode_FullMethodName = "/daemon.StartedService/SetClashMode" - StartedService_URLTest_FullMethodName = "/daemon.StartedService/URLTest" - StartedService_SelectOutbound_FullMethodName = "/daemon.StartedService/SelectOutbound" - StartedService_SetGroupExpand_FullMethodName = "/daemon.StartedService/SetGroupExpand" - StartedService_GetSystemProxyStatus_FullMethodName = "/daemon.StartedService/GetSystemProxyStatus" - StartedService_SetSystemProxyEnabled_FullMethodName = "/daemon.StartedService/SetSystemProxyEnabled" - StartedService_TriggerDebugCrash_FullMethodName = "/daemon.StartedService/TriggerDebugCrash" - StartedService_TriggerOOMReport_FullMethodName = "/daemon.StartedService/TriggerOOMReport" - StartedService_SubscribeConnections_FullMethodName = "/daemon.StartedService/SubscribeConnections" - StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection" - StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections" - StartedService_GetDeprecatedWarnings_FullMethodName = "/daemon.StartedService/GetDeprecatedWarnings" - StartedService_GetStartedAt_FullMethodName = "/daemon.StartedService/GetStartedAt" - StartedService_ListOutbounds_FullMethodName = "/daemon.StartedService/ListOutbounds" - StartedService_SubscribeOutbounds_FullMethodName = "/daemon.StartedService/SubscribeOutbounds" - StartedService_StartNetworkQualityTest_FullMethodName = "/daemon.StartedService/StartNetworkQualityTest" - StartedService_StartSTUNTest_FullMethodName = "/daemon.StartedService/StartSTUNTest" + StartedService_StopService_FullMethodName = "/daemon.StartedService/StopService" + StartedService_ReloadService_FullMethodName = "/daemon.StartedService/ReloadService" + StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus" + StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog" + StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel" + StartedService_ClearLogs_FullMethodName = "/daemon.StartedService/ClearLogs" + StartedService_SubscribeStatus_FullMethodName = "/daemon.StartedService/SubscribeStatus" + StartedService_SubscribeGroups_FullMethodName = "/daemon.StartedService/SubscribeGroups" + StartedService_GetClashModeStatus_FullMethodName = "/daemon.StartedService/GetClashModeStatus" + StartedService_SubscribeClashMode_FullMethodName = "/daemon.StartedService/SubscribeClashMode" + StartedService_SetClashMode_FullMethodName = "/daemon.StartedService/SetClashMode" + StartedService_URLTest_FullMethodName = "/daemon.StartedService/URLTest" + StartedService_SelectOutbound_FullMethodName = "/daemon.StartedService/SelectOutbound" + StartedService_SetGroupExpand_FullMethodName = "/daemon.StartedService/SetGroupExpand" + StartedService_GetSystemProxyStatus_FullMethodName = "/daemon.StartedService/GetSystemProxyStatus" + StartedService_SetSystemProxyEnabled_FullMethodName = "/daemon.StartedService/SetSystemProxyEnabled" + StartedService_TriggerDebugCrash_FullMethodName = "/daemon.StartedService/TriggerDebugCrash" + StartedService_TriggerOOMReport_FullMethodName = "/daemon.StartedService/TriggerOOMReport" + StartedService_SubscribeConnections_FullMethodName = "/daemon.StartedService/SubscribeConnections" + StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection" + StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections" + StartedService_GetDeprecatedWarnings_FullMethodName = "/daemon.StartedService/GetDeprecatedWarnings" + StartedService_GetStartedAt_FullMethodName = "/daemon.StartedService/GetStartedAt" + StartedService_ListOutbounds_FullMethodName = "/daemon.StartedService/ListOutbounds" + StartedService_SubscribeOutbounds_FullMethodName = "/daemon.StartedService/SubscribeOutbounds" + StartedService_StartNetworkQualityTest_FullMethodName = "/daemon.StartedService/StartNetworkQualityTest" + StartedService_StartSTUNTest_FullMethodName = "/daemon.StartedService/StartSTUNTest" + StartedService_SubscribeTailscaleStatus_FullMethodName = "/daemon.StartedService/SubscribeTailscaleStatus" ) // StartedServiceClient is the client API for StartedService service. @@ -75,6 +76,7 @@ type StartedServiceClient interface { SubscribeOutbounds(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OutboundList], error) StartNetworkQualityTest(ctx context.Context, in *NetworkQualityTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[NetworkQualityTestProgress], error) StartSTUNTest(ctx context.Context, in *STUNTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[STUNTestProgress], error) + SubscribeTailscaleStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscaleStatusUpdate], error) } type startedServiceClient struct { @@ -436,6 +438,25 @@ func (c *startedServiceClient) StartSTUNTest(ctx context.Context, in *STUNTestRe // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type StartedService_StartSTUNTestClient = grpc.ServerStreamingClient[STUNTestProgress] +func (c *startedServiceClient) SubscribeTailscaleStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscaleStatusUpdate], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[9], StartedService_SubscribeTailscaleStatus_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[emptypb.Empty, TailscaleStatusUpdate]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_SubscribeTailscaleStatusClient = grpc.ServerStreamingClient[TailscaleStatusUpdate] + // StartedServiceServer is the server API for StartedService service. // All implementations must embed UnimplementedStartedServiceServer // for forward compatibility. @@ -467,6 +488,7 @@ type StartedServiceServer interface { SubscribeOutbounds(*emptypb.Empty, grpc.ServerStreamingServer[OutboundList]) error StartNetworkQualityTest(*NetworkQualityTestRequest, grpc.ServerStreamingServer[NetworkQualityTestProgress]) error StartSTUNTest(*STUNTestRequest, grpc.ServerStreamingServer[STUNTestProgress]) error + SubscribeTailscaleStatus(*emptypb.Empty, grpc.ServerStreamingServer[TailscaleStatusUpdate]) error mustEmbedUnimplementedStartedServiceServer() } @@ -584,6 +606,10 @@ func (UnimplementedStartedServiceServer) StartNetworkQualityTest(*NetworkQuality func (UnimplementedStartedServiceServer) StartSTUNTest(*STUNTestRequest, grpc.ServerStreamingServer[STUNTestProgress]) error { return status.Error(codes.Unimplemented, "method StartSTUNTest not implemented") } + +func (UnimplementedStartedServiceServer) SubscribeTailscaleStatus(*emptypb.Empty, grpc.ServerStreamingServer[TailscaleStatusUpdate]) error { + return status.Error(codes.Unimplemented, "method SubscribeTailscaleStatus not implemented") +} func (UnimplementedStartedServiceServer) mustEmbedUnimplementedStartedServiceServer() {} func (UnimplementedStartedServiceServer) testEmbeddedByValue() {} @@ -1028,6 +1054,17 @@ func _StartedService_StartSTUNTest_Handler(srv interface{}, stream grpc.ServerSt // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type StartedService_StartSTUNTestServer = grpc.ServerStreamingServer[STUNTestProgress] +func _StartedService_SubscribeTailscaleStatus_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(emptypb.Empty) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StartedServiceServer).SubscribeTailscaleStatus(m, &grpc.GenericServerStream[emptypb.Empty, TailscaleStatusUpdate]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_SubscribeTailscaleStatusServer = grpc.ServerStreamingServer[TailscaleStatusUpdate] + // StartedService_ServiceDesc is the grpc.ServiceDesc for StartedService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -1154,6 +1191,11 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{ Handler: _StartedService_StartSTUNTest_Handler, ServerStreams: true, }, + { + StreamName: "SubscribeTailscaleStatus", + Handler: _StartedService_SubscribeTailscaleStatus_Handler, + ServerStreams: true, + }, }, Metadata: "daemon/started_service.proto", } diff --git a/experimental/libbox/command_client.go b/experimental/libbox/command_client.go index 8f511d4644..cc908b847a 100644 --- a/experimental/libbox/command_client.go +++ b/experimental/libbox/command_client.go @@ -720,3 +720,25 @@ func (c *CommandClient) StartSTUNTest(server string, outboundTag string, handler handler.OnProgress(stunTestProgressFromGRPC(event)) } } + +func (c *CommandClient) SubscribeTailscaleStatus(handler TailscaleStatusHandler) error { + client, err := c.getClientForCall() + if err != nil { + return err + } + if c.standalone { + defer c.closeConnection() + } + stream, err := client.SubscribeTailscaleStatus(context.Background(), &emptypb.Empty{}) + if err != nil { + return err + } + for { + event, recvErr := stream.Recv() + if recvErr != nil { + handler.OnError(recvErr.Error()) + return recvErr + } + handler.OnStatusUpdate(tailscaleStatusUpdateFromGRPC(event)) + } +} diff --git a/experimental/libbox/command_types_tailscale.go b/experimental/libbox/command_types_tailscale.go new file mode 100644 index 0000000000..dc17639df4 --- /dev/null +++ b/experimental/libbox/command_types_tailscale.go @@ -0,0 +1,132 @@ +package libbox + +import "github.com/sagernet/sing-box/daemon" + +type TailscaleStatusUpdate struct { + endpoints []*TailscaleEndpointStatus +} + +func (u *TailscaleStatusUpdate) Endpoints() TailscaleEndpointStatusIterator { + return newIterator(u.endpoints) +} + +type TailscaleEndpointStatusIterator interface { + Next() *TailscaleEndpointStatus + HasNext() bool +} + +type TailscaleEndpointStatus struct { + EndpointTag string + BackendState string + AuthURL string + NetworkName string + MagicDNSSuffix string + Self *TailscalePeer + userGroups []*TailscaleUserGroup +} + +func (s *TailscaleEndpointStatus) UserGroups() TailscaleUserGroupIterator { + return newIterator(s.userGroups) +} + +type TailscaleUserGroupIterator interface { + Next() *TailscaleUserGroup + HasNext() bool +} + +type TailscaleUserGroup struct { + UserID int64 + LoginName string + DisplayName string + ProfilePicURL string + peers []*TailscalePeer +} + +func (g *TailscaleUserGroup) Peers() TailscalePeerIterator { + return newIterator(g.peers) +} + +type TailscalePeerIterator interface { + Next() *TailscalePeer + HasNext() bool +} + +type TailscalePeer struct { + HostName string + DNSName string + OS string + tailscaleIPs []string + Online bool + ExitNode bool + ExitNodeOption bool + Active bool + RxBytes int64 + TxBytes int64 + KeyExpiry int64 +} + +func (p *TailscalePeer) TailscaleIPs() StringIterator { + return newIterator(p.tailscaleIPs) +} + +type TailscaleStatusHandler interface { + OnStatusUpdate(status *TailscaleStatusUpdate) + OnError(message string) +} + +func tailscaleStatusUpdateFromGRPC(update *daemon.TailscaleStatusUpdate) *TailscaleStatusUpdate { + endpoints := make([]*TailscaleEndpointStatus, len(update.Endpoints)) + for i, endpoint := range update.Endpoints { + endpoints[i] = tailscaleEndpointStatusFromGRPC(endpoint) + } + return &TailscaleStatusUpdate{endpoints: endpoints} +} + +func tailscaleEndpointStatusFromGRPC(status *daemon.TailscaleEndpointStatus) *TailscaleEndpointStatus { + userGroups := make([]*TailscaleUserGroup, len(status.UserGroups)) + for i, group := range status.UserGroups { + userGroups[i] = tailscaleUserGroupFromGRPC(group) + } + result := &TailscaleEndpointStatus{ + EndpointTag: status.EndpointTag, + BackendState: status.BackendState, + AuthURL: status.AuthURL, + NetworkName: status.NetworkName, + MagicDNSSuffix: status.MagicDNSSuffix, + userGroups: userGroups, + } + if status.Self != nil { + result.Self = tailscalePeerFromGRPC(status.Self) + } + return result +} + +func tailscaleUserGroupFromGRPC(group *daemon.TailscaleUserGroup) *TailscaleUserGroup { + peers := make([]*TailscalePeer, len(group.Peers)) + for i, peer := range group.Peers { + peers[i] = tailscalePeerFromGRPC(peer) + } + return &TailscaleUserGroup{ + UserID: group.UserID, + LoginName: group.LoginName, + DisplayName: group.DisplayName, + ProfilePicURL: group.ProfilePicURL, + peers: peers, + } +} + +func tailscalePeerFromGRPC(peer *daemon.TailscalePeer) *TailscalePeer { + return &TailscalePeer{ + HostName: peer.HostName, + DNSName: peer.DnsName, + OS: peer.Os, + tailscaleIPs: peer.TailscaleIPs, + Online: peer.Online, + ExitNode: peer.ExitNode, + ExitNodeOption: peer.ExitNodeOption, + Active: peer.Active, + RxBytes: peer.RxBytes, + TxBytes: peer.TxBytes, + KeyExpiry: peer.KeyExpiry, + } +} diff --git a/experimental/libbox/debug.go b/experimental/libbox/debug.go index 63f2b49e98..75942976f6 100644 --- a/experimental/libbox/debug.go +++ b/experimental/libbox/debug.go @@ -1,9 +1,12 @@ package libbox -import "time" +import ( + "time" + "unsafe" +) func TriggerGoPanic() { time.AfterFunc(200*time.Millisecond, func() { - panic("debug go crash") + *(*int)(unsafe.Pointer(uintptr(0))) = 0 }) } diff --git a/experimental/libbox/setup.go b/experimental/libbox/setup.go index ac706e9db6..9f8aa03cf9 100644 --- a/experimental/libbox/setup.go +++ b/experimental/libbox/setup.go @@ -12,6 +12,7 @@ import ( "github.com/sagernet/sing-box/common/networkquality" "github.com/sagernet/sing-box/common/stun" C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/experimental/locale" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/service/oomkiller" @@ -181,6 +182,10 @@ func FormatNATFiltering(value int32) string { return stun.NATFiltering(value).String() } +func FormatFQDN(fqdn string) string { + return dns.FqdnToDomain(fqdn) +} + func ProxyDisplayType(proxyType string) string { return C.ProxyDisplayName(proxyType) } diff --git a/protocol/tailscale/status.go b/protocol/tailscale/status.go new file mode 100644 index 0000000000..af6ce10393 --- /dev/null +++ b/protocol/tailscale/status.go @@ -0,0 +1,92 @@ +//go:build with_gvisor + +package tailscale + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/tailscale/ipn" + "github.com/sagernet/tailscale/ipn/ipnstate" + "github.com/sagernet/tailscale/tailcfg" +) + +var _ adapter.TailscaleStatusProvider = (*Endpoint)(nil) + +func (t *Endpoint) SubscribeTailscaleStatus(ctx context.Context, fn func(*adapter.TailscaleEndpointStatus)) error { + localBackend := t.server.ExportLocalBackend() + sendStatus := func() { + status := localBackend.Status() + fn(convertTailscaleStatus(status)) + } + sendStatus() + localBackend.WatchNotifications(ctx, ipn.NotifyInitialState|ipn.NotifyInitialNetMap|ipn.NotifyRateLimit, nil, func(roNotify *ipn.Notify) (keepGoing bool) { + select { + case <-ctx.Done(): + return false + default: + } + if roNotify.State != nil || roNotify.NetMap != nil || roNotify.BrowseToURL != nil { + sendStatus() + } + return true + }) + return ctx.Err() +} + +func convertTailscaleStatus(status *ipnstate.Status) *adapter.TailscaleEndpointStatus { + result := &adapter.TailscaleEndpointStatus{ + BackendState: status.BackendState, + AuthURL: status.AuthURL, + } + if status.CurrentTailnet != nil { + result.NetworkName = status.CurrentTailnet.Name + result.MagicDNSSuffix = status.CurrentTailnet.MagicDNSSuffix + } + if status.Self != nil { + result.Self = convertTailscalePeer(status.Self) + } + result.Users = make(map[int64]*adapter.TailscaleUser, len(status.User)) + for userID, profile := range status.User { + result.Users[int64(userID)] = convertTailscaleUser(userID, profile) + } + result.Peers = make([]*adapter.TailscalePeer, 0, len(status.Peer)) + for _, peer := range status.Peer { + result.Peers = append(result.Peers, convertTailscalePeer(peer)) + } + return result +} + +func convertTailscalePeer(peer *ipnstate.PeerStatus) *adapter.TailscalePeer { + ips := make([]string, len(peer.TailscaleIPs)) + for i, ip := range peer.TailscaleIPs { + ips[i] = ip.String() + } + var keyExpiry int64 + if peer.KeyExpiry != nil { + keyExpiry = peer.KeyExpiry.Unix() + } + return &adapter.TailscalePeer{ + HostName: peer.HostName, + DNSName: peer.DNSName, + OS: peer.OS, + TailscaleIPs: ips, + Online: peer.Online, + ExitNode: peer.ExitNode, + ExitNodeOption: peer.ExitNodeOption, + Active: peer.Active, + RxBytes: peer.RxBytes, + TxBytes: peer.TxBytes, + UserID: int64(peer.UserID), + KeyExpiry: keyExpiry, + } +} + +func convertTailscaleUser(id tailcfg.UserID, profile tailcfg.UserProfile) *adapter.TailscaleUser { + return &adapter.TailscaleUser{ + ID: int64(id), + LoginName: profile.LoginName, + DisplayName: profile.DisplayName, + ProfilePicURL: profile.ProfilePicURL, + } +} From a7b02f9cb208ee9fa3819363ad7fcc2c35922ccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 9 Apr 2026 19:34:54 +0800 Subject: [PATCH 16/93] Revert "Also enable certificate store by default on Apple platforms" This reverts commit 62cb06c02fc569beb7b2ffa3f0a10ef23e136748. --- box.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/box.go b/box.go index d21ab29a44..619b05bba8 100644 --- a/box.go +++ b/box.go @@ -170,7 +170,10 @@ func New(options Options) (*Box, error) { var internalServices []adapter.LifecycleService certificateOptions := common.PtrValueOrDefault(options.Certificate) - if C.IsAndroid || C.IsDarwin || certificateOptions.Store != "" { + if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem || + len(certificateOptions.Certificate) > 0 || + len(certificateOptions.CertificatePath) > 0 || + len(certificateOptions.CertificateDirectoryPath) > 0 { certificateStore, err := certificate.NewStore(ctx, logFactory.NewLogger("certificate"), certificateOptions) if err != nil { return nil, err From ec75e5ec0ab24e0980f1cb6d73cbc48a98bc9b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 9 Apr 2026 22:02:38 +0800 Subject: [PATCH 17/93] Fix rules lock --- dns/router.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dns/router.go b/dns/router.go index 8392da9113..8fbaa27297 100644 --- a/dns/router.go +++ b/dns/router.go @@ -589,12 +589,13 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte return &responseMessage, nil } r.rulesAccess.RLock() - defer r.rulesAccess.RUnlock() if r.closing { + r.rulesAccess.RUnlock() return nil, E.New("dns router closed") } rules := r.rules legacyDNSMode := r.legacyDNSMode + r.rulesAccess.RUnlock() r.logger.DebugContext(ctx, "exchange ", FormatQuestion(message.Question[0].String())) var ( response *mDNS.Msg @@ -701,12 +702,13 @@ done: func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) { r.rulesAccess.RLock() - defer r.rulesAccess.RUnlock() if r.closing { + r.rulesAccess.RUnlock() return nil, E.New("dns router closed") } rules := r.rules legacyDNSMode := r.legacyDNSMode + r.rulesAccess.RUnlock() var ( responseAddrs []netip.Addr err error From eade67726a7250096917b704c0027eb1c0594ec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 10 Apr 2026 09:24:27 +0800 Subject: [PATCH 18/93] Fix darwin local DNS transport --- dns/rcode.go | 9 +- dns/transport/local/local_darwin.go | 50 +------- dns/transport/local/local_darwin_cgo.go | 162 ++++++++++++++++++++++++ dns/transport/local/local_shared.go | 2 + 4 files changed, 170 insertions(+), 53 deletions(-) create mode 100644 dns/transport/local/local_darwin_cgo.go diff --git a/dns/rcode.go b/dns/rcode.go index 59c564b658..417d41fa1e 100644 --- a/dns/rcode.go +++ b/dns/rcode.go @@ -5,10 +5,11 @@ import ( ) const ( - RcodeSuccess RcodeError = mDNS.RcodeSuccess - RcodeFormatError RcodeError = mDNS.RcodeFormatError - RcodeNameError RcodeError = mDNS.RcodeNameError - RcodeRefused RcodeError = mDNS.RcodeRefused + RcodeSuccess RcodeError = mDNS.RcodeSuccess + RcodeServerFailure RcodeError = mDNS.RcodeServerFailure + RcodeFormatError RcodeError = mDNS.RcodeFormatError + RcodeNameError RcodeError = mDNS.RcodeNameError + RcodeRefused RcodeError = mDNS.RcodeRefused ) type RcodeError int diff --git a/dns/transport/local/local_darwin.go b/dns/transport/local/local_darwin.go index 5f1e60b15a..eb33d64fa7 100644 --- a/dns/transport/local/local_darwin.go +++ b/dns/transport/local/local_darwin.go @@ -4,8 +4,6 @@ package local import ( "context" - "errors" - "net" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" @@ -14,7 +12,6 @@ import ( "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "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" @@ -35,10 +32,8 @@ type Transport struct { logger logger.ContextLogger hosts *hosts.File dialer N.Dialer - preferGo bool fallback bool dhcpTransport dhcpTransport - resolver net.Resolver } type dhcpTransport interface { @@ -52,14 +47,12 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt if err != nil { return nil, err } - transportAdapter := dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options) return &Transport{ - TransportAdapter: transportAdapter, + TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options), ctx: ctx, logger: logger, hosts: hosts.NewFile(hosts.DefaultPath), dialer: transportDialer, - preferGo: options.PreferGo, }, nil } @@ -97,44 +90,3 @@ func (t *Transport) Reset() { t.dhcpTransport.Reset() } } - -func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { - question := message.Question[0] - if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA { - addresses := t.hosts.Lookup(dns.FqdnToDomain(question.Name)) - if len(addresses) > 0 { - return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil - } - } - if !t.fallback { - return t.exchange(ctx, message, question.Name) - } - if t.dhcpTransport != nil { - dhcpTransports := t.dhcpTransport.Fetch() - if len(dhcpTransports) > 0 { - return t.dhcpTransport.Exchange0(ctx, message, dhcpTransports) - } - } - if t.preferGo { - // Assuming the user knows what they are doing, we still execute the query which will fail. - return t.exchange(ctx, message, question.Name) - } - if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA { - var network string - if question.Qtype == mDNS.TypeA { - network = "ip4" - } else { - network = "ip6" - } - addresses, err := t.resolver.LookupNetIP(ctx, network, question.Name) - if err != nil { - var dnsError *net.DNSError - if errors.As(err, &dnsError) && dnsError.IsNotFound { - return nil, dns.RcodeRefused - } - return nil, err - } - return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil - } - return nil, E.New("only A and AAAA queries are supported on Apple platforms when using TUN and DHCP unavailable.") -} diff --git a/dns/transport/local/local_darwin_cgo.go b/dns/transport/local/local_darwin_cgo.go new file mode 100644 index 0000000000..00f5599548 --- /dev/null +++ b/dns/transport/local/local_darwin_cgo.go @@ -0,0 +1,162 @@ +//go:build darwin + +package local + +/* +#include +#include +#include + +static void *cgo_res_init() { + res_state state = calloc(1, sizeof(struct __res_state)); + if (state == NULL) return NULL; + if (res_ninit(state) != 0) { + free(state); + return NULL; + } + return state; +} + +static void cgo_res_destroy(void *opaque) { + res_state state = (res_state)opaque; + res_ndestroy(state); + free(state); +} + +static int cgo_res_nsearch(void *opaque, const char *dname, int class, int type, + unsigned char *answer, int anslen, + int timeout_seconds, + int *out_h_errno) { + res_state state = (res_state)opaque; + state->retrans = timeout_seconds; + state->retry = 1; + int n = res_nsearch(state, dname, class, type, answer, anslen); + if (n < 0) { + *out_h_errno = state->res_h_errno; + } + return n; +} +*/ +import "C" + +import ( + "context" + "errors" + "time" + "unsafe" + + boxC "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + E "github.com/sagernet/sing/common/exceptions" + + mDNS "github.com/miekg/dns" +) + +func resolvSearch(name string, class, qtype int, timeoutSeconds int) (*mDNS.Msg, error) { + state := C.cgo_res_init() + if state == nil { + return nil, E.New("res_ninit failed") + } + defer C.cgo_res_destroy(state) + + cName := C.CString(name) + defer C.free(unsafe.Pointer(cName)) + bufSize := 1232 + for { + answer := make([]byte, bufSize) + var hErrno C.int + n := C.cgo_res_nsearch(state, cName, C.int(class), C.int(qtype), + (*C.uchar)(unsafe.Pointer(&answer[0])), C.int(len(answer)), + C.int(timeoutSeconds), + &hErrno) + if n >= 0 { + if int(n) > bufSize { + bufSize = int(n) + continue + } + var response mDNS.Msg + err := response.Unpack(answer[:int(n)]) + if err != nil { + return nil, E.Cause(err, "unpack res_nsearch response") + } + return &response, nil + } + var response mDNS.Msg + _ = response.Unpack(answer[:bufSize]) + if response.Response { + if response.Truncated && bufSize < 65535 { + bufSize *= 2 + if bufSize > 65535 { + bufSize = 65535 + } + continue + } + return &response, nil + } + switch hErrno { + case C.HOST_NOT_FOUND: + return nil, dns.RcodeNameError + case C.TRY_AGAIN: + return nil, dns.RcodeNameError + case C.NO_RECOVERY: + return nil, dns.RcodeServerFailure + case C.NO_DATA: + return nil, dns.RcodeSuccess + default: + return nil, E.New("res_nsearch: unknown error ", int(hErrno), " for ", name) + } + } +} + +func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { + question := message.Question[0] + if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA { + addresses := t.hosts.Lookup(dns.FqdnToDomain(question.Name)) + if len(addresses) > 0 { + return dns.FixedResponse(message.Id, question, addresses, boxC.DefaultDNSTTL), nil + } + } + if t.fallback && t.dhcpTransport != nil { + dhcpServers := t.dhcpTransport.Fetch() + if len(dhcpServers) > 0 { + return t.dhcpTransport.Exchange0(ctx, message, dhcpServers) + } + } + name := question.Name + timeoutSeconds := int(boxC.DNSTimeout / time.Second) + if deadline, hasDeadline := ctx.Deadline(); hasDeadline { + remaining := time.Until(deadline) + if remaining <= 0 { + return nil, context.DeadlineExceeded + } + seconds := int(remaining.Seconds()) + if seconds < 1 { + seconds = 1 + } + timeoutSeconds = seconds + } + type resolvResult struct { + response *mDNS.Msg + err error + } + resultCh := make(chan resolvResult, 1) + go func() { + response, err := resolvSearch(name, int(question.Qclass), int(question.Qtype), timeoutSeconds) + resultCh <- resolvResult{response, err} + }() + var result resolvResult + select { + case <-ctx.Done(): + return nil, ctx.Err() + case result = <-resultCh: + } + if result.err != nil { + var rcodeError dns.RcodeError + if errors.As(result.err, &rcodeError) { + return dns.FixedResponseStatus(message, int(rcodeError)), nil + } + return nil, result.err + } + result.response.Id = message.Id + return result.response, nil +} diff --git a/dns/transport/local/local_shared.go b/dns/transport/local/local_shared.go index 7763545841..64a23a9fcb 100644 --- a/dns/transport/local/local_shared.go +++ b/dns/transport/local/local_shared.go @@ -1,3 +1,5 @@ +//go:build !darwin + package local import ( From 524578a63549eeeb28c25595e682326e0ace05a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 10 Apr 2026 09:24:42 +0800 Subject: [PATCH 19/93] tools: Tailscale status --- adapter/tailscale.go | 30 ++- daemon/started_service.go | 142 ++++++---- daemon/started_service.pb.go | 244 ++++++++++++++---- daemon/started_service.proto | 16 +- daemon/started_service_grpc.pb.go | 81 +++--- experimental/libbox/command_client.go | 40 ++- .../libbox/command_types_tailscale_ping.go | 28 ++ protocol/tailscale/hostinfo_tvos.go | 16 ++ protocol/tailscale/ping.go | 55 ++++ protocol/tailscale/status.go | 47 ++-- 10 files changed, 523 insertions(+), 176 deletions(-) create mode 100644 experimental/libbox/command_types_tailscale_ping.go create mode 100644 protocol/tailscale/hostinfo_tvos.go create mode 100644 protocol/tailscale/ping.go diff --git a/adapter/tailscale.go b/adapter/tailscale.go index 35809a542e..22f48e62b2 100644 --- a/adapter/tailscale.go +++ b/adapter/tailscale.go @@ -2,8 +2,18 @@ package adapter import "context" -type TailscaleStatusProvider interface { +type TailscaleEndpoint interface { SubscribeTailscaleStatus(ctx context.Context, fn func(*TailscaleEndpointStatus)) error + StartTailscalePing(ctx context.Context, peerIP string, fn func(*TailscalePingResult)) error +} + +type TailscalePingResult struct { + LatencyMs float64 + IsDirect bool + Endpoint string + DERPRegionID int32 + DERPRegionCode string + Error string } type TailscaleEndpointStatus struct { @@ -12,8 +22,15 @@ type TailscaleEndpointStatus struct { NetworkName string MagicDNSSuffix string Self *TailscalePeer - Users map[int64]*TailscaleUser - Peers []*TailscalePeer + UserGroups []*TailscaleUserGroup +} + +type TailscaleUserGroup struct { + UserID int64 + LoginName string + DisplayName string + ProfilePicURL string + Peers []*TailscalePeer } type TailscalePeer struct { @@ -30,10 +47,3 @@ type TailscalePeer struct { UserID int64 KeyExpiry int64 } - -type TailscaleUser struct { - ID int64 - LoginName string - DisplayName string - ProfilePicURL string -} diff --git a/daemon/started_service.go b/daemon/started_service.go index 3b7709ff40..aa15c7bec0 100644 --- a/daemon/started_service.go +++ b/daemon/started_service.go @@ -1085,31 +1085,6 @@ func (s *StartedService) GetStartedAt(ctx context.Context, empty *emptypb.Empty) return &StartedAt{StartedAt: s.startedAt.UnixMilli()}, nil } -func (s *StartedService) ListOutbounds(ctx context.Context, _ *emptypb.Empty) (*OutboundList, error) { - s.serviceAccess.RLock() - if s.serviceStatus.Status != ServiceStatus_STARTED { - s.serviceAccess.RUnlock() - return nil, os.ErrInvalid - } - boxService := s.instance - s.serviceAccess.RUnlock() - historyStorage := boxService.urlTestHistoryStorage - outbounds := boxService.instance.Outbound().Outbounds() - var list OutboundList - for _, ob := range outbounds { - item := &GroupItem{ - Tag: ob.Tag(), - Type: ob.Type(), - } - if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(ob)); history != nil { - item.UrlTestTime = history.Time.Unix() - item.UrlTestDelay = int32(history.Delay) - } - list.Outbounds = append(list.Outbounds, item) - } - return &list, nil -} - func (s *StartedService) SubscribeOutbounds(_ *emptypb.Empty, server grpc.ServerStreamingServer[OutboundList]) error { err := s.waitForStarted(server.Context()) if err != nil { @@ -1129,9 +1104,8 @@ func (s *StartedService) SubscribeOutbounds(_ *emptypb.Empty, server grpc.Server boxService := s.instance s.serviceAccess.RUnlock() historyStorage := boxService.urlTestHistoryStorage - outbounds := boxService.instance.Outbound().Outbounds() var list OutboundList - for _, ob := range outbounds { + for _, ob := range boxService.instance.Outbound().Outbounds() { item := &GroupItem{ Tag: ob.Tag(), Type: ob.Type(), @@ -1142,6 +1116,17 @@ func (s *StartedService) SubscribeOutbounds(_ *emptypb.Empty, server grpc.Server } list.Outbounds = append(list.Outbounds, item) } + for _, ep := range boxService.instance.Endpoint().Endpoints() { + item := &GroupItem{ + Tag: ep.Tag(), + Type: ep.Type(), + } + if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(ep)); history != nil { + item.UrlTestTime = history.Time.Unix() + item.UrlTestDelay = int32(history.Delay) + } + list.Outbounds = append(list.Outbounds, item) + } err = server.Send(&list) if err != nil { return err @@ -1308,14 +1293,14 @@ func (s *StartedService) SubscribeTailscaleStatus( type tailscaleEndpoint struct { tag string - provider adapter.TailscaleStatusProvider + provider adapter.TailscaleEndpoint } var endpoints []tailscaleEndpoint for _, endpoint := range endpointManager.Endpoints() { if endpoint.Type() != C.TypeTailscale { continue } - provider, loaded := endpoint.(adapter.TailscaleStatusProvider) + provider, loaded := endpoint.(adapter.TailscaleEndpoint) if !loaded { continue } @@ -1339,7 +1324,7 @@ func (s *StartedService) SubscribeTailscaleStatus( var waitGroup sync.WaitGroup for _, endpoint := range endpoints { waitGroup.Add(1) - go func(tag string, provider adapter.TailscaleStatusProvider) { + go func(tag string, provider adapter.TailscaleEndpoint) { defer waitGroup.Done() _ = provider.SubscribeTailscaleStatus(ctx, func(endpointStatus *adapter.TailscaleEndpointStatus) { select { @@ -1355,12 +1340,16 @@ func (s *StartedService) SubscribeTailscaleStatus( close(updates) }() + var tags []string statuses := make(map[string]*adapter.TailscaleEndpointStatus, len(endpoints)) for update := range updates { + if _, exists := statuses[update.tag]; !exists { + tags = append(tags, update.tag) + } statuses[update.tag] = update.status protoEndpoints := make([]*TailscaleEndpointStatus, 0, len(statuses)) - for tag, endpointStatus := range statuses { - protoEndpoints = append(protoEndpoints, tailscaleEndpointStatusToProto(tag, endpointStatus)) + for _, tag := range tags { + protoEndpoints = append(protoEndpoints, tailscaleEndpointStatusToProto(tag, statuses[tag])) } sendErr := server.Send(&TailscaleStatusUpdate{ Endpoints: protoEndpoints, @@ -1373,27 +1362,19 @@ func (s *StartedService) SubscribeTailscaleStatus( } func tailscaleEndpointStatusToProto(tag string, s *adapter.TailscaleEndpointStatus) *TailscaleEndpointStatus { - userGroupMap := make(map[int64]*TailscaleUserGroup) - for userID, user := range s.Users { - userGroupMap[userID] = &TailscaleUserGroup{ - UserID: userID, - LoginName: user.LoginName, - DisplayName: user.DisplayName, - ProfilePicURL: user.ProfilePicURL, + userGroups := make([]*TailscaleUserGroup, len(s.UserGroups)) + for i, group := range s.UserGroups { + peers := make([]*TailscalePeer, len(group.Peers)) + for j, peer := range group.Peers { + peers[j] = tailscalePeerToProto(peer) } - } - for _, peer := range s.Peers { - protoPeer := tailscalePeerToProto(peer) - group, loaded := userGroupMap[peer.UserID] - if !loaded { - group = &TailscaleUserGroup{UserID: peer.UserID} - userGroupMap[peer.UserID] = group + userGroups[i] = &TailscaleUserGroup{ + UserID: group.UserID, + LoginName: group.LoginName, + DisplayName: group.DisplayName, + ProfilePicURL: group.ProfilePicURL, + Peers: peers, } - group.Peers = append(group.Peers, protoPeer) - } - userGroups := make([]*TailscaleUserGroup, 0, len(userGroupMap)) - for _, group := range userGroupMap { - userGroups = append(userGroups, group) } result := &TailscaleEndpointStatus{ EndpointTag: tag, @@ -1425,6 +1406,65 @@ func tailscalePeerToProto(peer *adapter.TailscalePeer) *TailscalePeer { } } +func (s *StartedService) StartTailscalePing( + request *TailscalePingRequest, + server grpc.ServerStreamingServer[TailscalePingResponse], +) error { + err := s.waitForStarted(server.Context()) + if err != nil { + return err + } + s.serviceAccess.RLock() + boxService := s.instance + s.serviceAccess.RUnlock() + + endpointManager := service.FromContext[adapter.EndpointManager](boxService.ctx) + if endpointManager == nil { + return status.Error(codes.FailedPrecondition, "endpoint manager not available") + } + + var provider adapter.TailscaleEndpoint + if request.EndpointTag != "" { + endpoint, loaded := endpointManager.Get(request.EndpointTag) + if !loaded { + return status.Error(codes.NotFound, "endpoint not found: "+request.EndpointTag) + } + if endpoint.Type() != C.TypeTailscale { + return status.Error(codes.InvalidArgument, "endpoint is not Tailscale: "+request.EndpointTag) + } + pingProvider, loaded := endpoint.(adapter.TailscaleEndpoint) + if !loaded { + return status.Error(codes.FailedPrecondition, "endpoint does not support ping") + } + provider = pingProvider + } else { + for _, endpoint := range endpointManager.Endpoints() { + if endpoint.Type() != C.TypeTailscale { + continue + } + pingProvider, loaded := endpoint.(adapter.TailscaleEndpoint) + if loaded { + provider = pingProvider + break + } + } + if provider == nil { + return status.Error(codes.NotFound, "no Tailscale endpoint found") + } + } + + return provider.StartTailscalePing(server.Context(), request.PeerIP, func(result *adapter.TailscalePingResult) { + _ = server.Send(&TailscalePingResponse{ + LatencyMs: result.LatencyMs, + IsDirect: result.IsDirect, + Endpoint: result.Endpoint, + DerpRegionID: result.DERPRegionID, + DerpRegionCode: result.DERPRegionCode, + Error: result.Error, + }) + }) +} + func (s *StartedService) mustEmbedUnimplementedStartedServiceServer() { } diff --git a/daemon/started_service.pb.go b/daemon/started_service.pb.go index 402b01013d..289069608f 100644 --- a/daemon/started_service.pb.go +++ b/daemon/started_service.pb.go @@ -2584,6 +2584,142 @@ func (x *TailscalePeer) GetKeyExpiry() int64 { return 0 } +type TailscalePingRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + EndpointTag string `protobuf:"bytes,1,opt,name=endpointTag,proto3" json:"endpointTag,omitempty"` + PeerIP string `protobuf:"bytes,2,opt,name=peerIP,proto3" json:"peerIP,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TailscalePingRequest) Reset() { + *x = TailscalePingRequest{} + mi := &file_daemon_started_service_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TailscalePingRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TailscalePingRequest) ProtoMessage() {} + +func (x *TailscalePingRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TailscalePingRequest.ProtoReflect.Descriptor instead. +func (*TailscalePingRequest) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{35} +} + +func (x *TailscalePingRequest) GetEndpointTag() string { + if x != nil { + return x.EndpointTag + } + return "" +} + +func (x *TailscalePingRequest) GetPeerIP() string { + if x != nil { + return x.PeerIP + } + return "" +} + +type TailscalePingResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + LatencyMs float64 `protobuf:"fixed64,1,opt,name=latencyMs,proto3" json:"latencyMs,omitempty"` + IsDirect bool `protobuf:"varint,2,opt,name=isDirect,proto3" json:"isDirect,omitempty"` + Endpoint string `protobuf:"bytes,3,opt,name=endpoint,proto3" json:"endpoint,omitempty"` + DerpRegionID int32 `protobuf:"varint,4,opt,name=derpRegionID,proto3" json:"derpRegionID,omitempty"` + DerpRegionCode string `protobuf:"bytes,5,opt,name=derpRegionCode,proto3" json:"derpRegionCode,omitempty"` + Error string `protobuf:"bytes,6,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TailscalePingResponse) Reset() { + *x = TailscalePingResponse{} + mi := &file_daemon_started_service_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TailscalePingResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TailscalePingResponse) ProtoMessage() {} + +func (x *TailscalePingResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_started_service_proto_msgTypes[36] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TailscalePingResponse.ProtoReflect.Descriptor instead. +func (*TailscalePingResponse) Descriptor() ([]byte, []int) { + return file_daemon_started_service_proto_rawDescGZIP(), []int{36} +} + +func (x *TailscalePingResponse) GetLatencyMs() float64 { + if x != nil { + return x.LatencyMs + } + return 0 +} + +func (x *TailscalePingResponse) GetIsDirect() bool { + if x != nil { + return x.IsDirect + } + return false +} + +func (x *TailscalePingResponse) GetEndpoint() string { + if x != nil { + return x.Endpoint + } + return "" +} + +func (x *TailscalePingResponse) GetDerpRegionID() int32 { + if x != nil { + return x.DerpRegionID + } + return 0 +} + +func (x *TailscalePingResponse) GetDerpRegionCode() string { + if x != nil { + return x.DerpRegionCode + } + return "" +} + +func (x *TailscalePingResponse) GetError() string { + if x != nil { + return x.Error + } + return "" +} + type Log_Message struct { state protoimpl.MessageState `protogen:"open.v1"` Level LogLevel `protobuf:"varint,1,opt,name=level,proto3,enum=daemon.LogLevel" json:"level,omitempty"` @@ -2594,7 +2730,7 @@ type Log_Message struct { func (x *Log_Message) Reset() { *x = Log_Message{} - mi := &file_daemon_started_service_proto_msgTypes[35] + mi := &file_daemon_started_service_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2606,7 +2742,7 @@ func (x *Log_Message) String() string { func (*Log_Message) ProtoMessage() {} func (x *Log_Message) ProtoReflect() protoreflect.Message { - mi := &file_daemon_started_service_proto_msgTypes[35] + mi := &file_daemon_started_service_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2839,7 +2975,17 @@ const file_daemon_started_service_proto_rawDesc = "" + "\arxBytes\x18\t \x01(\x03R\arxBytes\x12\x18\n" + "\atxBytes\x18\n" + " \x01(\x03R\atxBytes\x12\x1c\n" + - "\tkeyExpiry\x18\v \x01(\x03R\tkeyExpiry*U\n" + + "\tkeyExpiry\x18\v \x01(\x03R\tkeyExpiry\"P\n" + + "\x14TailscalePingRequest\x12 \n" + + "\vendpointTag\x18\x01 \x01(\tR\vendpointTag\x12\x16\n" + + "\x06peerIP\x18\x02 \x01(\tR\x06peerIP\"\xcf\x01\n" + + "\x15TailscalePingResponse\x12\x1c\n" + + "\tlatencyMs\x18\x01 \x01(\x01R\tlatencyMs\x12\x1a\n" + + "\bisDirect\x18\x02 \x01(\bR\bisDirect\x12\x1a\n" + + "\bendpoint\x18\x03 \x01(\tR\bendpoint\x12\"\n" + + "\fderpRegionID\x18\x04 \x01(\x05R\fderpRegionID\x12&\n" + + "\x0ederpRegionCode\x18\x05 \x01(\tR\x0ederpRegionCode\x12\x14\n" + + "\x05error\x18\x06 \x01(\tR\x05error*U\n" + "\bLogLevel\x12\t\n" + "\x05PANIC\x10\x00\x12\t\n" + "\x05FATAL\x10\x01\x12\t\n" + @@ -2851,7 +2997,7 @@ const file_daemon_started_service_proto_rawDesc = "" + "\x13ConnectionEventType\x12\x18\n" + "\x14CONNECTION_EVENT_NEW\x10\x00\x12\x1b\n" + "\x17CONNECTION_EVENT_UPDATE\x10\x01\x12\x1b\n" + - "\x17CONNECTION_EVENT_CLOSED\x10\x022\x83\x10\n" + + "\x17CONNECTION_EVENT_CLOSED\x10\x022\x99\x10\n" + "\x0eStartedService\x12=\n" + "\vStopService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12?\n" + "\rReloadService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12K\n" + @@ -2875,12 +3021,12 @@ const file_daemon_started_service_proto_rawDesc = "" + "\x0fCloseConnection\x12\x1e.daemon.CloseConnectionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12G\n" + "\x13CloseAllConnections\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12M\n" + "\x15GetDeprecatedWarnings\x12\x16.google.protobuf.Empty\x1a\x1a.daemon.DeprecatedWarnings\"\x00\x12;\n" + - "\fGetStartedAt\x12\x16.google.protobuf.Empty\x1a\x11.daemon.StartedAt\"\x00\x12?\n" + - "\rListOutbounds\x12\x16.google.protobuf.Empty\x1a\x14.daemon.OutboundList\"\x00\x12F\n" + + "\fGetStartedAt\x12\x16.google.protobuf.Empty\x1a\x11.daemon.StartedAt\"\x00\x12F\n" + "\x12SubscribeOutbounds\x12\x16.google.protobuf.Empty\x1a\x14.daemon.OutboundList\"\x000\x01\x12d\n" + "\x17StartNetworkQualityTest\x12!.daemon.NetworkQualityTestRequest\x1a\".daemon.NetworkQualityTestProgress\"\x000\x01\x12F\n" + "\rStartSTUNTest\x12\x17.daemon.STUNTestRequest\x1a\x18.daemon.STUNTestProgress\"\x000\x01\x12U\n" + - "\x18SubscribeTailscaleStatus\x12\x16.google.protobuf.Empty\x1a\x1d.daemon.TailscaleStatusUpdate\"\x000\x01B%Z#github.com/sagernet/sing-box/daemonb\x06proto3" + "\x18SubscribeTailscaleStatus\x12\x16.google.protobuf.Empty\x1a\x1d.daemon.TailscaleStatusUpdate\"\x000\x01\x12U\n" + + "\x12StartTailscalePing\x12\x1c.daemon.TailscalePingRequest\x1a\x1d.daemon.TailscalePingResponse\"\x000\x01B%Z#github.com/sagernet/sing-box/daemonb\x06proto3" var ( file_daemon_started_service_proto_rawDescOnce sync.Once @@ -2896,7 +3042,7 @@ func file_daemon_started_service_proto_rawDescGZIP() []byte { var ( file_daemon_started_service_proto_enumTypes = make([]protoimpl.EnumInfo, 4) - file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 36) + file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 38) file_daemon_started_service_proto_goTypes = []any{ (LogLevel)(0), // 0: daemon.LogLevel (ConnectionEventType)(0), // 1: daemon.ConnectionEventType @@ -2937,14 +3083,16 @@ var ( (*TailscaleEndpointStatus)(nil), // 36: daemon.TailscaleEndpointStatus (*TailscaleUserGroup)(nil), // 37: daemon.TailscaleUserGroup (*TailscalePeer)(nil), // 38: daemon.TailscalePeer - (*Log_Message)(nil), // 39: daemon.Log.Message - (*emptypb.Empty)(nil), // 40: google.protobuf.Empty + (*TailscalePingRequest)(nil), // 39: daemon.TailscalePingRequest + (*TailscalePingResponse)(nil), // 40: daemon.TailscalePingResponse + (*Log_Message)(nil), // 41: daemon.Log.Message + (*emptypb.Empty)(nil), // 42: google.protobuf.Empty } ) var file_daemon_started_service_proto_depIdxs = []int32{ 2, // 0: daemon.ServiceStatus.status:type_name -> daemon.ServiceStatus.Type - 39, // 1: daemon.Log.messages:type_name -> daemon.Log.Message + 41, // 1: daemon.Log.messages:type_name -> daemon.Log.Message 0, // 2: daemon.DefaultLogLevel.level:type_name -> daemon.LogLevel 11, // 3: daemon.Groups.group:type_name -> daemon.Group 12, // 4: daemon.Group.items:type_name -> daemon.GroupItem @@ -2960,62 +3108,62 @@ var file_daemon_started_service_proto_depIdxs = []int32{ 37, // 14: daemon.TailscaleEndpointStatus.userGroups:type_name -> daemon.TailscaleUserGroup 38, // 15: daemon.TailscaleUserGroup.peers:type_name -> daemon.TailscalePeer 0, // 16: daemon.Log.Message.level:type_name -> daemon.LogLevel - 40, // 17: daemon.StartedService.StopService:input_type -> google.protobuf.Empty - 40, // 18: daemon.StartedService.ReloadService:input_type -> google.protobuf.Empty - 40, // 19: daemon.StartedService.SubscribeServiceStatus:input_type -> google.protobuf.Empty - 40, // 20: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty - 40, // 21: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty - 40, // 22: daemon.StartedService.ClearLogs:input_type -> google.protobuf.Empty + 42, // 17: daemon.StartedService.StopService:input_type -> google.protobuf.Empty + 42, // 18: daemon.StartedService.ReloadService:input_type -> google.protobuf.Empty + 42, // 19: daemon.StartedService.SubscribeServiceStatus:input_type -> google.protobuf.Empty + 42, // 20: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty + 42, // 21: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty + 42, // 22: daemon.StartedService.ClearLogs:input_type -> google.protobuf.Empty 6, // 23: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest - 40, // 24: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty - 40, // 25: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty - 40, // 26: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty + 42, // 24: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty + 42, // 25: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty + 42, // 26: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty 16, // 27: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode 13, // 28: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest 14, // 29: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest 15, // 30: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest - 40, // 31: daemon.StartedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty + 42, // 31: daemon.StartedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty 19, // 32: daemon.StartedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest 20, // 33: daemon.StartedService.TriggerDebugCrash:input_type -> daemon.DebugCrashRequest - 40, // 34: daemon.StartedService.TriggerOOMReport:input_type -> google.protobuf.Empty + 42, // 34: daemon.StartedService.TriggerOOMReport:input_type -> google.protobuf.Empty 21, // 35: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest 26, // 36: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest - 40, // 37: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty - 40, // 38: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty - 40, // 39: daemon.StartedService.GetStartedAt:input_type -> google.protobuf.Empty - 40, // 40: daemon.StartedService.ListOutbounds:input_type -> google.protobuf.Empty - 40, // 41: daemon.StartedService.SubscribeOutbounds:input_type -> google.protobuf.Empty - 31, // 42: daemon.StartedService.StartNetworkQualityTest:input_type -> daemon.NetworkQualityTestRequest - 33, // 43: daemon.StartedService.StartSTUNTest:input_type -> daemon.STUNTestRequest - 40, // 44: daemon.StartedService.SubscribeTailscaleStatus:input_type -> google.protobuf.Empty - 40, // 45: daemon.StartedService.StopService:output_type -> google.protobuf.Empty - 40, // 46: daemon.StartedService.ReloadService:output_type -> google.protobuf.Empty + 42, // 37: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty + 42, // 38: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty + 42, // 39: daemon.StartedService.GetStartedAt:input_type -> google.protobuf.Empty + 42, // 40: daemon.StartedService.SubscribeOutbounds:input_type -> google.protobuf.Empty + 31, // 41: daemon.StartedService.StartNetworkQualityTest:input_type -> daemon.NetworkQualityTestRequest + 33, // 42: daemon.StartedService.StartSTUNTest:input_type -> daemon.STUNTestRequest + 42, // 43: daemon.StartedService.SubscribeTailscaleStatus:input_type -> google.protobuf.Empty + 39, // 44: daemon.StartedService.StartTailscalePing:input_type -> daemon.TailscalePingRequest + 42, // 45: daemon.StartedService.StopService:output_type -> google.protobuf.Empty + 42, // 46: daemon.StartedService.ReloadService:output_type -> google.protobuf.Empty 4, // 47: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus 7, // 48: daemon.StartedService.SubscribeLog:output_type -> daemon.Log 8, // 49: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel - 40, // 50: daemon.StartedService.ClearLogs:output_type -> google.protobuf.Empty + 42, // 50: daemon.StartedService.ClearLogs:output_type -> google.protobuf.Empty 9, // 51: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status 10, // 52: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups 17, // 53: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus 16, // 54: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode - 40, // 55: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty - 40, // 56: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty - 40, // 57: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty - 40, // 58: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty + 42, // 55: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty + 42, // 56: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty + 42, // 57: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty + 42, // 58: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty 18, // 59: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus - 40, // 60: daemon.StartedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty - 40, // 61: daemon.StartedService.TriggerDebugCrash:output_type -> google.protobuf.Empty - 40, // 62: daemon.StartedService.TriggerOOMReport:output_type -> google.protobuf.Empty + 42, // 60: daemon.StartedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty + 42, // 61: daemon.StartedService.TriggerDebugCrash:output_type -> google.protobuf.Empty + 42, // 62: daemon.StartedService.TriggerOOMReport:output_type -> google.protobuf.Empty 23, // 63: daemon.StartedService.SubscribeConnections:output_type -> daemon.ConnectionEvents - 40, // 64: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty - 40, // 65: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty + 42, // 64: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty + 42, // 65: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty 27, // 66: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings 29, // 67: daemon.StartedService.GetStartedAt:output_type -> daemon.StartedAt - 30, // 68: daemon.StartedService.ListOutbounds:output_type -> daemon.OutboundList - 30, // 69: daemon.StartedService.SubscribeOutbounds:output_type -> daemon.OutboundList - 32, // 70: daemon.StartedService.StartNetworkQualityTest:output_type -> daemon.NetworkQualityTestProgress - 34, // 71: daemon.StartedService.StartSTUNTest:output_type -> daemon.STUNTestProgress - 35, // 72: daemon.StartedService.SubscribeTailscaleStatus:output_type -> daemon.TailscaleStatusUpdate + 30, // 68: daemon.StartedService.SubscribeOutbounds:output_type -> daemon.OutboundList + 32, // 69: daemon.StartedService.StartNetworkQualityTest:output_type -> daemon.NetworkQualityTestProgress + 34, // 70: daemon.StartedService.StartSTUNTest:output_type -> daemon.STUNTestProgress + 35, // 71: daemon.StartedService.SubscribeTailscaleStatus:output_type -> daemon.TailscaleStatusUpdate + 40, // 72: daemon.StartedService.StartTailscalePing:output_type -> daemon.TailscalePingResponse 45, // [45:73] is the sub-list for method output_type 17, // [17:45] is the sub-list for method input_type 17, // [17:17] is the sub-list for extension type_name @@ -3034,7 +3182,7 @@ func file_daemon_started_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_started_service_proto_rawDesc), len(file_daemon_started_service_proto_rawDesc)), NumEnums: 4, - NumMessages: 36, + NumMessages: 38, NumExtensions: 0, NumServices: 1, }, diff --git a/daemon/started_service.proto b/daemon/started_service.proto index f2531f08d1..2c3140a91a 100644 --- a/daemon/started_service.proto +++ b/daemon/started_service.proto @@ -35,11 +35,11 @@ service StartedService { rpc GetDeprecatedWarnings(google.protobuf.Empty) returns(DeprecatedWarnings) {} rpc GetStartedAt(google.protobuf.Empty) returns(StartedAt) {} - rpc ListOutbounds(google.protobuf.Empty) returns (OutboundList) {} rpc SubscribeOutbounds(google.protobuf.Empty) returns (stream OutboundList) {} rpc StartNetworkQualityTest(NetworkQualityTestRequest) returns (stream NetworkQualityTestProgress) {} rpc StartSTUNTest(STUNTestRequest) returns (stream STUNTestProgress) {} rpc SubscribeTailscaleStatus(google.protobuf.Empty) returns (stream TailscaleStatusUpdate) {} + rpc StartTailscalePing(TailscalePingRequest) returns (stream TailscalePingResponse) {} } message ServiceStatus { @@ -315,3 +315,17 @@ message TailscalePeer { int64 txBytes = 10; int64 keyExpiry = 11; } + +message TailscalePingRequest { + string endpointTag = 1; + string peerIP = 2; +} + +message TailscalePingResponse { + double latencyMs = 1; + bool isDirect = 2; + string endpoint = 3; + int32 derpRegionID = 4; + string derpRegionCode = 5; + string error = 6; +} diff --git a/daemon/started_service_grpc.pb.go b/daemon/started_service_grpc.pb.go index af8024035b..967757f1a6 100644 --- a/daemon/started_service_grpc.pb.go +++ b/daemon/started_service_grpc.pb.go @@ -38,11 +38,11 @@ const ( StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections" StartedService_GetDeprecatedWarnings_FullMethodName = "/daemon.StartedService/GetDeprecatedWarnings" StartedService_GetStartedAt_FullMethodName = "/daemon.StartedService/GetStartedAt" - StartedService_ListOutbounds_FullMethodName = "/daemon.StartedService/ListOutbounds" StartedService_SubscribeOutbounds_FullMethodName = "/daemon.StartedService/SubscribeOutbounds" StartedService_StartNetworkQualityTest_FullMethodName = "/daemon.StartedService/StartNetworkQualityTest" StartedService_StartSTUNTest_FullMethodName = "/daemon.StartedService/StartSTUNTest" StartedService_SubscribeTailscaleStatus_FullMethodName = "/daemon.StartedService/SubscribeTailscaleStatus" + StartedService_StartTailscalePing_FullMethodName = "/daemon.StartedService/StartTailscalePing" ) // StartedServiceClient is the client API for StartedService service. @@ -72,11 +72,11 @@ type StartedServiceClient interface { CloseAllConnections(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) GetDeprecatedWarnings(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DeprecatedWarnings, error) GetStartedAt(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StartedAt, error) - ListOutbounds(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*OutboundList, error) SubscribeOutbounds(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OutboundList], error) StartNetworkQualityTest(ctx context.Context, in *NetworkQualityTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[NetworkQualityTestProgress], error) StartSTUNTest(ctx context.Context, in *STUNTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[STUNTestProgress], error) SubscribeTailscaleStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscaleStatusUpdate], error) + StartTailscalePing(ctx context.Context, in *TailscalePingRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscalePingResponse], error) } type startedServiceClient struct { @@ -371,16 +371,6 @@ func (c *startedServiceClient) GetStartedAt(ctx context.Context, in *emptypb.Emp return out, nil } -func (c *startedServiceClient) ListOutbounds(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*OutboundList, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(OutboundList) - err := c.cc.Invoke(ctx, StartedService_ListOutbounds_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - func (c *startedServiceClient) SubscribeOutbounds(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OutboundList], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[6], StartedService_SubscribeOutbounds_FullMethodName, cOpts...) @@ -457,6 +447,25 @@ func (c *startedServiceClient) SubscribeTailscaleStatus(ctx context.Context, in // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type StartedService_SubscribeTailscaleStatusClient = grpc.ServerStreamingClient[TailscaleStatusUpdate] +func (c *startedServiceClient) StartTailscalePing(ctx context.Context, in *TailscalePingRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscalePingResponse], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[10], StartedService_StartTailscalePing_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[TailscalePingRequest, TailscalePingResponse]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_StartTailscalePingClient = grpc.ServerStreamingClient[TailscalePingResponse] + // StartedServiceServer is the server API for StartedService service. // All implementations must embed UnimplementedStartedServiceServer // for forward compatibility. @@ -484,11 +493,11 @@ type StartedServiceServer interface { CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error) GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error) GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, error) - ListOutbounds(context.Context, *emptypb.Empty) (*OutboundList, error) SubscribeOutbounds(*emptypb.Empty, grpc.ServerStreamingServer[OutboundList]) error StartNetworkQualityTest(*NetworkQualityTestRequest, grpc.ServerStreamingServer[NetworkQualityTestProgress]) error StartSTUNTest(*STUNTestRequest, grpc.ServerStreamingServer[STUNTestProgress]) error SubscribeTailscaleStatus(*emptypb.Empty, grpc.ServerStreamingServer[TailscaleStatusUpdate]) error + StartTailscalePing(*TailscalePingRequest, grpc.ServerStreamingServer[TailscalePingResponse]) error mustEmbedUnimplementedStartedServiceServer() } @@ -591,10 +600,6 @@ func (UnimplementedStartedServiceServer) GetStartedAt(context.Context, *emptypb. return nil, status.Error(codes.Unimplemented, "method GetStartedAt not implemented") } -func (UnimplementedStartedServiceServer) ListOutbounds(context.Context, *emptypb.Empty) (*OutboundList, error) { - return nil, status.Error(codes.Unimplemented, "method ListOutbounds not implemented") -} - func (UnimplementedStartedServiceServer) SubscribeOutbounds(*emptypb.Empty, grpc.ServerStreamingServer[OutboundList]) error { return status.Error(codes.Unimplemented, "method SubscribeOutbounds not implemented") } @@ -610,6 +615,10 @@ func (UnimplementedStartedServiceServer) StartSTUNTest(*STUNTestRequest, grpc.Se func (UnimplementedStartedServiceServer) SubscribeTailscaleStatus(*emptypb.Empty, grpc.ServerStreamingServer[TailscaleStatusUpdate]) error { return status.Error(codes.Unimplemented, "method SubscribeTailscaleStatus not implemented") } + +func (UnimplementedStartedServiceServer) StartTailscalePing(*TailscalePingRequest, grpc.ServerStreamingServer[TailscalePingResponse]) error { + return status.Error(codes.Unimplemented, "method StartTailscalePing not implemented") +} func (UnimplementedStartedServiceServer) mustEmbedUnimplementedStartedServiceServer() {} func (UnimplementedStartedServiceServer) testEmbeddedByValue() {} @@ -1003,24 +1012,6 @@ func _StartedService_GetStartedAt_Handler(srv interface{}, ctx context.Context, return interceptor(ctx, in, info, handler) } -func _StartedService_ListOutbounds_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(emptypb.Empty) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(StartedServiceServer).ListOutbounds(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: StartedService_ListOutbounds_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(StartedServiceServer).ListOutbounds(ctx, req.(*emptypb.Empty)) - } - return interceptor(ctx, in, info, handler) -} - func _StartedService_SubscribeOutbounds_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(emptypb.Empty) if err := stream.RecvMsg(m); err != nil { @@ -1065,6 +1056,17 @@ func _StartedService_SubscribeTailscaleStatus_Handler(srv interface{}, stream gr // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type StartedService_SubscribeTailscaleStatusServer = grpc.ServerStreamingServer[TailscaleStatusUpdate] +func _StartedService_StartTailscalePing_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(TailscalePingRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StartedServiceServer).StartTailscalePing(m, &grpc.GenericServerStream[TailscalePingRequest, TailscalePingResponse]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StartedService_StartTailscalePingServer = grpc.ServerStreamingServer[TailscalePingResponse] + // StartedService_ServiceDesc is the grpc.ServiceDesc for StartedService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -1140,10 +1142,6 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetStartedAt", Handler: _StartedService_GetStartedAt_Handler, }, - { - MethodName: "ListOutbounds", - Handler: _StartedService_ListOutbounds_Handler, - }, }, Streams: []grpc.StreamDesc{ { @@ -1196,6 +1194,11 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{ Handler: _StartedService_SubscribeTailscaleStatus_Handler, ServerStreams: true, }, + { + StreamName: "StartTailscalePing", + Handler: _StartedService_StartTailscalePing_Handler, + ServerStreams: true, + }, }, Metadata: "daemon/started_service.proto", } diff --git a/experimental/libbox/command_client.go b/experimental/libbox/command_client.go index cc908b847a..5223bf7e0b 100644 --- a/experimental/libbox/command_client.go +++ b/experimental/libbox/command_client.go @@ -14,8 +14,10 @@ import ( E "github.com/sagernet/sing/common/exceptions" "google.golang.org/grpc" + "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/emptypb" ) @@ -626,16 +628,6 @@ func (c *CommandClient) SetGroupExpand(groupTag string, isExpand bool) error { return err } -func (c *CommandClient) ListOutbounds() (OutboundGroupItemIterator, error) { - return callWithResult(c, func(client daemon.StartedServiceClient) (OutboundGroupItemIterator, error) { - list, err := client.ListOutbounds(context.Background(), &emptypb.Empty{}) - if err != nil { - return nil, err - } - return outboundGroupItemListFromGRPC(list), nil - }) -} - func (c *CommandClient) StartNetworkQualityTest(configURL string, outboundTag string, serial bool, maxRuntimeSeconds int32, http3 bool, handler NetworkQualityTestHandler) error { client, err := c.getClientForCall() if err != nil { @@ -736,9 +728,37 @@ func (c *CommandClient) SubscribeTailscaleStatus(handler TailscaleStatusHandler) for { event, recvErr := stream.Recv() if recvErr != nil { + if status.Code(recvErr) == codes.NotFound { + return nil + } handler.OnError(recvErr.Error()) return recvErr } handler.OnStatusUpdate(tailscaleStatusUpdateFromGRPC(event)) } } + +func (c *CommandClient) StartTailscalePing(endpointTag string, peerIP string, handler TailscalePingHandler) error { + client, err := c.getClientForCall() + if err != nil { + return err + } + if c.standalone { + defer c.closeConnection() + } + stream, err := client.StartTailscalePing(context.Background(), &daemon.TailscalePingRequest{ + EndpointTag: endpointTag, + PeerIP: peerIP, + }) + if err != nil { + return err + } + for { + event, recvErr := stream.Recv() + if recvErr != nil { + handler.OnError(recvErr.Error()) + return recvErr + } + handler.OnPingResult(tailscalePingResultFromGRPC(event)) + } +} diff --git a/experimental/libbox/command_types_tailscale_ping.go b/experimental/libbox/command_types_tailscale_ping.go new file mode 100644 index 0000000000..666789d007 --- /dev/null +++ b/experimental/libbox/command_types_tailscale_ping.go @@ -0,0 +1,28 @@ +package libbox + +import "github.com/sagernet/sing-box/daemon" + +type TailscalePingResult struct { + LatencyMs float64 + IsDirect bool + Endpoint string + DERPRegionID int32 + DERPRegionCode string + Error string +} + +type TailscalePingHandler interface { + OnPingResult(result *TailscalePingResult) + OnError(message string) +} + +func tailscalePingResultFromGRPC(response *daemon.TailscalePingResponse) *TailscalePingResult { + return &TailscalePingResult{ + LatencyMs: response.LatencyMs, + IsDirect: response.IsDirect, + Endpoint: response.Endpoint, + DERPRegionID: response.DerpRegionID, + DERPRegionCode: response.DerpRegionCode, + Error: response.Error, + } +} diff --git a/protocol/tailscale/hostinfo_tvos.go b/protocol/tailscale/hostinfo_tvos.go new file mode 100644 index 0000000000..d8e391bb58 --- /dev/null +++ b/protocol/tailscale/hostinfo_tvos.go @@ -0,0 +1,16 @@ +//go:build with_gvisor && tvos + +package tailscale + +import ( + _ "unsafe" + + "github.com/sagernet/tailscale/types/lazy" +) + +//go:linkname isAppleTV github.com/sagernet/tailscale/version.isAppleTV +var isAppleTV lazy.SyncValue[bool] + +func init() { + isAppleTV.Set(true) +} diff --git a/protocol/tailscale/ping.go b/protocol/tailscale/ping.go new file mode 100644 index 0000000000..8bb0476b27 --- /dev/null +++ b/protocol/tailscale/ping.go @@ -0,0 +1,55 @@ +//go:build with_gvisor + +package tailscale + +import ( + "context" + "net/netip" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/tailscale/ipn/ipnstate" + "github.com/sagernet/tailscale/tailcfg" +) + +func (t *Endpoint) StartTailscalePing(ctx context.Context, peerIP string, fn func(*adapter.TailscalePingResult)) error { + ip, err := netip.ParseAddr(peerIP) + if err != nil { + return err + } + localClient, err := t.server.LocalClient() + if err != nil { + return err + } + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + for { + result, pingErr := localClient.Ping(ctx, ip, tailcfg.PingDisco) + if ctx.Err() != nil { + return ctx.Err() + } + if pingErr != nil { + fn(&adapter.TailscalePingResult{ + Error: pingErr.Error(), + }) + } else { + fn(convertPingResult(result)) + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: + } + } +} + +func convertPingResult(result *ipnstate.PingResult) *adapter.TailscalePingResult { + return &adapter.TailscalePingResult{ + LatencyMs: result.LatencySeconds * 1000, + IsDirect: result.Endpoint != "", + Endpoint: result.Endpoint, + DERPRegionID: int32(result.DERPRegionID), + DERPRegionCode: result.DERPRegionCode, + Error: result.Err, + } +} diff --git a/protocol/tailscale/status.go b/protocol/tailscale/status.go index af6ce10393..a4d14ee14f 100644 --- a/protocol/tailscale/status.go +++ b/protocol/tailscale/status.go @@ -4,14 +4,14 @@ package tailscale import ( "context" + "slices" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/tailscale/ipn" "github.com/sagernet/tailscale/ipn/ipnstate" - "github.com/sagernet/tailscale/tailcfg" ) -var _ adapter.TailscaleStatusProvider = (*Endpoint)(nil) +var _ adapter.TailscaleEndpoint = (*Endpoint)(nil) func (t *Endpoint) SubscribeTailscaleStatus(ctx context.Context, fn func(*adapter.TailscaleEndpointStatus)) error { localBackend := t.server.ExportLocalBackend() @@ -46,13 +46,35 @@ func convertTailscaleStatus(status *ipnstate.Status) *adapter.TailscaleEndpointS if status.Self != nil { result.Self = convertTailscalePeer(status.Self) } - result.Users = make(map[int64]*adapter.TailscaleUser, len(status.User)) - for userID, profile := range status.User { - result.Users[int64(userID)] = convertTailscaleUser(userID, profile) + groupIndex := make(map[int64]*adapter.TailscaleUserGroup) + for _, peerKey := range status.Peers() { + peer := status.Peer[peerKey] + userID := int64(peer.UserID) + group, loaded := groupIndex[userID] + if !loaded { + group = &adapter.TailscaleUserGroup{ + UserID: userID, + } + if profile, hasProfile := status.User[peer.UserID]; hasProfile { + group.LoginName = profile.LoginName + group.DisplayName = profile.DisplayName + group.ProfilePicURL = profile.ProfilePicURL + } + groupIndex[userID] = group + result.UserGroups = append(result.UserGroups, group) + } + group.Peers = append(group.Peers, convertTailscalePeer(peer)) } - result.Peers = make([]*adapter.TailscalePeer, 0, len(status.Peer)) - for _, peer := range status.Peer { - result.Peers = append(result.Peers, convertTailscalePeer(peer)) + for _, group := range result.UserGroups { + slices.SortStableFunc(group.Peers, func(a, b *adapter.TailscalePeer) int { + if a.Online != b.Online { + if a.Online { + return -1 + } + return 1 + } + return 0 + }) } return result } @@ -81,12 +103,3 @@ func convertTailscalePeer(peer *ipnstate.PeerStatus) *adapter.TailscalePeer { KeyExpiry: keyExpiry, } } - -func convertTailscaleUser(id tailcfg.UserID, profile tailcfg.UserProfile) *adapter.TailscaleUser { - return &adapter.TailscaleUser{ - ID: int64(id), - LoginName: profile.LoginName, - DisplayName: profile.DisplayName, - ProfilePicURL: profile.ProfilePicURL, - } -} From e75e1c98a9c627b8b8981f0cc8f494c98565afe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 10 Apr 2026 10:15:02 +0800 Subject: [PATCH 20/93] Un-deprecate `ip_accept_any` DNS rule item --- dns/router.go | 11 ++++------- docs/configuration/dns/rule.md | 22 ++++++++-------------- docs/configuration/dns/rule.zh.md | 22 ++++++++-------------- docs/deprecated.md | 7 ------- docs/deprecated.zh.md | 7 ------- docs/migration.md | 2 +- docs/migration.zh.md | 2 +- experimental/deprecated/constants.go | 10 ---------- option/rule_dns.go | 3 +-- route/rule/rule_dns.go | 7 +------ 10 files changed, 24 insertions(+), 69 deletions(-) diff --git a/dns/router.go b/dns/router.go index 8fbaa27297..a14cecd0e7 100644 --- a/dns/router.go +++ b/dns/router.go @@ -841,10 +841,10 @@ func (r *Router) ResetNetwork() { } func defaultRuleNeedsLegacyDNSModeFromAddressFilter(rule option.DefaultDNSRule) bool { - if rule.IPAcceptAny || rule.RuleSetIPCIDRAcceptEmpty { //nolint:staticcheck + if rule.RuleSetIPCIDRAcceptEmpty { //nolint:staticcheck return true } - return !rule.MatchResponse && (len(rule.IPCIDR) > 0 || rule.IPIsPrivate) + return !rule.MatchResponse && (rule.IPAcceptAny || len(rule.IPCIDR) > 0 || rule.IPIsPrivate) } func hasResponseMatchFields(rule option.DefaultDNSRule) bool { @@ -1049,17 +1049,14 @@ func validateLegacyDNSModeDisabledRuleTree(rule option.DNSRule) (bool, error) { func validateLegacyDNSModeDisabledDefaultRule(rule option.DefaultDNSRule) (bool, error) { hasResponseRecords := hasResponseMatchFields(rule) - if (hasResponseRecords || len(rule.IPCIDR) > 0 || rule.IPIsPrivate) && !rule.MatchResponse { - return false, E.New("Response Match Fields (ip_cidr, ip_is_private, response_rcode, response_answer, response_ns, response_extra) require match_response to be enabled") + if (hasResponseRecords || len(rule.IPCIDR) > 0 || rule.IPIsPrivate || rule.IPAcceptAny) && !rule.MatchResponse { + return false, E.New("Response Match Fields (ip_cidr, ip_is_private, ip_accept_any, response_rcode, response_answer, response_ns, response_extra) require match_response to be enabled") } // Intentionally do not reject rule_set here. A referenced rule set may mix // destination-IP predicates with pre-response predicates such as domain items. // When match_response is false, those destination-IP branches fail closed during // pre-response evaluation instead of consuming DNS response state, while sibling // non-response branches remain matchable. - if rule.IPAcceptAny { //nolint:staticcheck - return false, E.New(deprecated.OptionIPAcceptAny.MessageWithLink()) - } if rule.RuleSetIPCIDRAcceptEmpty { //nolint:staticcheck return false, E.New(deprecated.OptionRuleSetIPCIDRAcceptEmpty.MessageWithLink()) } diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index aacdc003fd..9281271fd8 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -8,7 +8,6 @@ icon: material/alert-decagram :material-plus: [source_hostname](#source_hostname) :material-plus: [match_response](#match_response) :material-delete-clock: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty) - :material-delete-clock: [ip_accept_any](#ip_accept_any) :material-plus: [response_rcode](#response_rcode) :material-plus: [response_answer](#response_answer) :material-plus: [response_ns](#response_ns) @@ -178,6 +177,7 @@ icon: material/alert-decagram "192.168.0.1" ], "ip_is_private": false, + "ip_accept_any": false, "response_rcode": "", "response_answer": [], "response_ns": [], @@ -191,7 +191,6 @@ icon: material/alert-decagram // Deprecated - "ip_accept_any": false, "rule_set_ip_cidr_accept_empty": false, "rule_set_ipcidr_match_source": false, "geosite": [ @@ -500,7 +499,13 @@ instead of only matching the original query. The evaluated response can also be returned directly by a later [`respond`](/configuration/dns/rule_action/#respond) action. Required for Response Match Fields (`response_rcode`, `response_answer`, `response_ns`, `response_extra`). -Also required for `ip_cidr` and `ip_is_private` when used with `evaluate` or Response Match Fields. +Also required for `ip_cidr`, `ip_is_private`, and `ip_accept_any` when used with `evaluate` or Response Match Fields. + +#### ip_accept_any + +!!! question "Since sing-box 1.12.0" + +Match when the DNS query response contains at least one address. #### invert @@ -600,17 +605,6 @@ check [Migration](/migration/#migrate-address-filter-fields-to-response-matching Make `ip_cidr` rules in rule-sets accept empty query response. -#### ip_accept_any - -!!! question "Since sing-box 1.12.0" - -!!! failure "Deprecated in sing-box 1.14.0" - - `ip_accept_any` is deprecated and will be removed in sing-box 1.16.0, - check [Migration](/migration/#migrate-address-filter-fields-to-response-matching). - -Match any IP with query response. - ### Response Match Fields !!! question "Since sing-box 1.14.0" diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index a3633789f6..dabbe8c25a 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -8,7 +8,6 @@ icon: material/alert-decagram :material-plus: [source_hostname](#source_hostname) :material-plus: [match_response](#match_response) :material-delete-clock: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty) - :material-delete-clock: [ip_accept_any](#ip_accept_any) :material-plus: [response_rcode](#response_rcode) :material-plus: [response_answer](#response_answer) :material-plus: [response_ns](#response_ns) @@ -178,6 +177,7 @@ icon: material/alert-decagram "192.168.0.1" ], "ip_is_private": false, + "ip_accept_any": false, "response_rcode": "", "response_answer": [], "response_ns": [], @@ -191,7 +191,6 @@ icon: material/alert-decagram // 已弃用 - "ip_accept_any": false, "rule_set_ip_cidr_accept_empty": false, "rule_set_ipcidr_match_source": false, "geosite": [ @@ -498,7 +497,13 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`. 该已评估的响应也可以被后续的 [`respond`](/zh/configuration/dns/rule_action/#respond) 动作直接返回。 响应匹配字段(`response_rcode`、`response_answer`、`response_ns`、`response_extra`)需要此选项。 -当与 `evaluate` 或响应匹配字段一起使用时,`ip_cidr` 和 `ip_is_private` 也需要此选项。 +当与 `evaluate` 或响应匹配字段一起使用时,`ip_cidr`、`ip_is_private` 和 `ip_accept_any` 也需要此选项。 + +#### ip_accept_any + +!!! question "自 sing-box 1.12.0 起" + +当 DNS 查询响应包含至少一个地址时匹配。 #### invert @@ -599,17 +604,6 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`. 使规则集中的 `ip_cidr` 规则接受空查询响应。 -#### ip_accept_any - -!!! question "自 sing-box 1.12.0 起" - -!!! failure "已在 sing-box 1.14.0 废弃" - - `ip_accept_any` 已废弃且将在 sing-box 1.16.0 中被移除, - 参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。 - -匹配任意 IP。 - ### 响应匹配字段 !!! question "自 sing-box 1.14.0 起" diff --git a/docs/deprecated.md b/docs/deprecated.md index 70084b6df9..094ff9ea7b 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -21,13 +21,6 @@ check [Migration](../migration/#migrate-dns-rule-action-strategy-to-rule-items). Old fields will be removed in sing-box 1.16.0. -#### Legacy `ip_accept_any` DNS rule item - -Legacy `ip_accept_any` DNS rule item is deprecated, -check [Migration](../migration/#migrate-address-filter-fields-to-response-matching). - -Old fields will be removed in sing-box 1.16.0. - #### Legacy `rule_set_ip_cidr_accept_empty` DNS rule item Legacy `rule_set_ip_cidr_accept_empty` DNS rule item is deprecated, diff --git a/docs/deprecated.zh.md b/docs/deprecated.zh.md index f98b0c010a..8e299df9bb 100644 --- a/docs/deprecated.zh.md +++ b/docs/deprecated.zh.md @@ -21,13 +21,6 @@ TLS 中的内联 ACME 选项(`tls.acme`)已废弃, 旧字段将在 sing-box 1.16.0 中被移除。 -#### 旧版 `ip_accept_any` DNS 规则项 - -旧版 `ip_accept_any` DNS 规则项已废弃, -参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。 - -旧字段将在 sing-box 1.16.0 中被移除。 - #### 旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项 旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项已废弃, diff --git a/docs/migration.md b/docs/migration.md index 91e771babd..9bcd9764aa 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -129,7 +129,7 @@ Use `ip_version` or `query_type` rule items to control which query types a rule ### Migrate address filter fields to response matching Legacy Address Filter Fields (`ip_cidr`, `ip_is_private` without `match_response`) in DNS rules are deprecated, -along with Legacy `ip_accept_any` and Legacy `rule_set_ip_cidr_accept_empty` DNS rule items. +along with the Legacy `rule_set_ip_cidr_accept_empty` DNS rule item. In sing-box 1.14.0, use the [`evaluate`](/configuration/dns/rule_action/#evaluate) action to fetch a DNS response, then match against it explicitly with `match_response`. diff --git a/docs/migration.zh.md b/docs/migration.zh.md index 3f12740553..e8cbe1bdf8 100644 --- a/docs/migration.zh.md +++ b/docs/migration.zh.md @@ -129,7 +129,7 @@ sing-box 1.14.0 新增字段参阅 [ACME](/zh/configuration/shared/certificate-p ### 迁移地址筛选字段到响应匹配 旧版地址筛选字段(不使用 `match_response` 的 `ip_cidr`、`ip_is_private`)已废弃, -旧版 `ip_accept_any` 和旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项也已废弃。 +旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项也已废弃。 在 sing-box 1.14.0 中,请使用 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作 获取 DNS 响应,然后通过 `match_response` 显式匹配。 diff --git a/experimental/deprecated/constants.go b/experimental/deprecated/constants.go index 543a10bb6c..afe5c021ac 100644 --- a/experimental/deprecated/constants.go +++ b/experimental/deprecated/constants.go @@ -93,15 +93,6 @@ var OptionInlineACME = Note{ MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-inline-acme-to-certificate-provider", } -var OptionIPAcceptAny = Note{ - Name: "dns-rule-ip-accept-any", - Description: "Legacy `ip_accept_any` DNS rule item", - DeprecatedVersion: "1.14.0", - ScheduledVersion: "1.16.0", - EnvName: "DNS_RULE_IP_ACCEPT_ANY", - MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-address-filter-fields-to-response-matching", -} - var OptionRuleSetIPCIDRAcceptEmpty = Note{ Name: "dns-rule-rule-set-ip-cidr-accept-empty", Description: "Legacy `rule_set_ip_cidr_accept_empty` DNS rule item", @@ -134,7 +125,6 @@ var Options = []Note{ OptionMissingDomainResolver, OptionLegacyDomainStrategyOptions, OptionInlineACME, - OptionIPAcceptAny, OptionRuleSetIPCIDRAcceptEmpty, OptionLegacyDNSAddressFilter, OptionLegacyDNSRuleStrategy, diff --git a/option/rule_dns.go b/option/rule_dns.go index d1298635b8..5582e7df4f 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -107,6 +107,7 @@ type RawDefaultDNSRule struct { MatchResponse bool `json:"match_response,omitempty"` IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` IPIsPrivate bool `json:"ip_is_private,omitempty"` + IPAcceptAny bool `json:"ip_accept_any,omitempty"` ResponseRcode *DNSRCode `json:"response_rcode,omitempty"` ResponseAnswer badoption.Listable[DNSRecordOptions] `json:"response_answer,omitempty"` ResponseNs badoption.Listable[DNSRecordOptions] `json:"response_ns,omitempty"` @@ -117,8 +118,6 @@ type RawDefaultDNSRule struct { Geosite badoption.Listable[string] `json:"geosite,omitempty"` SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"` GeoIP badoption.Listable[string] `json:"geoip,omitempty"` - // Deprecated: use match_response with response items - IPAcceptAny bool `json:"ip_accept_any,omitempty"` // Deprecated: removed in sing-box 1.11.0 RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"` // Deprecated: renamed to rule_set_ip_cidr_match_source diff --git a/route/rule/rule_dns.go b/route/rule/rule_dns.go index 20fb195f13..c406f06745 100644 --- a/route/rule/rule_dns.go +++ b/route/rule/rule_dns.go @@ -177,12 +177,7 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) rule.allItems = append(rule.allItems, item) } - if options.IPAcceptAny { //nolint:staticcheck - if legacyDNSMode { - deprecated.Report(ctx, deprecated.OptionIPAcceptAny) - } else { - return nil, E.New(deprecated.OptionIPAcceptAny.MessageWithLink()) - } + if options.IPAcceptAny { item := NewIPAcceptAnyItem() rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) rule.allItems = append(rule.allItems, item) From e714efa792f968108ac64b882285ecbb0aa0155e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 10 Apr 2026 11:34:39 +0800 Subject: [PATCH 21/93] documentation: Fixes --- docs/configuration/dns/rule_action.md | 2 +- docs/configuration/dns/rule_action.zh.md | 2 +- docs/configuration/dns/server/hosts.md | 69 ++++++++++++----- docs/configuration/dns/server/hosts.zh.md | 69 ++++++++++++----- docs/configuration/dns/server/resolved.md | 75 +++++++++++++------ docs/configuration/dns/server/resolved.zh.md | 75 +++++++++++++------ docs/configuration/dns/server/tailscale.md | 75 +++++++++++++------ docs/configuration/dns/server/tailscale.zh.md | 75 +++++++++++++------ docs/deprecated.md | 3 +- docs/deprecated.zh.md | 3 +- docs/migration.md | 47 ------------ docs/migration.zh.md | 47 ------------ 12 files changed, 320 insertions(+), 222 deletions(-) diff --git a/docs/configuration/dns/rule_action.md b/docs/configuration/dns/rule_action.md index e71a28c8a9..dfc72dc76f 100644 --- a/docs/configuration/dns/rule_action.md +++ b/docs/configuration/dns/rule_action.md @@ -44,7 +44,7 @@ Tag of target server. `strategy` is deprecated in sing-box 1.14.0 and will be removed in sing-box 1.16.0. -Set domain strategy for this query. Deprecated, check [Migration](/migration/#migrate-dns-rule-action-strategy-to-rule-items). +Set domain strategy for this query. One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`. diff --git a/docs/configuration/dns/rule_action.zh.md b/docs/configuration/dns/rule_action.zh.md index f11bb58920..36c8111cea 100644 --- a/docs/configuration/dns/rule_action.zh.md +++ b/docs/configuration/dns/rule_action.zh.md @@ -44,7 +44,7 @@ icon: material/new-box `strategy` 已在 sing-box 1.14.0 废弃,且将在 sing-box 1.16.0 中被移除。 -为此查询设置域名策略。已废弃,参阅[迁移指南](/zh/migration/#迁移-dns-规则动作-strategy-到规则项)。 +为此查询设置域名策略。 可选项:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。 diff --git a/docs/configuration/dns/server/hosts.md b/docs/configuration/dns/server/hosts.md index ce859cca37..da76f61922 100644 --- a/docs/configuration/dns/server/hosts.md +++ b/docs/configuration/dns/server/hosts.md @@ -73,24 +73,55 @@ Example: === "Use hosts if available" - ```json - { - "dns": { - "servers": [ - { - ... - }, - { - "type": "hosts", - "tag": "hosts" + === ":material-card-multiple: sing-box 1.14.0" + + ```json + { + "dns": { + "servers": [ + { + ... + }, + { + "type": "hosts", + "tag": "hosts" + } + ], + "rules": [ + { + "action": "evaluate", + "server": "hosts" + }, + { + "match_response": true, + "ip_accept_any": true, + "action": "respond" + } + ] } - ], - "rules": [ - { - "ip_accept_any": true, - "server": "hosts" + } + ``` + + === ":material-card-remove: sing-box < 1.14.0" + + ```json + { + "dns": { + "servers": [ + { + ... + }, + { + "type": "hosts", + "tag": "hosts" + } + ], + "rules": [ + { + "ip_accept_any": true, + "server": "hosts" + } + ] } - ] - } - } - ``` \ No newline at end of file + } + ``` \ No newline at end of file diff --git a/docs/configuration/dns/server/hosts.zh.md b/docs/configuration/dns/server/hosts.zh.md index 43878f4e4b..3384adc746 100644 --- a/docs/configuration/dns/server/hosts.zh.md +++ b/docs/configuration/dns/server/hosts.zh.md @@ -73,24 +73,55 @@ hosts 文件路径列表。 === "如果可用则使用 hosts" - ```json - { - "dns": { - "servers": [ - { - ... - }, - { - "type": "hosts", - "tag": "hosts" + === ":material-card-multiple: sing-box 1.14.0" + + ```json + { + "dns": { + "servers": [ + { + ... + }, + { + "type": "hosts", + "tag": "hosts" + } + ], + "rules": [ + { + "action": "evaluate", + "server": "hosts" + }, + { + "match_response": true, + "ip_accept_any": true, + "action": "respond" + } + ] } - ], - "rules": [ - { - "ip_accept_any": true, - "server": "hosts" + } + ``` + + === ":material-card-remove: sing-box < 1.14.0" + + ```json + { + "dns": { + "servers": [ + { + ... + }, + { + "type": "hosts", + "tag": "hosts" + } + ], + "rules": [ + { + "ip_accept_any": true, + "server": "hosts" + } + ] } - ] - } - } - ``` \ No newline at end of file + } + ``` \ No newline at end of file diff --git a/docs/configuration/dns/server/resolved.md b/docs/configuration/dns/server/resolved.md index b299d31789..75835c6b85 100644 --- a/docs/configuration/dns/server/resolved.md +++ b/docs/configuration/dns/server/resolved.md @@ -43,29 +43,62 @@ If not enabled, `NXDOMAIN` will be returned for requests that do not match searc === "Split DNS only" - ```json - { - "dns": { - "servers": [ - { - "type": "local", - "tag": "local" - }, - { - "type": "resolved", - "tag": "resolved", - "service": "resolved" + === ":material-card-multiple: sing-box 1.14.0" + + ```json + { + "dns": { + "servers": [ + { + "type": "local", + "tag": "local" + }, + { + "type": "resolved", + "tag": "resolved", + "service": "resolved" + } + ], + "rules": [ + { + "action": "evaluate", + "server": "resolved" + }, + { + "match_response": true, + "ip_accept_any": true, + "action": "respond" + } + ] } - ], - "rules": [ - { - "ip_accept_any": true, - "server": "resolved" + } + ``` + + === ":material-card-remove: sing-box < 1.14.0" + + ```json + { + "dns": { + "servers": [ + { + "type": "local", + "tag": "local" + }, + { + "type": "resolved", + "tag": "resolved", + "service": "resolved" + } + ], + "rules": [ + { + "ip_accept_any": true, + "server": "resolved" + } + ] } - ] - } - } - ``` + } + ``` === "Use as global DNS" diff --git a/docs/configuration/dns/server/resolved.zh.md b/docs/configuration/dns/server/resolved.zh.md index d59f838465..8747e83132 100644 --- a/docs/configuration/dns/server/resolved.zh.md +++ b/docs/configuration/dns/server/resolved.zh.md @@ -42,29 +42,62 @@ icon: material/new-box === "仅分割 DNS" - ```json - { - "dns": { - "servers": [ - { - "type": "local", - "tag": "local" - }, - { - "type": "resolved", - "tag": "resolved", - "service": "resolved" + === ":material-card-multiple: sing-box 1.14.0" + + ```json + { + "dns": { + "servers": [ + { + "type": "local", + "tag": "local" + }, + { + "type": "resolved", + "tag": "resolved", + "service": "resolved" + } + ], + "rules": [ + { + "action": "evaluate", + "server": "resolved" + }, + { + "match_response": true, + "ip_accept_any": true, + "action": "respond" + } + ] } - ], - "rules": [ - { - "ip_accept_any": true, - "server": "resolved" + } + ``` + + === ":material-card-remove: sing-box < 1.14.0" + + ```json + { + "dns": { + "servers": [ + { + "type": "local", + "tag": "local" + }, + { + "type": "resolved", + "tag": "resolved", + "service": "resolved" + } + ], + "rules": [ + { + "ip_accept_any": true, + "server": "resolved" + } + ] } - ] - } - } - ``` + } + ``` === "用作全局 DNS" diff --git a/docs/configuration/dns/server/tailscale.md b/docs/configuration/dns/server/tailscale.md index 5ff1166064..2677f2b821 100644 --- a/docs/configuration/dns/server/tailscale.md +++ b/docs/configuration/dns/server/tailscale.md @@ -42,29 +42,62 @@ if not enabled, `NXDOMAIN` will be returned for non-Tailscale domain queries. === "MagicDNS only" - ```json - { - "dns": { - "servers": [ - { - "type": "local", - "tag": "local" - }, - { - "type": "tailscale", - "tag": "ts", - "endpoint": "ts-ep" + === ":material-card-multiple: sing-box 1.14.0" + + ```json + { + "dns": { + "servers": [ + { + "type": "local", + "tag": "local" + }, + { + "type": "tailscale", + "tag": "ts", + "endpoint": "ts-ep" + } + ], + "rules": [ + { + "action": "evaluate", + "server": "ts" + }, + { + "match_response": true, + "ip_accept_any": true, + "action": "respond" + } + ] } - ], - "rules": [ - { - "ip_accept_any": true, - "server": "ts" + } + ``` + + === ":material-card-remove: sing-box < 1.14.0" + + ```json + { + "dns": { + "servers": [ + { + "type": "local", + "tag": "local" + }, + { + "type": "tailscale", + "tag": "ts", + "endpoint": "ts-ep" + } + ], + "rules": [ + { + "ip_accept_any": true, + "server": "ts" + } + ] } - ] - } - } - ``` + } + ``` === "Use as global DNS" diff --git a/docs/configuration/dns/server/tailscale.zh.md b/docs/configuration/dns/server/tailscale.zh.md index 5cb2077693..10d84038c5 100644 --- a/docs/configuration/dns/server/tailscale.zh.md +++ b/docs/configuration/dns/server/tailscale.zh.md @@ -42,29 +42,62 @@ icon: material/new-box === "仅 MagicDNS" - ```json - { - "dns": { - "servers": [ - { - "type": "local", - "tag": "local" - }, - { - "type": "tailscale", - "tag": "ts", - "endpoint": "ts-ep" + === ":material-card-multiple: sing-box 1.14.0" + + ```json + { + "dns": { + "servers": [ + { + "type": "local", + "tag": "local" + }, + { + "type": "tailscale", + "tag": "ts", + "endpoint": "ts-ep" + } + ], + "rules": [ + { + "action": "evaluate", + "server": "ts" + }, + { + "match_response": true, + "ip_accept_any": true, + "action": "respond" + } + ] } - ], - "rules": [ - { - "ip_accept_any": true, - "server": "ts" + } + ``` + + === ":material-card-remove: sing-box < 1.14.0" + + ```json + { + "dns": { + "servers": [ + { + "type": "local", + "tag": "local" + }, + { + "type": "tailscale", + "tag": "ts", + "endpoint": "ts-ep" + } + ], + "rules": [ + { + "ip_accept_any": true, + "server": "ts" + } + ] } - ] - } - } - ``` + } + ``` === "用作全局 DNS" diff --git a/docs/deprecated.md b/docs/deprecated.md index 094ff9ea7b..47a9bffdd8 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -16,8 +16,7 @@ Old fields will be removed in sing-box 1.16.0. #### Legacy `strategy` DNS rule action option -Legacy `strategy` DNS rule action option is deprecated, -check [Migration](../migration/#migrate-dns-rule-action-strategy-to-rule-items). +Legacy `strategy` DNS rule action option is deprecated. Old fields will be removed in sing-box 1.16.0. diff --git a/docs/deprecated.zh.md b/docs/deprecated.zh.md index 8e299df9bb..a4995b5684 100644 --- a/docs/deprecated.zh.md +++ b/docs/deprecated.zh.md @@ -16,8 +16,7 @@ TLS 中的内联 ACME 选项(`tls.acme`)已废弃, #### 旧版 DNS 规则动作 `strategy` 选项 -旧版 DNS 规则动作 `strategy` 选项已废弃, -参阅[迁移指南](/zh/migration/#迁移-dns-规则动作-strategy-到规则项)。 +旧版 DNS 规则动作 `strategy` 选项已废弃。 旧字段将在 sing-box 1.16.0 中被移除。 diff --git a/docs/migration.md b/docs/migration.md index 9bcd9764aa..af434fc256 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -79,53 +79,6 @@ See [ACME](/configuration/shared/certificate-provider/acme/) for fields newly ad } ``` -### Migrate DNS rule action strategy to rule items - -Legacy `strategy` DNS rule action option is deprecated. - -In sing-box 1.14.0, internal domain resolution (Lookup) now splits A and AAAA queries -at the rule level, so each query type is evaluated independently through the full rule chain. -Use `ip_version` or `query_type` rule items to control which query types a rule matches. - -!!! info "References" - - [DNS Rule](/configuration/dns/rule/) / - [DNS Rule Action](/configuration/dns/rule_action/) - -=== ":material-card-remove: Deprecated" - - ```json - { - "dns": { - "rules": [ - { - "domain_suffix": ".cn", - "action": "route", - "server": "local", - "strategy": "ipv4_only" - } - ] - } - } - ``` - -=== ":material-card-multiple: New" - - ```json - { - "dns": { - "rules": [ - { - "domain_suffix": ".cn", - "ip_version": 4, - "action": "route", - "server": "local" - } - ] - } - } - ``` - ### Migrate address filter fields to response matching Legacy Address Filter Fields (`ip_cidr`, `ip_is_private` without `match_response`) in DNS rules are deprecated, diff --git a/docs/migration.zh.md b/docs/migration.zh.md index e8cbe1bdf8..b0d26e41a9 100644 --- a/docs/migration.zh.md +++ b/docs/migration.zh.md @@ -79,53 +79,6 @@ sing-box 1.14.0 新增字段参阅 [ACME](/zh/configuration/shared/certificate-p } ``` -### 迁移 DNS 规则动作 strategy 到规则项 - -旧版 DNS 规则动作 `strategy` 选项已废弃。 - -在 sing-box 1.14.0 中,内部域名解析(Lookup)现在在规则层拆分 A 和 AAAA 查询, -每种查询类型独立通过完整的规则链评估。 -请使用 `ip_version` 或 `query_type` 规则项来控制规则匹配的查询类型。 - -!!! info "参考" - - [DNS 规则](/zh/configuration/dns/rule/) / - [DNS 规则动作](/zh/configuration/dns/rule_action/) - -=== ":material-card-remove: 弃用的" - - ```json - { - "dns": { - "rules": [ - { - "domain_suffix": ".cn", - "action": "route", - "server": "local", - "strategy": "ipv4_only" - } - ] - } - } - ``` - -=== ":material-card-multiple: 新的" - - ```json - { - "dns": { - "rules": [ - { - "domain_suffix": ".cn", - "ip_version": 4, - "action": "route", - "server": "local" - } - ] - } - } - ``` - ### 迁移地址筛选字段到响应匹配 旧版地址筛选字段(不使用 `match_response` 的 `ip_cidr`、`ip_is_private`)已废弃, From 684ba791138167f9bce6094a2da6b194bb6c1d5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 10 Apr 2026 11:54:47 +0800 Subject: [PATCH 22/93] Add `package_name_regex` route, DNS and headless rule item --- cmd/sing-box/cmd_rule_set_compile.go | 5 ++ common/srs/binary.go | 12 ++++ constant/rule.go | 3 +- docs/clients/android/features.md | 1 + docs/clients/apple/features.md | 1 + docs/configuration/dns/rule.md | 12 +++- docs/configuration/dns/rule.zh.md | 12 +++- docs/configuration/route/rule.md | 12 +++- docs/configuration/route/rule.zh.md | 12 +++- docs/configuration/rule-set/headless-rule.md | 13 +++++ .../rule-set/headless-rule.zh.md | 13 +++++ docs/configuration/rule-set/source-format.md | 5 ++ .../rule-set/source-format.zh.md | 5 ++ option/rule.go | 1 + option/rule_dns.go | 1 + option/rule_set.go | 7 ++- route/rule/rule_default.go | 8 +++ route/rule/rule_dns.go | 8 +++ route/rule/rule_headless.go | 8 +++ route/rule/rule_item_package_name_regex.go | 56 +++++++++++++++++++ route/rule/rule_set.go | 2 +- route/rule_conds.go | 4 +- 22 files changed, 190 insertions(+), 11 deletions(-) create mode 100644 route/rule/rule_item_package_name_regex.go diff --git a/cmd/sing-box/cmd_rule_set_compile.go b/cmd/sing-box/cmd_rule_set_compile.go index 73655b12ea..e2cbefc7bf 100644 --- a/cmd/sing-box/cmd_rule_set_compile.go +++ b/cmd/sing-box/cmd_rule_set_compile.go @@ -82,6 +82,11 @@ func compileRuleSet(sourcePath string) error { } func downgradeRuleSetVersion(version uint8, options option.PlainRuleSet) uint8 { + if version == C.RuleSetVersion5 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool { + return len(rule.PackageNameRegex) > 0 + }) { + version = C.RuleSetVersion4 + } if version == C.RuleSetVersion4 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool { return rule.NetworkInterfaceAddress != nil && rule.NetworkInterfaceAddress.Size() > 0 || len(rule.DefaultInterfaceAddress) > 0 diff --git a/common/srs/binary.go b/common/srs/binary.go index ca12fff097..d5b644ae15 100644 --- a/common/srs/binary.go +++ b/common/srs/binary.go @@ -46,6 +46,7 @@ const ( ruleItemNetworkIsConstrained ruleItemNetworkInterfaceAddress ruleItemDefaultInterfaceAddress + ruleItemPackageNameRegex ruleItemFinal uint8 = 0xFF ) @@ -215,6 +216,8 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea rule.ProcessPathRegex, err = readRuleItemString(reader) case ruleItemPackageName: rule.PackageName, err = readRuleItemString(reader) + case ruleItemPackageNameRegex: + rule.PackageNameRegex, err = readRuleItemString(reader) case ruleItemWIFISSID: rule.WIFISSID, err = readRuleItemString(reader) case ruleItemWIFIBSSID: @@ -394,6 +397,15 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen return err } } + if len(rule.PackageNameRegex) > 0 { + if generateVersion < C.RuleSetVersion5 { + return E.New("`package_name_regex` rule item is only supported in version 5 or later") + } + err = writeRuleItemString(writer, ruleItemPackageNameRegex, rule.PackageNameRegex) + if err != nil { + return err + } + } if len(rule.NetworkType) > 0 { if generateVersion < C.RuleSetVersion3 { return E.New("`network_type` rule item is only supported in version 3 or later") diff --git a/constant/rule.go b/constant/rule.go index 15d71c5301..efd4a2d32d 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -23,7 +23,8 @@ const ( RuleSetVersion2 RuleSetVersion3 RuleSetVersion4 - RuleSetVersionCurrent = RuleSetVersion4 + RuleSetVersion5 + RuleSetVersionCurrent = RuleSetVersion5 ) const ( diff --git a/docs/clients/android/features.md b/docs/clients/android/features.md index f7f2caeec4..b76a6418e4 100644 --- a/docs/clients/android/features.md +++ b/docs/clients/android/features.md @@ -42,6 +42,7 @@ SFA provides an unprivileged TUN implementation through Android VpnService. | `process_path` | :material-close: | No permission | | `process_path_regex` | :material-close: | No permission | | `package_name` | :material-check: | / | +| `package_name_regex` | :material-check: | / | | `user` | :material-close: | Use `package_name` instead | | `user_id` | :material-close: | Use `package_name` instead | | `wifi_ssid` | :material-check: | Fine location permission required | diff --git a/docs/clients/apple/features.md b/docs/clients/apple/features.md index d638517322..e1f3d7ccd1 100644 --- a/docs/clients/apple/features.md +++ b/docs/clients/apple/features.md @@ -44,6 +44,7 @@ SFI/SFM/SFT provides an unprivileged TUN implementation through NetworkExtension | `process_path` | :material-close: | No permission | | `process_path_regex` | :material-close: | No permission | | `package_name` | :material-close: | / | +| `package_name_regex` | :material-close: | / | | `user` | :material-close: | No permission | | `user_id` | :material-close: | No permission | | `wifi_ssid` | :material-alert: | Only supported on iOS | diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 9281271fd8..e35f4d54d6 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -11,7 +11,8 @@ icon: material/alert-decagram :material-plus: [response_rcode](#response_rcode) :material-plus: [response_answer](#response_answer) :material-plus: [response_ns](#response_ns) - :material-plus: [response_extra](#response_extra) + :material-plus: [response_extra](#response_extra) + :material-plus: [package_name_regex](#package_name_regex) !!! quote "Changes in sing-box 1.13.0" @@ -129,6 +130,9 @@ icon: material/alert-decagram "package_name": [ "com.termux" ], + "package_name_regex": [ + "^com\\.termux.*" + ], "user": [ "sekai" ], @@ -347,6 +351,12 @@ Match process path using regular expression. Match android package name. +#### package_name_regex + +!!! question "Since sing-box 1.14.0" + +Match android package name using regular expression. + #### user !!! quote "" diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index dabbe8c25a..4ed721ca5b 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -11,7 +11,8 @@ icon: material/alert-decagram :material-plus: [response_rcode](#response_rcode) :material-plus: [response_answer](#response_answer) :material-plus: [response_ns](#response_ns) - :material-plus: [response_extra](#response_extra) + :material-plus: [response_extra](#response_extra) + :material-plus: [package_name_regex](#package_name_regex) !!! quote "sing-box 1.13.0 中的更改" @@ -129,6 +130,9 @@ icon: material/alert-decagram "package_name": [ "com.termux" ], + "package_name_regex": [ + "^com\\.termux.*" + ], "user": [ "sekai" ], @@ -347,6 +351,12 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 匹配 Android 应用包名。 +#### package_name_regex + +!!! question "自 sing-box 1.14.0 起" + +使用正则表达式匹配 Android 应用包名。 + #### user !!! quote "" diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md index 37e651c924..97bbe37606 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -5,7 +5,8 @@ icon: material/new-box !!! quote "Changes in sing-box 1.14.0" :material-plus: [source_mac_address](#source_mac_address) - :material-plus: [source_hostname](#source_hostname) + :material-plus: [source_hostname](#source_hostname) + :material-plus: [package_name_regex](#package_name_regex) !!! quote "Changes in sing-box 1.13.0" @@ -129,6 +130,9 @@ icon: material/new-box "package_name": [ "com.termux" ], + "package_name_regex": [ + "^com\\.termux.*" + ], "user": [ "sekai" ], @@ -354,6 +358,12 @@ Match process path using regular expression. Match android package name. +#### package_name_regex + +!!! question "Since sing-box 1.14.0" + +Match android package name using regular expression. + #### user !!! quote "" diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md index 181a57398d..d55b565dd9 100644 --- a/docs/configuration/route/rule.zh.md +++ b/docs/configuration/route/rule.zh.md @@ -5,7 +5,8 @@ icon: material/new-box !!! quote "sing-box 1.14.0 中的更改" :material-plus: [source_mac_address](#source_mac_address) - :material-plus: [source_hostname](#source_hostname) + :material-plus: [source_hostname](#source_hostname) + :material-plus: [package_name_regex](#package_name_regex) !!! quote "sing-box 1.13.0 中的更改" @@ -127,6 +128,9 @@ icon: material/new-box "package_name": [ "com.termux" ], + "package_name_regex": [ + "^com\\.termux.*" + ], "user": [ "sekai" ], @@ -352,6 +356,12 @@ icon: material/new-box 匹配 Android 应用包名。 +#### package_name_regex + +!!! question "自 sing-box 1.14.0 起" + +使用正则表达式匹配 Android 应用包名。 + #### user !!! quote "" diff --git a/docs/configuration/rule-set/headless-rule.md b/docs/configuration/rule-set/headless-rule.md index 89cccd394e..23f2f58063 100644 --- a/docs/configuration/rule-set/headless-rule.md +++ b/docs/configuration/rule-set/headless-rule.md @@ -2,6 +2,10 @@ icon: material/new-box --- +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [package_name_regex](#package_name_regex) + !!! quote "Changes in sing-box 1.13.0" :material-plus: [network_interface_address](#network_interface_address) @@ -78,6 +82,9 @@ icon: material/new-box "package_name": [ "com.termux" ], + "package_name_regex": [ + "^com\\.termux.*" + ], "network_type": [ "wifi" ], @@ -205,6 +212,12 @@ Match process path using regular expression. Match android package name. +#### package_name_regex + +!!! question "Since sing-box 1.14.0" + +Match android package name using regular expression. + #### network_type !!! question "Since sing-box 1.11.0" diff --git a/docs/configuration/rule-set/headless-rule.zh.md b/docs/configuration/rule-set/headless-rule.zh.md index f2b88631ac..c5ed636c91 100644 --- a/docs/configuration/rule-set/headless-rule.zh.md +++ b/docs/configuration/rule-set/headless-rule.zh.md @@ -2,6 +2,10 @@ icon: material/new-box --- +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [package_name_regex](#package_name_regex) + !!! quote "sing-box 1.13.0 中的更改" :material-plus: [network_interface_address](#network_interface_address) @@ -78,6 +82,9 @@ icon: material/new-box "package_name": [ "com.termux" ], + "package_name_regex": [ + "^com\\.termux.*" + ], "network_type": [ "wifi" ], @@ -201,6 +208,12 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 匹配 Android 应用包名。 +#### package_name_regex + +!!! question "自 sing-box 1.14.0 起" + +使用正则表达式匹配 Android 应用包名。 + #### network_type !!! question "自 sing-box 1.11.0 起" diff --git a/docs/configuration/rule-set/source-format.md b/docs/configuration/rule-set/source-format.md index 47d620b1c6..47e0e24553 100644 --- a/docs/configuration/rule-set/source-format.md +++ b/docs/configuration/rule-set/source-format.md @@ -2,6 +2,10 @@ icon: material/new-box --- +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: version `5` + !!! quote "Changes in sing-box 1.13.0" :material-plus: version `4` @@ -41,6 +45,7 @@ Version of rule-set. * 2: sing-box 1.10.0: Optimized memory usages of `domain_suffix` rules in binary rule-sets. * 3: sing-box 1.11.0: Added `network_type`, `network_is_expensive` and `network_is_constrainted` rule items. * 4: sing-box 1.13.0: Added `network_interface_address` and `default_interface_address` rule items. +* 5: sing-box 1.14.0: Added `package_name_regex` rule item. #### rules diff --git a/docs/configuration/rule-set/source-format.zh.md b/docs/configuration/rule-set/source-format.zh.md index 30c0679f6e..3f7108647b 100644 --- a/docs/configuration/rule-set/source-format.zh.md +++ b/docs/configuration/rule-set/source-format.zh.md @@ -2,6 +2,10 @@ icon: material/new-box --- +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: version `5` + !!! quote "sing-box 1.13.0 中的更改" :material-plus: version `4` @@ -41,6 +45,7 @@ icon: material/new-box * 2: sing-box 1.10.0: 优化了二进制规则集中 `domain_suffix` 规则的内存使用。 * 3: sing-box 1.11.0: 添加了 `network_type`、 `network_is_expensive` 和 `network_is_constrainted` 规则项。 * 4: sing-box 1.13.0: 添加了 `network_interface_address` 和 `default_interface_address` 规则项。 +* 5: sing-box 1.14.0: 添加了 `package_name_regex` 规则项。 #### rules diff --git a/option/rule.go b/option/rule.go index 9fd9437973..5759cf56e9 100644 --- a/option/rule.go +++ b/option/rule.go @@ -91,6 +91,7 @@ type RawDefaultRule struct { ProcessPath badoption.Listable[string] `json:"process_path,omitempty"` ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"` PackageName badoption.Listable[string] `json:"package_name,omitempty"` + PackageNameRegex badoption.Listable[string] `json:"package_name_regex,omitempty"` User badoption.Listable[string] `json:"user,omitempty"` UserID badoption.Listable[int32] `json:"user_id,omitempty"` ClashMode string `json:"clash_mode,omitempty"` diff --git a/option/rule_dns.go b/option/rule_dns.go index 5582e7df4f..74058a6544 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -88,6 +88,7 @@ type RawDefaultDNSRule struct { ProcessPath badoption.Listable[string] `json:"process_path,omitempty"` ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"` PackageName badoption.Listable[string] `json:"package_name,omitempty"` + PackageNameRegex badoption.Listable[string] `json:"package_name_regex,omitempty"` User badoption.Listable[string] `json:"user,omitempty"` UserID badoption.Listable[int32] `json:"user_id,omitempty"` Outbound badoption.Listable[string] `json:"outbound,omitempty"` diff --git a/option/rule_set.go b/option/rule_set.go index b06342280b..2ca2529af8 100644 --- a/option/rule_set.go +++ b/option/rule_set.go @@ -198,6 +198,7 @@ type DefaultHeadlessRule struct { ProcessPath badoption.Listable[string] `json:"process_path,omitempty"` ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"` PackageName badoption.Listable[string] `json:"package_name,omitempty"` + PackageNameRegex badoption.Listable[string] `json:"package_name_regex,omitempty"` NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"` NetworkIsExpensive bool `json:"network_is_expensive,omitempty"` NetworkIsConstrained bool `json:"network_is_constrained,omitempty"` @@ -243,7 +244,7 @@ type PlainRuleSetCompat _PlainRuleSetCompat func (r PlainRuleSetCompat) MarshalJSON() ([]byte, error) { var v any switch r.Version { - case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3, C.RuleSetVersion4: + case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3, C.RuleSetVersion4, C.RuleSetVersion5: v = r.Options default: return nil, E.New("unknown rule-set version: ", r.Version) @@ -258,7 +259,7 @@ func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error { } var v any switch r.Version { - case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3, C.RuleSetVersion4: + case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3, C.RuleSetVersion4, C.RuleSetVersion5: v = &r.Options case 0: return E.New("missing rule-set version") @@ -275,7 +276,7 @@ func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error { func (r PlainRuleSetCompat) Upgrade() (PlainRuleSet, error) { switch r.Version { - case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3, C.RuleSetVersion4: + case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3, C.RuleSetVersion4, C.RuleSetVersion5: default: return PlainRuleSet{}, E.New("unknown rule-set version: " + F.ToString(r.Version)) } diff --git a/route/rule/rule_default.go b/route/rule/rule_default.go index d4de6ff7ae..774e1b7c0e 100644 --- a/route/rule/rule_default.go +++ b/route/rule/rule_default.go @@ -209,6 +209,14 @@ func NewDefaultRule(ctx context.Context, logger log.ContextLogger, options optio rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.PackageNameRegex) > 0 { + item, err := NewPackageNameRegexItem(options.PackageNameRegex) + if err != nil { + return nil, E.Cause(err, "package_name_regex") + } + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } if len(options.User) > 0 { item := NewUserItem(options.User) rule.items = append(rule.items, item) diff --git a/route/rule/rule_dns.go b/route/rule/rule_dns.go index c406f06745..646f987edf 100644 --- a/route/rule/rule_dns.go +++ b/route/rule/rule_dns.go @@ -251,6 +251,14 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.PackageNameRegex) > 0 { + item, err := NewPackageNameRegexItem(options.PackageNameRegex) + if err != nil { + return nil, E.Cause(err, "package_name_regex") + } + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } if len(options.User) > 0 { item := NewUserItem(options.User) rule.items = append(rule.items, item) diff --git a/route/rule/rule_headless.go b/route/rule/rule_headless.go index c5146318b4..ab85e0d5f7 100644 --- a/route/rule/rule_headless.go +++ b/route/rule/rule_headless.go @@ -153,6 +153,14 @@ func NewDefaultHeadlessRule(ctx context.Context, options option.DefaultHeadlessR rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.PackageNameRegex) > 0 { + item, err := NewPackageNameRegexItem(options.PackageNameRegex) + if err != nil { + return nil, E.Cause(err, "package_name_regex") + } + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } if networkManager != nil { if len(options.NetworkType) > 0 { item := NewNetworkTypeItem(networkManager, common.Map(options.NetworkType, option.InterfaceType.Build)) diff --git a/route/rule/rule_item_package_name_regex.go b/route/rule/rule_item_package_name_regex.go new file mode 100644 index 0000000000..9db4504acf --- /dev/null +++ b/route/rule/rule_item_package_name_regex.go @@ -0,0 +1,56 @@ +package rule + +import ( + "regexp" + "strings" + + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*PackageNameRegexItem)(nil) + +type PackageNameRegexItem struct { + matchers []*regexp.Regexp + description string +} + +func NewPackageNameRegexItem(expressions []string) (*PackageNameRegexItem, error) { + matchers := make([]*regexp.Regexp, 0, len(expressions)) + for i, regex := range expressions { + matcher, err := regexp.Compile(regex) + if err != nil { + return nil, E.Cause(err, "parse expression ", i) + } + matchers = append(matchers, matcher) + } + description := "package_name_regex=" + eLen := len(expressions) + if eLen == 1 { + description += expressions[0] + } else if eLen > 3 { + description += F.ToString("[", strings.Join(expressions[:3], " "), "]") + } else { + description += F.ToString("[", strings.Join(expressions, " "), "]") + } + return &PackageNameRegexItem{matchers, description}, nil +} + +func (r *PackageNameRegexItem) Match(metadata *adapter.InboundContext) bool { + if metadata.ProcessInfo == nil || len(metadata.ProcessInfo.AndroidPackageNames) == 0 { + return false + } + for _, matcher := range r.matchers { + for _, packageName := range metadata.ProcessInfo.AndroidPackageNames { + if matcher.MatchString(packageName) { + return true + } + } + } + return false +} + +func (r *PackageNameRegexItem) String() string { + return r.description +} diff --git a/route/rule/rule_set.go b/route/rule/rule_set.go index d286a7941d..7c82b6022e 100644 --- a/route/rule/rule_set.go +++ b/route/rule/rule_set.go @@ -60,7 +60,7 @@ func HasHeadlessRule(rules []option.HeadlessRule, cond func(rule option.DefaultH } func isProcessHeadlessRule(rule option.DefaultHeadlessRule) bool { - return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.ProcessPathRegex) > 0 || len(rule.PackageName) > 0 + return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.ProcessPathRegex) > 0 || len(rule.PackageName) > 0 || len(rule.PackageNameRegex) > 0 } func isWIFIHeadlessRule(rule option.DefaultHeadlessRule) bool { diff --git a/route/rule_conds.go b/route/rule_conds.go index 22ce94fffd..2c62902949 100644 --- a/route/rule_conds.go +++ b/route/rule_conds.go @@ -38,11 +38,11 @@ func hasDNSRule(rules []option.DNSRule, cond func(rule option.DefaultDNSRule) bo } func isProcessRule(rule option.DefaultRule) bool { - return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.ProcessPathRegex) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0 + return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.ProcessPathRegex) > 0 || len(rule.PackageName) > 0 || len(rule.PackageNameRegex) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0 } func isProcessDNSRule(rule option.DefaultDNSRule) bool { - return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.ProcessPathRegex) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0 + return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.ProcessPathRegex) > 0 || len(rule.PackageName) > 0 || len(rule.PackageNameRegex) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0 } func isNeighborRule(rule option.DefaultRule) bool { From 012db0ef0ef9a7fa75cd7e0dbda68813874e30d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 10 Apr 2026 12:04:55 +0800 Subject: [PATCH 23/93] platform: Wrap command RPC error returns with E.Cause --- experimental/libbox/command_client.go | 131 +++++++++++++++++--------- experimental/libbox/command_server.go | 4 +- 2 files changed, 89 insertions(+), 46 deletions(-) diff --git a/experimental/libbox/command_client.go b/experimental/libbox/command_client.go index 5223bf7e0b..a8d18495b6 100644 --- a/experimental/libbox/command_client.go +++ b/experimental/libbox/command_client.go @@ -116,7 +116,7 @@ func dialTarget() (string, func(context.Context, string) (net.Conn, error)) { return "passthrough:///xpc", func(ctx context.Context, _ string) (net.Conn, error) { fileDescriptor, err := sXPCDialer.DialXPC() if err != nil { - return nil, err + return nil, E.Cause(err, "dial xpc") } return networkConnectionFromFileDescriptor(fileDescriptor) } @@ -165,7 +165,7 @@ func (c *CommandClient) dialWithRetry(target string, contextDialer func(context. if err != nil { lastError = err if !retryDial { - return nil, nil, err + return nil, nil, E.Cause(err, "create command client") } time.Sleep(commandClientDialDelay(attempt)) continue @@ -185,7 +185,7 @@ func (c *CommandClient) dialWithRetry(target string, contextDialer func(context. if connection != nil { connection.Close() } - return nil, nil, lastError + return nil, nil, E.Cause(lastError, "probe command server") } func (c *CommandClient) Connect() error { @@ -282,7 +282,7 @@ func (c *CommandClient) getClientForCall() (daemon.StartedServiceClient, error) target, contextDialer := dialTarget() connection, client, err := c.dialWithRetry(target, contextDialer, true) if err != nil { - return nil, err + return nil, E.Cause(err, "get command client") } c.grpcConn = connection c.grpcClient = client @@ -324,19 +324,19 @@ func (c *CommandClient) handleLogStream() { client, ctx := c.getStreamContext() stream, err := client.SubscribeLog(ctx, &emptypb.Empty{}) if err != nil { - c.handler.Disconnected(err.Error()) + c.handler.Disconnected(E.Cause(err, "subscribe log").Error()) return } defaultLogLevel, err := client.GetDefaultLogLevel(ctx, &emptypb.Empty{}) if err != nil { - c.handler.Disconnected(err.Error()) + c.handler.Disconnected(E.Cause(err, "get default log level").Error()) return } c.handler.SetDefaultLogLevel(int32(defaultLogLevel.Level)) for { logMessage, err := stream.Recv() if err != nil { - c.handler.Disconnected(err.Error()) + c.handler.Disconnected(E.Cause(err, "log stream recv").Error()) return } if logMessage.Reset_ { @@ -361,14 +361,14 @@ func (c *CommandClient) handleStatusStream() { Interval: interval, }) if err != nil { - c.handler.Disconnected(err.Error()) + c.handler.Disconnected(E.Cause(err, "subscribe status").Error()) return } for { status, err := stream.Recv() if err != nil { - c.handler.Disconnected(err.Error()) + c.handler.Disconnected(E.Cause(err, "status stream recv").Error()) return } c.handler.WriteStatus(statusMessageFromGRPC(status)) @@ -380,14 +380,14 @@ func (c *CommandClient) handleGroupStream() { stream, err := client.SubscribeGroups(ctx, &emptypb.Empty{}) if err != nil { - c.handler.Disconnected(err.Error()) + c.handler.Disconnected(E.Cause(err, "subscribe groups").Error()) return } for { groups, err := stream.Recv() if err != nil { - c.handler.Disconnected(err.Error()) + c.handler.Disconnected(E.Cause(err, "groups stream recv").Error()) return } c.handler.WriteGroups(outboundGroupIteratorFromGRPC(groups)) @@ -399,7 +399,7 @@ func (c *CommandClient) handleClashModeStream() { modeStatus, err := client.GetClashModeStatus(ctx, &emptypb.Empty{}) if err != nil { - c.handler.Disconnected(err.Error()) + c.handler.Disconnected(E.Cause(err, "get clash mode status").Error()) return } @@ -407,13 +407,13 @@ func (c *CommandClient) handleClashModeStream() { go func() { c.handler.InitializeClashMode(newIterator(modeStatus.ModeList), modeStatus.CurrentMode) if len(modeStatus.ModeList) == 0 { - c.handler.Disconnected(os.ErrInvalid.Error()) + c.handler.Disconnected(E.Cause(os.ErrInvalid, "empty clash mode list").Error()) } }() } else { c.handler.InitializeClashMode(newIterator(modeStatus.ModeList), modeStatus.CurrentMode) if len(modeStatus.ModeList) == 0 { - c.handler.Disconnected(os.ErrInvalid.Error()) + c.handler.Disconnected(E.Cause(os.ErrInvalid, "empty clash mode list").Error()) return } } @@ -424,14 +424,14 @@ func (c *CommandClient) handleClashModeStream() { stream, err := client.SubscribeClashMode(ctx, &emptypb.Empty{}) if err != nil { - c.handler.Disconnected(err.Error()) + c.handler.Disconnected(E.Cause(err, "subscribe clash mode").Error()) return } for { mode, err := stream.Recv() if err != nil { - c.handler.Disconnected(err.Error()) + c.handler.Disconnected(E.Cause(err, "clash mode stream recv").Error()) return } c.handler.UpdateClashMode(mode.Mode) @@ -446,14 +446,14 @@ func (c *CommandClient) handleConnectionsStream() { Interval: interval, }) if err != nil { - c.handler.Disconnected(err.Error()) + c.handler.Disconnected(E.Cause(err, "subscribe connections").Error()) return } for { events, err := stream.Recv() if err != nil { - c.handler.Disconnected(err.Error()) + c.handler.Disconnected(E.Cause(err, "connections stream recv").Error()) return } libboxEvents := connectionEventsFromGRPC(events) @@ -466,14 +466,14 @@ func (c *CommandClient) handleOutboundsStream() { stream, err := client.SubscribeOutbounds(ctx, &emptypb.Empty{}) if err != nil { - c.handler.Disconnected(err.Error()) + c.handler.Disconnected(E.Cause(err, "subscribe outbounds").Error()) return } for { list, err := stream.Recv() if err != nil { - c.handler.Disconnected(err.Error()) + c.handler.Disconnected(E.Cause(err, "outbounds stream recv").Error()) return } c.handler.WriteOutbounds(outboundGroupItemListFromGRPC(list)) @@ -487,7 +487,10 @@ func (c *CommandClient) SelectOutbound(groupTag string, outboundTag string) erro OutboundTag: outboundTag, }) }) - return err + if err != nil { + return E.Cause(err, "select outbound") + } + return nil } func (c *CommandClient) URLTest(groupTag string) error { @@ -496,7 +499,10 @@ func (c *CommandClient) URLTest(groupTag string) error { OutboundTag: groupTag, }) }) - return err + if err != nil { + return E.Cause(err, "url test") + } + return nil } func (c *CommandClient) SetClashMode(newMode string) error { @@ -505,7 +511,10 @@ func (c *CommandClient) SetClashMode(newMode string) error { Mode: newMode, }) }) - return err + if err != nil { + return E.Cause(err, "set clash mode") + } + return nil } func (c *CommandClient) CloseConnection(connId string) error { @@ -514,42 +523,57 @@ func (c *CommandClient) CloseConnection(connId string) error { Id: connId, }) }) - return err + if err != nil { + return E.Cause(err, "close connection") + } + return nil } func (c *CommandClient) CloseConnections() error { _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { return client.CloseAllConnections(context.Background(), &emptypb.Empty{}) }) - return err + if err != nil { + return E.Cause(err, "close all connections") + } + return nil } func (c *CommandClient) ServiceReload() error { _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { return client.ReloadService(context.Background(), &emptypb.Empty{}) }) - return err + if err != nil { + return E.Cause(err, "reload service") + } + return nil } func (c *CommandClient) ServiceClose() error { _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { return client.StopService(context.Background(), &emptypb.Empty{}) }) - return err + if err != nil { + return E.Cause(err, "stop service") + } + return nil } func (c *CommandClient) ClearLogs() error { _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { return client.ClearLogs(context.Background(), &emptypb.Empty{}) }) - return err + if err != nil { + return E.Cause(err, "clear logs") + } + return nil } func (c *CommandClient) GetSystemProxyStatus() (*SystemProxyStatus, error) { return callWithResult(c, func(client daemon.StartedServiceClient) (*SystemProxyStatus, error) { status, err := client.GetSystemProxyStatus(context.Background(), &emptypb.Empty{}) if err != nil { - return nil, err + return nil, E.Cause(err, "get system proxy status") } return systemProxyStatusFromGRPC(status), nil }) @@ -561,7 +585,10 @@ func (c *CommandClient) SetSystemProxyEnabled(isEnabled bool) error { Enabled: isEnabled, }) }) - return err + if err != nil { + return E.Cause(err, "set system proxy enabled") + } + return nil } func (c *CommandClient) TriggerGoCrash() error { @@ -570,7 +597,10 @@ func (c *CommandClient) TriggerGoCrash() error { Type: daemon.DebugCrashRequest_GO, }) }) - return err + if err != nil { + return E.Cause(err, "trigger debug crash") + } + return nil } func (c *CommandClient) TriggerNativeCrash() error { @@ -579,21 +609,27 @@ func (c *CommandClient) TriggerNativeCrash() error { Type: daemon.DebugCrashRequest_NATIVE, }) }) - return err + if err != nil { + return E.Cause(err, "trigger native crash") + } + return nil } func (c *CommandClient) TriggerOOMReport() error { _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { return client.TriggerOOMReport(context.Background(), &emptypb.Empty{}) }) - return err + if err != nil { + return E.Cause(err, "trigger oom report") + } + return nil } func (c *CommandClient) GetDeprecatedNotes() (DeprecatedNoteIterator, error) { return callWithResult(c, func(client daemon.StartedServiceClient) (DeprecatedNoteIterator, error) { warnings, err := client.GetDeprecatedWarnings(context.Background(), &emptypb.Empty{}) if err != nil { - return nil, err + return nil, E.Cause(err, "get deprecated warnings") } var notes []*DeprecatedNote for _, warning := range warnings.Warnings { @@ -612,7 +648,7 @@ func (c *CommandClient) GetStartedAt() (int64, error) { return callWithResult(c, func(client daemon.StartedServiceClient) (int64, error) { startedAt, err := client.GetStartedAt(context.Background(), &emptypb.Empty{}) if err != nil { - return 0, err + return 0, E.Cause(err, "get started at") } return startedAt.StartedAt, nil }) @@ -625,13 +661,16 @@ func (c *CommandClient) SetGroupExpand(groupTag string, isExpand bool) error { IsExpand: isExpand, }) }) - return err + if err != nil { + return E.Cause(err, "set group expand") + } + return nil } func (c *CommandClient) StartNetworkQualityTest(configURL string, outboundTag string, serial bool, maxRuntimeSeconds int32, http3 bool, handler NetworkQualityTestHandler) error { client, err := c.getClientForCall() if err != nil { - return err + return E.Cause(err, "start network quality test") } if c.standalone { defer c.closeConnection() @@ -644,11 +683,12 @@ func (c *CommandClient) StartNetworkQualityTest(configURL string, outboundTag st Http3: http3, }) if err != nil { - return err + return E.Cause(err, "start network quality test") } for { event, recvErr := stream.Recv() if recvErr != nil { + recvErr = E.Cause(recvErr, "network quality test recv") handler.OnError(recvErr.Error()) return recvErr } @@ -677,7 +717,7 @@ func (c *CommandClient) StartNetworkQualityTest(configURL string, outboundTag st func (c *CommandClient) StartSTUNTest(server string, outboundTag string, handler STUNTestHandler) error { client, err := c.getClientForCall() if err != nil { - return err + return E.Cause(err, "start stun test") } if c.standalone { defer c.closeConnection() @@ -687,11 +727,12 @@ func (c *CommandClient) StartSTUNTest(server string, outboundTag string, handler OutboundTag: outboundTag, }) if err != nil { - return err + return E.Cause(err, "start stun test") } for { event, recvErr := stream.Recv() if recvErr != nil { + recvErr = E.Cause(recvErr, "stun test recv") handler.OnError(recvErr.Error()) return recvErr } @@ -716,14 +757,14 @@ func (c *CommandClient) StartSTUNTest(server string, outboundTag string, handler func (c *CommandClient) SubscribeTailscaleStatus(handler TailscaleStatusHandler) error { client, err := c.getClientForCall() if err != nil { - return err + return E.Cause(err, "subscribe tailscale status") } if c.standalone { defer c.closeConnection() } stream, err := client.SubscribeTailscaleStatus(context.Background(), &emptypb.Empty{}) if err != nil { - return err + return E.Cause(err, "subscribe tailscale status") } for { event, recvErr := stream.Recv() @@ -731,6 +772,7 @@ func (c *CommandClient) SubscribeTailscaleStatus(handler TailscaleStatusHandler) if status.Code(recvErr) == codes.NotFound { return nil } + recvErr = E.Cause(recvErr, "tailscale status recv") handler.OnError(recvErr.Error()) return recvErr } @@ -741,7 +783,7 @@ func (c *CommandClient) SubscribeTailscaleStatus(handler TailscaleStatusHandler) func (c *CommandClient) StartTailscalePing(endpointTag string, peerIP string, handler TailscalePingHandler) error { client, err := c.getClientForCall() if err != nil { - return err + return E.Cause(err, "start tailscale ping") } if c.standalone { defer c.closeConnection() @@ -751,11 +793,12 @@ func (c *CommandClient) StartTailscalePing(endpointTag string, peerIP string, ha PeerIP: peerIP, }) if err != nil { - return err + return E.Cause(err, "start tailscale ping") } for { event, recvErr := stream.Recv() if recvErr != nil { + recvErr = E.Cause(recvErr, "tailscale ping recv") handler.OnError(recvErr.Error()) return recvErr } diff --git a/experimental/libbox/command_server.go b/experimental/libbox/command_server.go index c093cd6da4..60ec17a8f0 100644 --- a/experimental/libbox/command_server.go +++ b/experimental/libbox/command_server.go @@ -180,7 +180,7 @@ func (s *CommandServer) StartOrReloadService(configContent string, options *Over ExcludePackage: iteratorToArray(options.ExcludePackage), }) if err != nil { - return err + return E.Cause(err, "start or reload service") } return nil } @@ -267,7 +267,7 @@ func (h *platformHandler) ServiceReload() error { func (h *platformHandler) SystemProxyStatus() (*daemon.SystemProxyStatus, error) { status, err := (*CommandServer)(h).handler.GetSystemProxyStatus() if err != nil { - return nil, err + return nil, E.Cause(err, "get system proxy status") } return &daemon.SystemProxyStatus{ Enabled: status.Enabled, From 5779b46ca626ee5230dbd19119242144652ca711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 10 Apr 2026 12:10:02 +0800 Subject: [PATCH 24/93] Fix lint errors --- common/networkquality/http3.go | 2 +- experimental/libbox/internal/oomprofile/oomprofile.go | 9 ++++++--- experimental/libbox/internal/oomprofile/protobuf.go | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/common/networkquality/http3.go b/common/networkquality/http3.go index 5e28d9fd68..0e907821d2 100644 --- a/common/networkquality/http3.go +++ b/common/networkquality/http3.go @@ -37,7 +37,7 @@ func NewHTTP3MeasurementClientFactory(dialer N.Dialer) (MeasurementClientFactory if dialErr != nil { return nil, dialErr } - var wrappedConn net.Conn = udpConn + wrappedConn := udpConn if len(readCounters) > 0 || len(writeCounters) > 0 { wrappedConn = sBufio.NewCounterConn(udpConn, readCounters, writeCounters) } diff --git a/experimental/libbox/internal/oomprofile/oomprofile.go b/experimental/libbox/internal/oomprofile/oomprofile.go index f26d3b5894..cd0b9bec0d 100644 --- a/experimental/libbox/internal/oomprofile/oomprofile.go +++ b/experimental/libbox/internal/oomprofile/oomprofile.go @@ -95,7 +95,8 @@ func writeAlloc(w io.Writer) error { func writeHeapInternal(w io.Writer, defaultSampleType string) error { var profile []memProfileRecord - n, ok := runtimeMemProfileInternal(nil, true) + n, _ := runtimeMemProfileInternal(nil, true) + var ok bool for { profile = make([]memProfileRecord, n+50) n, ok = runtimeMemProfileInternal(profile, true) @@ -121,7 +122,8 @@ func writeRuntimeProfile(w io.Writer, name string, fetch func([]stackRecord, []u var profile []stackRecord var labels []unsafe.Pointer - n, ok := fetch(nil, nil) + n, _ := fetch(nil, nil) + var ok bool for { profile = make([]stackRecord, n+10) labels = make([]unsafe.Pointer, n+10) @@ -146,7 +148,8 @@ func writeMutex(w io.Writer) error { func writeCycleProfile(w io.Writer, countName string, cycleName string, fetch func([]blockProfileRecord) (int, bool)) error { var profile []blockProfileRecord - n, ok := fetch(nil) + n, _ := fetch(nil) + var ok bool for { profile = make([]blockProfileRecord, n+50) n, ok = fetch(profile) diff --git a/experimental/libbox/internal/oomprofile/protobuf.go b/experimental/libbox/internal/oomprofile/protobuf.go index 0f06e00d50..ed60df21c2 100644 --- a/experimental/libbox/internal/oomprofile/protobuf.go +++ b/experimental/libbox/internal/oomprofile/protobuf.go @@ -22,7 +22,7 @@ func (b *protobuf) length(tag int, length int) { } func (b *protobuf) uint64(tag int, x uint64) { - b.varint(uint64(tag)<<3 | 0) + b.varint(uint64(tag) << 3) b.varint(x) } From 08260fa770047e5cd28cff13cb21dd178a680946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 10 Apr 2026 13:23:42 +0800 Subject: [PATCH 25/93] Add cloudflared inbound --- constant/proxy.go | 3 + docs/configuration/inbound/cloudflared.md | 89 +++++++++++ docs/configuration/inbound/cloudflared.zh.md | 89 +++++++++++ docs/configuration/inbound/index.md | 1 + docs/configuration/inbound/index.zh.md | 1 + docs/installation/build-from-source.md | 1 + docs/installation/build-from-source.zh.md | 1 + go.mod | 5 + go.sum | 12 ++ include/cloudflared.go | 12 ++ include/cloudflared_stub.go | 20 +++ include/registry.go | 1 + mkdocs.yml | 1 + option/cloudflared.go | 16 ++ protocol/cloudflare/inbound.go | 160 +++++++++++++++++++ release/DEFAULT_BUILD_TAGS | 2 +- release/DEFAULT_BUILD_TAGS_OTHERS | 2 +- release/DEFAULT_BUILD_TAGS_WINDOWS | 2 +- 18 files changed, 415 insertions(+), 3 deletions(-) create mode 100644 docs/configuration/inbound/cloudflared.md create mode 100644 docs/configuration/inbound/cloudflared.zh.md create mode 100644 include/cloudflared.go create mode 100644 include/cloudflared_stub.go create mode 100644 option/cloudflared.go create mode 100644 protocol/cloudflare/inbound.go diff --git a/constant/proxy.go b/constant/proxy.go index add66c95e5..ffec80250b 100644 --- a/constant/proxy.go +++ b/constant/proxy.go @@ -25,6 +25,7 @@ const ( TypeTUIC = "tuic" TypeHysteria2 = "hysteria2" TypeTailscale = "tailscale" + TypeCloudflared = "cloudflared" TypeDERP = "derp" TypeResolved = "resolved" TypeSSMAPI = "ssm-api" @@ -90,6 +91,8 @@ func ProxyDisplayName(proxyType string) string { return "AnyTLS" case TypeTailscale: return "Tailscale" + case TypeCloudflared: + return "Cloudflared" case TypeSelector: return "Selector" case TypeURLTest: diff --git a/docs/configuration/inbound/cloudflared.md b/docs/configuration/inbound/cloudflared.md new file mode 100644 index 0000000000..e91d73e09b --- /dev/null +++ b/docs/configuration/inbound/cloudflared.md @@ -0,0 +1,89 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.14.0" + +`cloudflared` inbound runs an embedded Cloudflare Tunnel client and routes all +incoming tunnel traffic (TCP, UDP, ICMP) through sing-box's routing engine. + +### Structure + +```json +{ + "type": "cloudflared", + "tag": "", + + "token": "", + "ha_connections": 0, + "protocol": "", + "post_quantum": false, + "edge_ip_version": 0, + "datagram_version": "", + "grace_period": "", + "region": "", + "control_dialer": { + ... // Dial Fields + }, + "tunnel_dialer": { + ... // Dial Fields + } +} +``` + +### Fields + +#### token + +==Required== + +Base64-encoded tunnel token from the Cloudflare Zero Trust dashboard +(`Networks → Tunnels → Install connector`). + +#### ha_connections + +Number of high-availability connections to the Cloudflare edge. + +Capped by the number of discovered edge addresses. + +#### protocol + +Transport protocol for edge connections. + +One of `quic` `http2`. + +#### post_quantum + +Enable post-quantum key exchange on the control connection. + +#### edge_ip_version + +IP version used when connecting to the Cloudflare edge. + +One of `0` (automatic) `4` `6`. + +#### datagram_version + +Datagram protocol version used for UDP proxying over QUIC. + +One of `v2` `v3`. Only meaningful when `protocol` is `quic`. + +#### grace_period + +Graceful shutdown window for in-flight edge connections. + +#### region + +Cloudflare edge region selector. + +Conflict with endpoints embedded in `token`. + +#### control_dialer + +[Dial Fields](/configuration/shared/dial/) used when the tunnel client dials the +Cloudflare control plane. + +#### tunnel_dialer + +[Dial Fields](/configuration/shared/dial/) used when the tunnel client dials the +Cloudflare edge data plane. diff --git a/docs/configuration/inbound/cloudflared.zh.md b/docs/configuration/inbound/cloudflared.zh.md new file mode 100644 index 0000000000..65aa7dcf81 --- /dev/null +++ b/docs/configuration/inbound/cloudflared.zh.md @@ -0,0 +1,89 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.14.0 起" + +`cloudflared` 入站运行一个内嵌的 Cloudflare Tunnel 客户端,并将所有传入的隧道流量 +(TCP、UDP、ICMP)通过 sing-box 的路由引擎转发。 + +### 结构 + +```json +{ + "type": "cloudflared", + "tag": "", + + "token": "", + "ha_connections": 0, + "protocol": "", + "post_quantum": false, + "edge_ip_version": 0, + "datagram_version": "", + "grace_period": "", + "region": "", + "control_dialer": { + ... // 拨号字段 + }, + "tunnel_dialer": { + ... // 拨号字段 + } +} +``` + +### 字段 + +#### token + +==必填== + +来自 Cloudflare Zero Trust 仪表板的 Base64 编码隧道令牌 +(`Networks → Tunnels → Install connector`)。 + +#### ha_connections + +到 Cloudflare edge 的高可用连接数。 + +上限为已发现的 edge 地址数量。 + +#### protocol + +edge 连接使用的传输协议。 + +`quic` `http2` 之一。 + +#### post_quantum + +在控制连接上启用后量子密钥交换。 + +#### edge_ip_version + +连接 Cloudflare edge 时使用的 IP 版本。 + +`0`(自动)`4` `6` 之一。 + +#### datagram_version + +通过 QUIC 进行 UDP 代理时使用的数据报协议版本。 + +`v2` `v3` 之一。仅在 `protocol` 为 `quic` 时有效。 + +#### grace_period + +正在处理的 edge 连接的优雅关闭窗口。 + +#### region + +Cloudflare edge 区域选择器。 + +与 `token` 中嵌入的 endpoint 冲突。 + +#### control_dialer + +隧道客户端拨向 Cloudflare 控制面时使用的 +[拨号字段](/zh/configuration/shared/dial/)。 + +#### tunnel_dialer + +隧道客户端拨向 Cloudflare edge 数据面时使用的 +[拨号字段](/zh/configuration/shared/dial/)。 diff --git a/docs/configuration/inbound/index.md b/docs/configuration/inbound/index.md index 27cc9fdbbd..274a378063 100644 --- a/docs/configuration/inbound/index.md +++ b/docs/configuration/inbound/index.md @@ -34,6 +34,7 @@ | `tun` | [Tun](./tun/) | :material-close: | | `redirect` | [Redirect](./redirect/) | :material-close: | | `tproxy` | [TProxy](./tproxy/) | :material-close: | +| `cloudflared` | [Cloudflared](./cloudflared/) | :material-close: | #### tag diff --git a/docs/configuration/inbound/index.zh.md b/docs/configuration/inbound/index.zh.md index 1e0c0c4f65..99f8df3bd7 100644 --- a/docs/configuration/inbound/index.zh.md +++ b/docs/configuration/inbound/index.zh.md @@ -34,6 +34,7 @@ | `tun` | [Tun](./tun/) | :material-close: | | `redirect` | [Redirect](./redirect/) | :material-close: | | `tproxy` | [TProxy](./tproxy/) | :material-close: | +| `cloudflared` | [Cloudflared](./cloudflared/) | :material-close: | #### tag diff --git a/docs/installation/build-from-source.md b/docs/installation/build-from-source.md index 8152a89e22..48b53b178d 100644 --- a/docs/installation/build-from-source.md +++ b/docs/installation/build-from-source.md @@ -61,6 +61,7 @@ go build -tags "tag_a tag_b" ./cmd/sing-box | `with_ccm` | :material-check: | Build with Claude Code Multiplexer service support. | | `with_ocm` | :material-check: | Build with OpenAI Codex Multiplexer service support. | | `with_naive_outbound` | :material-check: | Build with NaiveProxy outbound support, see [NaiveProxy outbound](/configuration/outbound/naive/). | +| `with_cloudflared` | :material-check: | Build with Cloudflare Tunnel inbound support, see [Cloudflared inbound](/configuration/inbound/cloudflared/). | | `badlinkname` | :material-check: | Enable `go:linkname` access to internal standard library functions. Required because the Go standard library does not expose many low-level APIs needed by this project, and reimplementing them externally is impractical. Used for kTLS (kernel TLS offload) and raw TLS record manipulation. | | `tfogo_checklinkname0` | :material-check: | Companion to `badlinkname`. Go 1.23+ enforces `go:linkname` restrictions via the linker; this tag signals the build uses `-checklinkname=0` to bypass that enforcement. | diff --git a/docs/installation/build-from-source.zh.md b/docs/installation/build-from-source.zh.md index d6cd03b516..4ffebdc65d 100644 --- a/docs/installation/build-from-source.zh.md +++ b/docs/installation/build-from-source.zh.md @@ -65,6 +65,7 @@ go build -tags "tag_a tag_b" ./cmd/sing-box | `with_ccm` | :material-check: | 构建 Claude Code Multiplexer 服务支持。 | | `with_ocm` | :material-check: | 构建 OpenAI Codex Multiplexer 服务支持。 | | `with_naive_outbound` | :material-check: | 构建 NaiveProxy 出站支持,参阅 [NaiveProxy 出站](/zh/configuration/outbound/naive/)。 | +| `with_cloudflared` | :material-check: | 构建 Cloudflare Tunnel 入站支持,参阅 [Cloudflared 入站](/zh/configuration/inbound/cloudflared/)。 | | `badlinkname` | :material-check: | 启用 `go:linkname` 以访问标准库内部函数。Go 标准库未提供本项目需要的许多底层 API,且在外部重新实现不切实际。用于 kTLS(内核 TLS 卸载)和原始 TLS 记录操作。 | | `tfogo_checklinkname0` | :material-check: | `badlinkname` 的伴随标记。Go 1.23+ 链接器强制限制 `go:linkname` 使用;此标记表示构建使用 `-checklinkname=0` 以绕过该限制。 | diff --git a/go.mod b/go.mod index 46aadde68a..f652867a0d 100644 --- a/go.mod +++ b/go.mod @@ -38,6 +38,7 @@ require ( github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 github.com/sagernet/sing v0.8.10-0.20260421111925-3e730f2301b4 + github.com/sagernet/sing-cloudflared v0.1.0 github.com/sagernet/sing-mux v0.3.4 github.com/sagernet/sing-quic v0.6.2-0.20260330152607-bf674c163212 github.com/sagernet/sing-shadowsocks v0.2.8 @@ -73,6 +74,7 @@ require ( github.com/andybalholm/brotli v1.1.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect + github.com/coreos/go-oidc/v3 v3.17.0 // indirect github.com/database64128/netx-go v0.1.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect @@ -82,6 +84,7 @@ require ( github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/gaissmai/bart v0.18.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect @@ -99,6 +102,7 @@ require ( github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/mdlayher/socket v0.5.1 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect + github.com/philhofer/fwd v1.2.0 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pires/go-proxyproto v0.8.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -165,4 +169,5 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.3.0 // indirect + zombiezen.com/go/capnproto2 v2.18.2+incompatible // indirect ) diff --git a/go.sum b/go.sum index 263305fde8..89ed708e01 100644 --- a/go.sum +++ b/go.sum @@ -28,6 +28,8 @@ github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9 github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0= github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= +github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= +github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo= github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI= @@ -110,6 +112,8 @@ github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zt github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/letsencrypt/challtestsrv v1.4.2 h1:0ON3ldMhZyWlfVNYYpFuWRTmZNnyfiL9Hh5YzC3JVwU= github.com/letsencrypt/challtestsrv v1.4.2/go.mod h1:GhqMqcSoeGpYd5zX5TgwA6er/1MbWzx/o7yuuVya+Wk= github.com/letsencrypt/pebble/v2 v2.10.0 h1:Wq6gYXlsY6ubqI3hhxsTzdyotvfdjFBxuwYqCLCnj/U= @@ -142,6 +146,8 @@ github.com/openai/openai-go/v3 v3.26.0 h1:bRt6H/ozMNt/dDkN4gobnLqaEGrRGBzmbVs0xx github.com/openai/openai-go/v3 v3.26.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE= github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8= +github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= +github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0= @@ -238,6 +244,8 @@ github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgj github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4= github.com/sagernet/sing v0.8.10-0.20260421111925-3e730f2301b4 h1:pqb1VEG6BXNTid2Llp/AT8ok7FZuCCis41glydWhgno= github.com/sagernet/sing v0.8.10-0.20260421111925-3e730f2301b4/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing-cloudflared v0.1.0 h1:to+2fcCx8zu4X/DirRw9Ihc+FrEZ7oEyIqeCoJiwIpw= +github.com/sagernet/sing-cloudflared v0.1.0/go.mod h1:bH2NKX+NpDTY1Zkxfboxw6MXB/ZywaNLmrDJYgKMJ2Y= github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s= github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= github.com/sagernet/sing-quic v0.6.2-0.20260330152607-bf674c163212 h1:7mFOUqy+DyOj7qKGd1X54UMXbnbJiiMileK/tn17xYc= @@ -294,6 +302,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 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/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= @@ -401,3 +411,5 @@ lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= +zombiezen.com/go/capnproto2 v2.18.2+incompatible h1:v3BD1zbruvffn7zjJUU5Pn8nZAB11bhZSQC4W+YnnKo= +zombiezen.com/go/capnproto2 v2.18.2+incompatible/go.mod h1:XO5Pr2SbXgqZwn0m0Ru54QBqpOf4K5AYBO+8LAOBQEQ= diff --git a/include/cloudflared.go b/include/cloudflared.go new file mode 100644 index 0000000000..6320010825 --- /dev/null +++ b/include/cloudflared.go @@ -0,0 +1,12 @@ +//go:build with_cloudflared + +package include + +import ( + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/protocol/cloudflare" +) + +func registerCloudflaredInbound(registry *inbound.Registry) { + cloudflare.RegisterInbound(registry) +} diff --git a/include/cloudflared_stub.go b/include/cloudflared_stub.go new file mode 100644 index 0000000000..8f49aecc69 --- /dev/null +++ b/include/cloudflared_stub.go @@ -0,0 +1,20 @@ +//go:build !with_cloudflared + +package include + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +func registerCloudflaredInbound(registry *inbound.Registry) { + inbound.Register[option.CloudflaredInboundOptions](registry, C.TypeCloudflared, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.CloudflaredInboundOptions) (adapter.Inbound, error) { + return nil, E.New(`Cloudflared is not included in this build, rebuild with -tags with_cloudflared`) + }) +} diff --git a/include/registry.go b/include/registry.go index eb22cce1fe..5a1a2f973a 100644 --- a/include/registry.go +++ b/include/registry.go @@ -66,6 +66,7 @@ func InboundRegistry() *inbound.Registry { anytls.RegisterInbound(registry) registerQUICInbounds(registry) + registerCloudflaredInbound(registry) registerStubForRemovedInbounds(registry) return registry diff --git a/mkdocs.yml b/mkdocs.yml index 65c9db71f4..5387be9d51 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -158,6 +158,7 @@ nav: - Tun: configuration/inbound/tun.md - Redirect: configuration/inbound/redirect.md - TProxy: configuration/inbound/tproxy.md + - Cloudflared: configuration/inbound/cloudflared.md - Outbound: - configuration/outbound/index.md - Direct: configuration/outbound/direct.md diff --git a/option/cloudflared.go b/option/cloudflared.go new file mode 100644 index 0000000000..e94a20fefd --- /dev/null +++ b/option/cloudflared.go @@ -0,0 +1,16 @@ +package option + +import "github.com/sagernet/sing/common/json/badoption" + +type CloudflaredInboundOptions struct { + Token string `json:"token,omitempty"` + HighAvailabilityConnections int `json:"ha_connections,omitempty"` + Protocol string `json:"protocol,omitempty"` + PostQuantum bool `json:"post_quantum,omitempty"` + EdgeIPVersion int `json:"edge_ip_version,omitempty"` + DatagramVersion string `json:"datagram_version,omitempty"` + GracePeriod badoption.Duration `json:"grace_period,omitempty"` + Region string `json:"region,omitempty"` + ControlDialer DialerOptions `json:"control_dialer,omitempty"` + TunnelDialer DialerOptions `json:"tunnel_dialer,omitempty"` +} diff --git a/protocol/cloudflare/inbound.go b/protocol/cloudflare/inbound.go new file mode 100644 index 0000000000..f445ab956b --- /dev/null +++ b/protocol/cloudflare/inbound.go @@ -0,0 +1,160 @@ +//go:build with_cloudflared + +package cloudflare + +import ( + "context" + "net" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + boxDialer "github.com/sagernet/sing-box/common/dialer" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/route/rule" + cloudflared "github.com/sagernet/sing-cloudflared" + tun "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/pipe" +) + +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[option.CloudflaredInboundOptions](registry, C.TypeCloudflared, NewInbound) +} + +func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.CloudflaredInboundOptions) (adapter.Inbound, error) { + controlDialer, err := boxDialer.NewWithOptions(boxDialer.Options{ + Context: ctx, + Options: options.ControlDialer, + RemoteIsDomain: true, + }) + if err != nil { + return nil, E.Cause(err, "build cloudflared control dialer") + } + tunnelDialer, err := boxDialer.NewWithOptions(boxDialer.Options{ + Context: ctx, + Options: options.TunnelDialer, + RemoteIsDomain: true, + }) + if err != nil { + return nil, E.Cause(err, "build cloudflared tunnel dialer") + } + + service, err := cloudflared.NewService(cloudflared.ServiceOptions{ + Logger: logger, + ConnectionDialer: &routerDialer{router: router, tag: tag}, + ControlDialer: controlDialer, + TunnelDialer: tunnelDialer, + ICMPHandler: &icmpRouterHandler{router: router, logger: logger, tag: tag}, + ConnContext: func(connCtx context.Context) context.Context { + return adapter.WithContext(connCtx, &adapter.InboundContext{ + Inbound: tag, + InboundType: C.TypeCloudflared, + }) + }, + Token: options.Token, + HAConnections: options.HighAvailabilityConnections, + Protocol: options.Protocol, + PostQuantum: options.PostQuantum, + EdgeIPVersion: options.EdgeIPVersion, + DatagramVersion: options.DatagramVersion, + GracePeriod: time.Duration(options.GracePeriod), + Region: options.Region, + }) + if err != nil { + return nil, err + } + + return &Inbound{ + Adapter: inbound.NewAdapter(C.TypeCloudflared, tag), + service: service, + }, nil +} + +type Inbound struct { + inbound.Adapter + service *cloudflared.Service +} + +func (i *Inbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + return i.service.Start() +} + +func (i *Inbound) Close() error { + return i.service.Close() +} + +type routerDialer struct { + router adapter.Router + tag string +} + +func (d *routerDialer) newMetadata(network string, destination M.Socksaddr) adapter.InboundContext { + return adapter.InboundContext{ + Inbound: d.tag, + InboundType: C.TypeCloudflared, + Network: network, + Destination: destination, + } +} + +func (d *routerDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + input, output := pipe.Pipe() + go d.router.RouteConnectionEx(ctx, output, d.newMetadata(N.NetworkTCP, destination), N.OnceClose(func(it error) { + input.Close() + })) + return input, nil +} + +func (d *routerDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + input, output := pipe.Pipe() + routerConn := bufio.NewUnbindPacketConn(output) + go d.router.RoutePacketConnectionEx(ctx, routerConn, d.newMetadata(N.NetworkUDP, destination), N.OnceClose(func(it error) { + input.Close() + })) + return bufio.NewUnbindPacketConn(input), nil +} + +type icmpRouterHandler struct { + router adapter.Router + logger log.ContextLogger + tag string +} + +func (h *icmpRouterHandler) RouteICMPConnection(ctx context.Context, session tun.DirectRouteSession, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { + var ipVersion uint8 + if session.Destination.Is4() { + ipVersion = 4 + } else { + ipVersion = 6 + } + destination := M.SocksaddrFrom(session.Destination, 0) + routeDestination, err := h.router.PreMatch(adapter.InboundContext{ + Inbound: h.tag, + InboundType: C.TypeCloudflared, + IPVersion: ipVersion, + Network: N.NetworkICMP, + Source: M.SocksaddrFrom(session.Source, 0), + Destination: destination, + OriginDestination: destination, + }, routeContext, timeout, false) + if err != nil { + switch { + case rule.IsBypassed(err): + err = nil + case rule.IsRejected(err): + h.logger.Trace("reject ICMP connection from ", session.Source, " to ", session.Destination) + default: + h.logger.Warn(E.Cause(err, "link ICMP connection from ", session.Source, " to ", session.Destination)) + } + } + return routeDestination, err +} diff --git a/release/DEFAULT_BUILD_TAGS b/release/DEFAULT_BUILD_TAGS index 4374ea93b6..e06bc120e0 100644 --- a/release/DEFAULT_BUILD_TAGS +++ b/release/DEFAULT_BUILD_TAGS @@ -1 +1 @@ -with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,with_naive_outbound,badlinkname,tfogo_checklinkname0 \ No newline at end of file +with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,with_cloudflared,with_naive_outbound,badlinkname,tfogo_checklinkname0 \ No newline at end of file diff --git a/release/DEFAULT_BUILD_TAGS_OTHERS b/release/DEFAULT_BUILD_TAGS_OTHERS index 814b53f063..a28e900e9d 100644 --- a/release/DEFAULT_BUILD_TAGS_OTHERS +++ b/release/DEFAULT_BUILD_TAGS_OTHERS @@ -1 +1 @@ -with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0 \ No newline at end of file +with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,with_cloudflared,badlinkname,tfogo_checklinkname0 \ No newline at end of file diff --git a/release/DEFAULT_BUILD_TAGS_WINDOWS b/release/DEFAULT_BUILD_TAGS_WINDOWS index 746827a736..af4fe41620 100644 --- a/release/DEFAULT_BUILD_TAGS_WINDOWS +++ b/release/DEFAULT_BUILD_TAGS_WINDOWS @@ -1 +1 @@ -with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,with_naive_outbound,with_purego,badlinkname,tfogo_checklinkname0 \ No newline at end of file +with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,with_cloudflared,with_naive_outbound,with_purego,badlinkname,tfogo_checklinkname0 \ No newline at end of file From 3828b4f30eda3455730c97629bbbea90b259323f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 10 Apr 2026 15:02:55 +0800 Subject: [PATCH 26/93] documentation: Fix missing update for `ip_version` and `query_type` --- docs/configuration/dns/rule.md | 38 +++++++++++++- docs/configuration/dns/rule.zh.md | 30 ++++++++++- docs/configuration/rule-set/headless-rule.md | 17 ++++++- .../rule-set/headless-rule.zh.md | 14 +++++- docs/migration.md | 50 +++++++++++++++++++ docs/migration.zh.md | 41 +++++++++++++++ 6 files changed, 186 insertions(+), 4 deletions(-) diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index e35f4d54d6..b0785b7783 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -12,7 +12,9 @@ icon: material/alert-decagram :material-plus: [response_answer](#response_answer) :material-plus: [response_ns](#response_ns) :material-plus: [response_extra](#response_extra) - :material-plus: [package_name_regex](#package_name_regex) + :material-plus: [package_name_regex](#package_name_regex) + :material-alert: [ip_version](#ip_version) + :material-alert: [query_type](#query_type) !!! quote "Changes in sing-box 1.13.0" @@ -243,12 +245,46 @@ Tags of [Inbound](/configuration/inbound/). #### ip_version +!!! quote "Changes in sing-box 1.14.0" + + This field now also applies when a DNS rule is matched from an internal + domain resolution that does not target a specific DNS server, such as a + [`resolve`](../../route/rule_action/#resolve) route rule action without a + `server` set. In earlier versions, only DNS queries received from a + client evaluated this field. See + [Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules) + for the full list. + + Setting this field makes the DNS rule incompatible in the same DNS + configuration with Legacy Address Filter Fields in DNS rules, the Legacy + `strategy` DNS rule action option, and the Legacy + `rule_set_ip_cidr_accept_empty` DNS rule item. To combine with + address-based filtering, use the [`evaluate`](../rule_action/#evaluate) + action and [`match_response`](#match_response). + 4 (A DNS query) or 6 (AAAA DNS query). Not limited if empty. #### query_type +!!! quote "Changes in sing-box 1.14.0" + + This field now also applies when a DNS rule is matched from an internal + domain resolution that does not target a specific DNS server, such as a + [`resolve`](../../route/rule_action/#resolve) route rule action without a + `server` set. In earlier versions, only DNS queries received from a + client evaluated this field. See + [Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules) + for the full list. + + Setting this field makes the DNS rule incompatible in the same DNS + configuration with Legacy Address Filter Fields in DNS rules, the Legacy + `strategy` DNS rule action option, and the Legacy + `rule_set_ip_cidr_accept_empty` DNS rule item. To combine with + address-based filtering, use the [`evaluate`](../rule_action/#evaluate) + action and [`match_response`](#match_response). + DNS query type. Values can be integers or type name strings. #### network diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index 4ed721ca5b..cc0a3037e0 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -12,7 +12,9 @@ icon: material/alert-decagram :material-plus: [response_answer](#response_answer) :material-plus: [response_ns](#response_ns) :material-plus: [response_extra](#response_extra) - :material-plus: [package_name_regex](#package_name_regex) + :material-plus: [package_name_regex](#package_name_regex) + :material-alert: [ip_version](#ip_version) + :material-alert: [query_type](#query_type) !!! quote "sing-box 1.13.0 中的更改" @@ -243,12 +245,38 @@ icon: material/alert-decagram #### ip_version +!!! quote "sing-box 1.14.0 中的更改" + + 此字段现在也会在 DNS 规则被未指定具体 DNS 服务器的内部域名解析匹配时生效, + 例如未设置 `server` 的 [`resolve`](../../route/rule_action/#resolve) 路由规则动作。 + 此前只有来自客户端的 DNS 查询才会评估此字段。完整列表参阅 + [迁移指南](/zh/migration/#dns-规则中的-ip_version-和-query_type-行为更改)。 + + 在 DNS 规则中设置此字段后,该 DNS 规则在同一 DNS 配置中不能与 + 旧版地址筛选字段 (DNS 规则)、旧版 DNS 规则动作 `strategy` 选项, + 或旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项共存。如需与 + 基于地址的筛选组合,请使用 [`evaluate`](../rule_action/#evaluate) 动作和 + [`match_response`](#match_response)。 + 4 (A DNS 查询) 或 6 (AAAA DNS 查询)。 默认不限制。 #### query_type +!!! quote "sing-box 1.14.0 中的更改" + + 此字段现在也会在 DNS 规则被未指定具体 DNS 服务器的内部域名解析匹配时生效, + 例如未设置 `server` 的 [`resolve`](../../route/rule_action/#resolve) 路由规则动作。 + 此前只有来自客户端的 DNS 查询才会评估此字段。完整列表参阅 + [迁移指南](/zh/migration/#dns-规则中的-ip_version-和-query_type-行为更改)。 + + 在 DNS 规则中设置此字段后,该 DNS 规则在同一 DNS 配置中不能与 + 旧版地址筛选字段 (DNS 规则)、旧版 DNS 规则动作 `strategy` 选项, + 或旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项共存。如需与 + 基于地址的筛选组合,请使用 [`evaluate`](../rule_action/#evaluate) 动作和 + [`match_response`](#match_response)。 + DNS 查询类型。值可以为整数或者类型名称字符串。 #### network diff --git a/docs/configuration/rule-set/headless-rule.md b/docs/configuration/rule-set/headless-rule.md index 23f2f58063..81a5e9a0a4 100644 --- a/docs/configuration/rule-set/headless-rule.md +++ b/docs/configuration/rule-set/headless-rule.md @@ -4,7 +4,8 @@ icon: material/new-box !!! quote "Changes in sing-box 1.14.0" - :material-plus: [package_name_regex](#package_name_regex) + :material-plus: [package_name_regex](#package_name_regex) + :material-alert: [query_type](#query_type) !!! quote "Changes in sing-box 1.13.0" @@ -132,6 +133,20 @@ icon: material/new-box #### query_type +!!! quote "Changes in sing-box 1.14.0" + + When a DNS rule references this rule-set, this field now also applies + when the DNS rule is matched from an internal domain resolution that + does not target a specific DNS server. In earlier versions, only DNS + queries received from a client evaluated this field. See + [Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules) + for the full list. + + When a DNS rule references a rule-set containing this field, the DNS + rule is incompatible in the same DNS configuration with Legacy Address + Filter Fields in DNS rules, the Legacy `strategy` DNS rule action + option, and the Legacy `rule_set_ip_cidr_accept_empty` DNS rule item. + DNS query type. Values can be integers or type name strings. #### network diff --git a/docs/configuration/rule-set/headless-rule.zh.md b/docs/configuration/rule-set/headless-rule.zh.md index c5ed636c91..ad78ffe449 100644 --- a/docs/configuration/rule-set/headless-rule.zh.md +++ b/docs/configuration/rule-set/headless-rule.zh.md @@ -4,7 +4,8 @@ icon: material/new-box !!! quote "sing-box 1.14.0 中的更改" - :material-plus: [package_name_regex](#package_name_regex) + :material-plus: [package_name_regex](#package_name_regex) + :material-alert: [query_type](#query_type) !!! quote "sing-box 1.13.0 中的更改" @@ -132,6 +133,17 @@ icon: material/new-box #### query_type +!!! quote "sing-box 1.14.0 中的更改" + + 当 DNS 规则引用此规则集时,此字段现在也会在 DNS 规则被未指定具体 + DNS 服务器的内部域名解析匹配时生效。此前只有来自客户端的 DNS 查询 + 才会评估此字段。完整列表参阅 + [迁移指南](/zh/migration/#dns-规则中的-ip_version-和-query_type-行为更改)。 + + 当 DNS 规则引用了包含此字段的规则集时,该 DNS 规则在同一 DNS 配置中 + 不能与旧版地址筛选字段 (DNS 规则)、旧版 DNS 规则动作 `strategy` 选项, + 或旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项共存。 + DNS 查询类型。值可以为整数或者类型名称字符串。 #### network diff --git a/docs/migration.md b/docs/migration.md index af434fc256..129f387faf 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -137,6 +137,56 @@ to fetch a DNS response, then match against it explicitly with `match_response`. } ``` +### ip_version and query_type behavior changes in DNS rules + +In sing-box 1.14.0, the behavior of +[`ip_version`](/configuration/dns/rule/#ip_version) and +[`query_type`](/configuration/dns/rule/#query_type) in DNS rules, together with +[`query_type`](/configuration/rule-set/headless-rule/#query_type) in referenced +rule-sets, changes in two ways. + +First, these fields now take effect on every DNS rule evaluation. In earlier +versions they were evaluated only for DNS queries received from a client +(for example, from a DNS inbound or intercepted by `tun`), and were silently +ignored when a DNS rule was matched from an internal domain resolution that +did not target a specific DNS server. Such internal resolutions include: + +- The [`resolve`](/configuration/route/rule_action/#resolve) route rule + action without a `server` set. +- ICMP traffic routed to a domain destination through a `direct` outbound. +- A [WireGuard](/configuration/endpoint/wireguard/) or + [Tailscale](/configuration/endpoint/tailscale/) endpoint used as an + outbound, when resolving its own destination address. +- A [SOCKS4](/configuration/outbound/socks/) outbound, which must resolve + the destination locally because the protocol has no in-protocol domain + support. +- The [DERP](/configuration/service/derp/) `bootstrap-dns` endpoint and the + [`resolved`](/configuration/service/resolved/) service (when resolving a + hostname or an SRV target). + +Resolutions that target a specific DNS server — via +[`domain_resolver`](/configuration/shared/dial/#domain_resolver) on a dial +field, [`default_domain_resolver`](/configuration/route/#default_domain_resolver) +in route options, or an explicit `server` on a DNS rule action or the +`resolve` route rule action — do not go through DNS rule matching and are +unaffected. + +Second, setting `ip_version` or `query_type` in a DNS rule, or referencing a +rule-set containing `query_type`, is no longer compatible in the same DNS +configuration with Legacy Address Filter Fields in DNS rules, the Legacy +`strategy` DNS rule action option, or the Legacy `rule_set_ip_cidr_accept_empty` +DNS rule item. Such a configuration will be rejected at startup. To combine +these fields with address-based filtering, migrate to response matching via +the [`evaluate`](/configuration/dns/rule_action/#evaluate) action and +[`match_response`](/configuration/dns/rule/#match_response), see +[Migrate address filter fields to response matching](#migrate-address-filter-fields-to-response-matching). + +!!! info "References" + + [DNS Rule](/configuration/dns/rule/) / + [Headless Rule](/configuration/rule-set/headless-rule/) / + [Route Rule Action](/configuration/route/rule_action/#resolve) + ## 1.12.0 ### Migrate to new DNS server formats diff --git a/docs/migration.zh.md b/docs/migration.zh.md index b0d26e41a9..995b0d9416 100644 --- a/docs/migration.zh.md +++ b/docs/migration.zh.md @@ -137,6 +137,47 @@ sing-box 1.14.0 新增字段参阅 [ACME](/zh/configuration/shared/certificate-p } ``` +### DNS 规则中的 ip_version 和 query_type 行为更改 + +在 sing-box 1.14.0 中,DNS 规则中的 +[`ip_version`](/zh/configuration/dns/rule/#ip_version) 和 +[`query_type`](/zh/configuration/dns/rule/#query_type),以及被引用规则集中的 +[`query_type`](/zh/configuration/rule-set/headless-rule/#query_type), +行为有两项更改。 + +其一,这些字段现在对每一次 DNS 规则评估都会生效。此前它们仅对来自客户端的 DNS 查询 +(例如来自 DNS 入站或被 `tun` 截获的查询)生效,当 DNS 规则被未指定具体 DNS 服务器的 +内部域名解析匹配时,会被静默忽略。此类内部解析包括: + +- 未设置 `server` 的 [`resolve`](/zh/configuration/route/rule_action/#resolve) 路由规则动作。 +- 通过 `direct` 出站路由到域名目标的 ICMP 流量。 +- 作为出站使用的 [WireGuard](/zh/configuration/endpoint/wireguard/) 或 + [Tailscale](/zh/configuration/endpoint/tailscale/) 端点在解析自身目标地址时。 +- [SOCKS4](/zh/configuration/outbound/socks/) 出站,因为协议本身不支持域名, + 必须在本地解析目标。 +- [DERP](/zh/configuration/service/derp/) 的 `bootstrap-dns` 端点,以及 + [`resolved`](/zh/configuration/service/resolved/) 服务在解析主机名或 SRV 目标时。 + +通过拨号字段中的 +[`domain_resolver`](/zh/configuration/shared/dial/#domain_resolver)、 +路由选项中的 [`default_domain_resolver`](/zh/configuration/route/#default_domain_resolver), +或 DNS 规则动作与 `resolve` 路由规则动作上显式的 `server` 指定具体 DNS 服务器的 +解析,不会经过 DNS 规则匹配,不受此次更改影响。 + +其二,设置了 `ip_version` 或 `query_type` 的 DNS 规则,或引用了包含 `query_type` 的 +规则集的 DNS 规则,在同一 DNS 配置中不再能与旧版地址筛选字段 (DNS 规则)、旧版 +DNS 规则动作 `strategy` 选项,或旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项共存。 +此类配置将在启动时被拒绝。如需将这些字段与基于地址的筛选组合,请通过 +[`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作和 +[`match_response`](/zh/configuration/dns/rule/#match_response) 迁移到响应匹配, +参阅 [迁移地址筛选字段到响应匹配](#迁移地址筛选字段到响应匹配)。 + +!!! info "参考" + + [DNS 规则](/zh/configuration/dns/rule/) / + [Headless 规则](/zh/configuration/rule-set/headless-rule/) / + [路由规则动作](/zh/configuration/route/rule_action/#resolve) + ## 1.12.0 ### 迁移到新的 DNS 服务器格式 From 104c7ae24d50d8a785a633606c20d08d40ed3736 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 10 Apr 2026 20:59:59 +0800 Subject: [PATCH 27/93] Fix stun test --- common/stun/stun.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/common/stun/stun.go b/common/stun/stun.go index b4c2313f02..a4bb9d5cc8 100644 --- a/common/stun/stun.go +++ b/common/stun/stun.go @@ -9,6 +9,8 @@ import ( "net/netip" "time" + "github.com/sagernet/sing/common/bufio" + "github.com/sagernet/sing/common/bufio/deadline" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" @@ -431,6 +433,9 @@ func Run(options Options) (*Result, error) { defer func() { _ = packetConn.Close() }() + if deadline.NeedAdditionalReadDeadline(packetConn) { + packetConn = deadline.NewPacketConn(bufio.NewPacketConn(packetConn)) + } select { case <-ctx.Done(): From 8fb019164edcd1cb110db44915d72608eeef70a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 10 Apr 2026 23:17:56 +0800 Subject: [PATCH 28/93] Fix darwin cgo DNS again --- dns/transport/local/local_darwin_cgo.go | 147 +++++++++++++++++++----- 1 file changed, 117 insertions(+), 30 deletions(-) diff --git a/dns/transport/local/local_darwin_cgo.go b/dns/transport/local/local_darwin_cgo.go index 00f5599548..318c38f387 100644 --- a/dns/transport/local/local_darwin_cgo.go +++ b/dns/transport/local/local_darwin_cgo.go @@ -4,8 +4,24 @@ package local /* #include +#include #include -#include + +static void *cgo_dns_open_super() { + return (void *)dns_open(NULL); +} + +static void cgo_dns_close(void *opaque) { + if (opaque != NULL) dns_free((dns_handle_t)opaque); +} + +static int cgo_dns_search(void *opaque, const char *name, int class, int type, + unsigned char *answer, int anslen) { + dns_handle_t handle = (dns_handle_t)opaque; + struct sockaddr_storage from; + uint32_t fromlen = sizeof(from); + return dns_search(handle, name, class, type, (char *)answer, anslen, (struct sockaddr *)&from, &fromlen); +} static void *cgo_res_init() { res_state state = calloc(1, sizeof(struct __res_state)); @@ -52,7 +68,59 @@ import ( mDNS "github.com/miekg/dns" ) -func resolvSearch(name string, class, qtype int, timeoutSeconds int) (*mDNS.Msg, error) { +const ( + darwinResolverHostNotFound = 1 + darwinResolverTryAgain = 2 + darwinResolverNoRecovery = 3 + darwinResolverNoData = 4 + + darwinResolverMaxPacketSize = 65535 +) + +var errDarwinNeedLargerBuffer = errors.New("darwin resolver response truncated") + +func darwinLookupSystemDNS(name string, class, qtype, timeoutSeconds int) (*mDNS.Msg, error) { + response, err := darwinSearchWithSystemRouting(name, class, qtype) + if err == nil { + return response, nil + } + fallbackResponse, fallbackErr := darwinSearchWithResolv(name, class, qtype, timeoutSeconds) + if fallbackErr == nil || fallbackResponse != nil { + return fallbackResponse, fallbackErr + } + return nil, E.Errors( + E.Cause(err, "dns_search"), + E.Cause(fallbackErr, "res_nsearch"), + ) +} + +func darwinSearchWithSystemRouting(name string, class, qtype int) (*mDNS.Msg, error) { + handle := C.cgo_dns_open_super() + if handle == nil { + return nil, E.New("dns_open failed") + } + defer C.cgo_dns_close(handle) + + cName := C.CString(name) + defer C.free(unsafe.Pointer(cName)) + + bufSize := 1232 + for { + answer := make([]byte, bufSize) + n := C.cgo_dns_search(handle, cName, C.int(class), C.int(qtype), + (*C.uchar)(unsafe.Pointer(&answer[0])), C.int(len(answer))) + if n <= 0 { + return nil, E.New("dns_search failed for ", name) + } + if int(n) > bufSize { + bufSize = int(n) + continue + } + return unpackDarwinResolverMessage(answer[:int(n)], "dns_search") + } +} + +func darwinSearchWithResolv(name string, class, qtype int, timeoutSeconds int) (*mDNS.Msg, error) { state := C.cgo_res_init() if state == nil { return nil, E.New("res_ninit failed") @@ -61,6 +129,7 @@ func resolvSearch(name string, class, qtype int, timeoutSeconds int) (*mDNS.Msg, cName := C.CString(name) defer C.free(unsafe.Pointer(cName)) + bufSize := 1232 for { answer := make([]byte, bufSize) @@ -74,37 +143,55 @@ func resolvSearch(name string, class, qtype int, timeoutSeconds int) (*mDNS.Msg, bufSize = int(n) continue } - var response mDNS.Msg - err := response.Unpack(answer[:int(n)]) - if err != nil { - return nil, E.Cause(err, "unpack res_nsearch response") - } - return &response, nil + return unpackDarwinResolverMessage(answer[:int(n)], "res_nsearch") } - var response mDNS.Msg - _ = response.Unpack(answer[:bufSize]) - if response.Response { - if response.Truncated && bufSize < 65535 { - bufSize *= 2 - if bufSize > 65535 { - bufSize = 65535 - } - continue + response, err := handleDarwinResolvFailure(name, answer, int(hErrno)) + if err == nil { + return response, nil + } + if errors.Is(err, errDarwinNeedLargerBuffer) && bufSize < darwinResolverMaxPacketSize { + bufSize *= 2 + if bufSize > darwinResolverMaxPacketSize { + bufSize = darwinResolverMaxPacketSize } - return &response, nil + continue } - switch hErrno { - case C.HOST_NOT_FOUND: - return nil, dns.RcodeNameError - case C.TRY_AGAIN: - return nil, dns.RcodeNameError - case C.NO_RECOVERY: - return nil, dns.RcodeServerFailure - case C.NO_DATA: - return nil, dns.RcodeSuccess - default: - return nil, E.New("res_nsearch: unknown error ", int(hErrno), " for ", name) + return nil, err + } +} + +func unpackDarwinResolverMessage(packet []byte, source string) (*mDNS.Msg, error) { + var response mDNS.Msg + err := response.Unpack(packet) + if err != nil { + return nil, E.Cause(err, "unpack ", source, " response") + } + return &response, nil +} + +func handleDarwinResolvFailure(name string, answer []byte, hErrno int) (*mDNS.Msg, error) { + response, err := unpackDarwinResolverMessage(answer, "res_nsearch failure") + if err == nil && response.Response { + if response.Truncated && len(answer) < darwinResolverMaxPacketSize { + return nil, errDarwinNeedLargerBuffer } + return response, nil + } + return nil, darwinResolverHErrno(name, hErrno) +} + +func darwinResolverHErrno(name string, hErrno int) error { + switch hErrno { + case darwinResolverHostNotFound: + return dns.RcodeNameError + case darwinResolverTryAgain: + return dns.RcodeServerFailure + case darwinResolverNoRecovery: + return dns.RcodeServerFailure + case darwinResolverNoData: + return dns.RcodeSuccess + default: + return E.New("res_nsearch: unknown error ", hErrno, " for ", name) } } @@ -141,7 +228,7 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, } resultCh := make(chan resolvResult, 1) go func() { - response, err := resolvSearch(name, int(question.Qclass), int(question.Qtype), timeoutSeconds) + response, err := darwinLookupSystemDNS(name, int(question.Qclass), int(question.Qtype), timeoutSeconds) resultCh <- resolvResult{response, err} }() var result resolvResult From e6f5c2438b29e8b31bbce805d06ce16742e0a854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 11 Apr 2026 11:48:44 +0800 Subject: [PATCH 29/93] Fix tailscale error --- experimental/libbox/command_client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experimental/libbox/command_client.go b/experimental/libbox/command_client.go index a8d18495b6..d4347e109e 100644 --- a/experimental/libbox/command_client.go +++ b/experimental/libbox/command_client.go @@ -769,7 +769,7 @@ func (c *CommandClient) SubscribeTailscaleStatus(handler TailscaleStatusHandler) for { event, recvErr := stream.Recv() if recvErr != nil { - if status.Code(recvErr) == codes.NotFound { + if status.Code(recvErr) == codes.NotFound || status.Code(recvErr) == codes.Unavailable { return nil } recvErr = E.Cause(recvErr, "tailscale status recv") From 0319b22c76dd0270913cc25257761ed572d638fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 11 Apr 2026 12:10:52 +0800 Subject: [PATCH 30/93] Add optimistic DNS cache --- adapter/dns.go | 32 +- adapter/experimental.go | 6 + box.go | 7 +- common/dialer/dialer.go | 11 +- dns/client.go | 550 +++++++++--------- dns/client_log.go | 13 + dns/router.go | 64 +- docs/configuration/dns/index.md | 42 ++ docs/configuration/dns/index.zh.md | 42 ++ docs/configuration/dns/rule_action.md | 18 +- docs/configuration/dns/rule_action.zh.md | 18 +- docs/configuration/experimental/cache-file.md | 18 +- .../experimental/cache-file.zh.md | 18 +- docs/configuration/route/rule_action.md | 11 + docs/configuration/route/rule_action.zh.md | 11 + docs/deprecated.md | 15 + docs/deprecated.zh.md | 15 + docs/migration.md | 62 ++ docs/migration.zh.md | 62 ++ experimental/cachefile/cache.go | 96 ++- experimental/cachefile/dns_cache.go | 299 ++++++++++ experimental/cachefile/rdrc.go | 4 +- experimental/deprecated/constants.go | 20 + option/dns.go | 23 + option/experimental.go | 1 + option/outbound.go | 12 +- option/rule_action.go | 31 +- route/network.go | 9 +- route/route.go | 11 +- route/rule/rule_action.go | 67 ++- 30 files changed, 1219 insertions(+), 369 deletions(-) create mode 100644 experimental/cachefile/dns_cache.go diff --git a/adapter/dns.go b/adapter/dns.go index f527e7ccd3..7545f16633 100644 --- a/adapter/dns.go +++ b/adapter/dns.go @@ -3,6 +3,7 @@ package adapter import ( "context" "net/netip" + "time" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" @@ -31,12 +32,13 @@ type DNSClient interface { } type DNSQueryOptions struct { - Transport DNSTransport - Strategy C.DomainStrategy - LookupStrategy C.DomainStrategy - DisableCache bool - RewriteTTL *uint32 - ClientSubnet netip.Prefix + Transport DNSTransport + Strategy C.DomainStrategy + LookupStrategy C.DomainStrategy + DisableCache bool + DisableOptimisticCache bool + RewriteTTL *uint32 + ClientSubnet netip.Prefix } func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) { @@ -49,11 +51,12 @@ func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptio return nil, E.New("domain resolver not found: " + options.Server) } return &DNSQueryOptions{ - Transport: transport, - Strategy: C.DomainStrategy(options.Strategy), - DisableCache: options.DisableCache, - RewriteTTL: options.RewriteTTL, - ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}), + Transport: transport, + Strategy: C.DomainStrategy(options.Strategy), + DisableCache: options.DisableCache, + DisableOptimisticCache: options.DisableOptimisticCache, + RewriteTTL: options.RewriteTTL, + ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}), }, nil } @@ -63,6 +66,13 @@ type RDRCStore interface { SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger) } +type DNSCacheStore interface { + LoadDNSCache(transportName string, qName string, qType uint16) (rawMessage []byte, expireAt time.Time, loaded bool) + SaveDNSCache(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time) error + SaveDNSCacheAsync(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time, logger logger.Logger) + ClearDNSCache() error +} + type DNSTransport interface { Lifecycle Type() string diff --git a/adapter/experimental.go b/adapter/experimental.go index 1bd8d2d928..49fd2bd317 100644 --- a/adapter/experimental.go +++ b/adapter/experimental.go @@ -47,6 +47,12 @@ type CacheFile interface { StoreRDRC() bool RDRCStore + StoreDNS() bool + DNSCacheStore + + SetDisableExpire(disableExpire bool) + SetOptimisticTimeout(timeout time.Duration) + LoadMode() string StoreMode(mode string) error LoadSelected(group string) string diff --git a/box.go b/box.go index 619b05bba8..c5b3fc68c2 100644 --- a/box.go +++ b/box.go @@ -196,7 +196,10 @@ func New(options Options) (*Box, error) { service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager) service.MustRegister[adapter.ServiceManager](ctx, serviceManager) service.MustRegister[adapter.CertificateProviderManager](ctx, certificateProviderManager) - dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions) + dnsRouter, err := dns.NewRouter(ctx, logFactory, dnsOptions) + if err != nil { + return nil, E.Cause(err, "initialize DNS router") + } service.MustRegister[adapter.DNSRouter](ctx, dnsRouter) service.MustRegister[adapter.DNSRuleSetUpdateValidator](ctx, dnsRouter) networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions) @@ -372,7 +375,7 @@ func New(options Options) (*Box, error) { } } if needCacheFile { - cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile)) + cacheFile := cachefile.New(ctx, logFactory.NewLogger("cache-file"), common.PtrValueOrDefault(experimentalOptions.CacheFile)) service.MustRegister[adapter.CacheFile](ctx, cacheFile) internalServices = append(internalServices, cacheFile) } diff --git a/common/dialer/dialer.go b/common/dialer/dialer.go index 2ba559f9e0..ca6f905fe0 100644 --- a/common/dialer/dialer.go +++ b/common/dialer/dialer.go @@ -87,11 +87,12 @@ func NewWithOptions(options Options) (N.Dialer, error) { } server = dialOptions.DomainResolver.Server dnsQueryOptions = adapter.DNSQueryOptions{ - Transport: transport, - Strategy: strategy, - DisableCache: dialOptions.DomainResolver.DisableCache, - RewriteTTL: dialOptions.DomainResolver.RewriteTTL, - ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}), + Transport: transport, + Strategy: strategy, + DisableCache: dialOptions.DomainResolver.DisableCache, + DisableOptimisticCache: dialOptions.DomainResolver.DisableOptimisticCache, + RewriteTTL: dialOptions.DomainResolver.RewriteTTL, + ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}), } resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay) } else if options.DirectResolver { diff --git a/dns/client.go b/dns/client.go index 08468b352a..37ba98a84f 100644 --- a/dns/client.go +++ b/dns/client.go @@ -30,59 +30,63 @@ var ( var _ adapter.DNSClient = (*Client)(nil) type Client struct { - timeout time.Duration - disableCache bool - disableExpire bool - independentCache bool - clientSubnet netip.Prefix - rdrc adapter.RDRCStore - initRDRCFunc func() adapter.RDRCStore - logger logger.ContextLogger - cache freelru.Cache[dns.Question, *dns.Msg] - cacheLock compatible.Map[dns.Question, chan struct{}] - transportCache freelru.Cache[transportCacheKey, *dns.Msg] - transportCacheLock compatible.Map[dns.Question, chan struct{}] + ctx context.Context + timeout time.Duration + disableCache bool + disableExpire bool + optimisticTimeout time.Duration + cacheCapacity uint32 + clientSubnet netip.Prefix + rdrc adapter.RDRCStore + initRDRCFunc func() adapter.RDRCStore + dnsCache adapter.DNSCacheStore + initDNSCacheFunc func() adapter.DNSCacheStore + logger logger.ContextLogger + cache freelru.Cache[dnsCacheKey, *dns.Msg] + cacheLock compatible.Map[dnsCacheKey, chan struct{}] + backgroundRefresh compatible.Map[dnsCacheKey, struct{}] } type ClientOptions struct { - Timeout time.Duration - DisableCache bool - DisableExpire bool - IndependentCache bool - CacheCapacity uint32 - ClientSubnet netip.Prefix - RDRC func() adapter.RDRCStore - Logger logger.ContextLogger + Context context.Context + Timeout time.Duration + DisableCache bool + DisableExpire bool + OptimisticTimeout time.Duration + CacheCapacity uint32 + ClientSubnet netip.Prefix + RDRC func() adapter.RDRCStore + DNSCache func() adapter.DNSCacheStore + Logger logger.ContextLogger } func NewClient(options ClientOptions) *Client { + cacheCapacity := options.CacheCapacity + if cacheCapacity < 1024 { + cacheCapacity = 1024 + } client := &Client{ - timeout: options.Timeout, - disableCache: options.DisableCache, - disableExpire: options.DisableExpire, - independentCache: options.IndependentCache, - clientSubnet: options.ClientSubnet, - initRDRCFunc: options.RDRC, - logger: options.Logger, + ctx: options.Context, + timeout: options.Timeout, + disableCache: options.DisableCache, + disableExpire: options.DisableExpire, + optimisticTimeout: options.OptimisticTimeout, + cacheCapacity: cacheCapacity, + clientSubnet: options.ClientSubnet, + initRDRCFunc: options.RDRC, + initDNSCacheFunc: options.DNSCache, + logger: options.Logger, } if client.timeout == 0 { client.timeout = C.DNSTimeout } - cacheCapacity := options.CacheCapacity - if cacheCapacity < 1024 { - cacheCapacity = 1024 - } - if !client.disableCache { - if !client.independentCache { - client.cache = common.Must1(freelru.NewSharded[dns.Question, *dns.Msg](cacheCapacity, maphash.NewHasher[dns.Question]().Hash32)) - } else { - client.transportCache = common.Must1(freelru.NewSharded[transportCacheKey, *dns.Msg](cacheCapacity, maphash.NewHasher[transportCacheKey]().Hash32)) - } + if !client.disableCache && client.initDNSCacheFunc == nil { + client.initializeMemoryCache() } return client } -type transportCacheKey struct { +type dnsCacheKey struct { dns.Question transportTag string } @@ -91,6 +95,19 @@ func (c *Client) Start() { if c.initRDRCFunc != nil { c.rdrc = c.initRDRCFunc() } + if c.initDNSCacheFunc != nil { + c.dnsCache = c.initDNSCacheFunc() + } + if c.dnsCache == nil { + c.initializeMemoryCache() + } +} + +func (c *Client) initializeMemoryCache() { + if c.disableCache || c.cache != nil { + return + } + c.cache = common.Must1(freelru.NewSharded[dnsCacheKey, *dns.Msg](c.cacheCapacity, maphash.NewHasher[dnsCacheKey]().Hash32)) } func extractNegativeTTL(response *dns.Msg) (uint32, bool) { @@ -107,6 +124,37 @@ func extractNegativeTTL(response *dns.Msg) (uint32, bool) { return 0, false } +func computeTimeToLive(response *dns.Msg) uint32 { + var timeToLive uint32 + if len(response.Answer) == 0 { + if soaTTL, hasSOA := extractNegativeTTL(response); hasSOA { + return soaTTL + } + } + for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { + for _, record := range recordList { + if record.Header().Rrtype == dns.TypeOPT { + continue + } + if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive { + timeToLive = record.Header().Ttl + } + } + } + return timeToLive +} + +func normalizeTTL(response *dns.Msg, timeToLive uint32) { + for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { + for _, record := range recordList { + if record.Header().Rrtype == dns.TypeOPT { + continue + } + record.Header().Ttl = timeToLive + } + } +} + func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) (*dns.Msg, error) { if len(message.Question) == 0 { if c.logger != nil { @@ -121,13 +169,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m } return FixedResponseStatus(message, dns.RcodeSuccess), nil } - clientSubnet := options.ClientSubnet - if !clientSubnet.IsValid() { - clientSubnet = c.clientSubnet - } - if clientSubnet.IsValid() { - message = SetClientSubnet(message, clientSubnet) - } + message = c.prepareExchangeMessage(message, options) isSimpleRequest := len(message.Question) == 1 && len(message.Ns) == 0 && @@ -139,40 +181,32 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m !options.ClientSubnet.IsValid() disableCache := !isSimpleRequest || c.disableCache || options.DisableCache if !disableCache { - if c.cache != nil { - cond, loaded := c.cacheLock.LoadOrStore(question, make(chan struct{})) - if loaded { - select { - case <-cond: - case <-ctx.Done(): - return nil, ctx.Err() - } - } else { - defer func() { - c.cacheLock.Delete(question) - close(cond) - }() - } - } else if c.transportCache != nil { - cond, loaded := c.transportCacheLock.LoadOrStore(question, make(chan struct{})) - if loaded { - select { - case <-cond: - case <-ctx.Done(): - return nil, ctx.Err() - } - } else { - defer func() { - c.transportCacheLock.Delete(question) - close(cond) - }() + cacheKey := dnsCacheKey{Question: question, transportTag: transport.Tag()} + cond, loaded := c.cacheLock.LoadOrStore(cacheKey, make(chan struct{})) + if loaded { + select { + case <-cond: + case <-ctx.Done(): + return nil, ctx.Err() } + } else { + defer func() { + c.cacheLock.Delete(cacheKey) + close(cond) + }() } - response, ttl := c.loadResponse(question, transport) + response, ttl, isStale := c.loadResponse(question, transport) if response != nil { - logCachedResponse(c.logger, ctx, response, ttl) - response.Id = message.Id - return response, nil + if isStale && !options.DisableOptimisticCache { + c.backgroundRefreshDNS(transport, question, message.Copy(), options, responseChecker) + logOptimisticResponse(c.logger, ctx, response) + response.Id = message.Id + return response, nil + } else if !isStale { + logCachedResponse(c.logger, ctx, response, ttl) + response.Id = message.Id + return response, nil + } } } @@ -188,52 +222,10 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m return nil, ErrResponseRejectedCached } } - ctx, cancel := context.WithTimeout(ctx, c.timeout) - response, err := transport.Exchange(ctx, message) - cancel() + response, err := c.exchangeToTransport(ctx, transport, message) if err != nil { - var rcodeError RcodeError - if errors.As(err, &rcodeError) { - response = FixedResponseStatus(message, int(rcodeError)) - } else { - return nil, err - } + return nil, err } - /*if question.Qtype == dns.TypeA || question.Qtype == dns.TypeAAAA { - validResponse := response - loop: - for { - var ( - addresses int - queryCNAME string - ) - for _, rawRR := range validResponse.Answer { - switch rr := rawRR.(type) { - case *dns.A: - break loop - case *dns.AAAA: - break loop - case *dns.CNAME: - queryCNAME = rr.Target - } - } - if queryCNAME == "" { - break - } - exMessage := *message - exMessage.Question = []dns.Question{{ - Name: queryCNAME, - Qtype: question.Qtype, - }} - validResponse, err = c.Exchange(ctx, transport, &exMessage, options, responseChecker) - if err != nil { - return nil, err - } - } - if validResponse != response { - response.Answer = append(response.Answer, validResponse.Answer...) - } - }*/ disableCache = disableCache || (response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError) if responseChecker != nil { var rejected bool @@ -250,54 +242,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m return response, ErrResponseRejected } } - if question.Qtype == dns.TypeHTTPS { - if options.Strategy == C.DomainStrategyIPv4Only || options.Strategy == C.DomainStrategyIPv6Only { - for _, rr := range response.Answer { - https, isHTTPS := rr.(*dns.HTTPS) - if !isHTTPS { - continue - } - content := https.SVCB - content.Value = common.Filter(content.Value, func(it dns.SVCBKeyValue) bool { - if options.Strategy == C.DomainStrategyIPv4Only { - return it.Key() != dns.SVCB_IPV6HINT - } else { - return it.Key() != dns.SVCB_IPV4HINT - } - }) - https.SVCB = content - } - } - } - var timeToLive uint32 - if len(response.Answer) == 0 { - if soaTTL, hasSOA := extractNegativeTTL(response); hasSOA { - timeToLive = soaTTL - } - } - if timeToLive == 0 { - for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { - for _, record := range recordList { - if record.Header().Rrtype == dns.TypeOPT { - continue - } - if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive { - timeToLive = record.Header().Ttl - } - } - } - } - if options.RewriteTTL != nil { - timeToLive = *options.RewriteTTL - } - for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { - for _, record := range recordList { - if record.Header().Rrtype == dns.TypeOPT { - continue - } - record.Header().Ttl = timeToLive - } - } + timeToLive := applyResponseOptions(question, response, options) if !disableCache { c.storeCache(transport, question, response, timeToLive) } @@ -363,8 +308,12 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom func (c *Client) ClearCache() { if c.cache != nil { c.cache.Purge() - } else if c.transportCache != nil { - c.transportCache.Purge() + } + if c.dnsCache != nil { + err := c.dnsCache.ClearDNSCache() + if err != nil && c.logger != nil { + c.logger.Warn("clear DNS cache: ", err) + } } } @@ -380,24 +329,22 @@ func (c *Client) storeCache(transport adapter.DNSTransport, question dns.Questio if timeToLive == 0 { return } - if c.disableExpire { - if !c.independentCache { - c.cache.Add(question, message.Copy()) - } else { - c.transportCache.Add(transportCacheKey{ - Question: question, - transportTag: transport.Tag(), - }, message.Copy()) + if c.dnsCache != nil { + packed, err := message.Pack() + if err == nil { + expireAt := time.Now().Add(time.Second * time.Duration(timeToLive)) + c.dnsCache.SaveDNSCacheAsync(transport.Tag(), question.Name, question.Qtype, packed, expireAt, c.logger) } + return + } + if c.cache == nil { + return + } + key := dnsCacheKey{Question: question, transportTag: transport.Tag()} + if c.disableExpire { + c.cache.Add(key, message.Copy()) } else { - if !c.independentCache { - c.cache.AddWithLifetime(question, message.Copy(), time.Second*time.Duration(timeToLive)) - } else { - c.transportCache.AddWithLifetime(transportCacheKey{ - Question: question, - transportTag: transport.Tag(), - }, message.Copy(), time.Second*time.Duration(timeToLive)) - } + c.cache.AddWithLifetime(key, message.Copy(), time.Second*time.Duration(timeToLive)) } } @@ -407,19 +354,19 @@ func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTran Qtype: qType, Qclass: dns.ClassINET, } - disableCache := c.disableCache || options.DisableCache - if !disableCache { - cachedAddresses, err := c.questionCache(question, transport) - if err != ErrNotCached { - return cachedAddresses, err - } - } message := dns.Msg{ MsgHdr: dns.MsgHdr{ RecursionDesired: true, }, Question: []dns.Question{question}, } + disableCache := c.disableCache || options.DisableCache + if !disableCache { + cachedAddresses, err := c.questionCache(ctx, transport, &message, options, responseChecker) + if err != ErrNotCached { + return cachedAddresses, err + } + } response, err := c.Exchange(ctx, transport, &message, options, responseChecker) if err != nil { return nil, err @@ -430,98 +377,177 @@ func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTran return MessageToAddresses(response), nil } -func (c *Client) questionCache(question dns.Question, transport adapter.DNSTransport) ([]netip.Addr, error) { - response, _ := c.loadResponse(question, transport) +func (c *Client) questionCache(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) { + question := message.Question[0] + response, _, isStale := c.loadResponse(question, transport) if response == nil { return nil, ErrNotCached } + if isStale { + if options.DisableOptimisticCache { + return nil, ErrNotCached + } + c.backgroundRefreshDNS(transport, question, c.prepareExchangeMessage(message.Copy(), options), options, responseChecker) + logOptimisticResponse(c.logger, ctx, response) + } if response.Rcode != dns.RcodeSuccess { return nil, RcodeError(response.Rcode) } return MessageToAddresses(response), nil } -func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int) { - var ( - response *dns.Msg - loaded bool - ) +func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int, bool) { + if c.dnsCache != nil { + return c.loadPersistentResponse(question, transport) + } + if c.cache == nil { + return nil, 0, false + } + key := dnsCacheKey{Question: question, transportTag: transport.Tag()} if c.disableExpire { - if !c.independentCache { - response, loaded = c.cache.Get(question) - } else { - response, loaded = c.transportCache.Get(transportCacheKey{ - Question: question, - transportTag: transport.Tag(), - }) - } + response, loaded := c.cache.Get(key) if !loaded { - return nil, 0 + return nil, 0, false } - return response.Copy(), 0 - } else { - var expireAt time.Time - if !c.independentCache { - response, expireAt, loaded = c.cache.GetWithLifetime(question) - } else { - response, expireAt, loaded = c.transportCache.GetWithLifetime(transportCacheKey{ - Question: question, - transportTag: transport.Tag(), - }) + return response.Copy(), 0, false + } + response, expireAt, loaded := c.cache.GetWithLifetimeNoExpire(key) + if !loaded { + return nil, 0, false + } + timeNow := time.Now() + if timeNow.After(expireAt) { + if c.optimisticTimeout > 0 && timeNow.Before(expireAt.Add(c.optimisticTimeout)) { + response = response.Copy() + normalizeTTL(response, 1) + return response, 0, true } - if !loaded { - return nil, 0 + c.cache.Remove(key) + return nil, 0, false + } + nowTTL := int(expireAt.Sub(timeNow).Seconds()) + if nowTTL < 0 { + nowTTL = 0 + } + response = response.Copy() + normalizeTTL(response, uint32(nowTTL)) + return response, nowTTL, false +} + +func (c *Client) loadPersistentResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int, bool) { + rawMessage, expireAt, loaded := c.dnsCache.LoadDNSCache(transport.Tag(), question.Name, question.Qtype) + if !loaded { + return nil, 0, false + } + response := new(dns.Msg) + err := response.Unpack(rawMessage) + if err != nil { + return nil, 0, false + } + if c.disableExpire { + return response, 0, false + } + timeNow := time.Now() + if timeNow.After(expireAt) { + if c.optimisticTimeout > 0 && timeNow.Before(expireAt.Add(c.optimisticTimeout)) { + normalizeTTL(response, 1) + return response, 0, true } - timeNow := time.Now() - if timeNow.After(expireAt) { - if !c.independentCache { - c.cache.Remove(question) - } else { - c.transportCache.Remove(transportCacheKey{ - Question: question, - transportTag: transport.Tag(), - }) + return nil, 0, false + } + nowTTL := int(expireAt.Sub(timeNow).Seconds()) + if nowTTL < 0 { + nowTTL = 0 + } + normalizeTTL(response, uint32(nowTTL)) + return response, nowTTL, false +} + +func applyResponseOptions(question dns.Question, response *dns.Msg, options adapter.DNSQueryOptions) uint32 { + if question.Qtype == dns.TypeHTTPS && (options.Strategy == C.DomainStrategyIPv4Only || options.Strategy == C.DomainStrategyIPv6Only) { + for _, rr := range response.Answer { + https, isHTTPS := rr.(*dns.HTTPS) + if !isHTTPS { + continue } - return nil, 0 - } - var originTTL int - for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { - for _, record := range recordList { - if record.Header().Rrtype == dns.TypeOPT { - continue - } - if originTTL == 0 || record.Header().Ttl > 0 && int(record.Header().Ttl) < originTTL { - originTTL = int(record.Header().Ttl) + content := https.SVCB + content.Value = common.Filter(content.Value, func(it dns.SVCBKeyValue) bool { + if options.Strategy == C.DomainStrategyIPv4Only { + return it.Key() != dns.SVCB_IPV6HINT } - } + return it.Key() != dns.SVCB_IPV4HINT + }) + https.SVCB = content } - nowTTL := int(expireAt.Sub(timeNow).Seconds()) - if nowTTL < 0 { - nowTTL = 0 + } + timeToLive := computeTimeToLive(response) + if options.RewriteTTL != nil { + timeToLive = *options.RewriteTTL + } + normalizeTTL(response, timeToLive) + return timeToLive +} + +func (c *Client) backgroundRefreshDNS(transport adapter.DNSTransport, question dns.Question, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) { + key := dnsCacheKey{Question: question, transportTag: transport.Tag()} + _, loaded := c.backgroundRefresh.LoadOrStore(key, struct{}{}) + if loaded { + return + } + go func() { + defer c.backgroundRefresh.Delete(key) + ctx := contextWithTransportTag(c.ctx, transport.Tag()) + response, err := c.exchangeToTransport(ctx, transport, message) + if err != nil { + if c.logger != nil { + c.logger.Debug("optimistic refresh failed for ", FqdnToDomain(question.Name), ": ", err) + } + return } - response = response.Copy() - if originTTL > 0 { - duration := uint32(originTTL - nowTTL) - for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { - for _, record := range recordList { - if record.Header().Rrtype == dns.TypeOPT { - continue - } - record.Header().Ttl = record.Header().Ttl - duration - } + if responseChecker != nil { + var rejected bool + if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError { + rejected = true + } else { + rejected = !responseChecker(response) } - } else { - for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { - for _, record := range recordList { - if record.Header().Rrtype == dns.TypeOPT { - continue - } - record.Header().Ttl = uint32(nowTTL) + if rejected { + if c.rdrc != nil { + c.rdrc.SaveRDRCAsync(transport.Tag(), question.Name, question.Qtype, c.logger) } + return } + } else if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError { + return } - return response, nowTTL + timeToLive := applyResponseOptions(question, response, options) + c.storeCache(transport, question, response, timeToLive) + }() +} + +func (c *Client) prepareExchangeMessage(message *dns.Msg, options adapter.DNSQueryOptions) *dns.Msg { + clientSubnet := options.ClientSubnet + if !clientSubnet.IsValid() { + clientSubnet = c.clientSubnet + } + if clientSubnet.IsValid() { + message = SetClientSubnet(message, clientSubnet) + } + return message +} + +func (c *Client) exchangeToTransport(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg) (*dns.Msg, error) { + ctx, cancel := context.WithTimeout(ctx, c.timeout) + defer cancel() + response, err := transport.Exchange(ctx, message) + if err == nil { + return response, nil + } + var rcodeError RcodeError + if errors.As(err, &rcodeError) { + return FixedResponseStatus(message, int(rcodeError)), nil } + return nil, err } func MessageToAddresses(response *dns.Msg) []netip.Addr { diff --git a/dns/client_log.go b/dns/client_log.go index 67d0070841..129e273c4b 100644 --- a/dns/client_log.go +++ b/dns/client_log.go @@ -22,6 +22,19 @@ func logCachedResponse(logger logger.ContextLogger, ctx context.Context, respons } } +func logOptimisticResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg) { + if logger == nil || len(response.Question) == 0 { + return + } + domain := FqdnToDomain(response.Question[0].Name) + logger.DebugContext(ctx, "optimistic ", domain, " ", dns.RcodeToString[response.Rcode]) + for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { + for _, record := range recordList { + logger.InfoContext(ctx, "optimistic ", dns.Type(record.Header().Rrtype).String(), " ", FormatQuestion(record.String())) + } + } +} + func logExchangedResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg, ttl uint32) { if logger == nil || len(response.Question) == 0 { return diff --git a/dns/router.go b/dns/router.go index a14cecd0e7..b9fc8f9775 100644 --- a/dns/router.go +++ b/dns/router.go @@ -51,7 +51,7 @@ type Router struct { closing bool } -func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOptions) *Router { +func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOptions) (*Router, error) { router := &Router{ ctx: ctx, logger: logFactory.NewLogger("dns"), @@ -61,12 +61,30 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOp rules: make([]adapter.DNSRule, 0, len(options.Rules)), defaultDomainStrategy: C.DomainStrategy(options.Strategy), } + if options.DNSClientOptions.IndependentCache { + deprecated.Report(ctx, deprecated.OptionIndependentDNSCache) + } + var optimisticTimeout time.Duration + optimisticOptions := common.PtrValueOrDefault(options.DNSClientOptions.Optimistic) + if optimisticOptions.Enabled { + if options.DNSClientOptions.DisableCache { + return nil, E.New("`optimistic` is conflict with `disable_cache`") + } + if options.DNSClientOptions.DisableExpire { + return nil, E.New("`optimistic` is conflict with `disable_expire`") + } + optimisticTimeout = time.Duration(optimisticOptions.Timeout) + if optimisticTimeout == 0 { + optimisticTimeout = 3 * 24 * time.Hour + } + } router.client = NewClient(ClientOptions{ - DisableCache: options.DNSClientOptions.DisableCache, - DisableExpire: options.DNSClientOptions.DisableExpire, - IndependentCache: options.DNSClientOptions.IndependentCache, - CacheCapacity: options.DNSClientOptions.CacheCapacity, - ClientSubnet: options.DNSClientOptions.ClientSubnet.Build(netip.Prefix{}), + Context: ctx, + DisableCache: options.DNSClientOptions.DisableCache, + DisableExpire: options.DNSClientOptions.DisableExpire, + OptimisticTimeout: optimisticTimeout, + CacheCapacity: options.DNSClientOptions.CacheCapacity, + ClientSubnet: options.DNSClientOptions.ClientSubnet.Build(netip.Prefix{}), RDRC: func() adapter.RDRCStore { cacheFile := service.FromContext[adapter.CacheFile](ctx) if cacheFile == nil { @@ -77,12 +95,24 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOp } return cacheFile }, + DNSCache: func() adapter.DNSCacheStore { + cacheFile := service.FromContext[adapter.CacheFile](ctx) + if cacheFile == nil { + return nil + } + if !cacheFile.StoreDNS() { + return nil + } + cacheFile.SetDisableExpire(options.DNSClientOptions.DisableExpire) + cacheFile.SetOptimisticTimeout(optimisticTimeout) + return cacheFile + }, Logger: router.logger, }) if options.ReverseMapping { router.dnsReverseMapping = common.Must1(freelru.NewSharded[netip.Addr, string](1024, maphash.NewHasher[netip.Addr]().Hash32)) } - return router + return router, nil } func (r *Router) Initialize(rules []option.DNSRule) error { @@ -319,6 +349,9 @@ func (r *Router) applyDNSRouteOptions(options *adapter.DNSQueryOptions, routeOpt if routeOptions.DisableCache { options.DisableCache = true } + if routeOptions.DisableOptimisticCache { + options.DisableOptimisticCache = true + } if routeOptions.RewriteTTL != nil { options.RewriteTTL = routeOptions.RewriteTTL } @@ -907,7 +940,9 @@ func dnsRuleModeRequirementsInRule(router adapter.Router, rule option.DNSRule, m return dnsRuleModeRequirementsInDefaultRule(router, rule.DefaultOptions, metadataOverrides) case C.RuleTypeLogical: flags := dnsRuleModeFlags{ - disabled: dnsRuleActionType(rule) == C.RuleActionTypeEvaluate || dnsRuleActionType(rule) == C.RuleActionTypeRespond, + disabled: dnsRuleActionType(rule) == C.RuleActionTypeEvaluate || + dnsRuleActionType(rule) == C.RuleActionTypeRespond || + dnsRuleActionDisablesLegacyDNSMode(rule.LogicalOptions.DNSRuleAction), neededFromStrategy: dnsRuleActionHasStrategy(rule.LogicalOptions.DNSRuleAction), } flags.needed = flags.neededFromStrategy @@ -926,7 +961,7 @@ func dnsRuleModeRequirementsInRule(router adapter.Router, rule option.DNSRule, m func dnsRuleModeRequirementsInDefaultRule(router adapter.Router, rule option.DefaultDNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) (dnsRuleModeFlags, error) { flags := dnsRuleModeFlags{ - disabled: defaultRuleDisablesLegacyDNSMode(rule), + disabled: defaultRuleDisablesLegacyDNSMode(rule) || dnsRuleActionDisablesLegacyDNSMode(rule.DNSRuleAction), neededFromStrategy: dnsRuleActionHasStrategy(rule.DNSRuleAction), } flags.needed = defaultRuleNeedsLegacyDNSModeFromAddressFilter(rule) || flags.neededFromStrategy @@ -1063,6 +1098,17 @@ func validateLegacyDNSModeDisabledDefaultRule(rule option.DefaultDNSRule) (bool, return rule.MatchResponse || rule.Action == C.RuleActionTypeRespond, nil } +func dnsRuleActionDisablesLegacyDNSMode(action option.DNSRuleAction) bool { + switch action.Action { + case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate: + return action.RouteOptions.DisableOptimisticCache + case C.RuleActionTypeRouteOptions: + return action.RouteOptionsOptions.DisableOptimisticCache + default: + return false + } +} + func dnsRuleActionHasStrategy(action option.DNSRuleAction) bool { switch action.Action { case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate: diff --git a/docs/configuration/dns/index.md b/docs/configuration/dns/index.md index cbb58906f1..b78a49e7ac 100644 --- a/docs/configuration/dns/index.md +++ b/docs/configuration/dns/index.md @@ -2,6 +2,11 @@ icon: material/alert-decagram --- +!!! quote "Changes in sing-box 1.14.0" + + :material-delete-clock: [independent_cache](#independent_cache) + :material-plus: [optimistic](#optimistic) + !!! quote "Changes in sing-box 1.12.0" :material-decagram: [servers](#servers) @@ -25,6 +30,7 @@ icon: material/alert-decagram "disable_expire": false, "independent_cache": false, "cache_capacity": 0, + "optimistic": false, // or {} "reverse_mapping": false, "client_subnet": "", "fakeip": {} @@ -57,12 +63,20 @@ One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`. Disable dns cache. +Conflict with `optimistic`. + #### disable_expire Disable dns cache expire. +Conflict with `optimistic`. + #### independent_cache +!!! failure "Deprecated in sing-box 1.14.0" + + `independent_cache` is deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-independent-dns-cache). + Make each DNS server's cache independent for special purposes. If enabled, will slightly degrade performance. #### cache_capacity @@ -73,6 +87,34 @@ LRU cache capacity. Value less than 1024 will be ignored. +#### optimistic + +!!! question "Since sing-box 1.14.0" + +Enable optimistic DNS caching. When a cached DNS entry has expired but is still within the timeout window, +the stale response is returned immediately while a background refresh is triggered. + +Conflict with `disable_cache` and `disable_expire`. + +Accepts a boolean or an object. When set to `true`, the default timeout of `3d` is used. + +```json +{ + "enabled": true, + "timeout": "3d" +} +``` + +##### enabled + +Enable optimistic DNS caching. + +##### timeout + +The maximum time an expired cache entry can be served optimistically. + +`3d` is used by default. + #### reverse_mapping Stores a reverse mapping of IP addresses after responding to a DNS query in order to provide domain names when routing. diff --git a/docs/configuration/dns/index.zh.md b/docs/configuration/dns/index.zh.md index cd2518107c..ae06b8ab6d 100644 --- a/docs/configuration/dns/index.zh.md +++ b/docs/configuration/dns/index.zh.md @@ -2,6 +2,11 @@ icon: material/alert-decagram --- +!!! quote "sing-box 1.14.0 中的更改" + + :material-delete-clock: [independent_cache](#independent_cache) + :material-plus: [optimistic](#optimistic) + !!! quote "sing-box 1.12.0 中的更改" :material-decagram: [servers](#servers) @@ -25,6 +30,7 @@ icon: material/alert-decagram "disable_expire": false, "independent_cache": false, "cache_capacity": 0, + "optimistic": false, // or {} "reverse_mapping": false, "client_subnet": "", "fakeip": {} @@ -56,12 +62,20 @@ icon: material/alert-decagram 禁用 DNS 缓存。 +与 `optimistic` 冲突。 + #### disable_expire 禁用 DNS 缓存过期。 +与 `optimistic` 冲突。 + #### independent_cache +!!! failure "已在 sing-box 1.14.0 废弃" + + `independent_cache` 已在 sing-box 1.14.0 废弃,且将在 sing-box 1.16.0 中被移除,参阅[迁移指南](/zh/migration/#迁移-independent-dns-cache)。 + 使每个 DNS 服务器的缓存独立,以满足特殊目的。如果启用,将轻微降低性能。 #### cache_capacity @@ -72,6 +86,34 @@ LRU 缓存容量。 小于 1024 的值将被忽略。 +#### optimistic + +!!! question "自 sing-box 1.14.0 起" + +启用乐观 DNS 缓存。当缓存的 DNS 条目已过期但仍在超时窗口内时, +立即返回过期的响应,同时在后台触发刷新。 + +与 `disable_cache` 和 `disable_expire` 冲突。 + +接受布尔值或对象。当设置为 `true` 时,使用默认超时 `3d`。 + +```json +{ + "enabled": true, + "timeout": "3d" +} +``` + +##### enabled + +启用乐观 DNS 缓存。 + +##### timeout + +过期缓存条目可被乐观提供的最长时间。 + +默认使用 `3d`。 + #### reverse_mapping 在响应 DNS 查询后存储 IP 地址的反向映射以为路由目的提供域名。 diff --git a/docs/configuration/dns/rule_action.md b/docs/configuration/dns/rule_action.md index dfc72dc76f..e5a99be3c8 100644 --- a/docs/configuration/dns/rule_action.md +++ b/docs/configuration/dns/rule_action.md @@ -6,7 +6,8 @@ icon: material/new-box :material-delete-clock: [strategy](#strategy) :material-plus: [evaluate](#evaluate) - :material-plus: [respond](#respond) + :material-plus: [respond](#respond) + :material-plus: [disable_optimistic_cache](#disable_optimistic_cache) !!! quote "Changes in sing-box 1.12.0" @@ -23,6 +24,7 @@ icon: material/new-box "server": "", "strategy": "", "disable_cache": false, + "disable_optimistic_cache": false, "rewrite_ttl": null, "client_subnet": null } @@ -52,6 +54,12 @@ One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`. Disable cache and save cache in this query. +#### disable_optimistic_cache + +!!! question "Since sing-box 1.14.0" + +Disable optimistic DNS caching in this query. + #### rewrite_ttl Rewrite TTL in DNS responses. @@ -73,6 +81,7 @@ Will override `dns.client_subnet`. "action": "evaluate", "server": "", "disable_cache": false, + "disable_optimistic_cache": false, "rewrite_ttl": null, "client_subnet": null } @@ -97,6 +106,12 @@ Tag of target server. Disable cache and save cache in this query. +#### disable_optimistic_cache + +!!! question "Since sing-box 1.14.0" + +Disable optimistic DNS caching in this query. + #### rewrite_ttl Rewrite TTL in DNS responses. @@ -131,6 +146,7 @@ Only allowed after a preceding top-level `evaluate` rule. If the action is reach { "action": "route-options", "disable_cache": false, + "disable_optimistic_cache": false, "rewrite_ttl": null, "client_subnet": null } diff --git a/docs/configuration/dns/rule_action.zh.md b/docs/configuration/dns/rule_action.zh.md index 36c8111cea..24179977f0 100644 --- a/docs/configuration/dns/rule_action.zh.md +++ b/docs/configuration/dns/rule_action.zh.md @@ -6,7 +6,8 @@ icon: material/new-box :material-delete-clock: [strategy](#strategy) :material-plus: [evaluate](#evaluate) - :material-plus: [respond](#respond) + :material-plus: [respond](#respond) + :material-plus: [disable_optimistic_cache](#disable_optimistic_cache) !!! quote "sing-box 1.12.0 中的更改" @@ -23,6 +24,7 @@ icon: material/new-box "server": "", "strategy": "", "disable_cache": false, + "disable_optimistic_cache": false, "rewrite_ttl": null, "client_subnet": null } @@ -52,6 +54,12 @@ icon: material/new-box 在此查询中禁用缓存。 +#### disable_optimistic_cache + +!!! question "自 sing-box 1.14.0 起" + +在此查询中禁用乐观 DNS 缓存。 + #### rewrite_ttl 重写 DNS 回应中的 TTL。 @@ -73,6 +81,7 @@ icon: material/new-box "action": "evaluate", "server": "", "disable_cache": false, + "disable_optimistic_cache": false, "rewrite_ttl": null, "client_subnet": null } @@ -95,6 +104,12 @@ icon: material/new-box 在此查询中禁用缓存。 +#### disable_optimistic_cache + +!!! question "自 sing-box 1.14.0 起" + +在此查询中禁用乐观 DNS 缓存。 + #### rewrite_ttl 重写 DNS 回应中的 TTL。 @@ -129,6 +144,7 @@ icon: material/new-box { "action": "route-options", "disable_cache": false, + "disable_optimistic_cache": false, "rewrite_ttl": null, "client_subnet": null } diff --git a/docs/configuration/experimental/cache-file.md b/docs/configuration/experimental/cache-file.md index f91ee50fde..b93aa19065 100644 --- a/docs/configuration/experimental/cache-file.md +++ b/docs/configuration/experimental/cache-file.md @@ -1,5 +1,10 @@ !!! question "Since sing-box 1.8.0" +!!! quote "Changes in sing-box 1.14.0" + + :material-delete-clock: [store_rdrc](#store_rdrc) + :material-plus: [store_dns](#store_dns) + !!! quote "Changes in sing-box 1.9.0" :material-plus: [store_rdrc](#store_rdrc) @@ -14,7 +19,8 @@ "cache_id": "", "store_fakeip": false, "store_rdrc": false, - "rdrc_timeout": "" + "rdrc_timeout": "", + "store_dns": false } ``` @@ -42,6 +48,10 @@ Store fakeip in the cache file #### store_rdrc +!!! failure "Deprecated in sing-box 1.14.0" + + `store_rdrc` is deprecated and will be removed in sing-box 1.16.0, check [Migration](/migration/#migrate-store-rdrc). + Store rejected DNS response cache in the cache file The check results of [Legacy Address Filter Fields](/configuration/dns/rule/#legacy-address-filter-fields) @@ -52,3 +62,9 @@ will be cached until expiration. Timeout of rejected DNS response cache. `7d` is used by default. + +#### store_dns + +!!! question "Since sing-box 1.14.0" + +Store DNS cache in the cache file. diff --git a/docs/configuration/experimental/cache-file.zh.md b/docs/configuration/experimental/cache-file.zh.md index a998aa7736..5382f3a185 100644 --- a/docs/configuration/experimental/cache-file.zh.md +++ b/docs/configuration/experimental/cache-file.zh.md @@ -1,5 +1,10 @@ !!! question "自 sing-box 1.8.0 起" +!!! quote "sing-box 1.14.0 中的更改" + + :material-delete-clock: [store_rdrc](#store_rdrc) + :material-plus: [store_dns](#store_dns) + !!! quote "sing-box 1.9.0 中的更改" :material-plus: [store_rdrc](#store_rdrc) @@ -14,7 +19,8 @@ "cache_id": "", "store_fakeip": false, "store_rdrc": false, - "rdrc_timeout": "" + "rdrc_timeout": "", + "store_dns": false } ``` @@ -40,6 +46,10 @@ #### store_rdrc +!!! failure "已在 sing-box 1.14.0 废弃" + + `store_rdrc` 已在 sing-box 1.14.0 废弃,且将在 sing-box 1.16.0 中被移除,参阅[迁移指南](/zh/migration/#迁移-store_rdrc)。 + 将拒绝的 DNS 响应缓存存储在缓存文件中。 [旧版地址筛选字段](/zh/configuration/dns/rule/#旧版地址筛选字段) 的检查结果将被缓存至过期。 @@ -49,3 +59,9 @@ 拒绝的 DNS 响应缓存超时。 默认使用 `7d`。 + +#### store_dns + +!!! question "自 sing-box 1.14.0 起" + +将 DNS 缓存存储在缓存文件中。 diff --git a/docs/configuration/route/rule_action.md b/docs/configuration/route/rule_action.md index 4f2a35cbd6..1ba690398d 100644 --- a/docs/configuration/route/rule_action.md +++ b/docs/configuration/route/rule_action.md @@ -7,6 +7,10 @@ icon: material/new-box :material-plus: [bypass](#bypass) :material-alert: [reject](#reject) +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [resolve.disable_optimistic_cache](#disable_optimistic_cache) + !!! quote "Changes in sing-box 1.12.0" :material-plus: [tls_fragment](#tls_fragment) @@ -279,6 +283,7 @@ Timeout for sniffing. "server": "", "strategy": "", "disable_cache": false, + "disable_optimistic_cache": false, "rewrite_ttl": null, "client_subnet": null } @@ -302,6 +307,12 @@ DNS resolution strategy, available values are: `prefer_ipv4`, `prefer_ipv6`, `ip Disable cache and save cache in this query. +#### disable_optimistic_cache + +!!! question "Since sing-box 1.14.0" + +Disable optimistic DNS caching in this query. + #### rewrite_ttl !!! question "Since sing-box 1.12.0" diff --git a/docs/configuration/route/rule_action.zh.md b/docs/configuration/route/rule_action.zh.md index 16e1618082..5b13219b2f 100644 --- a/docs/configuration/route/rule_action.zh.md +++ b/docs/configuration/route/rule_action.zh.md @@ -7,6 +7,10 @@ icon: material/new-box :material-plus: [bypass](#bypass) :material-alert: [reject](#reject) +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [resolve.disable_optimistic_cache](#disable_optimistic_cache) + !!! quote "sing-box 1.12.0 中的更改" :material-plus: [tls_fragment](#tls_fragment) @@ -268,6 +272,7 @@ UDP 连接超时时间。 "server": "", "strategy": "", "disable_cache": false, + "disable_optimistic_cache": false, "rewrite_ttl": null, "client_subnet": null } @@ -291,6 +296,12 @@ DNS 解析策略,可用值有:`prefer_ipv4`、`prefer_ipv6`、`ipv4_only`、 在此查询中禁用缓存。 +#### disable_optimistic_cache + +!!! question "自 sing-box 1.14.0 起" + +在此查询中禁用乐观 DNS 缓存。 + #### rewrite_ttl !!! question "自 sing-box 1.12.0 起" diff --git a/docs/deprecated.md b/docs/deprecated.md index 47a9bffdd8..1eeab10d33 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -27,6 +27,21 @@ check [Migration](../migration/#migrate-address-filter-fields-to-response-matchi Old fields will be removed in sing-box 1.16.0. +#### `independent_cache` DNS option + +`independent_cache` DNS option is deprecated. +The DNS cache now always keys by transport, making this option unnecessary, +check [Migration](../migration/#migrate-independent-dns-cache). + +Old fields will be removed in sing-box 1.16.0. + +#### `store_rdrc` cache file option + +`store_rdrc` cache file option is deprecated, +check [Migration](../migration/#migrate-store-rdrc). + +Old fields will be removed in sing-box 1.16.0. + #### Legacy Address Filter Fields in DNS rules Legacy Address Filter Fields (`ip_cidr`, `ip_is_private` without `match_response`) diff --git a/docs/deprecated.zh.md b/docs/deprecated.zh.md index a4995b5684..5dabd69fb4 100644 --- a/docs/deprecated.zh.md +++ b/docs/deprecated.zh.md @@ -27,6 +27,21 @@ TLS 中的内联 ACME 选项(`tls.acme`)已废弃, 旧字段将在 sing-box 1.16.0 中被移除。 +#### `independent_cache` DNS 选项 + +`independent_cache` DNS 选项已废弃。 +DNS 缓存现在始终按传输分离,使此选项不再需要, +参阅[迁移指南](/zh/migration/#迁移-independent-dns-cache)。 + +旧字段将在 sing-box 1.16.0 中被移除。 + +#### `store_rdrc` 缓存文件选项 + +`store_rdrc` 缓存文件选项已废弃, +参阅[迁移指南](/zh/migration/#迁移-store_rdrc)。 + +旧字段将在 sing-box 1.16.0 中被移除。 + #### 旧版地址筛选字段 (DNS 规则) 旧版地址筛选字段(不使用 `match_response` 的 `ip_cidr`、`ip_is_private`)已废弃, diff --git a/docs/migration.md b/docs/migration.md index 129f387faf..867f903b69 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -137,6 +137,68 @@ to fetch a DNS response, then match against it explicitly with `match_response`. } ``` +### Migrate independent DNS cache + +The DNS cache now always keys by transport name, making `independent_cache` unnecessary. +Simply remove the field. + +!!! info "References" + + [DNS](/configuration/dns/) + +=== ":material-card-remove: Deprecated" + + ```json + { + "dns": { + "independent_cache": true + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "dns": {} + } + ``` + +### Migrate store_rdrc + +`store_rdrc` is deprecated and can be replaced by `store_dns`, +which persists the full DNS cache to the cache file. + +!!! info "References" + + [Cache File](/configuration/experimental/cache-file/) + +=== ":material-card-remove: Deprecated" + + ```json + { + "experimental": { + "cache_file": { + "enabled": true, + "store_rdrc": true + } + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "experimental": { + "cache_file": { + "enabled": true, + "store_dns": true + } + } + } + ``` + ### ip_version and query_type behavior changes in DNS rules In sing-box 1.14.0, the behavior of diff --git a/docs/migration.zh.md b/docs/migration.zh.md index 995b0d9416..54dec47e4b 100644 --- a/docs/migration.zh.md +++ b/docs/migration.zh.md @@ -137,6 +137,68 @@ sing-box 1.14.0 新增字段参阅 [ACME](/zh/configuration/shared/certificate-p } ``` +### 迁移 independent DNS cache + +DNS 缓存现在始终按传输名称分离,使 `independent_cache` 不再需要。 +直接移除该字段即可。 + +!!! info "参考" + + [DNS](/zh/configuration/dns/) + +=== ":material-card-remove: 弃用的" + + ```json + { + "dns": { + "independent_cache": true + } + } + ``` + +=== ":material-card-multiple: 新的" + + ```json + { + "dns": {} + } + ``` + +### 迁移 store_rdrc + +`store_rdrc` 已废弃,且可以被 `store_dns` 替代, +后者将完整的 DNS 缓存持久化到缓存文件中。 + +!!! info "参考" + + [缓存文件](/zh/configuration/experimental/cache-file/) + +=== ":material-card-remove: 弃用的" + + ```json + { + "experimental": { + "cache_file": { + "enabled": true, + "store_rdrc": true + } + } + } + ``` + +=== ":material-card-multiple: 新的" + + ```json + { + "experimental": { + "cache_file": { + "enabled": true, + "store_dns": true + } + } + } + ``` + ### DNS 规则中的 ip_version 和 query_type 行为更改 在 sing-box 1.14.0 中,DNS 规则中的 diff --git a/experimental/cachefile/cache.go b/experimental/cachefile/cache.go index ac2d700280..3198fc6ae0 100644 --- a/experimental/cachefile/cache.go +++ b/experimental/cachefile/cache.go @@ -12,9 +12,11 @@ import ( "github.com/sagernet/bbolt" bboltErrors "github.com/sagernet/bbolt/errors" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/experimental/deprecated" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/service/filemanager" ) @@ -30,6 +32,7 @@ var ( string(bucketMode), string(bucketRuleSet), string(bucketRDRC), + string(bucketDNSCache), } cacheIDDefault = []byte("default") @@ -38,30 +41,43 @@ var ( var _ adapter.CacheFile = (*CacheFile)(nil) type CacheFile struct { - ctx context.Context - path string - cacheID []byte - storeFakeIP bool - storeRDRC bool - rdrcTimeout time.Duration - DB *bbolt.DB - resetAccess sync.Mutex - saveMetadataTimer *time.Timer - saveFakeIPAccess sync.RWMutex - saveDomain map[netip.Addr]string - saveAddress4 map[string]netip.Addr - saveAddress6 map[string]netip.Addr - saveRDRCAccess sync.RWMutex - saveRDRC map[saveRDRCCacheKey]bool + ctx context.Context + logger logger.Logger + path string + cacheID []byte + storeFakeIP bool + storeRDRC bool + storeDNS bool + disableExpire bool + rdrcTimeout time.Duration + optimisticTimeout time.Duration + DB *bbolt.DB + resetAccess sync.Mutex + saveMetadataTimer *time.Timer + saveFakeIPAccess sync.RWMutex + saveDomain map[netip.Addr]string + saveAddress4 map[string]netip.Addr + saveAddress6 map[string]netip.Addr + saveRDRCAccess sync.RWMutex + saveRDRC map[saveCacheKey]bool + saveDNSCacheAccess sync.RWMutex + saveDNSCache map[saveCacheKey]saveDNSCacheEntry } -type saveRDRCCacheKey struct { +type saveCacheKey struct { TransportName string QuestionName string QType uint16 } -func New(ctx context.Context, options option.CacheFileOptions) *CacheFile { +type saveDNSCacheEntry struct { + rawMessage []byte + expireAt time.Time + sequence uint64 + saving bool +} + +func New(ctx context.Context, logger logger.Logger, options option.CacheFileOptions) *CacheFile { var path string if options.Path != "" { path = options.Path @@ -72,6 +88,9 @@ func New(ctx context.Context, options option.CacheFileOptions) *CacheFile { if options.CacheID != "" { cacheIDBytes = append([]byte{0}, []byte(options.CacheID)...) } + if options.StoreRDRC { + deprecated.Report(ctx, deprecated.OptionStoreRDRC) + } var rdrcTimeout time.Duration if options.StoreRDRC { if options.RDRCTimeout > 0 { @@ -82,15 +101,18 @@ func New(ctx context.Context, options option.CacheFileOptions) *CacheFile { } return &CacheFile{ ctx: ctx, + logger: logger, path: filemanager.BasePath(ctx, path), cacheID: cacheIDBytes, storeFakeIP: options.StoreFakeIP, storeRDRC: options.StoreRDRC, + storeDNS: options.StoreDNS, rdrcTimeout: rdrcTimeout, saveDomain: make(map[netip.Addr]string), saveAddress4: make(map[string]netip.Addr), saveAddress6: make(map[string]netip.Addr), - saveRDRC: make(map[saveRDRCCacheKey]bool), + saveRDRC: make(map[saveCacheKey]bool), + saveDNSCache: make(map[saveCacheKey]saveDNSCacheEntry), } } @@ -102,10 +124,44 @@ func (c *CacheFile) Dependencies() []string { return nil } +func (c *CacheFile) SetOptimisticTimeout(timeout time.Duration) { + c.optimisticTimeout = timeout +} + +func (c *CacheFile) SetDisableExpire(disableExpire bool) { + c.disableExpire = disableExpire +} + func (c *CacheFile) Start(stage adapter.StartStage) error { - if stage != adapter.StartStateInitialize { - return nil + switch stage { + case adapter.StartStateInitialize: + return c.start() + case adapter.StartStateStart: + c.startCacheCleanup() } + return nil +} + +func (c *CacheFile) startCacheCleanup() { + if c.storeDNS { + c.clearRDRC() + c.cleanupDNSCache() + interval := c.optimisticTimeout / 2 + if interval <= 0 { + interval = time.Hour + } + go c.loopCacheCleanup(interval, c.cleanupDNSCache) + } else if c.storeRDRC { + c.cleanupRDRC() + interval := c.rdrcTimeout / 2 + if interval <= 0 { + interval = time.Hour + } + go c.loopCacheCleanup(interval, c.cleanupRDRC) + } +} + +func (c *CacheFile) start() error { const fileMode = 0o666 options := bbolt.Options{Timeout: time.Second} var ( diff --git a/experimental/cachefile/dns_cache.go b/experimental/cachefile/dns_cache.go new file mode 100644 index 0000000000..914c7e5adc --- /dev/null +++ b/experimental/cachefile/dns_cache.go @@ -0,0 +1,299 @@ +package cachefile + +import ( + "encoding/binary" + "time" + + "github.com/sagernet/bbolt" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/logger" +) + +var bucketDNSCache = []byte("dns_cache") + +func (c *CacheFile) StoreDNS() bool { + return c.storeDNS +} + +func (c *CacheFile) LoadDNSCache(transportName string, qName string, qType uint16) (rawMessage []byte, expireAt time.Time, loaded bool) { + c.saveDNSCacheAccess.RLock() + entry, cached := c.saveDNSCache[saveCacheKey{transportName, qName, qType}] + c.saveDNSCacheAccess.RUnlock() + if cached { + return entry.rawMessage, entry.expireAt, true + } + key := buf.Get(2 + len(qName)) + binary.BigEndian.PutUint16(key, qType) + copy(key[2:], qName) + defer buf.Put(key) + err := c.view(func(tx *bbolt.Tx) error { + bucket := c.bucket(tx, bucketDNSCache) + if bucket == nil { + return nil + } + bucket = bucket.Bucket([]byte(transportName)) + if bucket == nil { + return nil + } + content := bucket.Get(key) + if len(content) < 8 { + return nil + } + expireAt = time.Unix(int64(binary.BigEndian.Uint64(content[:8])), 0) + rawMessage = make([]byte, len(content)-8) + copy(rawMessage, content[8:]) + loaded = true + return nil + }) + if err != nil { + return nil, time.Time{}, false + } + return +} + +func (c *CacheFile) SaveDNSCache(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time) error { + return c.batch(func(tx *bbolt.Tx) error { + bucket, err := c.createBucket(tx, bucketDNSCache) + if err != nil { + return err + } + bucket, err = bucket.CreateBucketIfNotExists([]byte(transportName)) + if err != nil { + return err + } + key := buf.Get(2 + len(qName)) + binary.BigEndian.PutUint16(key, qType) + copy(key[2:], qName) + defer buf.Put(key) + value := buf.Get(8 + len(rawMessage)) + defer buf.Put(value) + binary.BigEndian.PutUint64(value[:8], uint64(expireAt.Unix())) + copy(value[8:], rawMessage) + return bucket.Put(key, value) + }) +} + +func (c *CacheFile) SaveDNSCacheAsync(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time, logger logger.Logger) { + saveKey := saveCacheKey{transportName, qName, qType} + if !c.queueDNSCacheSave(saveKey, rawMessage, expireAt) { + return + } + go c.flushPendingDNSCache(saveKey, logger) +} + +func (c *CacheFile) queueDNSCacheSave(saveKey saveCacheKey, rawMessage []byte, expireAt time.Time) bool { + c.saveDNSCacheAccess.Lock() + defer c.saveDNSCacheAccess.Unlock() + entry := c.saveDNSCache[saveKey] + entry.rawMessage = append([]byte(nil), rawMessage...) + entry.expireAt = expireAt + entry.sequence++ + startFlush := !entry.saving + entry.saving = true + c.saveDNSCache[saveKey] = entry + return startFlush +} + +func (c *CacheFile) flushPendingDNSCache(saveKey saveCacheKey, logger logger.Logger) { + c.flushPendingDNSCacheWith(saveKey, logger, func(entry saveDNSCacheEntry) error { + return c.SaveDNSCache(saveKey.TransportName, saveKey.QuestionName, saveKey.QType, entry.rawMessage, entry.expireAt) + }) +} + +func (c *CacheFile) flushPendingDNSCacheWith(saveKey saveCacheKey, logger logger.Logger, save func(saveDNSCacheEntry) error) { + for { + c.saveDNSCacheAccess.RLock() + entry, loaded := c.saveDNSCache[saveKey] + c.saveDNSCacheAccess.RUnlock() + if !loaded { + return + } + err := save(entry) + if err != nil { + logger.Warn("save DNS cache: ", err) + } + c.saveDNSCacheAccess.Lock() + currentEntry, loaded := c.saveDNSCache[saveKey] + if !loaded { + c.saveDNSCacheAccess.Unlock() + return + } + if currentEntry.sequence != entry.sequence { + c.saveDNSCacheAccess.Unlock() + continue + } + delete(c.saveDNSCache, saveKey) + c.saveDNSCacheAccess.Unlock() + return + } +} + +func (c *CacheFile) ClearDNSCache() error { + c.saveDNSCacheAccess.Lock() + clear(c.saveDNSCache) + c.saveDNSCacheAccess.Unlock() + return c.batch(func(tx *bbolt.Tx) error { + if c.cacheID == nil { + bucket := tx.Bucket(bucketDNSCache) + if bucket == nil { + return nil + } + return tx.DeleteBucket(bucketDNSCache) + } + bucket := tx.Bucket(c.cacheID) + if bucket == nil || bucket.Bucket(bucketDNSCache) == nil { + return nil + } + return bucket.DeleteBucket(bucketDNSCache) + }) +} + +func (c *CacheFile) loopCacheCleanup(interval time.Duration, cleanupFunc func()) { + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + select { + case <-c.ctx.Done(): + return + case <-ticker.C: + cleanupFunc() + } + } +} + +func (c *CacheFile) cleanupDNSCache() { + now := time.Now() + err := c.batch(func(tx *bbolt.Tx) error { + bucket := c.bucket(tx, bucketDNSCache) + if bucket == nil { + return nil + } + var emptyTransports [][]byte + err := bucket.ForEachBucket(func(transportName []byte) error { + transportBucket := bucket.Bucket(transportName) + if transportBucket == nil { + return nil + } + var expiredKeys [][]byte + err := transportBucket.ForEach(func(key, value []byte) error { + if len(value) < 8 { + expiredKeys = append(expiredKeys, append([]byte(nil), key...)) + return nil + } + if c.disableExpire { + return nil + } + expireAt := time.Unix(int64(binary.BigEndian.Uint64(value[:8])), 0) + if now.After(expireAt.Add(c.optimisticTimeout)) { + expiredKeys = append(expiredKeys, append([]byte(nil), key...)) + } + return nil + }) + if err != nil { + return err + } + for _, key := range expiredKeys { + err = transportBucket.Delete(key) + if err != nil { + return err + } + } + first, _ := transportBucket.Cursor().First() + if first == nil { + emptyTransports = append(emptyTransports, append([]byte(nil), transportName...)) + } + return nil + }) + if err != nil { + return err + } + for _, name := range emptyTransports { + err = bucket.DeleteBucket(name) + if err != nil { + return err + } + } + return nil + }) + if err != nil { + c.logger.Warn("cleanup DNS cache: ", err) + } +} + +func (c *CacheFile) clearRDRC() { + c.saveRDRCAccess.Lock() + clear(c.saveRDRC) + c.saveRDRCAccess.Unlock() + err := c.batch(func(tx *bbolt.Tx) error { + if c.cacheID == nil { + if tx.Bucket(bucketRDRC) == nil { + return nil + } + return tx.DeleteBucket(bucketRDRC) + } + bucket := tx.Bucket(c.cacheID) + if bucket == nil || bucket.Bucket(bucketRDRC) == nil { + return nil + } + return bucket.DeleteBucket(bucketRDRC) + }) + if err != nil { + c.logger.Warn("clear RDRC: ", err) + } +} + +func (c *CacheFile) cleanupRDRC() { + now := time.Now() + err := c.batch(func(tx *bbolt.Tx) error { + bucket := c.bucket(tx, bucketRDRC) + if bucket == nil { + return nil + } + var emptyTransports [][]byte + err := bucket.ForEachBucket(func(transportName []byte) error { + transportBucket := bucket.Bucket(transportName) + if transportBucket == nil { + return nil + } + var expiredKeys [][]byte + err := transportBucket.ForEach(func(key, value []byte) error { + if len(value) < 8 { + expiredKeys = append(expiredKeys, append([]byte(nil), key...)) + return nil + } + expiresAt := time.Unix(int64(binary.BigEndian.Uint64(value)), 0) + if now.After(expiresAt) { + expiredKeys = append(expiredKeys, append([]byte(nil), key...)) + } + return nil + }) + if err != nil { + return err + } + for _, key := range expiredKeys { + err = transportBucket.Delete(key) + if err != nil { + return err + } + } + first, _ := transportBucket.Cursor().First() + if first == nil { + emptyTransports = append(emptyTransports, append([]byte(nil), transportName...)) + } + return nil + }) + if err != nil { + return err + } + for _, name := range emptyTransports { + err = bucket.DeleteBucket(name) + if err != nil { + return err + } + } + return nil + }) + if err != nil { + c.logger.Warn("cleanup RDRC: ", err) + } +} diff --git a/experimental/cachefile/rdrc.go b/experimental/cachefile/rdrc.go index dde324c3c9..c02259c389 100644 --- a/experimental/cachefile/rdrc.go +++ b/experimental/cachefile/rdrc.go @@ -21,7 +21,7 @@ func (c *CacheFile) RDRCTimeout() time.Duration { func (c *CacheFile) LoadRDRC(transportName string, qName string, qType uint16) (rejected bool) { c.saveRDRCAccess.RLock() - rejected, cached := c.saveRDRC[saveRDRCCacheKey{transportName, qName, qType}] + rejected, cached := c.saveRDRC[saveCacheKey{transportName, qName, qType}] c.saveRDRCAccess.RUnlock() if cached { return @@ -93,7 +93,7 @@ func (c *CacheFile) SaveRDRC(transportName string, qName string, qType uint16) e } func (c *CacheFile) SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger) { - saveKey := saveRDRCCacheKey{transportName, qName, qType} + saveKey := saveCacheKey{transportName, qName, qType} c.saveRDRCAccess.Lock() c.saveRDRC[saveKey] = true c.saveRDRCAccess.Unlock() diff --git a/experimental/deprecated/constants.go b/experimental/deprecated/constants.go index afe5c021ac..108eba575b 100644 --- a/experimental/deprecated/constants.go +++ b/experimental/deprecated/constants.go @@ -120,6 +120,24 @@ var OptionLegacyDNSRuleStrategy = Note{ MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-dns-rule-action-strategy-to-rule-items", } +var OptionIndependentDNSCache = Note{ + Name: "independent-dns-cache", + Description: "`independent_cache` DNS option", + DeprecatedVersion: "1.14.0", + ScheduledVersion: "1.16.0", + EnvName: "INDEPENDENT_DNS_CACHE", + MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-independent-dns-cache", +} + +var OptionStoreRDRC = Note{ + Name: "store-rdrc", + Description: "`store_rdrc` cache file option", + DeprecatedVersion: "1.14.0", + ScheduledVersion: "1.16.0", + EnvName: "STORE_RDRC", + MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-store-rdrc", +} + var Options = []Note{ OptionOutboundDNSRuleItem, OptionMissingDomainResolver, @@ -128,4 +146,6 @@ var Options = []Note{ OptionRuleSetIPCIDRAcceptEmpty, OptionLegacyDNSAddressFilter, OptionLegacyDNSRuleStrategy, + OptionIndependentDNSCache, + OptionStoreRDRC, } diff --git a/option/dns.go b/option/dns.go index ee29ce096f..c09b3d5f32 100644 --- a/option/dns.go +++ b/option/dns.go @@ -52,9 +52,32 @@ type DNSClientOptions struct { DisableExpire bool `json:"disable_expire,omitempty"` IndependentCache bool `json:"independent_cache,omitempty"` CacheCapacity uint32 `json:"cache_capacity,omitempty"` + Optimistic *OptimisticDNSOptions `json:"optimistic,omitempty"` ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` } +type _OptimisticDNSOptions struct { + Enabled bool `json:"enabled,omitempty"` + Timeout badoption.Duration `json:"timeout,omitempty"` +} + +type OptimisticDNSOptions _OptimisticDNSOptions + +func (o OptimisticDNSOptions) MarshalJSON() ([]byte, error) { + if o.Timeout == 0 { + return json.Marshal(o.Enabled) + } + return json.Marshal((_OptimisticDNSOptions)(o)) +} + +func (o *OptimisticDNSOptions) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, &o.Enabled) + if err == nil { + return nil + } + return json.UnmarshalDisallowUnknownFields(bytes, (*_OptimisticDNSOptions)(o)) +} + type DNSTransportOptionsRegistry interface { CreateOptions(transportType string) (any, bool) } diff --git a/option/experimental.go b/option/experimental.go index bf0df9e78c..2f00decfed 100644 --- a/option/experimental.go +++ b/option/experimental.go @@ -16,6 +16,7 @@ type CacheFileOptions struct { StoreFakeIP bool `json:"store_fakeip,omitempty"` StoreRDRC bool `json:"store_rdrc,omitempty"` RDRCTimeout badoption.Duration `json:"rdrc_timeout,omitempty"` + StoreDNS bool `json:"store_dns,omitempty"` } type ClashAPIOptions struct { diff --git a/option/outbound.go b/option/outbound.go index 6676a3e923..d8fcb82214 100644 --- a/option/outbound.go +++ b/option/outbound.go @@ -93,11 +93,12 @@ type DialerOptions struct { } type _DomainResolveOptions struct { - Server string `json:"server"` - Strategy DomainStrategy `json:"strategy,omitempty"` - DisableCache bool `json:"disable_cache,omitempty"` - RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` - ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` + Server string `json:"server"` + Strategy DomainStrategy `json:"strategy,omitempty"` + DisableCache bool `json:"disable_cache,omitempty"` + DisableOptimisticCache bool `json:"disable_optimistic_cache,omitempty"` + RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` + ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` } type DomainResolveOptions _DomainResolveOptions @@ -107,6 +108,7 @@ func (o DomainResolveOptions) MarshalJSON() ([]byte, error) { return []byte("{}"), nil } else if o.Strategy == DomainStrategy(C.DomainStrategyAsIS) && !o.DisableCache && + !o.DisableOptimisticCache && o.RewriteTTL == nil && o.ClientSubnet == nil { return json.Marshal(o.Server) diff --git a/option/rule_action.go b/option/rule_action.go index 212396b7b9..c369cfeb36 100644 --- a/option/rule_action.go +++ b/option/rule_action.go @@ -201,18 +201,20 @@ func (r *RouteOptionsActionOptions) UnmarshalJSON(data []byte) error { } type DNSRouteActionOptions struct { - Server string `json:"server,omitempty"` - Strategy DomainStrategy `json:"strategy,omitempty"` - DisableCache bool `json:"disable_cache,omitempty"` - RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` - ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` + Server string `json:"server,omitempty"` + Strategy DomainStrategy `json:"strategy,omitempty"` + DisableCache bool `json:"disable_cache,omitempty"` + DisableOptimisticCache bool `json:"disable_optimistic_cache,omitempty"` + RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` + ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` } type _DNSRouteOptionsActionOptions struct { - Strategy DomainStrategy `json:"strategy,omitempty"` - DisableCache bool `json:"disable_cache,omitempty"` - RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` - ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` + Strategy DomainStrategy `json:"strategy,omitempty"` + DisableCache bool `json:"disable_cache,omitempty"` + DisableOptimisticCache bool `json:"disable_optimistic_cache,omitempty"` + RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` + ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` } type DNSRouteOptionsActionOptions _DNSRouteOptionsActionOptions @@ -321,11 +323,12 @@ type RouteActionSniff struct { } type RouteActionResolve struct { - Server string `json:"server,omitempty"` - Strategy DomainStrategy `json:"strategy,omitempty"` - DisableCache bool `json:"disable_cache,omitempty"` - RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` - ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` + Server string `json:"server,omitempty"` + Strategy DomainStrategy `json:"strategy,omitempty"` + DisableCache bool `json:"disable_cache,omitempty"` + DisableOptimisticCache bool `json:"disable_optimistic_cache,omitempty"` + RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` + ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` } type DNSRouteActionPredefined struct { diff --git a/route/network.go b/route/network.go index 03e94879bf..858ea3b24f 100644 --- a/route/network.go +++ b/route/network.go @@ -78,10 +78,11 @@ func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, options RoutingMark: uint32(options.DefaultMark), DomainResolver: defaultDomainResolver.Server, DomainResolveOptions: adapter.DNSQueryOptions{ - Strategy: C.DomainStrategy(defaultDomainResolver.Strategy), - DisableCache: defaultDomainResolver.DisableCache, - RewriteTTL: defaultDomainResolver.RewriteTTL, - ClientSubnet: defaultDomainResolver.ClientSubnet.Build(netip.Prefix{}), + Strategy: C.DomainStrategy(defaultDomainResolver.Strategy), + DisableCache: defaultDomainResolver.DisableCache, + DisableOptimisticCache: defaultDomainResolver.DisableOptimisticCache, + RewriteTTL: defaultDomainResolver.RewriteTTL, + ClientSubnet: defaultDomainResolver.ClientSubnet.Build(netip.Prefix{}), }, NetworkStrategy: (*C.NetworkStrategy)(options.DefaultNetworkStrategy), NetworkType: common.Map(options.DefaultNetworkType, option.InterfaceType.Build), diff --git a/route/route.go b/route/route.go index 62a9e4af57..3dc3ea7669 100644 --- a/route/route.go +++ b/route/route.go @@ -786,11 +786,12 @@ func (r *Router) actionResolve(ctx context.Context, metadata *adapter.InboundCon } } addresses, err := r.dns.Lookup(adapter.WithContext(ctx, metadata), metadata.Destination.Fqdn, adapter.DNSQueryOptions{ - Transport: transport, - Strategy: action.Strategy, - DisableCache: action.DisableCache, - RewriteTTL: action.RewriteTTL, - ClientSubnet: action.ClientSubnet, + Transport: transport, + Strategy: action.Strategy, + DisableCache: action.DisableCache, + DisableOptimisticCache: action.DisableOptimisticCache, + RewriteTTL: action.RewriteTTL, + ClientSubnet: action.ClientSubnet, }) if err != nil { return err diff --git a/route/rule/rule_action.go b/route/rule/rule_action.go index 2fe6ba98a4..ea239b68cb 100644 --- a/route/rule/rule_action.go +++ b/route/rule/rule_action.go @@ -107,11 +107,12 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti return sniffAction, sniffAction.build() case C.RuleActionTypeResolve: return &RuleActionResolve{ - Server: action.ResolveOptions.Server, - Strategy: C.DomainStrategy(action.ResolveOptions.Strategy), - DisableCache: action.ResolveOptions.DisableCache, - RewriteTTL: action.ResolveOptions.RewriteTTL, - ClientSubnet: action.ResolveOptions.ClientSubnet.Build(netip.Prefix{}), + Server: action.ResolveOptions.Server, + Strategy: C.DomainStrategy(action.ResolveOptions.Strategy), + DisableCache: action.ResolveOptions.DisableCache, + DisableOptimisticCache: action.ResolveOptions.DisableOptimisticCache, + RewriteTTL: action.ResolveOptions.RewriteTTL, + ClientSubnet: action.ResolveOptions.ClientSubnet.Build(netip.Prefix{}), }, nil default: panic(F.ToString("unknown rule action: ", action.Action)) @@ -126,30 +127,33 @@ func NewDNSRuleAction(logger logger.ContextLogger, action option.DNSRuleAction) return &RuleActionDNSRoute{ Server: action.RouteOptions.Server, RuleActionDNSRouteOptions: RuleActionDNSRouteOptions{ - Strategy: C.DomainStrategy(action.RouteOptions.Strategy), - DisableCache: action.RouteOptions.DisableCache, - RewriteTTL: action.RouteOptions.RewriteTTL, - ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptions.ClientSubnet)), + Strategy: C.DomainStrategy(action.RouteOptions.Strategy), + DisableCache: action.RouteOptions.DisableCache, + DisableOptimisticCache: action.RouteOptions.DisableOptimisticCache, + RewriteTTL: action.RouteOptions.RewriteTTL, + ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptions.ClientSubnet)), }, } case C.RuleActionTypeEvaluate: return &RuleActionEvaluate{ Server: action.RouteOptions.Server, RuleActionDNSRouteOptions: RuleActionDNSRouteOptions{ - Strategy: C.DomainStrategy(action.RouteOptions.Strategy), - DisableCache: action.RouteOptions.DisableCache, - RewriteTTL: action.RouteOptions.RewriteTTL, - ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptions.ClientSubnet)), + Strategy: C.DomainStrategy(action.RouteOptions.Strategy), + DisableCache: action.RouteOptions.DisableCache, + DisableOptimisticCache: action.RouteOptions.DisableOptimisticCache, + RewriteTTL: action.RouteOptions.RewriteTTL, + ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptions.ClientSubnet)), }, } case C.RuleActionTypeRespond: return &RuleActionRespond{} case C.RuleActionTypeRouteOptions: return &RuleActionDNSRouteOptions{ - Strategy: C.DomainStrategy(action.RouteOptionsOptions.Strategy), - DisableCache: action.RouteOptionsOptions.DisableCache, - RewriteTTL: action.RouteOptionsOptions.RewriteTTL, - ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptionsOptions.ClientSubnet)), + Strategy: C.DomainStrategy(action.RouteOptionsOptions.Strategy), + DisableCache: action.RouteOptionsOptions.DisableCache, + DisableOptimisticCache: action.RouteOptionsOptions.DisableOptimisticCache, + RewriteTTL: action.RouteOptionsOptions.RewriteTTL, + ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptionsOptions.ClientSubnet)), } case C.RuleActionTypeReject: return &RuleActionReject{ @@ -310,6 +314,9 @@ func formatDNSRouteAction(action string, server string, options RuleActionDNSRou if options.DisableCache { descriptions = append(descriptions, "disable-cache") } + if options.DisableOptimisticCache { + descriptions = append(descriptions, "disable-optimistic-cache") + } if options.RewriteTTL != nil { descriptions = append(descriptions, F.ToString("rewrite-ttl=", *options.RewriteTTL)) } @@ -320,10 +327,11 @@ func formatDNSRouteAction(action string, server string, options RuleActionDNSRou } type RuleActionDNSRouteOptions struct { - Strategy C.DomainStrategy - DisableCache bool - RewriteTTL *uint32 - ClientSubnet netip.Prefix + Strategy C.DomainStrategy + DisableCache bool + DisableOptimisticCache bool + RewriteTTL *uint32 + ClientSubnet netip.Prefix } func (r *RuleActionDNSRouteOptions) Type() string { @@ -335,6 +343,9 @@ func (r *RuleActionDNSRouteOptions) String() string { if r.DisableCache { descriptions = append(descriptions, "disable-cache") } + if r.DisableOptimisticCache { + descriptions = append(descriptions, "disable-optimistic-cache") + } if r.RewriteTTL != nil { descriptions = append(descriptions, F.ToString("rewrite-ttl=", *r.RewriteTTL)) } @@ -510,11 +521,12 @@ func (r *RuleActionSniff) String() string { } type RuleActionResolve struct { - Server string - Strategy C.DomainStrategy - DisableCache bool - RewriteTTL *uint32 - ClientSubnet netip.Prefix + Server string + Strategy C.DomainStrategy + DisableCache bool + DisableOptimisticCache bool + RewriteTTL *uint32 + ClientSubnet netip.Prefix } func (r *RuleActionResolve) Type() string { @@ -532,6 +544,9 @@ func (r *RuleActionResolve) String() string { if r.DisableCache { options = append(options, "disable_cache") } + if r.DisableOptimisticCache { + options = append(options, "disable_optimistic_cache") + } if r.RewriteTTL != nil { options = append(options, F.ToString("rewrite_ttl=", *r.RewriteTTL)) } From 0d7230e42cbe6107d5ad11e4c22617e9c1f11f04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 14 Apr 2026 15:05:52 +0800 Subject: [PATCH 31/93] oom-killer: Record report before reset network --- service/oomkiller/timer.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/service/oomkiller/timer.go b/service/oomkiller/timer.go index 6f13d825ae..a5bef3a710 100644 --- a/service/oomkiller/timer.go +++ b/service/oomkiller/timer.go @@ -161,6 +161,10 @@ func (t *adaptiveTimer) stop() { } func (t *adaptiveTimer) poll() { + if t.timerConfig.policyMode == policyModeNetworkExtension { + runtimeDebug.FreeOSMemory() + } + var triggered bool var rateTriggered bool sample := readMemorySample(t.policyMode) @@ -205,6 +209,7 @@ func (t *adaptiveTimer) poll() { if !triggered { return } + t.onTriggered(sample.usage) if rateTriggered { if t.killerDisabled { t.logger.Warn("memory growth rate critical (report only), usage: ", byteformats.FormatMemoryBytes(sample.usage), t.logDetails(sample)) @@ -220,7 +225,6 @@ func (t *adaptiveTimer) poll() { t.router.ResetNetwork() } } - t.onTriggered(sample.usage) runtimeDebug.FreeOSMemory() } From 5a618c6b68bcf6d30f7c72a7cf84aceee3a672ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 14 Apr 2026 22:59:46 +0800 Subject: [PATCH 32/93] Refactor: HTTP clients, unified HTTP2/QUIC options, Apple engines --- adapter/http.go | 43 + adapter/router.go | 49 - box.go | 35 +- cmd/internal/build_libbox/main.go | 3 + cmd/internal/update_certificates/main.go | 38 +- common/certificate/chrome.go | 1900 ++++---- common/certificate/chrome.pem | 2650 ++++++++++ common/certificate/mozilla.go | 1358 ++---- common/certificate/mozilla.pem | 4256 +++++++++++++++++ common/certificate/store.go | 39 +- common/dialer/detour.go | 24 +- common/dialer/dialer.go | 10 +- common/httpclient/apple_transport_darwin.go | 423 ++ common/httpclient/apple_transport_darwin.h | 71 + common/httpclient/apple_transport_darwin.m | 398 ++ .../httpclient/apple_transport_darwin_test.go | 855 ++++ common/httpclient/apple_transport_stub.go | 16 + common/httpclient/client.go | 130 + common/httpclient/context.go | 14 + common/httpclient/helpers.go | 86 + common/httpclient/http1_transport.go | 42 + common/httpclient/http2_config.go | 42 + common/httpclient/http2_fallback_transport.go | 84 + common/httpclient/http2_transport.go | 52 + common/httpclient/http3_transport.go | 297 ++ common/httpclient/http3_transport_stub.go | 30 + common/httpclient/managed_transport.go | 209 + common/httpclient/manager.go | 175 + common/proxybridge/bridge.go | 115 + common/tls/apple_client.go | 218 + common/tls/apple_client_platform.go | 517 ++ common/tls/apple_client_platform_darwin.h | 39 + common/tls/apple_client_platform_darwin.m | 667 +++ common/tls/apple_client_platform_test.go | 453 ++ common/tls/apple_client_stub.go | 15 + common/tls/client.go | 29 +- common/tls/reality_client.go | 14 +- common/tls/reality_server.go | 25 +- common/tls/server.go | 7 +- common/tls/std_client.go | 102 +- common/tls/std_server.go | 25 +- common/tls/utls_client.go | 73 +- common/tls/utls_stub.go | 8 + constant/tls.go | 5 + dns/transport/https.go | 5 +- docs/configuration/endpoint/tailscale.md | 18 +- docs/configuration/endpoint/tailscale.zh.md | 18 +- docs/configuration/inbound/hysteria.md | 43 +- docs/configuration/inbound/hysteria.zh.md | 43 +- docs/configuration/inbound/hysteria2.md | 7 + docs/configuration/inbound/hysteria2.zh.md | 7 + docs/configuration/inbound/tuic.md | 10 +- docs/configuration/inbound/tuic.zh.md | 10 +- docs/configuration/index.md | 2 + docs/configuration/index.zh.md | 2 + docs/configuration/outbound/hysteria.md | 57 +- docs/configuration/outbound/hysteria.zh.md | 56 +- docs/configuration/outbound/hysteria2.md | 7 + docs/configuration/outbound/hysteria2.zh.md | 7 + docs/configuration/outbound/tuic.md | 8 +- docs/configuration/outbound/tuic.zh.md | 8 +- docs/configuration/route/index.md | 10 + docs/configuration/route/index.zh.md | 10 + docs/configuration/rule-set/index.md | 31 +- docs/configuration/rule-set/index.zh.md | 31 +- docs/configuration/service/derp.md | 6 +- docs/configuration/service/derp.zh.md | 4 +- .../shared/certificate-provider/acme.md | 10 +- .../shared/certificate-provider/acme.zh.md | 10 +- .../cloudflare-origin-ca.md | 8 +- .../cloudflare-origin-ca.zh.md | 8 +- docs/configuration/shared/http-client.md | 114 + docs/configuration/shared/http-client.zh.md | 114 + docs/configuration/shared/http2.md | 43 + docs/configuration/shared/http2.zh.md | 43 + docs/configuration/shared/quic.md | 30 + docs/configuration/shared/quic.zh.md | 30 + docs/configuration/shared/tls.md | 57 + docs/configuration/shared/tls.zh.md | 56 + docs/deprecated.md | 21 + docs/deprecated.zh.md | 21 + experimental/deprecated/constants.go | 27 + go.mod | 2 +- go.sum | 4 +- mkdocs.yml | 3 + option/acme.go | 2 +- option/http.go | 126 + option/hysteria.go | 55 +- option/hysteria2.go | 2 + option/options.go | 19 + option/origin_ca.go | 2 +- option/route.go | 1 + option/rule_set.go | 4 +- option/tailscale.go | 33 +- option/tls.go | 3 + option/tuic.go | 2 + protocol/hysteria/inbound.go | 8 +- protocol/hysteria/outbound.go | 28 +- protocol/hysteria/quic.go | 49 + protocol/hysteria2/inbound.go | 24 +- protocol/hysteria2/outbound.go | 14 +- protocol/tailscale/certificate_provider.go | 3 - protocol/tailscale/endpoint.go | 37 +- protocol/tor/outbound.go | 13 +- protocol/tor/proxy.go | 121 - protocol/tuic/inbound.go | 16 +- protocol/tuic/outbound.go | 18 +- route/router.go | 13 +- route/rule/rule_set.go | 2 +- route/rule/rule_set_remote.go | 92 +- service/acme/service.go | 39 +- service/derp/service.go | 32 +- service/origin_ca/service.go | 49 +- 113 files changed, 14910 insertions(+), 2539 deletions(-) create mode 100644 adapter/http.go create mode 100644 common/certificate/chrome.pem create mode 100644 common/certificate/mozilla.pem create mode 100644 common/httpclient/apple_transport_darwin.go create mode 100644 common/httpclient/apple_transport_darwin.h create mode 100644 common/httpclient/apple_transport_darwin.m create mode 100644 common/httpclient/apple_transport_darwin_test.go create mode 100644 common/httpclient/apple_transport_stub.go create mode 100644 common/httpclient/client.go create mode 100644 common/httpclient/context.go create mode 100644 common/httpclient/helpers.go create mode 100644 common/httpclient/http1_transport.go create mode 100644 common/httpclient/http2_config.go create mode 100644 common/httpclient/http2_fallback_transport.go create mode 100644 common/httpclient/http2_transport.go create mode 100644 common/httpclient/http3_transport.go create mode 100644 common/httpclient/http3_transport_stub.go create mode 100644 common/httpclient/managed_transport.go create mode 100644 common/httpclient/manager.go create mode 100644 common/proxybridge/bridge.go create mode 100644 common/tls/apple_client.go create mode 100644 common/tls/apple_client_platform.go create mode 100644 common/tls/apple_client_platform_darwin.h create mode 100644 common/tls/apple_client_platform_darwin.m create mode 100644 common/tls/apple_client_platform_test.go create mode 100644 common/tls/apple_client_stub.go create mode 100644 docs/configuration/shared/http-client.md create mode 100644 docs/configuration/shared/http-client.zh.md create mode 100644 docs/configuration/shared/http2.md create mode 100644 docs/configuration/shared/http2.zh.md create mode 100644 docs/configuration/shared/quic.md create mode 100644 docs/configuration/shared/quic.zh.md create mode 100644 option/http.go create mode 100644 protocol/hysteria/quic.go delete mode 100644 protocol/tor/proxy.go diff --git a/adapter/http.go b/adapter/http.go new file mode 100644 index 0000000000..3de4f1ce33 --- /dev/null +++ b/adapter/http.go @@ -0,0 +1,43 @@ +package adapter + +import ( + "context" + "net/http" + "sync" + + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/logger" +) + +type HTTPTransport interface { + http.RoundTripper + CloseIdleConnections() + Reset() +} + +type HTTPClientManager interface { + ResolveTransport(ctx context.Context, logger logger.ContextLogger, options option.HTTPClientOptions) (HTTPTransport, error) + DefaultTransport() HTTPTransport + ResetNetwork() +} + +type HTTPStartContext struct { + access sync.Mutex + transports []HTTPTransport +} + +func NewHTTPStartContext() *HTTPStartContext { + return &HTTPStartContext{} +} + +func (c *HTTPStartContext) Register(transport HTTPTransport) { + c.access.Lock() + defer c.access.Unlock() + c.transports = append(c.transports, transport) +} + +func (c *HTTPStartContext) Close() { + for _, transport := range c.transports { + transport.CloseIdleConnections() + } +} diff --git a/adapter/router.go b/adapter/router.go index f1e3da9a0c..26f4612578 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -2,17 +2,11 @@ package adapter import ( "context" - "crypto/tls" "net" - "net/http" - "sync" "time" - C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-tun" - M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" - "github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/x/list" "go4.org/netipx" @@ -77,46 +71,3 @@ type RuleSetMetadata struct { ContainsIPCIDRRule bool ContainsDNSQueryTypeRule bool } -type HTTPStartContext struct { - ctx context.Context - access sync.Mutex - httpClientCache map[string]*http.Client -} - -func NewHTTPStartContext(ctx context.Context) *HTTPStartContext { - return &HTTPStartContext{ - ctx: ctx, - httpClientCache: make(map[string]*http.Client), - } -} - -func (c *HTTPStartContext) HTTPClient(detour string, dialer N.Dialer) *http.Client { - c.access.Lock() - defer c.access.Unlock() - if httpClient, loaded := c.httpClientCache[detour]; loaded { - return httpClient - } - httpClient := &http.Client{ - Transport: &http.Transport{ - ForceAttemptHTTP2: true, - TLSHandshakeTimeout: C.TCPTimeout, - DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) - }, - TLSClientConfig: &tls.Config{ - Time: ntp.TimeFuncFromContext(c.ctx), - RootCAs: RootPoolFromContext(c.ctx), - }, - }, - } - c.httpClientCache[detour] = httpClient - return httpClient -} - -func (c *HTTPStartContext) Close() { - c.access.Lock() - defer c.access.Unlock() - for _, client := range c.httpClientCache { - client.CloseIdleConnections() - } -} diff --git a/box.go b/box.go index c5b3fc68c2..5a0868238f 100644 --- a/box.go +++ b/box.go @@ -16,12 +16,14 @@ import ( boxService "github.com/sagernet/sing-box/adapter/service" "github.com/sagernet/sing-box/common/certificate" "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/httpclient" "github.com/sagernet/sing-box/common/taskmonitor" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/experimental" "github.com/sagernet/sing-box/experimental/cachefile" + "github.com/sagernet/sing-box/experimental/deprecated" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/protocol/direct" @@ -50,6 +52,7 @@ type Box struct { dnsRouter *dns.Router connection *route.ConnectionManager router *route.Router + httpClientService adapter.LifecycleService internalService []adapter.LifecycleService done chan struct{} } @@ -169,6 +172,7 @@ func New(options Options) (*Box, error) { } var internalServices []adapter.LifecycleService + routeOptions := common.PtrValueOrDefault(options.Route) certificateOptions := common.PtrValueOrDefault(options.Certificate) if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem || len(certificateOptions.Certificate) > 0 || @@ -181,8 +185,6 @@ func New(options Options) (*Box, error) { service.MustRegister[adapter.CertificateStore](ctx, certificateStore) internalServices = append(internalServices, certificateStore) } - - routeOptions := common.PtrValueOrDefault(options.Route) dnsOptions := common.PtrValueOrDefault(options.DNS) endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry) inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager) @@ -209,6 +211,10 @@ func New(options Options) (*Box, error) { service.MustRegister[adapter.NetworkManager](ctx, networkManager) connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection")) service.MustRegister[adapter.ConnectionManager](ctx, connectionManager) + // Must register after ConnectionManager: the Apple HTTP engine's proxy bridge reads it from the context when Manager.Start resolves the default client. + httpClientManager := httpclient.NewManager(ctx, logFactory.NewLogger("httpclient"), options.HTTPClients, routeOptions.DefaultHTTPClient) + service.MustRegister[adapter.HTTPClientManager](ctx, httpClientManager) + httpClientService := adapter.LifecycleService(httpClientManager) router := route.NewRouter(ctx, logFactory, routeOptions, dnsOptions) service.MustRegister[adapter.Router](ctx, router) err = router.Initialize(routeOptions.Rules, routeOptions.RuleSet) @@ -368,6 +374,12 @@ func New(options Options) (*Box, error) { &option.LocalDNSServerOptions{}, ) }) + httpClientManager.Initialize(func() (*httpclient.ManagedTransport, error) { + deprecated.Report(ctx, deprecated.OptionImplicitDefaultHTTPClient) + var httpClientOptions option.HTTPClientOptions + httpClientOptions.DefaultOutbound = true + return httpclient.NewTransport(ctx, logFactory.NewLogger("httpclient"), "", httpClientOptions) + }) if platformInterface != nil { err = platformInterface.Initialize(networkManager) if err != nil { @@ -428,6 +440,7 @@ func New(options Options) (*Box, error) { dnsRouter: dnsRouter, connection: connectionManager, router: router, + httpClientService: httpClientService, createdAt: createdAt, logFactory: logFactory, logger: logFactory.Logger(), @@ -490,7 +503,15 @@ func (s *Box) preStart() error { if err != nil { return err } - err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.network, s.connection, s.router, s.dnsRouter) + err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.network, s.connection) + if err != nil { + return err + } + err = adapter.StartNamed(s.logger, adapter.StartStateStart, []adapter.LifecycleService{s.httpClientService}) + if err != nil { + return err + } + err = adapter.Start(s.logger, adapter.StartStateStart, s.router, s.dnsRouter) if err != nil { return err } @@ -566,6 +587,14 @@ func (s *Box) Close() error { }) done() } + if s.httpClientService != nil { + s.logger.Trace("close ", s.httpClientService.Name()) + startTime := time.Now() + err = E.Append(err, s.httpClientService.Close(), func(err error) error { + return E.Cause(err, "close ", s.httpClientService.Name()) + }) + s.logger.Trace("close ", s.httpClientService.Name(), " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + } for _, lifecycleService := range s.internalService { done := adapter.LogElapsed(s.logger, "close ", lifecycleService.Name()) err = E.Append(err, lifecycleService.Close(), func(err error) error { diff --git a/cmd/internal/build_libbox/main.go b/cmd/internal/build_libbox/main.go index c128216932..0f914499e0 100644 --- a/cmd/internal/build_libbox/main.go +++ b/cmd/internal/build_libbox/main.go @@ -204,6 +204,9 @@ func buildApple() { "-target", bindTarget, "-libname=box", "-tags-not-macos=with_low_memory", + "-iosversion=15.0", + "-macosversion=13.0", + "-tvosversion=17.0", } //if !withTailscale { // args = append(args, "-tags-macos="+strings.Join(memcTags, ",")) diff --git a/cmd/internal/update_certificates/main.go b/cmd/internal/update_certificates/main.go index 03323c0661..d19eb7f1cb 100644 --- a/cmd/internal/update_certificates/main.go +++ b/cmd/internal/update_certificates/main.go @@ -43,11 +43,8 @@ func updateMozillaIncludedRootCAs() error { package certificate -import "crypto/x509" - -func newMozillaIncluded() *x509.CertPool { - pool := x509.NewCertPool() -`) +func mozillaIncludedPEM() string { + return ` + "`") for { record, err := reader.Read() if err == io.EOF { @@ -58,17 +55,14 @@ func newMozillaIncluded() *x509.CertPool { if record[geoIndex] == "China" { continue } - generated.WriteString("\n // ") + cert := strings.Trim(record[certIndex], "'") + generated.WriteString("\n// ") generated.WriteString(record[nameIndex]) generated.WriteString("\n") - generated.WriteString(" pool.AppendCertsFromPEM([]byte(`") - cert := record[certIndex] - // Remove single quotes - cert = cert[1 : len(cert)-1] generated.WriteString(cert) - generated.WriteString("`))\n") + generated.WriteString("\n") } - generated.WriteString("\treturn pool\n}\n") + generated.WriteString("`\n}\n") return os.WriteFile("common/certificate/mozilla.go", []byte(generated.String()), 0o644) } @@ -127,11 +121,8 @@ func updateChromeIncludedRootCAs() error { package certificate -import "crypto/x509" - -func newChromeIncluded() *x509.CertPool { - pool := x509.NewCertPool() -`) +func chromeIncludedPEM() string { + return ` + "`") for { record, err := reader.Read() if err == io.EOF { @@ -145,18 +136,13 @@ func newChromeIncluded() *x509.CertPool { if chinaFingerprints[record[fingerprintIndex]] { continue } - generated.WriteString("\n // ") + cert := strings.Trim(record[certIndex], "'") + generated.WriteString("\n// ") generated.WriteString(record[subjectIndex]) generated.WriteString("\n") - generated.WriteString(" pool.AppendCertsFromPEM([]byte(`") - cert := record[certIndex] - // Remove single quotes if present - if len(cert) > 0 && cert[0] == '\'' { - cert = cert[1 : len(cert)-1] - } generated.WriteString(cert) - generated.WriteString("`))\n") + generated.WriteString("\n") } - generated.WriteString("\treturn pool\n}\n") + generated.WriteString("`\n}\n") return os.WriteFile("common/certificate/chrome.go", []byte(generated.String()), 0o644) } diff --git a/common/certificate/chrome.go b/common/certificate/chrome.go index ea341b0c52..220462561b 100644 --- a/common/certificate/chrome.go +++ b/common/certificate/chrome.go @@ -2,13 +2,10 @@ package certificate -import "crypto/x509" - -func newChromeIncluded() *x509.CertPool { - pool := x509.NewCertPool() - - // CN=Actalis Authentication Root CA; O=Actalis S.p.A./03358520967; L=Milan; C=IT - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +func chromeIncludedPEM() string { + return ` +// CN=Actalis Authentication Root CA; O=Actalis S.p.A./03358520967; L=Milan; C=IT +-----BEGIN CERTIFICATE----- MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 @@ -40,10 +37,10 @@ K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=TunTrust Root CA; O=Agence Nationale de Certification Electronique; C=TN - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=TunTrust Root CA; O=Agence Nationale de Certification Electronique; C=TN +-----BEGIN CERTIFICATE----- MIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQEL BQAwYTELMAkGA1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUg Q2VydGlmaWNhdGlvbiBFbGVjdHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJv @@ -75,10 +72,43 @@ nV2UqL1g52KAdoGDDIzMMEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQ CvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9zZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZH u/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3rAZ3r2OvEhJn7wAzMMujj d9qDRIueVSjAi1jTkD5OGwDxFa2DK5o= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- + +// CN=Amazon Root CA 2; O=Amazon; C=US +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK +gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ +W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg +1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K +8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r +2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me +z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR +8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj +mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz +7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6 ++XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI +0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm +UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2 +LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY ++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS +k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl +7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm +btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl +urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+ +fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63 +n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE +76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H +9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT +4PsJYGw= +-----END CERTIFICATE----- - // CN=Amazon Root CA 4; O=Amazon; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=Amazon Root CA 4; O=Amazon; C=US +-----BEGIN CERTIFICATE----- MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5 MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG @@ -90,10 +120,10 @@ M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW 1KyLa2tJElMzrdfkviT8tQp21KW8EA== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=Amazon Root CA 1; O=Amazon; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=Amazon Root CA 1; O=Amazon; C=US +-----BEGIN CERTIFICATE----- MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL @@ -112,43 +142,10 @@ N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU 5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy rqXRfboQnoZsG4q5WTP468SQvvG5 ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=Amazon Root CA 2; O=Amazon; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF -ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 -b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL -MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv -b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK -gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ -W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg -1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K -8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r -2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me -z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR -8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj -mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz -7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6 -+XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI -0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB -Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm -UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2 -LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY -+gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS -k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl -7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm -btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl -urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+ -fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63 -n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE -76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H -9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT -4PsJYGw= ------END CERTIFICATE-----`)) - - // CN=Amazon Root CA 3; O=Amazon; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=Amazon Root CA 3; O=Amazon; C=US +-----BEGIN CERTIFICATE----- MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5 MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG @@ -159,10 +156,10 @@ QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM YyRIHN8wfdVoOw== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=Certum Trusted Network CA; OU=Certum Certification Authority; O=Unizeto Technologies S.A.; C=PL - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=Certum Trusted Network CA; OU=Certum Certification Authority; O=Unizeto Technologies S.A.; C=PL +-----BEGIN CERTIFICATE----- MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU @@ -183,10 +180,10 @@ I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8 J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI 03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=Certum EC-384 CA; OU=Certum Certification Authority; O=Asseco Data Systems S.A.; C=PL - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=Certum EC-384 CA; OU=Certum Certification Authority; O=Asseco Data Systems S.A.; C=PL +-----BEGIN CERTIFICATE----- MIICZTCCAeugAwIBAgIQeI8nXIESUiClBNAt3bpz9DAKBggqhkjOPQQDAzB0MQsw CQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScw JQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAXBgNVBAMT @@ -200,10 +197,10 @@ QjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI0GZnQkdjrzife81r1HfS+8 EF9LMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjADVS2m5hjEfO/J UG7BJw+ch69u1RsIGL2SKcHvlJF40jocVYli5RsJHrpka/F2tNQCMQC0QoSZ/6vn nvuRlydd3LBbMHHOXjgaatkl5+r3YZJW+OraNsKHZZYuciUvf9/DE8k= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=Certum Trusted Root CA; OU=Certum Certification Authority; O=Asseco Data Systems S.A.; C=PL - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=Certum Trusted Root CA; OU=Certum Certification Authority; O=Asseco Data Systems S.A.; C=PL +-----BEGIN CERTIFICATE----- MIIFwDCCA6igAwIBAgIQHr9ZULjJgDdMBvfrVU+17TANBgkqhkiG9w0BAQ0FADB6 MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu MScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxHzAdBgNV @@ -235,10 +232,10 @@ P78v3DSk+yshzWePS/Tj6tQ/50+6uaWTRRxmHyH6ZF5v4HaUMst19W7l9o/HuKTM qJZ9ZPskWkoDbGs4xugDQ5r3V7mzKWmTOPQD8rv7gmsHINFSH5pkAnuYZttcTVoP 0ISVoDwUQwbKytu4QTbaakRnh6+v40URFWkIsr4WOZckbxJF0WddCajJFdr60qZf E2Efv4WstK2tBZQIgx51F9NxO5NQI1mg7TyRVJ12AMXDuDjb ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=Certum Trusted Network CA 2; OU=Certum Certification Authority; O=Unizeto Technologies S.A.; C=PL - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=Certum Trusted Network CA 2; OU=Certum Certification Authority; O=Unizeto Technologies S.A.; C=PL +-----BEGIN CERTIFICATE----- MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCB gDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu QS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG @@ -271,10 +268,10 @@ b7a6bINDd82Kkhehnlt4Fj1F4jNy3eFmypnTycUm/Q1oBEauttmbjL4ZvrHG8hnj XALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P 5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi DrW5viSP ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=Autoridad de Certificacion Firmaprofesional CIF A62634068; C=ES - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=Autoridad de Certificacion Firmaprofesional CIF A62634068; C=ES +-----BEGIN CERTIFICATE----- MIIGFDCCA/ygAwIBAgIIG3Dp0v+ubHEwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UE BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0xNDA5MjMxNTIyMDdaFw0zNjA1 @@ -308,10 +305,10 @@ CsvMo2ebKHTEm9caPARYpoKdrcd7b/+Alun4jWq9GJAd/0kakFI3ky88Al2CdgtR f9DEMnDAGf/JOC0ULGb0QkTmVXYbgBVX/8Cnp6o5qtjTcNAuuuuUavpfNIbnYrX9 ivAwhZTJryQCL2/W3Wf+47BVTwSYT6RBVuKT0Gro1vP7ZeDOdcQxWQzugsgMYDNK GbqEZycPvEJdvSRUDewdcAZfpLz6IHxV ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=ANF Secure Server Root CA; OU=ANF CA Raiz; O=ANF Autoridad de Certificacion; C=ES; SerialNumber=G63287510 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=ANF Secure Server Root CA; OU=ANF CA Raiz; O=ANF Autoridad de Certificacion; C=ES; SerialNumber=G63287510 +-----BEGIN CERTIFICATE----- MIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNV BAUTCUc2MzI4NzUxMDELMAkGA1UEBhMCRVMxJzAlBgNVBAoTHkFORiBBdXRvcmlk YWQgZGUgQ2VydGlmaWNhY2lvbjEUMBIGA1UECxMLQU5GIENBIFJhaXoxIjAgBgNV @@ -344,10 +341,10 @@ OknkJjCb5RyKqKkVMoaZkgoQI1YS4PbOTOK7vtuNknMBZi9iPrJyJ0U27U1W45eZ +PbZa/wSMVYIwFNCr5zQM378BvAxRAMU8Vjq8moNqRGyg77FGr8H6lnco4g175x2 MjxNBiLOFeXdntiP2t7SxDnlF4HPOEfrf4htWRvfn0IUrn7PqLBmZdo3r5+qPeoo tt7VMVgWglvquxl1AnMaykgaIZOQCo6ThKd9OyMYkomgjaw= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=Buypass Class 2 Root CA; O=Buypass AS-983163327; C=NO - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=Buypass Class 2 Root CA; O=Buypass AS-983163327; C=NO +-----BEGIN CERTIFICATE----- MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow @@ -377,10 +374,10 @@ I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h 3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=Buypass Class 3 Root CA; O=Buypass AS-983163327; C=NO - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=Buypass Class 3 Root CA; O=Buypass AS-983163327; C=NO +-----BEGIN CERTIFICATE----- MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow @@ -410,10 +407,10 @@ kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41 3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq 4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=Certainly Root R1; O=Certainly; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=Certainly Root R1; O=Certainly; C=US +-----BEGIN CERTIFICATE----- MIIFRzCCAy+gAwIBAgIRAI4P+UuQcWhlM1T01EQ5t+AwDQYJKoZIhvcNAQELBQAw PTELMAkGA1UEBhMCVVMxEjAQBgNVBAoTCUNlcnRhaW5seTEaMBgGA1UEAxMRQ2Vy dGFpbmx5IFJvb3QgUjEwHhcNMjEwNDAxMDAwMDAwWhcNNDYwNDAxMDAwMDAwWjA9 @@ -443,10 +440,10 @@ Kx3upPvaJVQTA945xsMfTZDsjxtK0hzthZU4UHlG1sGQUDGpXJpuHfUzVounmdLy yCwzk5Iwx06MZTMQZBf9JBeW0Y3COmor6xOLRPIh80oat3df1+2IpHLlOR+Vnb5n wXARPbv0+Em34yaXOp/SX3z7wJl8OSngex2/DaeP0ik0biQVy96QXr8axGbqwua6 OV+KmalBWQewLK8= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=Certainly Root E1; O=Certainly; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=Certainly Root E1; O=Certainly; C=US +-----BEGIN CERTIFICATE----- MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQsw CQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlu bHkgUm9vdCBFMTAeFw0yMTA0MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJ @@ -458,34 +455,10 @@ BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4 hevIIgcwCgYIKoZIzj0EAwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozm ut6Dacpps6kFtZaSF4fC0urQe87YQVt8rgIwRt7qy12a7DLCZRawTDBcMPPaTnOG BtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR ------END CERTIFICATE-----`)) - - // CN=Certigna; O=Dhimyotis; C=FR - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV -BAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4X -DTA3MDYyOTE1MTMwNVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQ -BgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwIQ2VydGlnbmEwggEiMA0GCSqGSIb3 -DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7qXOEm7RFHYeGifBZ4 -QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyHGxny -gQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbw -zBfsV1/pogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q -130yGLMLLGq/jj8UEYkgDncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2 -JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKfIrjxwo1p3Po6WAbfAgMBAAGjgbwwgbkw -DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQtCRZvgHyUtVF9lo53BEw -ZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJBgNVBAYT -AkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzj -AQ/JSP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG -9w0BAQUFAAOCAQEAhQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8h -bV6lUmPOEvjvKtpv6zf+EwLHyzs+ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFnc -fca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9qcEQCYsuu -HWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w -t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw -WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== ------END CERTIFICATE-----`)) - - // CN=Certigna Root CA; OU=0002 48146308100036; O=Dhimyotis; C=FR - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +-----END CERTIFICATE----- + +// CN=Certigna Root CA; OU=0002 48146308100036; O=Dhimyotis; C=FR +-----BEGIN CERTIFICATE----- MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAw WjELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAw MiA0ODE0NjMwODEwMDAzNjEZMBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0x @@ -520,32 +493,10 @@ faci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaYtlu3zM63 Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayh jWZSaX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw 3kAP+HwV96LOPNdeE4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0= ------END CERTIFICATE-----`)) - - // OU=certSIGN ROOT CA; O=certSIGN; C=RO - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYT -AlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBD -QTAeFw0wNjA3MDQxNzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJP -MREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCC -ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7IJUqOtdu0KBuqV5Do -0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHHrfAQ -UySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5d -RdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQ -OA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv -JoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08C -AwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0O -BBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJ -LjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecY -MnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ -44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6I -Jd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNw -i/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN -9u6wWk5JRFRYX0KD ------END CERTIFICATE-----`)) - - // OU=certSIGN ROOT CA G2; O=CERTSIGN SA; C=RO - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +-----END CERTIFICATE----- + +// OU=certSIGN ROOT CA G2; O=CERTSIGN SA; C=RO +-----BEGIN CERTIFICATE----- MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV BAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04g Uk9PVCBDQSBHMjAeFw0xNzAyMDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJ @@ -575,10 +526,10 @@ pwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tUSxfj PuXaTH4MNMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE 1LlSVHJ7liXMvGnjSG4N0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MX QRBdJ3NghVdJIgc= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=HiPKI Root CA - G1; O=Chunghwa Telecom Co., Ltd.; C=TW - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=HiPKI Root CA - G1; O=Chunghwa Telecom Co., Ltd.; C=TW +-----BEGIN CERTIFICATE----- MIIFajCCA1KgAwIBAgIQLd2szmKXlKFD6LDNdmpeYDANBgkqhkiG9w0BAQsFADBP MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 ZC4xGzAZBgNVBAMMEkhpUEtJIFJvb3QgQ0EgLSBHMTAeFw0xOTAyMjIwOTQ2MDRa @@ -608,10 +559,10 @@ Ne85akEez3GoorKGB1s6yeHvP2UEgEcyRHCVTjFnanRbEEV16rCf0OY1/k6fi8wr kkVbbiVghUbN0aqwdmaTd5a+g744tiROJgvM7XpWGuDpWsZkrUx6AEhEL7lAuxM+ vhV4nYWBSipX3tUZQ9rbyltHhoMLP7YNdnhzeSJesYAfz77RP1YQmCuVh6EfnWQU YDksswBVLuT1sw5XxJFBAJw/6KXf6vb/yPCtbVKoF6ubYfwSUTXkJf2vqmqGOQ== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // OU=ePKI Root Certification Authority; O=Chunghwa Telecom Co., Ltd.; C=TW - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// OU=ePKI Root Certification Authority; O=Chunghwa Telecom Co., Ltd.; C=TW +-----BEGIN CERTIFICATE----- MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 ZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe @@ -643,10 +594,99 @@ o2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS Gp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE W9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D hNQ+IIX3Sj0rnP0qCglN6oH4EZw= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=D-TRUST BR Root CA 1 2020; O=D-Trust GmbH; C=DE - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=D-TRUST Root Class 3 CA 2 2009; O=D-Trust GmbH; C=DE +-----BEGIN CERTIFICATE----- +MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha +ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM +HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 +UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 +tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R +ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM +lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp +/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G +A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G +A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj +dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy +MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl +cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js +L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL +BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni +acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 +o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K +zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 +PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y +Johw1+qRzT65ysCQblrGXnRl11z+o+I= +-----END CERTIFICATE----- + +// CN=D-TRUST EV Root CA 2 2023; O=D-Trust GmbH; C=DE +-----BEGIN CERTIFICATE----- +MIIFqTCCA5GgAwIBAgIQaSYJfoBLTKCnjHhiU19abzANBgkqhkiG9w0BAQ0FADBI +MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE +LVRSVVNUIEVWIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA5MTAzM1oXDTM4MDUw +OTA5MTAzMlowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi +MCAGA1UEAxMZRC1UUlVTVCBFViBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBANiOo4mAC7JXUtypU0w3uX9jFxPvp1sjW2l1sJkK +F8GLxNuo4MwxusLyzV3pt/gdr2rElYfXR8mV2IIEUD2BCP/kPbOx1sWy/YgJ25yE +7CUXFId/MHibaljJtnMoPDT3mfd/06b4HEV8rSyMlD/YZxBTfiLNTiVR8CUkNRFe +EMbsh2aJgWi6zCudR3Mfvc2RpHJqnKIbGKBv7FD0fUDCqDDPvXPIEysQEx6Lmqg6 +lHPTGGkKSv/BAQP/eX+1SH977ugpbzZMlWGG2Pmic4ruri+W7mjNPU0oQvlFKzIb +RlUWaqZLKfm7lVa/Rh3sHZMdwGWyH6FDrlaeoLGPaxK3YG14C8qKXO0elg6DpkiV +jTujIcSuWMYAsoS0I6SWhjW42J7YrDRJmGOVxcttSEfi8i4YHtAxq9107PncjLgc +jmgjutDzUNzPZY9zOjLHfP7KgiJPvo5iR2blzYfi6NUPGJ/lBHJLRjwQ8kTCZFZx +TnXonMkmdMV9WdEKWw9t/p51HBjGGjp82A0EzM23RWV6sY+4roRIPrN6TagD4uJ+ +ARZZaBhDM7DS3LAaQzXupdqpRlyuhoFBAUp0JuyfBr/CBTdkdXgpaP3F9ev+R/nk +hbDhezGdpn9yo7nELC7MmVcOIQxFAZRl62UJxmMiCzNJkkg8/M3OsD6Onov4/knF +NXJHAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUqvyREBuH +kV8Wub9PS5FeAByxMoAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG +OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfZXZfcm9vdF9jYV8y +XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQCTy6UfmRHsmg1fLBWTxj++EI14 +QvBukEdHjqOSMo1wj/Zbjb6JzkcBahsgIIlbyIIQbODnmaprxiqgYzWRaoUlrRc4 +pZt+UPJ26oUFKidBK7GB0aL2QHWpDsvxVUjY7NHss+jOFKE17MJeNRqrphYBBo7q +3C+jisosketSjl8MmxfPy3MHGcRqwnNU73xDUmPBEcrCRbH0O1P1aa4846XerOhU +t7KR/aypH/KH5BfGSah82ApB9PI+53c0BFLd6IHyTS9URZ0V4U/M5d40VxDJI3IX +cI1QcB9WbMy5/zpaT2N6w25lBx2Eof+pDGOJbbJAiDnXH3dotfyc1dZnaVuodNv8 +ifYbMvekJKZ2t0dT741Jj6m2g1qllpBFYfXeA08mD6iL8AOWsKwV0HFaanuU5nCT +2vFp4LJiTZ6P/4mdm13NRemUAiKN4DV/6PEEeXFsVIP4M7kFMhtYVRFP0OUnR3Hs +7dpn1mKmS00PaaLJvOwiS5THaJQXfuKOKD62xur1NGyfN4gHONuGcfrNlUhDbqNP +gofXNJhuS5N5YHVpD/Aa1VP6IQzCP+k/HxiMkl14p3ZnGbuy6n/pcAlWVqOwDAst +Nl7F6cTVg8uGF5csbBNvh1qvSaYd2804BC5f4ko1Di1L+KIkBI3Y4WNeApI02phh +XBxvWHZks/wCuPWdCg== +-----END CERTIFICATE----- + +// CN=D-TRUST Root Class 3 CA 2 EV 2009; O=D-Trust GmbH; C=DE +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw +NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV +BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn +ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 +3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z +qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR +p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 +HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw +ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea +HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw +Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh +c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E +RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt +dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku +Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp +3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 +nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF +CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na +xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX +KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 +-----END CERTIFICATE----- + +// CN=D-TRUST BR Root CA 1 2020; O=D-Trust GmbH; C=DE +-----BEGIN CERTIFICATE----- MIIC2zCCAmCgAwIBAgIQfMmPK4TX3+oPyWWa00tNljAKBggqhkjOPQQDAzBIMQsw CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS VVNUIEJSIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTA5NDUwMFoXDTM1MDIxMTA5 @@ -663,10 +703,10 @@ c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO PQQDAwNpADBmAjEAlJAtE/rhY/hhY+ithXhUkZy4kzg+GkHaQBZTQgjKL47xPoFW wKrY7RjEsK70PvomAjEA8yjixtsrmfu3Ubgko6SUeho/5jbiA1czijDLgsfWFBHV dWNbFJWcHwHP2NVypw87 ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=D-TRUST EV Root CA 1 2020; O=D-Trust GmbH; C=DE - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=D-TRUST EV Root CA 1 2020; O=D-Trust GmbH; C=DE +-----BEGIN CERTIFICATE----- MIIC2zCCAmCgAwIBAgIQXwJB13qHfEwDo6yWjfv/0DAKBggqhkjOPQQDAzBIMQsw CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS VVNUIEVWIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTEwMDAwMFoXDTM1MDIxMTA5 @@ -683,89 +723,45 @@ c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO PQQDAwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CA y/m0sRtW9XLS/BnRAjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJb gfM0agPnIjhQW+0ZT0MW ------END CERTIFICATE-----`)) - - // CN=D-TRUST Root Class 3 CA 2 EV 2009; O=D-Trust GmbH; C=DE - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF -MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD -bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw -NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV -BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn -ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 -3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z -qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR -p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 -HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw -ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea -HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw -Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh -c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E -RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt -dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku -Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp -3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 -nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF -CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na -xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX -KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 ------END CERTIFICATE-----`)) - - // CN=D-TRUST Root Class 3 CA 2 2009; O=D-Trust GmbH; C=DE - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF -MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD -bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha -ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM -HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB -BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 -UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 -tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R -ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM -lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp -/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G -A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G -A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj -dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy -MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl -cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js -L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL -BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni -acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 -o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K -zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 -PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y -Johw1+qRzT65ysCQblrGXnRl11z+o+I= ------END CERTIFICATE-----`)) - - // CN=T-TeleSec GlobalRoot Class 3; OU=T-Systems Trust Center; O=T-Systems Enterprise Services GmbH; C=DE - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx -KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd -BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl -YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 -OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy -aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 -ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G -CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN -8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ -RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 -hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 -ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM -EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj -QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 -A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy -WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ -1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 -6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT -91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml -e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p -TpPDpFQUWw== ------END CERTIFICATE-----`)) - - // CN=T-TeleSec GlobalRoot Class 2; OU=T-Systems Trust Center; O=T-Systems Enterprise Services GmbH; C=DE - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +-----END CERTIFICATE----- + +// CN=D-TRUST BR Root CA 2 2023; O=D-Trust GmbH; C=DE +-----BEGIN CERTIFICATE----- +MIIFqTCCA5GgAwIBAgIQczswBEhb2U14LnNLyaHcZjANBgkqhkiG9w0BAQ0FADBI +MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE +LVRSVVNUIEJSIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA4NTYzMVoXDTM4MDUw +OTA4NTYzMFowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi +MCAGA1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAK7/CVmRgApKaOYkP7in5Mg6CjoWzckjYaCTcfKr +i3OPoGdlYNJUa2NRb0kz4HIHE304zQaSBylSa053bATTlfrdTIzZXcFhfUvnKLNE +gXtRr90zsWh81k5M/itoucpmacTsXld/9w3HnDY25QdgrMBM6ghs7wZ8T1soegj8 +k12b9py0i4a6Ibn08OhZWiihNIQaJZG2tY/vsvmA+vk9PBFy2OMvhnbFeSzBqZCT +Rphny4NqoFAjpzv2gTng7fC5v2Xx2Mt6++9zA84A9H3X4F07ZrjcjrqDy4d2A/wl +2ecjbwb9Z/Pg/4S8R7+1FhhGaRTMBffb00msa8yr5LULQyReS2tNZ9/WtT5PeB+U +cSTq3nD88ZP+npNa5JRal1QMNXtfbO4AHyTsA7oC9Xb0n9Sa7YUsOCIvx9gvdhFP +/Wxc6PWOJ4d/GUohR5AdeY0cW/jPSoXk7bNbjb7EZChdQcRurDhaTyN0dKkSw/bS +uREVMweR2Ds3OmMwBtHFIjYoYiMQ4EbMl6zWK11kJNXuHA7e+whadSr2Y23OC0K+ +0bpwHJwh5Q8xaRfX/Aq03u2AnMuStIv13lmiWAmlY0cL4UEyNEHZmrHZqLAbWt4N +DfTisl01gLmB1IRpkQLLddCNxbU9CZEJjxShFHR5PtbJFR2kWVki3PaKRT08EtY+ +XTIvAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUZ5Dw1t61 +GNVGKX5cq/ieCLxklRAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG +OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfYnJfcm9vdF9jYV8y +XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQA097N3U9swFrktpSHxQCF16+tI +FoE9c+CeJyrrd6kTpGoKWloUMz1oH4Guaf2Mn2VsNELZLdB/eBaxOqwjMa1ef67n +riv6uvw8l5VAk1/DLQOj7aRvU9f6QA4w9QAgLABMjDu0ox+2v5Eyq6+SmNMW5tTR +VFxDWy6u71cqqLRvpO8NVhTaIasgdp4D/Ca4nj8+AybmTNudX0KEPUUDAxxZiMrc +LmEkWqTqJwtzEr5SswrPMhfiHocaFpVIbVrg0M8JkiZmkdijYQ6qgYF/6FKC0ULn +4B0Y+qSFNueG4A3rvNTJ1jxD8V1Jbn6Bm2m1iWKPiFLY1/4nwSPFyysCu7Ff/vtD +hQNGvl3GyiEm/9cCnnRK3PgTFbGBVzbLZVzRHTF36SXDw7IyN9XxmAnkbWOACKsG +koHU6XCPpz+y7YaMgmo1yEJagtFSGkUPFaUA8JR7ZSdXOUPPfH/mvTWze/EZTN46 +ls/pdu4D58JDUjxqgejBWoC9EV2Ta/vH5mQ/u2kc6d0li690yVRAysuTEwrt+2aS +Ecr1wPrYg1UDfNPFIkZ1cGt5SAYqgpq/5usWDiJFAbzdNpQ0qTUmiteXue4Icr80 +knCDgKs4qllo3UCkGJCy89UDyibK79XH4I9TjvAA46jtn/mtd+ArY0+ew+43u3gJ +hJ65bvspmZDogNOfJA== +-----END CERTIFICATE----- + +// CN=T-TeleSec GlobalRoot Class 2; OU=T-Systems Trust Center; O=T-Systems Enterprise Services GmbH; C=DE +-----BEGIN CERTIFICATE----- MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl @@ -787,10 +783,87 @@ IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6 g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN 9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP BSeOE6Fuwg== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- + +// CN=Telekom Security TLS ECC Root 2020; O=Deutsche Telekom Security GmbH; C=DE +-----BEGIN CERTIFICATE----- +MIICQjCCAcmgAwIBAgIQNjqWjMlcsljN0AFdxeVXADAKBggqhkjOPQQDAzBjMQsw +CQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBH +bWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBFQ0MgUm9vdCAyMDIw +MB4XDTIwMDgyNTA3NDgyMFoXDTQ1MDgyNTIzNTk1OVowYzELMAkGA1UEBhMCREUx +JzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkGA1UE +AwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgRUNDIFJvb3QgMjAyMDB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABM6//leov9Wq9xCazbzREaK9Z0LMkOsVGJDZos0MKiXrPk/O +tdKPD/M12kOLAoC+b1EkHQ9rK8qfwm9QMuU3ILYg/4gND21Ju9sGpIeQkpT0CdDP +f8iAC8GXs7s1J8nCG6NCMEAwHQYDVR0OBBYEFONyzG6VmUex5rNhTNHLq+O6zd6f +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA +MGQCMHVSi7ekEE+uShCLsoRbQuHmKjYC2qBuGT8lv9pZMo7k+5Dck2TOrbRBR2Di +z6fLHgIwN0GMZt9Ba9aDAEH9L1r3ULRn0SyocddDypwnJJGDSA3PzfdUga/sf+Rn +27iQ7t0l +-----END CERTIFICATE----- + +// CN=Telekom Security TLS RSA Root 2023; O=Deutsche Telekom Security GmbH; C=DE +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIQIZxULej27HF3+k7ow3BXlzANBgkqhkiG9w0BAQwFADBj +MQswCQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0 +eSBHbWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBSU0EgUm9vdCAy +MDIzMB4XDTIzMDMyODEyMTY0NVoXDTQ4MDMyNzIzNTk1OVowYzELMAkGA1UEBhMC +REUxJzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkG +A1UEAwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgUlNBIFJvb3QgMjAyMzCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAO01oYGA88tKaVvC+1GDrib94W7zgRJ9 +cUD/h3VCKSHtgVIs3xLBGYSJwb3FKNXVS2xE1kzbB5ZKVXrKNoIENqil/Cf2SfHV +cp6R+SPWcHu79ZvB7JPPGeplfohwoHP89v+1VmLhc2o0mD6CuKyVU/QBoCcHcqMA +U6DksquDOFczJZSfvkgdmOGjup5czQRxUX11eKvzWarE4GC+j4NSuHUaQTXtvPM6 +Y+mpFEXX5lLRbtLevOP1Czvm4MS9Q2QTps70mDdsipWol8hHD/BeEIvnHRz+sTug +BTNoBUGCwQMrAcjnj02r6LX2zWtEtefdi+zqJbQAIldNsLGyMcEWzv/9FIS3R/qy +8XDe24tsNlikfLMR0cN3f1+2JeANxdKz+bi4d9s3cXFH42AYTyS2dTd4uaNir73J +co4vzLuu2+QVUhkHM/tqty1LkCiCc/4YizWN26cEar7qwU02OxY2kTLvtkCJkUPg +8qKrBC7m8kwOFjQgrIfBLX7JZkcXFBGk8/ehJImr2BrIoVyxo/eMbcgByU/J7MT8 +rFEz0ciD0cmfHdRHNCk+y7AO+oMLKFjlKdw/fKifybYKu6boRhYPluV75Gp6SG12 +mAWl3G0eQh5C2hrgUve1g8Aae3g1LDj1H/1Joy7SWWO/gLCMk3PLNaaZlSJhZQNg ++y+TS/qanIA7AgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtqeX +gj10hZv3PJ+TmpV5dVKMbUcwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS2 +p5eCPXSFm/c8n5OalXl1UoxtRzANBgkqhkiG9w0BAQwFAAOCAgEAqMxhpr51nhVQ +pGv7qHBFfLp+sVr8WyP6Cnf4mHGCDG3gXkaqk/QeoMPhk9tLrbKmXauw1GLLXrtm +9S3ul0A8Yute1hTWjOKWi0FpkzXmuZlrYrShF2Y0pmtjxrlO8iLpWA1WQdH6DErw +M807u20hOq6OcrXDSvvpfeWxm4bu4uB9tPcy/SKE8YXJN3nptT+/XOR0so8RYgDd +GGah2XsjX/GO1WfoVNpbOms2b/mBsTNHM3dA+VKq3dSDz4V4mZqTuXNnQkYRIer+ +CqkbGmVps4+uFrb2S1ayLfmlyOw7YqPta9BO1UAJpB+Y1zqlklkg5LB9zVtzaL1t +xKITDmcZuI1CfmwMmm6gJC3VRRvcxAIU/oVbZZfKTpBQCHpCNfnqwmbU+AGuHrS+ +w6jv/naaoqYfRvaE7fzbzsQCzndILIyy7MMAo+wsVRjBfhnu4S/yrYObnqsZ38aK +L4x35bcF7DvB7L6Gs4a8wPfc5+pbrrLMtTWGS9DiP7bY+A4A7l3j941Y/8+LN+lj +X273CXE2whJdV/LItM3z7gLfEdxquVeEHVlNjM7IDiPCtyaaEBRx/pOyiriA8A4Q +ntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0o82bNSQ3+pCTE4FCxpgm +dTdmQRCsu/WU48IxK63nI1bMNSWSs1A= +-----END CERTIFICATE----- + +// CN=T-TeleSec GlobalRoot Class 3; OU=T-Systems Trust Center; O=T-Systems Enterprise Services GmbH; C=DE +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN +8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ +RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 +hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 +ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM +EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 +A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy +WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ +1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 +6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT +91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml +e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p +TpPDpFQUWw== +-----END CERTIFICATE----- - // CN=DigiCert TLS RSA4096 Root G5; O=DigiCert, Inc.; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=DigiCert TLS RSA4096 Root G5; O=DigiCert, Inc.; C=US +-----BEGIN CERTIFICATE----- MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBN MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMT HERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN @@ -820,10 +893,10 @@ p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilw MUc/dNAUFvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WF qUITVuwhd4GTWgzqltlJyqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCK ovfepEWFJqgejF0pW8hL2JpqA15w8oVPbEtoL8pU9ozaMv7Da4M/OMZ+ ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=DigiCert TLS ECC P384 Root G5; O=DigiCert, Inc.; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=DigiCert TLS ECC P384 Root G5; O=DigiCert, Inc.; C=US +-----BEGIN CERTIFICATE----- MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 @@ -836,34 +909,76 @@ B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 DXZDjC5Ty3zfDBeWUA== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=DigiCert Assured ID Root CA; OU=www.digicert.com; O=DigiCert Inc; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv -b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG -EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl -cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi -MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c -JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP -mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+ -wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4 -VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/ -AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB -AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW -BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun -pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC -dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf -fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm -NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx -H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe -+o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== ------END CERTIFICATE-----`)) - - // CN=DigiCert Assured ID Root G2; OU=www.digicert.com; O=DigiCert Inc; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=QuoVadis Root CA 3 G3; O=QuoVadis Limited; C=BM +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 +MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR +/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu +FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR +U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c +ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR +FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k +A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw +eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl +sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp +VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q +A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ +ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD +ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI +FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv +oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg +u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP +0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf +3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl +8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ +DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN +PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ +ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE----- + +// CN=QuoVadis Root CA 2 G3; O=QuoVadis Limited; C=BM +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 +MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf +qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW +n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym +c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ +O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 +o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j +IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq +IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz +8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh +vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l +7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG +cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD +ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC +roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga +W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n +lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE ++V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV +csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd +dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg +KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM +HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 +WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M +-----END CERTIFICATE----- + +// CN=DigiCert Assured ID Root G2; OU=www.digicert.com; O=DigiCert Inc; C=US +-----BEGIN CERTIFICATE----- MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv @@ -884,10 +999,10 @@ lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo IhNzbM8m9Yop5w== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=DigiCert Assured ID Root G3; OU=www.digicert.com; O=DigiCert Inc; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=DigiCert Assured ID Root G3; OU=www.digicert.com; O=DigiCert Inc; C=US +-----BEGIN CERTIFICATE----- MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg @@ -901,34 +1016,10 @@ BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv 6pZjamVFkpUBtA== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=DigiCert Global Root CA; OU=www.digicert.com; O=DigiCert Inc; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD -QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT -MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j -b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG -9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB -CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 -nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt -43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P -T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 -gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO -BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR -TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw -DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr -hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg -06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF -PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls -YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk -CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= ------END CERTIFICATE-----`)) - - // CN=DigiCert Global Root G2; OU=www.digicert.com; O=DigiCert Inc; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=DigiCert Global Root G2; OU=www.digicert.com; O=DigiCert Inc; C=US +-----BEGIN CERTIFICATE----- MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH @@ -949,10 +1040,10 @@ Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl MrY= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=DigiCert Global Root G3; OU=www.digicert.com; O=DigiCert Inc; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=DigiCert Global Root G3; OU=www.digicert.com; O=DigiCert Inc; C=US +-----BEGIN CERTIFICATE----- MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe @@ -966,35 +1057,10 @@ BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 sycX ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=DigiCert High Assurance EV Root CA; OU=www.digicert.com; O=DigiCert Inc; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j -ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL -MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 -LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug -RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm -+9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW -PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM -xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB -Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 -hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg -EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF -MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA -FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec -nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z -eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF -hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 -Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe -vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep -+OkuE6N36B9K ------END CERTIFICATE-----`)) - - // CN=DigiCert Trusted Root G4; OU=www.digicert.com; O=DigiCert Inc; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=DigiCert Trusted Root G4; OU=www.digicert.com; O=DigiCert Inc; C=US +-----BEGIN CERTIFICATE----- MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg @@ -1025,111 +1091,10 @@ cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 /YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ ------END CERTIFICATE-----`)) - - // CN=QuoVadis Root CA 2; O=QuoVadis Limited; C=BM - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x -GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv -b3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV -BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W -YWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa -GMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg -Fyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J -WpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB -rrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp -+ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1 -ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i -Ucw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz -PtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og -/zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH -oycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI -yV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1Ud -EwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwzJQTU7tD2 -A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL -MAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT -ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2f -BluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn -g/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl -fF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K -WWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha -B0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc -hLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR -TUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD -mbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z -ohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y -4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza -8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u ------END CERTIFICATE-----`)) - - // CN=QuoVadis Root CA 2 G3; O=QuoVadis Limited; C=BM - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL -BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc -BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 -MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM -aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf -qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW -n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym -c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ -O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 -o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j -IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq -IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz -8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh -vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l -7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG -cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB -BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD -ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 -AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC -roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga -W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n -lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE -+V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV -csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd -dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg -KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM -HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 -WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=QuoVadis Root CA 3 G3; O=QuoVadis Limited; C=BM - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL -BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc -BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 -MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM -aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR -/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu -FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR -U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c -ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR -FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k -A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw -eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl -sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp -VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q -A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ -ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB -BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD -ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px -KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI -FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv -oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg -u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP -0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf -3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl -8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ -DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN -PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ -ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 ------END CERTIFICATE-----`)) - - // CN=CA Disig Root R2; O=Disig a.s.; L=Bratislava; C=SK - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=CA Disig Root R2; O=Disig a.s.; L=Bratislava; C=SK +-----BEGIN CERTIFICATE----- MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy @@ -1159,27 +1124,10 @@ gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=emSign ECC Root CA - G3; OU=emSign PKI; O=eMudhra Technologies Limited; C=IN - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG -EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo -bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g -RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ -TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s -b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw -djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0 -WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS -fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB -zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq -hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB -CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD -+JbNR6iC8hZVdyR+EhCVBCyj ------END CERTIFICATE-----`)) - - // CN=emSign Root CA - G1; OU=emSign PKI; O=eMudhra Technologies Limited; C=IN - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=emSign Root CA - G1; OU=emSign PKI; O=eMudhra Technologies Limited; C=IN +-----BEGIN CERTIFICATE----- MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYD VQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBU ZWNobm9sb2dpZXMgTGltaXRlZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBH @@ -1200,32 +1148,43 @@ GNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q+Mri/Tm3R7nrft8EI6/6nAYH 6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeihU80Bv2noWgby RQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx iN66zB+Afko= ------END CERTIFICATE-----`)) - - // CN=AffirmTrust Commercial; O=AffirmTrust; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE -BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz -dCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL -MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp -cm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC -AQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP -Hx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr -ba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL -MeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1 -yHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr -VwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/ -nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ -KoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG -XUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj -vbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt -Z8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g -N53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC -nlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= ------END CERTIFICATE-----`)) - - // CN=Atos TrustedRoot 2011; O=Atos; C=DE - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +-----END CERTIFICATE----- + +// CN=emSign ECC Root CA - G3; OU=emSign PKI; O=eMudhra Technologies Limited; C=IN +-----BEGIN CERTIFICATE----- +MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG +EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo +bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g +RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ +TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s +b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0 +WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS +fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB +zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq +hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB +CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD ++JbNR6iC8hZVdyR+EhCVBCyj +-----END CERTIFICATE----- + +// CN=Atos TrustedRoot Root CA ECC TLS 2021; O=Atos; C=DE +-----BEGIN CERTIFICATE----- +MIICFTCCAZugAwIBAgIQPZg7pmY9kGP3fiZXOATvADAKBggqhkjOPQQDAzBMMS4w +LAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgRUNDIFRMUyAyMDIxMQ0w +CwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTI2MjNaFw00MTA0 +MTcwOTI2MjJaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBDQSBF +Q0MgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMHYwEAYHKoZI +zj0CAQYFK4EEACIDYgAEloZYKDcKZ9Cg3iQZGeHkBQcfl+3oZIK59sRxUM6KDP/X +tXa7oWyTbIOiaG6l2b4siJVBzV3dscqDY4PMwL502eCdpO5KTlbgmClBk1IQ1SQ4 +AjJn8ZQSb+/Xxd4u/RmAo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR2 +KCXWfeBmmnoJsmo7jjPXNtNPojAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMD +aAAwZQIwW5kp85wxtolrbNa9d+F851F+uDrNozZffPc8dz7kUK2o59JZDCaOMDtu +CCrCp1rIAjEAmeMM56PDr9NJLkaCI2ZdyQAUEv049OGYa3cpetskz2VAv9LcjBHo +9H1/IISpQuQo +-----END CERTIFICATE----- + +// CN=Atos TrustedRoot 2011; O=Atos; C=DE +-----BEGIN CERTIFICATE----- MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM @@ -1245,26 +1204,10 @@ DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed ------END CERTIFICATE-----`)) - - // CN=Atos TrustedRoot Root CA ECC TLS 2021; O=Atos; C=DE - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIICFTCCAZugAwIBAgIQPZg7pmY9kGP3fiZXOATvADAKBggqhkjOPQQDAzBMMS4w -LAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgRUNDIFRMUyAyMDIxMQ0w -CwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTI2MjNaFw00MTA0 -MTcwOTI2MjJaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBDQSBF -Q0MgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMHYwEAYHKoZI -zj0CAQYFK4EEACIDYgAEloZYKDcKZ9Cg3iQZGeHkBQcfl+3oZIK59sRxUM6KDP/X -tXa7oWyTbIOiaG6l2b4siJVBzV3dscqDY4PMwL502eCdpO5KTlbgmClBk1IQ1SQ4 -AjJn8ZQSb+/Xxd4u/RmAo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR2 -KCXWfeBmmnoJsmo7jjPXNtNPojAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMD -aAAwZQIwW5kp85wxtolrbNa9d+F851F+uDrNozZffPc8dz7kUK2o59JZDCaOMDtu -CCrCp1rIAjEAmeMM56PDr9NJLkaCI2ZdyQAUEv049OGYa3cpetskz2VAv9LcjBHo -9H1/IISpQuQo ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=Atos TrustedRoot Root CA RSA TLS 2021; O=Atos; C=DE - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=Atos TrustedRoot Root CA RSA TLS 2021; O=Atos; C=DE +-----BEGIN CERTIFICATE----- MIIFZDCCA0ygAwIBAgIQU9XP5hmTC/srBRLYwiqipDANBgkqhkiG9w0BAQwFADBM MS4wLAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgUlNBIFRMUyAyMDIx MQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTIxMTBaFw00 @@ -1294,59 +1237,10 @@ AXcOPUIjJsyacmdRIXrMPIWo6iFqO9taPKU0nprALN+AnCng33eU0aKAQv9qTFsR o7Ey7Nmj1m+UI/87tyll5gfp77YZ6ufCOB0yiJA8EytuzO+rdwY0d4RPcuSBhPm5 dDTedk+SKlOxJTnbPP/lPqYO5Wue/9vsL3SD3460s6neFE3/MaNFcyT6lSnMEpcE oji2jbDwN/zIIX8/syQbPYtuzE2wFg2WHYMfRsCbvUOZ58SWLs5fyQ== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=GlobalSign; OU=GlobalSign Root CA - R6; O=GlobalSign - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg -MB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh -bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx -MjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSNjET -MBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCAiIwDQYJ -KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQssgrRI -xutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1k -ZguSgMpE3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxD -aNc9PIrFsmbVkJq3MQbFvuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJw -LnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqMPKq0pPbzlUoSB239jLKJz9CgYXfIWHSw -1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+azayOeSsJDa38O+2HBNX -k7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05OWgtH8wY2 -SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/h -bguyCLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4n -WUx2OVvq+aWh2IMP0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpY -rZxCRXluDocZXFSxZba/jJvcE+kNb7gu3GduyYsRtYQUigAZcIN5kZeR1Bonvzce -MgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD -AQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNVHSMEGDAWgBSu -bAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN -nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGt -Ixg93eFyRJa0lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr61 -55wsTLxDKZmOMNOsIeDjHfrYBzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLj -vUYAGm0CuiVdjaExUd1URhxN25mW7xocBFymFe944Hn+Xds+qkxV/ZoVqW/hpvvf -cDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr3TsTjxKM4kEaSHpz -oHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB10jZp -nOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfs -pA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v -JJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R -8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4 -5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= ------END CERTIFICATE-----`)) - - // CN=GlobalSign Root E46; O=GlobalSign nv-sa; C=BE - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYx -CzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQD -ExNHbG9iYWxTaWduIFJvb3QgRTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAw -MDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex -HDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA -IgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkBjtjq -R+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGdd -yXqBPCCjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud -DgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ -7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZkvLtoURMMA/cVi4RguYv/Uo7njLwcAjA8 -+RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A= ------END CERTIFICATE-----`)) - - // CN=GlobalSign Root R46; O=GlobalSign nv-sa; C=BE - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=GlobalSign Root R46; O=GlobalSign nv-sa; C=BE +-----BEGIN CERTIFICATE----- MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUA MEYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYD VQQDExNHbG9iYWxTaWduIFJvb3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMy @@ -1376,10 +1270,25 @@ u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP 4vkYxboznxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6 N3ec592kD3ZDZopD8p/7DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3 vouXsXgxT7PntgMTzlSdriVZzH81Xwj3QEUxeCp6 ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=GlobalSign; OU=GlobalSign ECC Root CA - R5; O=GlobalSign - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=GlobalSign Root E46; O=GlobalSign nv-sa; C=BE +-----BEGIN CERTIFICATE----- +MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYx +CzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQD +ExNHbG9iYWxTaWduIFJvb3QgRTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAw +MDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex +HDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkBjtjq +R+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGdd +yXqBPCCjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ +7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZkvLtoURMMA/cVi4RguYv/Uo7njLwcAjA8 ++RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A= +-----END CERTIFICATE----- + +// CN=GlobalSign; OU=GlobalSign ECC Root CA - R5; O=GlobalSign +-----BEGIN CERTIFICATE----- MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX @@ -1392,10 +1301,10 @@ VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg 515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO xwy8p2Fp8fc74SrL+SvzZpA3 ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=GlobalSign; OU=GlobalSign Root CA - R3; O=GlobalSign - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=GlobalSign; OU=GlobalSign Root CA - R3; O=GlobalSign +-----BEGIN CERTIFICATE----- MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 @@ -1415,10 +1324,44 @@ jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH WD9f ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=Starfield Root Certificate Authority - G2; O=Starfield Technologies, Inc.; L=Scottsdale; ST=Arizona; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=GlobalSign; OU=GlobalSign Root CA - R6; O=GlobalSign +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg +MB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx +MjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSNjET +MBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQssgrRI +xutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1k +ZguSgMpE3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxD +aNc9PIrFsmbVkJq3MQbFvuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJw +LnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqMPKq0pPbzlUoSB239jLKJz9CgYXfIWHSw +1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+azayOeSsJDa38O+2HBNX +k7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05OWgtH8wY2 +SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/h +bguyCLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4n +WUx2OVvq+aWh2IMP0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpY +rZxCRXluDocZXFSxZba/jJvcE+kNb7gu3GduyYsRtYQUigAZcIN5kZeR1Bonvzce +MgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNVHSMEGDAWgBSu +bAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN +nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGt +Ixg93eFyRJa0lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr61 +55wsTLxDKZmOMNOsIeDjHfrYBzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLj +vUYAGm0CuiVdjaExUd1URhxN25mW7xocBFymFe944Hn+Xds+qkxV/ZoVqW/hpvvf +cDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr3TsTjxKM4kEaSHpz +oHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB10jZp +nOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfs +pA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v +JJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R +8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4 +5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= +-----END CERTIFICATE----- + +// CN=Starfield Root Certificate Authority - G2; O=Starfield Technologies, Inc.; L=Scottsdale; ST=Arizona; C=US +-----BEGIN CERTIFICATE----- MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs @@ -1440,10 +1383,10 @@ sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=Go Daddy Root Certificate Authority - G2; O=GoDaddy.com, Inc.; L=Scottsdale; ST=Arizona; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=Go Daddy Root Certificate Authority - G2; O=GoDaddy.com, Inc.; L=Scottsdale; ST=Arizona; C=US +-----BEGIN CERTIFICATE----- MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp @@ -1465,24 +1408,10 @@ gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo 2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI 4uJEvlz36hz1 ------END CERTIFICATE-----`)) - - // CN=GlobalSign; OU=GlobalSign ECC Root CA - R4; O=GlobalSign - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYD -VQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2Jh -bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgw -MTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0g -UjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wWTAT -BgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkWymOx -uYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNV -HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/ -+wpu+74zyTyjhNUwCgYIKoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147 -bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=GTS Root R4; O=Google Trust Services LLC; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=GTS Root R4; O=Google Trust Services LLC; C=US +-----BEGIN CERTIFICATE----- MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw @@ -1494,10 +1423,10 @@ HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D 9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8 p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=GTS Root R2; O=Google Trust Services LLC; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=GTS Root R2; O=Google Trust Services LLC; C=US +-----BEGIN CERTIFICATE----- MIIFVzCCAz+gAwIBAgINAgPlrsWNBCUaqxElqjANBgkqhkiG9w0BAQwFADBHMQsw CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU MBIGA1UEAxMLR1RTIFJvb3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw @@ -1527,10 +1456,10 @@ TOlZ4iBgxcJlkoKM8xAfDoqXvneCbT+PHV28SSe9zE8P4c52hgQjxcCMElv924Sg JPFI/2R80L5cFtHvma3AH/vLrrw4IgYmZNralw4/KBVEqE8AyvCazM90arQ+POuV 7LXTWtiBmelDGDfrs7vRWGJB82bSj6p4lVQgw1oudCvV0b4YacCs1aTPObpRhANl 6WLAYv7YTVWW4tAR+kg0Eeye7QUd5MjWHYbL ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=GTS Root R1; O=Google Trust Services LLC; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=GTS Root R1; O=Google Trust Services LLC; C=US +-----BEGIN CERTIFICATE----- MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw @@ -1560,10 +1489,10 @@ Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT 0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm 2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=GTS Root R3; O=Google Trust Services LLC; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=GTS Root R3; O=Google Trust Services LLC; C=US +-----BEGIN CERTIFICATE----- MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYD VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG A1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw @@ -1575,10 +1504,24 @@ jOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL2 BBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7 VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azTL818+FsuVbu/3ZL3pAzcMeGiAjEA/Jdm ZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV11RZt+cRLInUue4X ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=ACCVRAIZ1; OU=PKIACCV; O=ACCV; C=ES - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=GlobalSign; OU=GlobalSign ECC Root CA - R4; O=GlobalSign +-----BEGIN CERTIFICATE----- +MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYD +VQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgw +MTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0g +UjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wWTAT +BgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkWymOx +uYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNV +HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/ ++wpu+74zyTyjhNUwCgYIKoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147 +bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm +-----END CERTIFICATE----- + +// CN=ACCVRAIZ1; OU=PKIACCV; O=ACCV; C=ES +-----BEGIN CERTIFICATE----- MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ @@ -1621,10 +1564,28 @@ I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7 ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- + +// CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS; OU=Ceres; O=FNMT-RCM; C=ES; OrganizationIdentifier=VATES-Q2826004J +-----BEGIN CERTIFICATE----- +MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQsw +CQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgw +FgYDVQRhDA9WQVRFUy1RMjgyNjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1S +Q00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4MTIyMDA5MzczM1oXDTQzMTIyMDA5 +MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQtUkNNMQ4wDAYDVQQL +DAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNBQyBS +QUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LH +sbI6GA60XYyzZl2hNPk2LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oK +Um8BA06Oi6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqGSM49BAMDA2kAMGYCMQCu +SuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoDzBOQn5IC +MQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJy +v+c= +-----END CERTIFICATE----- - // OU=AC RAIZ FNMT-RCM; O=FNMT-RCM; C=ES - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// OU=AC RAIZ FNMT-RCM; O=FNMT-RCM; C=ES +-----BEGIN CERTIFICATE----- MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsx CzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJ WiBGTk1ULVJDTTAeFw0wODEwMjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJ @@ -1655,28 +1616,10 @@ fZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7Ixjp 9kiABdcPUXmsEKvU7ANm5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wok RqEIr9baRRmW1FMdW4R58MD3R++Lj8UGrp1MYp3/RgT408m2ECVAdf4WqslKYIYv uu8wd+RU4riEmViAqhOLUTpPSPaLtrM= ------END CERTIFICATE-----`)) - - // CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS; OU=Ceres; O=FNMT-RCM; C=ES; OrganizationIdentifier=VATES-Q2826004J - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQsw -CQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgw -FgYDVQRhDA9WQVRFUy1RMjgyNjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1S -Q00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4MTIyMDA5MzczM1oXDTQzMTIyMDA5 -MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQtUkNNMQ4wDAYDVQQL -DAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNBQyBS -QUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuB -BAAiA2IABPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LH -sbI6GA60XYyzZl2hNPk2LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oK -Um8BA06Oi6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD -VR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqGSM49BAMDA2kAMGYCMQCu -SuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoDzBOQn5IC -MQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJy -v+c= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1; OU=Kamu Sertifikasyon Merkezi - Kamu SM; O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK; L=Gebze - Kocaeli; C=TR - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1; OU=Kamu Sertifikasyon Merkezi - Kamu SM; O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK; L=Gebze - Kocaeli; C=TR +-----BEGIN CERTIFICATE----- MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIx GDAWBgNVBAcTD0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxp bXNlbCB2ZSBUZWtub2xvamlrIEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0w @@ -1701,10 +1644,10 @@ IPP54+M638yclNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4 lzwDGrpDxpa5RXI4s6ehlj2Re37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c 8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0jq5Rm+K37DwhuJi1/FwcJsoz7UMCf lo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=HARICA TLS RSA Root CA 2021; O=Hellenic Academic and Research Institutions CA; C=GR - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=HARICA TLS RSA Root CA 2021; O=Hellenic Academic and Research Institutions CA; C=GR +-----BEGIN CERTIFICATE----- MIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBs MQswCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl c2VhcmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0Eg @@ -1736,10 +1679,10 @@ BGIBnfHAT+7hOtSLIBD6Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79 aPib8qXPMThcFarmlwDB31qlpzmq6YR/PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YW xw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnnkf3/W9b3raYvAwtt41dU 63ZTGI0RmLo= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=HARICA TLS ECC Root CA 2021; O=Hellenic Academic and Research Institutions CA; C=GR - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=HARICA TLS ECC Root CA 2021; O=Hellenic Academic and Research Institutions CA; C=GR +-----BEGIN CERTIFICATE----- MIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQsw CQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2Vh cmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9v @@ -1753,10 +1696,10 @@ AwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQD AgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAircJRQO9gcS3ujwLEXQNw SaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/QwCZ61IygN nxS2PFOiTAZpffpskcYqSUXm7LcT4Tps ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=IdenTrust Commercial Root CA 1; O=IdenTrust; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=IdenTrust Commercial Root CA 1; O=IdenTrust; C=US +-----BEGIN CERTIFICATE----- MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw @@ -1786,10 +1729,10 @@ l1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG 4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ mUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A 7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=ISRG Root X1; O=Internet Security Research Group; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=ISRG Root X1; O=Internet Security Research Group; C=US +-----BEGIN CERTIFICATE----- MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 @@ -1819,10 +1762,10 @@ oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq 4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=ISRG Root X2; O=Internet Security Research Group; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=ISRG Root X2; O=Internet Security Research Group; C=US +-----BEGIN CERTIFICATE----- MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00 @@ -1835,10 +1778,10 @@ AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1 /q4AaOeMSQ+2b1tbFfLn ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=Izenpe.com; O=IZENPE S.A.; C=ES - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=Izenpe.com; O=IZENPE S.A.; C=ES +-----BEGIN CERTIFICATE----- MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4 MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6 ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD @@ -1871,10 +1814,10 @@ UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/ QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+ naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=SZAFIR ROOT CA2; O=Krajowa Izba Rozliczeniowa S.A.; C=PL - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=SZAFIR ROOT CA2; O=Krajowa Izba Rozliczeniowa S.A.; C=PL +-----BEGIN CERTIFICATE----- MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQEL BQAwUTELMAkGA1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6 ZW5pb3dhIFMuQS4xGDAWBgNVBAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkw @@ -1894,27 +1837,30 @@ nXkZ7/e7DDWQw4rtTw/1zBLZpD67oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCP oky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul4+vJhaAlIDf7js4MNIThPIGy d05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6+/NNIxuZMzSg LvWpCz/UXeHPhJ/iGcJfitYgHuNztw== ------END CERTIFICATE-----`)) - - // CN=e-Szigno Root CA 2017; O=Microsec Ltd.; L=Budapest; C=HU; OrganizationIdentifier=VATHU-23584497 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNV -BAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRk -LjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJv -b3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZaFw00MjA4MjIxMjA3MDZaMHExCzAJ -BgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMg -THRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25v -IFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtv -xie+RJCxs1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+H -Wyx7xf58etqjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G -A1UdDgQWBBSHERUI0arBeAyxr87GyZDvvzAEwDAfBgNVHSMEGDAWgBSHERUI0arB -eAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEAtVfd14pVCzbhhkT61Nlo -jbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxOsvxyqltZ -+efcMQ== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=Microsec e-Szigno Root CA 2009; O=Microsec Ltd.; L=Budapest; C=HU; EmailAddress=info@e-szigno.hu - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=e-Szigno TLS Root CA 2023; O=Microsec Ltd.; L=Budapest; C=HU; OrganizationIdentifier=VATHU-23584497 +-----BEGIN CERTIFICATE----- +MIICzzCCAjGgAwIBAgINAOhvGHvWOWuYSkmYCjAKBggqhkjOPQQDBDB1MQswCQYD +VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 +ZC4xFzAVBgNVBGEMDlZBVEhVLTIzNTg0NDk3MSIwIAYDVQQDDBllLVN6aWdubyBU +TFMgUm9vdCBDQSAyMDIzMB4XDTIzMDcxNzE0MDAwMFoXDTM4MDcxNzE0MDAwMFow +dTELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRYwFAYDVQQKDA1NaWNy +b3NlYyBMdGQuMRcwFQYDVQRhDA5WQVRIVS0yMzU4NDQ5NzEiMCAGA1UEAwwZZS1T +emlnbm8gVExTIFJvb3QgQ0EgMjAyMzCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAE +AGgP36J8PKp0iGEKjcJMpQEiFNT3YHdCnAo4YKGMZz6zY+n6kbCLS+Y53wLCMAFS +AL/fjO1ZrTJlqwlZULUZwmgcAOAFX9pQJhzDrAQixTpN7+lXWDajwRlTEArRzT/v +SzUaQ49CE0y5LBqcvjC2xN7cS53kpDzLLtmt3999Cd8ukv+ho2MwYTAPBgNVHRMB +Af8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUWYQCYlpGePVd3I8K +ECgj3NXW+0UwHwYDVR0jBBgwFoAUWYQCYlpGePVd3I8KECgj3NXW+0UwCgYIKoZI +zj0EAwQDgYsAMIGHAkIBLdqu9S54tma4n7Zwf2Z0z+yOfP7AAXmazlIC58PRDHpt +y7Ve7hekm9sEdu4pKeiv+62sUvTXK9Z3hBC9xdIoaDQCQTV2WnXzkoYI9bIeCvZl +C9p2x1L/Cx6AcCIwwzPbGO2E14vs7dOoY4G1VnxHx1YwlGhza9IuqbnZLBwpvQy6 +uWWL +-----END CERTIFICATE----- + +// CN=Microsec e-Szigno Root CA 2009; O=Microsec Ltd.; L=Budapest; C=HU; EmailAddress=info@e-szigno.hu +-----BEGIN CERTIFICATE----- MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G @@ -1937,10 +1883,27 @@ ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c 2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- + +// CN=e-Szigno Root CA 2017; O=Microsec Ltd.; L=Budapest; C=HU; OrganizationIdentifier=VATHU-23584497 +-----BEGIN CERTIFICATE----- +MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNV +BAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRk +LjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJv +b3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZaFw00MjA4MjIxMjA3MDZaMHExCzAJ +BgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMg +THRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25v +IFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtv +xie+RJCxs1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+H +Wyx7xf58etqjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBSHERUI0arBeAyxr87GyZDvvzAEwDAfBgNVHSMEGDAWgBSHERUI0arB +eAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEAtVfd14pVCzbhhkT61Nlo +jbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxOsvxyqltZ ++efcMQ== +-----END CERTIFICATE----- - // CN=Microsoft ECC Root Certificate Authority 2017; O=Microsoft Corporation; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=Microsoft ECC Root Certificate Authority 2017; O=Microsoft Corporation; C=US +-----BEGIN CERTIFICATE----- MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQsw CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYD VQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw @@ -1954,10 +1917,10 @@ BTADAQH/MB0GA1UdDgQWBBTIy5lycFIM+Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3 FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlfXu5gKcs68tvWMoQZP3zV L8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaReNtUjGUB iudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=Microsoft RSA Root Certificate Authority 2017; O=Microsoft Corporation; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=Microsoft RSA Root Certificate Authority 2017; O=Microsoft Corporation; C=US +-----BEGIN CERTIFICATE----- MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBl MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw NAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 @@ -1989,10 +1952,10 @@ GWSZI1b7rCoucL5mxAyE7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9 dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKTc0QWbej09+CVgI+WXTik9KveCjCHk9hN AHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB RA+GsCyRxj3qrg+E ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=NAVER Global Root Certification Authority; O=NAVER BUSINESS PLATFORM Corp.; C=KR - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=NAVER Global Root Certification Authority; O=NAVER BUSINESS PLATFORM Corp.; C=KR +-----BEGIN CERTIFICATE----- MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEM BQAwaTELMAkGA1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRG T1JNIENvcnAuMTIwMAYDVQQDDClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0 @@ -2024,10 +1987,10 @@ LBT/DShycpWbXgnbiUSYqqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX 5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oGI/hGoiLtk/bdmuYqh7GYVPEi92tF4+KO dh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmgkpzNNIaRkPpkUZ3+/uul 9XXeifdy ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=NetLock Arany (Class Gold) Főtanúsítvány; OU=Tanúsítványkiadók (Certification Services); O=NetLock Kft.; L=Budapest; C=HU - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=NetLock Arany (Class Gold) Főtanúsítvány; OU=Tanúsítványkiadók (Certification Services); O=NetLock Kft.; L=Budapest; C=HU +-----BEGIN CERTIFICATE----- MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3 MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl @@ -2050,10 +2013,10 @@ bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=OISTE WISeKey Global Root GC CA; OU=OISTE Foundation Endorsed; O=WISeKey; C=CH - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=OISTE WISeKey Global Root GC CA; OU=OISTE Foundation Endorsed; O=WISeKey; C=CH +-----BEGIN CERTIFICATE----- MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQsw CQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91 bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwg @@ -2067,10 +2030,10 @@ BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7T rYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0EAwMDaAAwZQIwJsdpW9zV 57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtkAjEA2zQg Mgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9 ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=OISTE WISeKey Global Root GB CA; OU=OISTE Foundation Endorsed; O=WISeKey; C=CH - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=OISTE WISeKey Global Root GB CA; OU=OISTE Foundation Endorsed; O=WISeKey; C=CH +-----BEGIN CERTIFICATE----- MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg Rm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9i @@ -2091,10 +2054,10 @@ gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0VQreUGdNZtGn//3ZwLWoo4rO ZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEuiHZeeevJuQHHf aPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=Security Communication ECC RootCA1; O=SECOM Trust Systems CO.,LTD.; C=JP - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=Security Communication ECC RootCA1; O=SECOM Trust Systems CO.,LTD.; C=JP +-----BEGIN CERTIFICATE----- MIICODCCAb6gAwIBAgIJANZdm7N4gS7rMAoGCCqGSM49BAMDMGExCzAJBgNVBAYT AkpQMSUwIwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMSswKQYD VQQDEyJTZWN1cml0eSBDb21tdW5pY2F0aW9uIEVDQyBSb290Q0ExMB4XDTE2MDYx @@ -2107,10 +2070,10 @@ ULGjQjBAMB0GA1UdDgQWBBSGHOf+LaVKiwj+KBH6vqNm+GBZLzAOBgNVHQ8BAf8E BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjAVXUI9/Lbu 9zuxNuie9sRGKEkz0FhDKmMpzE2xtHqiuQ04pV1IKv3LsnNdo4gIxwwCMQDAqy0O be0YottT6SXbVQjgUMzfRGEWgqtJsLKB7HOHeLRMsmIbEvoWTSVLY70eN9k= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // OU=Security Communication RootCA2; O=SECOM Trust Systems CO.,LTD.; C=JP - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// OU=Security Communication RootCA2; O=SECOM Trust Systems CO.,LTD.; C=JP +-----BEGIN CERTIFICATE----- MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX @@ -2130,73 +2093,10 @@ okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8 t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy 1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/ SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 ------END CERTIFICATE-----`)) - - // CN=Entrust Root Certification Authority; OU=www.entrust.net/CPS is incorporated by reference, (c) 2006 Entrust, Inc.; O=Entrust, Inc.; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC -VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0 -Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW -KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl -cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw -NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw -NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy -ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV -BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ -KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo -Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4 -4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9 -KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI -rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi -94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB -sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi -gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo -kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE -vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA -A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t -O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua -AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP -9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/ -eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m -0vdXcDazv/wor3ElhVsT/h5/WrQ8 ------END CERTIFICATE-----`)) - - // CN=Sectigo Public Server Authentication Root E46; O=Sectigo Limited; C=GB - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQsw -CQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T -ZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcN -MjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYG -A1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBT -ZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA -IgNiAAR2+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccC -WvkEN/U0NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+ -6xnOQ6OjQjBAMB0GA1UdDgQWBBTRItpMWfFLXyY4qp3W7usNw/upYTAOBgNVHQ8B -Af8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNnADBkAjAn7qRa -qCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RHlAFWovgzJQxC36oCMB3q -4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21USAGKcw== ------END CERTIFICATE-----`)) - - // CN=COMODO ECC Certification Authority; O=COMODO CA Limited; L=Salford; ST=Greater Manchester; C=GB - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL -MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE -BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT -IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw -MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy -ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N -T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv -biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR -FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J -cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW -BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ -BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm -fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv -GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=COMODO Certification Authority; O=COMODO CA Limited; L=Salford; ST=Greater Manchester; C=GB - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=COMODO Certification Authority; O=COMODO CA Limited; L=Salford; ST=Greater Manchester; C=GB +-----BEGIN CERTIFICATE----- MIID0DCCArigAwIBAgIQIKTEf93f4cdTYwcTiHdgEjANBgkqhkiG9w0BAQUFADCB gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV @@ -2218,10 +2118,10 @@ HWLrp+cxEmbDD6ZLLkGC9/3JG6gbr+48zuOcrigHoSybJMIPIyaDMouGDx8rEkYl Fo92kANr3ryqImhrjKGsKxE5pttwwn1y6TPn/CbxdFqR5p2ErPioBhlG5qfpqjQi pKGfeq23sqSaM4hxAjwu1nqyH6LKwN0vEJT9s4yEIHlG1QXUEOTS22RPuFvuG8Ug R1uUq27UlTMdphVx8fiUylQ5PsE= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=COMODO RSA Certification Authority; O=COMODO CA Limited; L=Salford; ST=Greater Manchester; C=GB - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=COMODO RSA Certification Authority; O=COMODO CA Limited; L=Salford; ST=Greater Manchester; C=GB +-----BEGIN CERTIFICATE----- MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV @@ -2254,10 +2154,10 @@ S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl 0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB NVOFBkpdn627G190 ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=USERTrust RSA Certification Authority; O=The USERTRUST Network; L=Jersey City; ST=New Jersey; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=USERTrust RSA Certification Authority; O=The USERTRUST Network; L=Jersey City; ST=New Jersey; C=US +-----BEGIN CERTIFICATE----- MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV @@ -2290,10 +2190,10 @@ qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG jjxDah2nGN59PRbxYvnKkKj9 ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=USERTrust ECC Certification Authority; O=The USERTRUST Network; L=Jersey City; ST=New Jersey; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=USERTrust ECC Certification Authority; O=The USERTRUST Network; L=Jersey City; ST=New Jersey; C=US +-----BEGIN CERTIFICATE----- MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT @@ -2308,10 +2208,44 @@ A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- + +// CN=COMODO ECC Certification Authority; O=COMODO CA Limited; L=Salford; ST=Greater Manchester; C=GB +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT +IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw +MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N +T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR +FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J +cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW +BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm +fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv +GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- + +// CN=Sectigo Public Server Authentication Root E46; O=Sectigo Limited; C=GB +-----BEGIN CERTIFICATE----- +MIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQsw +CQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T +ZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcN +MjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYG +A1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBT +ZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAR2+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccC +WvkEN/U0NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+ +6xnOQ6OjQjBAMB0GA1UdDgQWBBTRItpMWfFLXyY4qp3W7usNw/upYTAOBgNVHQ8B +Af8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNnADBkAjAn7qRa +qCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RHlAFWovgzJQxC36oCMB3q +4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21USAGKcw== +-----END CERTIFICATE----- - // CN=Sectigo Public Server Authentication Root R46; O=Sectigo Limited; C=GB - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=Sectigo Public Server Authentication Root R46; O=Sectigo Limited; C=GB +-----BEGIN CERTIFICATE----- MIIFijCCA3KgAwIBAgIQdY39i658BwD6qSWn4cetFDANBgkqhkiG9w0BAQwFADBf MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQD Ey1TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYw @@ -2342,93 +2276,10 @@ dHn5HrwdVw1Hr8Mvn4dGp+smWg9WY7ViYG4A++MnESLn/pmPNPW56MORcr3Ywx65 LvKRRFHQV80MNNVIIb/bE/FmJUNS0nAiNs2fxBx1IK1jcmMGDw4nztJqDby1ORrp 0XZ60Vzk50lJLVU3aPAaOpg+VBeHVOmmJ1CJeyAvP/+/oYtKR5j/K3tJPsMpRmAY QqszKbrAKbkTidOIijlBO8n9pu0f9GBj39ItVQGL ------END CERTIFICATE-----`)) - - // CN=Entrust Root Certification Authority - G2; OU=See www.entrust.net/legal-terms, (c) 2009 Entrust, Inc. - for authorized use only; O=Entrust, Inc.; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC -VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50 -cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs -IEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVz -dCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcy -NTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVu -dHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwt -dGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0 -aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj -YXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK -AoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/T -RU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWN -cCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hW -wcKUs/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1 -U1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0 -jaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP -BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2fkBJmqzAN -BgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/ -jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ -Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v -1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R -nAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH -VHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g== ------END CERTIFICATE-----`)) - - // CN=Entrust Root Certification Authority - EC1; OU=See www.entrust.net/legal-terms, (c) 2012 Entrust, Inc. - for authorized use only; O=Entrust, Inc.; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkG -A1UEBhMCVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3 -d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVu -dHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEzMDEGA1UEAxMq -RW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRUMxMB4XDTEy -MTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYwFAYD -VQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0 -L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0g -Zm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBD -ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEVDMTB2MBAGByqGSM49AgEGBSuBBAAi -A2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHyAsWfoPZb1YsGGYZPUxBt -ByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef9eNi1KlH -Bz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O -BBYEFLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVC -R98crlOZF7ZvHH3hvxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nX -hTcGtXsI/esni0qU+eH6p44mCOh8kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G ------END CERTIFICATE-----`)) - - // CN=SSL.com Root Certification Authority RSA; O=SSL Corporation; L=Houston; ST=Texas; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE -BhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK -DA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp -Y2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz -OTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv -dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv -bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN -AQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R -xFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX -qhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC -C52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3 -6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh -/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF -YD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E -JNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc -US4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8 -ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm -+Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi -M+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV -HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G -A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV -cpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc -Hadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs -PgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/ -q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0 -cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr -a6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I -H37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y -K9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu -nLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf -oYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY -Ic2wBlX7Jz9TkHCpBB5XJ7k= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=SSL.com TLS ECC Root CA 2022; O=SSL Corporation; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=SSL.com TLS ECC Root CA 2022; O=SSL Corporation; C=US +-----BEGIN CERTIFICATE----- MIICOjCCAcCgAwIBAgIQFAP1q/s3ixdAW+JDsqXRxDAKBggqhkjOPQQDAzBOMQsw CQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxT U0wuY29tIFRMUyBFQ0MgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzM0OFoXDTQ2 @@ -2441,10 +2292,28 @@ GDAWgBSJjy+j6CugFFR781a4Jl9nOAuc0DAdBgNVHQ4EFgQUiY8vo+groBRUe/NW uCZfZzgLnNAwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2gAMGUCMFXjIlbp 15IkWE8elDIPDAI2wv2sdDJO4fscgIijzPvX6yv/N33w7deedWo1dlJF4AIxAMeN b0Igj762TVntd00pxCAgRWSGOlDGxK0tk/UYfXLtqc/ErFc2KAhl3zx5Zn6g6g== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- + +// CN=SSL.com EV Root Certification Authority ECC; O=SSL Corporation; L=Houston; ST=Texas; C=US +-----BEGIN CERTIFICATE----- +MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx +NTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv +bSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA +VIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku +WnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP +MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX +5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ +ytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg +h5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== +-----END CERTIFICATE----- - // CN=SSL.com TLS RSA Root CA 2022; O=SSL Corporation; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=SSL.com TLS RSA Root CA 2022; O=SSL Corporation; C=US +-----BEGIN CERTIFICATE----- MIIFiTCCA3GgAwIBAgIQb77arXO9CEDii02+1PdbkTANBgkqhkiG9w0BAQsFADBO MQswCQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQD DBxTU0wuY29tIFRMUyBSU0EgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzQyMloX @@ -2475,10 +2344,10 @@ AgDHbICivRZQIA9ygV/MlP+7mea6kMvq+cYMwq7FGc4zoWtcu358NFcXrfA/rs3q r5nsLFR+jM4uElZI7xc7P0peYNLcdDa8pUNjyw9bowJWCZ4kLOGGgYz+qxcs+sji Mho6/4UIyYOf8kpIEFR3N+2ivEC+5BB09+Rbu7nzifmPQdjH5FCQNYA+HLhNkNPU 98OwoX6EyneSMSy4kLGCenROmxMmtNVQZlR4rmA= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=SSL.com Root Certification Authority ECC; O=SSL Corporation; L=Houston; ST=Texas; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=SSL.com Root Certification Authority ECC; O=SSL Corporation; L=Houston; ST=Texas; C=US +-----BEGIN CERTIFICATE----- MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T U0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0 @@ -2493,10 +2362,10 @@ EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD VR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T kdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+ gA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=SSL.com EV Root Certification Authority RSA R2; O=SSL Corporation; L=Houston; ST=Texas; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=SSL.com EV Root Certification Authority RSA R2; O=SSL Corporation; L=Houston; ST=Texas; C=US +-----BEGIN CERTIFICATE----- MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE CgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2Vy @@ -2529,63 +2398,113 @@ QbNmC1r7fSOl8hqw/96bg5Qu0T/fkreRrwU7ZcegbLHNYhLDkBvjJc40vG93drEQ w/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi S9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07 mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=SSL.com EV Root Certification Authority ECC; O=SSL Corporation; L=Houston; ST=Texas; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC -VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T -U0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp -Y2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx -NTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv -dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv -bSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49 -AgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA -VIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku -WnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP -MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX -5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ -ytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg -h5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== ------END CERTIFICATE-----`)) - - // CN=SwissSign Gold CA - G2; O=SwissSign AG; C=CH - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV -BAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2ln -biBHb2xkIENBIC0gRzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBF -MQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMR8wHQYDVQQDExZT -d2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC -CgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUqt2/8 -76LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+ -bbqBHH5CjCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c -6bM8K8vzARO/Ws/BtQpgvd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqE -emA8atufK+ze3gE/bk3lUIbLtK/tREDFylqM2tIrfKjuvqblCqoOpd8FUrdVxyJd -MmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvRAiTysybUa9oEVeXBCsdt -MDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuendjIj3o02y -MszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69y -FGkOpeUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPi -aG59je883WX0XaxR7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxM -gI93e2CaHt+28kgeDrpOVG2Y4OGiGqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCB -qTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUWyV7 -lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64OfPAeGZe6Drn -8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov -L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe6 -45R88a7A3hfm5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczO -UYrHUDFu4Up+GC9pWbY9ZIEr44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5 -O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOfMke6UiI0HTJ6CVanfCU2qT1L2sCC -bwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6mGu6uLftIdxf+u+yv -GPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxpmo/a -77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCC -hdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3 -92qgQmwLOM7XdVAyksLfKzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEpp -Ld6leNcG2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+w -ZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+htt -Qc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ ------END CERTIFICATE-----`)) - - // CN=TWCA CYBER Root CA; OU=Root CA; O=TAIWAN-CA; C=TW - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=SSL.com Root Certification Authority RSA; O=SSL Corporation; L=Houston; ST=Texas; C=US +-----BEGIN CERTIFICATE----- +MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE +BhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK +DA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz +OTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv +bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R +xFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX +qhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC +C52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3 +6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh +/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF +YD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E +JNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc +US4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8 +ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm ++Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi +M+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G +A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV +cpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc +Hadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs +PgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/ +q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0 +cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr +a6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I +H37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y +K9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu +nLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf +oYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY +Ic2wBlX7Jz9TkHCpBB5XJ7k= +-----END CERTIFICATE----- + +// CN=SwissSign RSA TLS Root CA 2022 - 1; O=SwissSign AG; C=CH +-----BEGIN CERTIFICATE----- +MIIFkzCCA3ugAwIBAgIUQ/oMX04bgBhE79G0TzUfRPSA7cswDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzErMCkGA1UE +AxMiU3dpc3NTaWduIFJTQSBUTFMgUm9vdCBDQSAyMDIyIC0gMTAeFw0yMjA2MDgx +MTA4MjJaFw00NzA2MDgxMTA4MjJaMFExCzAJBgNVBAYTAkNIMRUwEwYDVQQKEwxT +d2lzc1NpZ24gQUcxKzApBgNVBAMTIlN3aXNzU2lnbiBSU0EgVExTIFJvb3QgQ0Eg +MjAyMiAtIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDLKmjiC8NX +vDVjvHClO/OMPE5Xlm7DTjak9gLKHqquuN6orx122ro10JFwB9+zBvKK8i5VUXu7 +LCTLf5ImgKO0lPaCoaTo+nUdWfMHamFk4saMla+ju45vVs9xzF6BYQ1t8qsCLqSX +5XH8irCRIFucdFJtrhUnWXjyCcplDn/L9Ovn3KlMd/YrFgSVrpxxpT8q2kFC5zyE +EPThPYxr4iuRR1VPuFa+Rd4iUU1OKNlfGUEGjw5NBuBwQCMBauTLE5tzrE0USJIt +/m2n+IdreXXhvhCxqohAWVTXz8TQm0SzOGlkjIHRI36qOTw7D59Ke4LKa2/KIj4x +0LDQKhySio/YGZxH5D4MucLNvkEM+KRHBdvBFzA4OmnczcNpI/2aDwLOEGrOyvi5 +KaM2iYauC8BPY7kGWUleDsFpswrzd34unYyzJ5jSmY0lpx+Gs6ZUcDj8fV3oT4MM +0ZPlEuRU2j7yrTrePjxF8CgPBrnh25d7mUWe3f6VWQQvdT/TromZhqwUtKiE+shd +OxtYk8EXlFXIC+OCeYSf8wCENO7cMdWP8vpPlkwGqnj73mSiI80fPsWMvDdUDrta +clXvyFu1cvh43zcgTFeRc5JzrBh3Q4IgaezprClG5QtO+DdziZaKHG29777YtvTK +wP1H8K4LWCDFyB02rpeNUIMmJCn3nTsPBQIDAQABo2MwYTAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBRvjmKLk0Ow4UD2p8P98Q+4 +DxU4pTAdBgNVHQ4EFgQUb45ii5NDsOFA9qfD/fEPuA8VOKUwDQYJKoZIhvcNAQEL +BQADggIBAKwsKUF9+lz1GpUYvyypiqkkVHX1uECry6gkUSsYP2OprphWKwVDIqO3 +10aewCoSPY6WlkDfDDOLazeROpW7OSltwAJsipQLBwJNGD77+3v1dj2b9l4wBlgz +Hqp41eZUBDqyggmNzhYzWUUo8aWjlw5DI/0LIICQ/+Mmz7hkkeUFjxOgdg3XNwwQ +iJb0Pr6VvfHDffCjw3lHC1ySFWPtUnWK50Zpy1FVCypM9fJkT6lc/2cyjlUtMoIc +gC9qkfjLvH4YoiaoLqNTKIftV+Vlek4ASltOU8liNr3CjlvrzG4ngRhZi0Rjn9UM +ZfQpZX+RLOV/fuiJz48gy20HQhFRJjKKLjpHE7iNvUcNCfAWpO2Whi4Z2L6MOuhF +LhG6rlrnub+xzI/goP+4s9GFe3lmozm1O2bYQL7Pt2eLSMkZJVX8vY3PXtpOpvJp +zv1/THfQwUY1mFwjmwJFQ5Ra3bxHrSL+ul4vkSkphnsh3m5kt8sNjzdbowhq6/Td +Ao9QAwKxuDdollDruF/UKIqlIgyKhPBZLtU30WHlQnNYKoH3dtvi4k0NX/a3vgW0 +rk4N3hY9A4GzJl5LuEsAz/+MF7psYC0nhzck5npgL7XTgwSqT0N1osGDsieYK7EO +gLrAhV5Cud+xYJHT6xh+cHiudoO+cVrQkOPKwRYlZ0rwtnu64ZzZ +-----END CERTIFICATE----- + +// CN=TWCA Global Root CA; OU=Root CA; O=TAIWAN-CA; C=TW +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx +EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT +VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 +NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT +B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF +10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz +0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh +MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH +zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc +46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2 +yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi +laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP +oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA +BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE +qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm +4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL +1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn +LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF +H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo +RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+ +nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh +15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW +6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW +nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j +wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz +aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy +KwbQBM0= +-----END CERTIFICATE----- + +// CN=TWCA CYBER Root CA; OU=Root CA; O=TAIWAN-CA; C=TW +-----BEGIN CERTIFICATE----- MIIFjTCCA3WgAwIBAgIQQAE0jMIAAAAAAAAAATzyxjANBgkqhkiG9w0BAQwFADBQ MQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290 IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3QgQ0EwHhcNMjIxMTIyMDY1NDI5 @@ -2616,75 +2535,10 @@ Qh0UqEL20kCGoE8jypZFVmAGzbdVAaYBlGX+bgUJurSkquLvWL69J1bY73NxW0Qz NxKfKjLji7gh7MMrZQzvIt6IKTtM1/r+t+FHvpw+PoP7UV31aPcuIYXcv/Fa4nzX xeSDwWrruoBa3lwtcHb4yOWHh8qgnaHlIhInD0Q9HWzq1MKLL295q39QpsQZp6F6 t5b5wR9iWqJDB0BeJsas7a5wFsWqynKKTbDPAYsDP27X ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CN=TWCA Global Root CA; OU=Root CA; O=TAIWAN-CA; C=TW - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx -EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT -VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 -NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT -B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG -SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF -10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz -0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh -MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH -zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc -46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2 -yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi -laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP -oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA -BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE -qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm -4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB -/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL -1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn -LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF -H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo -RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+ -nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh -15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW -6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW -nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j -wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz -aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy -KwbQBM0= ------END CERTIFICATE-----`)) - - // CN=TeliaSonera Root CA v1; O=TeliaSonera - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAw -NzEUMBIGA1UECgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJv -b3QgQ0EgdjEwHhcNMDcxMDE4MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYD -VQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwWVGVsaWFTb25lcmEgUm9vdCBDQSB2 -MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+6yfwIaPzaSZVfp3F -VRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA3GV1 -7CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+X -Z75Ljo1kB1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+ -/jXh7VB7qTCNGdMJjmhnXb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs -81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxHoLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkm -dtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3F0fUTPHSiXk+TT2YqGHe -Oh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJoWjiUIMu -sDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4 -pgd7gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fs -slESl1MpWtTwEhDcTwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQ -arMCpgKIv7NHfirZ1fpoeDVNAgMBAAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYD -VR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qWDNXr+nuqF+gTEjANBgkqhkiG -9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNmzqjMDfz1mgbl -dxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx -0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1Tj -TQpgcmLNkQfWpb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBed -Y2gea+zDTYa4EzAvXUYNR0PVG6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7 -Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpcc41teyWRyu5FrgZLAMzTsVlQ2jqI -OylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOTJsjrDNYmiLbAJM+7 -vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2qReW -t88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcn -HL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx -SK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= ------END CERTIFICATE-----`)) - - // CN=Telia Root CA v2; O=Telia Finland Oyj; C=FI - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CN=Telia Root CA v2; O=Telia Finland Oyj; C=FI +-----BEGIN CERTIFICATE----- MIIFdDCCA1ygAwIBAgIPAWdfJ9b+euPkrL4JWwWeMA0GCSqGSIb3DQEBCwUAMEQx CzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UE AwwQVGVsaWEgUm9vdCBDQSB2MjAeFw0xODExMjkxMTU1NTRaFw00MzExMjkxMTU1 @@ -2715,102 +2569,6 @@ Ty3EHD70sz+rFQ47GUGKpMFXEmZxTPpT41frYpUJnlTd0cI8Vzy9OK2YZLe4A5pT VmBds9hCG1xLEooc6+t9xnppxyd/pPiL8uSUZodL6ZQHCRJ5irLrdATczvREWeAW ysUsWNc8e89ihmpQfTU2Zqf7N+cox9jQraVplI/owd8k+BsHMYeB2F326CjYSlKA rBPuUBQemMc= ------END CERTIFICATE-----`)) - - // CN=Trustwave Global ECC P384 Certification Authority; O=Trustwave Holdings, Inc.; L=Chicago; ST=Illinois; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIICnTCCAiSgAwIBAgIMCL2Fl2yZJ6SAaEc7MAoGCCqGSM49BAMDMIGRMQswCQYD -VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf -BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 -YXZlIEdsb2JhbCBFQ0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x -NzA4MjMxOTM2NDNaFw00MjA4MjMxOTM2NDNaMIGRMQswCQYDVQQGEwJVUzERMA8G -A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 -d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF -Q0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTB2MBAGByqGSM49AgEGBSuB -BAAiA2IABGvaDXU1CDFHBa5FmVXxERMuSvgQMSOjfoPTfygIOiYaOs+Xgh+AtycJ -j9GOMMQKmw6sWASr9zZ9lCOkmwqKi6vr/TklZvFe/oyujUF5nQlgziip04pt89ZF -1PKYhDhloKNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNVHQ8BAf8EBQMDBwYAMB0G -A1UdDgQWBBRVqYSJ0sEyvRjLbKYHTsjnnb6CkDAKBggqhkjOPQQDAwNnADBkAjA3 -AZKXRRJ+oPM+rRk6ct30UJMDEr5E0k9BpIycnR+j9sKS50gU/k6bpZFXrsY3crsC -MGclCrEMXu6pY5Jv5ZAL/mYiykf9ijH3g/56vxC+GCsej/YpHpRZ744hN8tRmKVu -Sw== ------END CERTIFICATE-----`)) - - // CN=Trustwave Global ECC P256 Certification Authority; O=Trustwave Holdings, Inc.; L=Chicago; ST=Illinois; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIICYDCCAgegAwIBAgIMDWpfCD8oXD5Rld9dMAoGCCqGSM49BAMCMIGRMQswCQYD -VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf -BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 -YXZlIEdsb2JhbCBFQ0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x -NzA4MjMxOTM1MTBaFw00MjA4MjMxOTM1MTBaMIGRMQswCQYDVQQGEwJVUzERMA8G -A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 -d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF -Q0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqG -SM49AwEHA0IABH77bOYj43MyCMpg5lOcunSNGLB4kFKA3TjASh3RqMyTpJcGOMoN -FWLGjgEqZZ2q3zSRLoHB5DOSMcT9CTqmP62jQzBBMA8GA1UdEwEB/wQFMAMBAf8w -DwYDVR0PAQH/BAUDAwcGADAdBgNVHQ4EFgQUo0EGrJBt0UrrdaVKEJmzsaGLSvcw -CgYIKoZIzj0EAwIDRwAwRAIgB+ZU2g6gWrKuEZ+Hxbb/ad4lvvigtwjzRM4q3wgh -DDcCIC0mA6AFvWvR9lz4ZcyGbbOcNEhjhAnFjXca4syc4XR7 ------END CERTIFICATE-----`)) - - // CN=SecureTrust CA; O=SecureTrust Corporation; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI -MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x -FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz -MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv -cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz -Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO -0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao -wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj -7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS -8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT -BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB -/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg -JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC -NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3 -6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/ -3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm -D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS -CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR -3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= ------END CERTIFICATE-----`)) - - // CN=Trustwave Global Certification Authority; O=Trustwave Holdings, Inc.; L=Chicago; ST=Illinois; C=US - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIF2jCCA8KgAwIBAgIMBfcOhtpJ80Y1LrqyMA0GCSqGSIb3DQEBCwUAMIGIMQsw -CQYDVQQGEwJVUzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28x -ITAfBgNVBAoMGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1 -c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMx -OTM0MTJaFw00MjA4MjMxOTM0MTJaMIGIMQswCQYDVQQGEwJVUzERMA8GA1UECAwI -SWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2ZSBI -b2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZp -Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB -ALldUShLPDeS0YLOvR29zd24q88KPuFd5dyqCblXAj7mY2Hf8g+CY66j96xz0Xzn -swuvCAAJWX/NKSqIk4cXGIDtiLK0thAfLdZfVaITXdHG6wZWiYj+rDKd/VzDBcdu -7oaJuogDnXIhhpCujwOl3J+IKMujkkkP7NAP4m1ET4BqstTnoApTAbqOl5F2brz8 -1Ws25kCI1nsvXwXoLG0R8+eyvpJETNKXpP7ScoFDB5zpET71ixpZfR9oWN0EACyW -80OzfpgZdNmcc9kYvkHHNHnZ9GLCQ7mzJ7Aiy/k9UscwR7PJPrhq4ufogXBeQotP -JqX+OsIgbrv4Fo7NDKm0G2x2EOFYeUY+VM6AqFcJNykbmROPDMjWLBz7BegIlT1l -RtzuzWniTY+HKE40Cz7PFNm73bZQmq131BnW2hqIyE4bJ3XYsgjxroMwuREOzYfw -hI0Vcnyh78zyiGG69Gm7DIwLdVcEuE4qFC49DxweMqZiNu5m4iK4BUBjECLzMx10 -coos9TkpoNPnG4CELcU9402x/RpvumUHO1jsQkUm+9jaJXLE9gCxInm943xZYkqc -BW89zubWR2OZxiRvchLIrH+QtAuRcOi35hYQcRfO3gZPSEF9NUqjifLJS3tBEW1n -twiYTOURGa5CgNz7kAXU+FDKvuStx8KU1xad5hePrzb7AgMBAAGjQjBAMA8GA1Ud -EwEB/wQFMAMBAf8wHQYDVR0OBBYEFJngGWcNYtt2s9o9uFvo/ULSMQ6HMA4GA1Ud -DwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAmHNw4rDT7TnsTGDZqRKGFx6W -0OhUKDtkLSGm+J1WE2pIPU/HPinbbViDVD2HfSMF1OQc3Og4ZYbFdada2zUFvXfe -uyk3QAUHw5RSn8pk3fEbK9xGChACMf1KaA0HZJDmHvUqoai7PF35owgLEQzxPy0Q -lG/+4jSHg9bP5Rs1bdID4bANqKCqRieCNqcVtgimQlRXtpla4gt5kNdXElE1GYhB -aCXUNxeEFfsBctyV3lImIJgm4nb1J2/6ADtKYdkNy1GTKv0WBpanI5ojSP5RvbbE -sLFUzt5sQa0WZ37b/TjNuThOssFgy50X31ieemKyJo90lZvkWx3SD92YHJtZuSPT -MaCm/zjdzyBP6VhWOmfD0faZmZ26NraAL4hHT4a/RDqA5Dccprrql5gR0IRiR2Qe -qu5AvzSxnI9O4fKSTx+O856X3vOmeWqJcU9LJxdI/uz0UA9PSX3MReO9ekDFQdxh -VicGaeVyQYHTtgGJoC86cnn+OjC/QezHYj6RS8fZMXZC+fc8Y+wmjHMMfRod6qh8 -h6jCJ3zhM0EPz8/8AKAigJ5Kp28AsEFFtyLKaEjFQqKu3R3y4G5OBVixwJAWKqQ9 -EEC+j2Jjg6mcgn0tAumDMHzLJ8n9HmYAsC7TIS+OMxZsmO0QqAfWzJPP29FpHOTK -yeC2nOnOcXHebD8WpHk= ------END CERTIFICATE-----`)) - return pool +-----END CERTIFICATE----- +` } diff --git a/common/certificate/chrome.pem b/common/certificate/chrome.pem new file mode 100644 index 0000000000..0d6ac2370b --- /dev/null +++ b/common/certificate/chrome.pem @@ -0,0 +1,2650 @@ +-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE +BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w +MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 +IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC +SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1 +ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv +UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX +4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9 +KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/ +gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb +rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ +51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F +be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe +KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F +v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn +fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7 +jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz +ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt +ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL +e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70 +jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz +WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V +SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j +pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX +X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok +fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R +K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU +ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU +LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT +LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQEL +BQAwYTELMAkGA1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUg +Q2VydGlmaWNhdGlvbiBFbGVjdHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJv +b3QgQ0EwHhcNMTkwNDI2MDg1NzU2WhcNNDQwNDI2MDg1NzU2WjBhMQswCQYDVQQG +EwJUTjE3MDUGA1UECgwuQWdlbmNlIE5hdGlvbmFsZSBkZSBDZXJ0aWZpY2F0aW9u +IEVsZWN0cm9uaXF1ZTEZMBcGA1UEAwwQVHVuVHJ1c3QgUm9vdCBDQTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAMPN0/y9BFPdDCA61YguBUtB9YOCfvdZ +n56eY+hz2vYGqU8ftPkLHzmMmiDQfgbU7DTZhrx1W4eI8NLZ1KMKsmwb60ksPqxd +2JQDoOw05TDENX37Jk0bbjBU2PWARZw5rZzJJQRNmpA+TkBuimvNKWfGzC3gdOgF +VwpIUPp6Q9p+7FuaDmJ2/uqdHYVy7BG7NegfJ7/Boce7SBbdVtfMTqDhuazb1YMZ +GoXRlJfXyqNlC/M4+QKu3fZnz8k/9YosRxqZbwUN/dAdgjH8KcwAWJeRTIAAHDOF +li/LQcKLEITDCSSJH7UP2dl3RxiSlGBcx5kDPP73lad9UKGAwqmDrViWVSHbhlnU +r8a83YFuB9tgYv7sEG7aaAH0gxupPqJbI9dkxt/con3YS7qC0lH4Zr8GRuR5KiY2 +eY8fTpkdso8MDhz/yV3A/ZAQprE38806JG60hZC/gLkMjNWb1sjxVj8agIl6qeIb +MlEsPvLfe/ZdeikZjuXIvTZxi11Mwh0/rViizz1wTaZQmCXcI/m4WEEIcb9PuISg +jwBUFfyRbVinljvrS5YnzWuioYasDXxU5mZMZl+QviGaAkYt5IPCgLnPSz7ofzwB +7I9ezX/SKEIBlYrilz0QIX32nRzFNKHsLA4KUiwSVXAkPcvCFDVDXSdOvsC9qnyW +5/yeYa1E0wCXAgMBAAGjYzBhMB0GA1UdDgQWBBQGmpsfU33x9aTI04Y+oXNZtPdE +ITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFAaamx9TffH1pMjThj6hc1m0 +90QhMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAqgVutt0Vyb+z +xiD2BkewhpMl0425yAA/l/VSJ4hxyXT968pk21vvHl26v9Hr7lxpuhbI87mP0zYu +QEkHDVneixCwSQXi/5E/S7fdAo74gShczNxtr18UnH1YeA32gAm56Q6XKRm4t+v4 +FstVEuTGfbvE7Pi1HE4+Z7/FXxttbUcoqgRYYdZ2vyJ/0Adqp2RT8JeNnYA/u8EH +22Wv5psymsNUk8QcCMNE+3tjEUPRahphanltkE8pjkcFwRJpadbGNjHh/PqAulxP +xOu3Mqz4dWEX1xAZufHSCe96Qp1bWgvUxpVOKs7/B9dPfhgGiPEZtdmYu65xxBzn +dFlY7wyJz4sfdZMaBBSSSFCp61cpABbjNhzI+L/wM9VBD8TMPN3pM0MBkRArHtG5 +Xc0yGYuPjCB31yLEQtyEFpslbei0VXF/sHyz03FJuc9SpAQ/3D2gu68zngowYI7b +nV2UqL1g52KAdoGDDIzMMEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQ +CvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9zZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZH +u/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3rAZ3r2OvEhJn7wAzMMujj +d9qDRIueVSjAi1jTkD5OGwDxFa2DK5o= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj +ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM +9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw +IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 +VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L +93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm +jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA +A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI +U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs +N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv +o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU +5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy +rqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi +9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk +M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB +MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw +CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW +1KyLa2tJElMzrdfkviT8tQp21KW8EA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK +gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ +W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg +1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K +8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r +2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me +z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR +8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj +mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz +7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6 ++XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI +0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm +UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2 +LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY ++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS +k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl +7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm +btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl +urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+ +fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63 +n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE +76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H +9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT +4PsJYGw= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl +ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr +ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr +BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM +YyRIHN8wfdVoOw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFwDCCA6igAwIBAgIQHr9ZULjJgDdMBvfrVU+17TANBgkqhkiG9w0BAQ0FADB6 +MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu +MScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxHzAdBgNV +BAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwHhcNMTgwMzE2MTIxMDEzWhcNNDMw +MzE2MTIxMDEzWjB6MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEg +U3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRo +b3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQDRLY67tzbqbTeRn06TpwXkKQMlzhyC93yZ +n0EGze2jusDbCSzBfN8pfktlL5On1AFrAygYo9idBcEq2EXxkd7fO9CAAozPOA/q +p1x4EaTByIVcJdPTsuclzxFUl6s1wB52HO8AU5853BSlLCIls3Jy/I2z5T4IHhQq +NwuIPMqw9MjCoa68wb4pZ1Xi/K1ZXP69VyywkI3C7Te2fJmItdUDmj0VDT06qKhF +8JVOJVkdzZhpu9PMMsmN74H+rX2Ju7pgE8pllWeg8xn2A1bUatMn4qGtg/BKEiJ3 +HAVz4hlxQsDsdUaakFjgao4rpUYwBI4Zshfjvqm6f1bxJAPXsiEodg42MEx51UGa +mqi4NboMOvJEGyCI98Ul1z3G4z5D3Yf+xOr1Uz5MZf87Sst4WmsXXw3Hw09Omiqi +7VdNIuJGmj8PkTQkfVXjjJU30xrwCSss0smNtA0Aq2cpKNgB9RkEth2+dv5yXMSF +ytKAQd8FqKPVhJBPC/PgP5sZ0jeJP/J7UhyM9uH3PAeXjA6iWYEMspA90+NZRu0P +qafegGtaqge2Gcu8V/OXIXoMsSt0Puvap2ctTMSYnjYJdmZm/Bo/6khUHL4wvYBQ +v3y1zgD2DGHZ5yQD4OMBgQ692IU0iL2yNqh7XAjlRICMb/gv1SHKHRzQ+8S1h9E6 +Tsd2tTVItQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSM+xx1 +vALTn04uSNn5YFSqxLNP+jAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQAD +ggIBAEii1QALLtA/vBzVtVRJHlpr9OTy4EA34MwUe7nJ+jW1dReTagVphZzNTxl4 +WxmB82M+w85bj/UvXgF2Ez8sALnNllI5SW0ETsXpD4YN4fqzX4IS8TrOZgYkNCvo +zMrnadyHncI013nR03e4qllY/p0m+jiGPp2Kh2RX5Rc64vmNueMzeMGQ2Ljdt4NR +5MTMI9UGfOZR0800McD2RrsLrfw9EAUqO0qRJe6M1ISHgCq8CYyqOhNf6DR5UMEQ +GfnTKB7U0VEwKbOukGfWHwpjscWpxkIxYxeU72nLL/qMFH3EQxiJ2fAyQOaA4kZf +5ePBAFmo+eggvIksDkc0C+pXwlM2/KfUrzHN/gLldfq5Jwn58/U7yn2fqSLLiMmq +0Uc9NneoWWRrJ8/vJ8HjJLWG965+Mk2weWjROeiQWMODvA8s1pfrzgzhIMfatz7D +P78v3DSk+yshzWePS/Tj6tQ/50+6uaWTRRxmHyH6ZF5v4HaUMst19W7l9o/HuKTM +qJZ9ZPskWkoDbGs4xugDQ5r3V7mzKWmTOPQD8rv7gmsHINFSH5pkAnuYZttcTVoP +0ISVoDwUQwbKytu4QTbaakRnh6+v40URFWkIsr4WOZckbxJF0WddCajJFdr60qZf +E2Efv4WstK2tBZQIgx51F9NxO5NQI1mg7TyRVJ12AMXDuDjb +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM +MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D +ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU +cnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3 +WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg +Uy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw +IAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH +UV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM +TXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU +BBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM +kUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x +AcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV +HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y +sHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL +I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8 +J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY +VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI +03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICZTCCAeugAwIBAgIQeI8nXIESUiClBNAt3bpz9DAKBggqhkjOPQQDAzB0MQsw +CQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScw +JQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAXBgNVBAMT +EENlcnR1bSBFQy0zODQgQ0EwHhcNMTgwMzI2MDcyNDU0WhcNNDMwMzI2MDcyNDU0 +WjB0MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBT +LkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAX +BgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATE +KI6rGFtqvm5kN2PkzeyrOvfMobgOgknXhimfoZTy42B4mIF4Bk3y7JoOV2CDn7Tm +Fy8as10CW4kjPMIRBSqniBMY81CE1700LCeJVf/OTOffph8oxPBUw7l8t1Ot68Kj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI0GZnQkdjrzife81r1HfS+8 +EF9LMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjADVS2m5hjEfO/J +UG7BJw+ch69u1RsIGL2SKcHvlJF40jocVYli5RsJHrpka/F2tNQCMQC0QoSZ/6vn +nvuRlydd3LBbMHHOXjgaatkl5+r3YZJW+OraNsKHZZYuciUvf9/DE8k= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCB +gDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu +QS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG +A1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMCIYDzIwMTExMDA2MDgz +OTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UEChMZ +VW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3 +b3JrIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWA +DGSdhhuWZGc/IjoedQF97/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn +0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+oCgCXhVqqndwpyeI1B+twTUrWwbNWuKFB +OJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40bRr5HMNUuctHFY9rnY3lE +fktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2puTRZCr+E +Sv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1m +o130GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02i +sx7QBlrd9pPPV3WZ9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOW +OZV7bIBaTxNyxtd9KXpEulKkKtVBRgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgez +Tv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pyehizKV/Ma5ciSixqClnrDvFAS +adgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vMBhBgu4M1t15n +3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQ +F/xlhMcQSZDe28cmk4gmb3DWAl45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTf +CVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuAL55MYIR4PSFk1vtBHxgP58l1cb29 +XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMoclm2q8KMZiYcdywm +djWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tMpkT/ +WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jb +AoJnwTnbw3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksq +P/ujmv5zMnHCnsZy4YpoJ/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Ko +b7a6bINDd82Kkhehnlt4Fj1F4jNy3eFmypnTycUm/Q1oBEauttmbjL4ZvrHG8hnj +XALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P +5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi +DrW5viSP +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIG3Dp0v+ubHEwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UE +BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h +cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0xNDA5MjMxNTIyMDdaFw0zNjA1 +MDUxNTIyMDdaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg +Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9 +thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM +cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG +L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i +NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h +X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b +m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy +Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja +EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T +KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF +6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh +OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMB0GA1UdDgQWBBRlzeurNR4APn7VdMAc +tHNHDhpkLzASBgNVHRMBAf8ECDAGAQH/AgEBMIGmBgNVHSAEgZ4wgZswgZgGBFUd +IAAwgY8wLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuZmlybWFwcm9mZXNpb25hbC5j +b20vY3BzMFwGCCsGAQUFBwICMFAeTgBQAGEAcwBlAG8AIABkAGUAIABsAGEAIABC +AG8AbgBhAG4AbwB2AGEAIAA0ADcAIABCAGEAcgBjAGUAbABvAG4AYQAgADAAOAAw +ADEANzAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAHSHKAIrdx9m +iWTtj3QuRhy7qPj4Cx2Dtjqn6EWKB7fgPiDL4QjbEwj4KKE1soCzC1HA01aajTNF +Sa9J8OA9B3pFE1r/yJfY0xgsfZb43aJlQ3CTkBW6kN/oGbDbLIpgD7dvlAceHabJ +hfa9NPhAeGIQcDq+fUs5gakQ1JZBu/hfHAsdCPKxsIl68veg4MSPi3i1O1ilI45P +Vf42O+AMt8oqMEEgtIDNrvx2ZnOorm7hfNoD6JQg5iKj0B+QXSBTFCZX2lSX3xZE +EAEeiGaPcjiT3SC3NL7X8e5jjkd5KAb881lFJWAiMxujX6i6KtoaPc1A6ozuBRWV +1aUsIC+nmCjuRfzxuIgALI9C2lHVnOUTaHFFQ4ueCyE8S1wF3BqfmI7avSKecs2t +CsvMo2ebKHTEm9caPARYpoKdrcd7b/+Alun4jWq9GJAd/0kakFI3ky88Al2CdgtR +5xbHV/g4+afNmyJU72OwFW1TZQNKXkqgsqeOSQBZONXH9IBk9W6VULgRfhVwOEqw +f9DEMnDAGf/JOC0ULGb0QkTmVXYbgBVX/8Cnp6o5qtjTcNAuuuuUavpfNIbnYrX9 +ivAwhZTJryQCL2/W3Wf+47BVTwSYT6RBVuKT0Gro1vP7ZeDOdcQxWQzugsgMYDNK +GbqEZycPvEJdvSRUDewdcAZfpLz6IHxV +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNV +BAUTCUc2MzI4NzUxMDELMAkGA1UEBhMCRVMxJzAlBgNVBAoTHkFORiBBdXRvcmlk +YWQgZGUgQ2VydGlmaWNhY2lvbjEUMBIGA1UECxMLQU5GIENBIFJhaXoxIjAgBgNV +BAMTGUFORiBTZWN1cmUgU2VydmVyIFJvb3QgQ0EwHhcNMTkwOTA0MTAwMDM4WhcN +MzkwODMwMTAwMDM4WjCBhDESMBAGA1UEBRMJRzYzMjg3NTEwMQswCQYDVQQGEwJF +UzEnMCUGA1UEChMeQU5GIEF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uMRQwEgYD +VQQLEwtBTkYgQ0EgUmFpejEiMCAGA1UEAxMZQU5GIFNlY3VyZSBTZXJ2ZXIgUm9v +dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANvrayvmZFSVgpCj +cqQZAZ2cC4Ffc0m6p6zzBE57lgvsEeBbphzOG9INgxwruJ4dfkUyYA8H6XdYfp9q +yGFOtibBTI3/TO80sh9l2Ll49a2pcbnvT1gdpd50IJeh7WhM3pIXS7yr/2WanvtH +2Vdy8wmhrnZEE26cLUQ5vPnHO6RYPUG9tMJJo8gN0pcvB2VSAKduyK9o7PQUlrZX +H1bDOZ8rbeTzPvY1ZNoMHKGESy9LS+IsJJ1tk0DrtSOOMspvRdOoiXsezx76W0OL +zc2oD2rKDF65nkeP8Nm2CgtYZRczuSPkdxl9y0oukntPLxB3sY0vaJxizOBQ+OyR +p1RMVwnVdmPF6GUe7m1qzwmd+nxPrWAI/VaZDxUse6mAq4xhj0oHdkLePfTdsiQz +W7i1o0TJrH93PB0j7IKppuLIBkwC/qxcmZkLLxCKpvR/1Yd0DVlJRfbwcVw5Kda/ +SiOL9V8BY9KHcyi1Swr1+KuCLH5zJTIdC2MKF4EA/7Z2Xue0sUDKIbvVgFHlSFJn +LNJhiQcND85Cd8BEc5xEUKDbEAotlRyBr+Qc5RQe8TZBAQIvfXOn3kLMTOmJDVb3 +n5HUA8ZsyY/b2BzgQJhdZpmYgG4t/wHFzstGH6wCxkPmrqKEPMVOHj1tyRRM4y5B +u8o5vzY8KhmqQYdOpc5LMnndkEl/AgMBAAGjYzBhMB8GA1UdIwQYMBaAFJxf0Gxj +o1+TypOYCK2Mh6UsXME3MB0GA1UdDgQWBBScX9BsY6Nfk8qTmAitjIelLFzBNzAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AgEATh65isagmD9uw2nAalxJUqzLK114OMHVVISfk/CHGT0sZonrDUL8zPB1hT+L +9IBdeeUXZ701guLyPI59WzbLWoAAKfLOKyzxj6ptBZNscsdW699QIyjlRRA96Gej +rw5VD5AJYu9LWaL2U/HANeQvwSS9eS9OICI7/RogsKQOLHDtdD+4E5UGUcjohybK +pFtqFiGS3XNgnhAY3jyB6ugYw3yJ8otQPr0R4hUDqDZ9MwFsSBXXiJCZBMXM5gf0 +vPSQ7RPi6ovDj6MzD8EpTBNO2hVWcXNyglD2mjN8orGoGjR0ZVzO0eurU+AagNjq +OknkJjCb5RyKqKkVMoaZkgoQI1YS4PbOTOK7vtuNknMBZi9iPrJyJ0U27U1W45eZ +/zo1PqVUSlJZS2Db7v54EX9K3BR5YLZrZAPbFYPhor72I5dQ8AkzNqdxliXzuUJ9 +2zg/LFis6ELhDtjTO0wugumDLmsx2d1Hhk9tl5EuT+IocTUW0fJz/iUrB0ckYyfI ++PbZa/wSMVYIwFNCr5zQM378BvAxRAMU8Vjq8moNqRGyg77FGr8H6lnco4g175x2 +MjxNBiLOFeXdntiP2t7SxDnlF4HPOEfrf4htWRvfn0IUrn7PqLBmZdo3r5+qPeoo +tt7VMVgWglvquxl1AnMaykgaIZOQCo6ThKd9OyMYkomgjaw= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr +6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV +L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91 +1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx +MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ +QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB +arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr +Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi +FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS +P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN +9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz +uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h +9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s +A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t +OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo ++fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7 +KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2 +DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us +H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ +I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 +5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h +3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz +Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y +ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E +N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9 +tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX +0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c +/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X +KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY +zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS +O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D +34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP +K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3 +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv +Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj +QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV +cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS +IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2 +HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa +O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv +033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u +dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE +kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41 +3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD +u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq +4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIRAI4P+UuQcWhlM1T01EQ5t+AwDQYJKoZIhvcNAQELBQAw +PTELMAkGA1UEBhMCVVMxEjAQBgNVBAoTCUNlcnRhaW5seTEaMBgGA1UEAxMRQ2Vy +dGFpbmx5IFJvb3QgUjEwHhcNMjEwNDAxMDAwMDAwWhcNNDYwNDAxMDAwMDAwWjA9 +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0 +YWlubHkgUm9vdCBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANA2 +1B/q3avk0bbm+yLA3RMNansiExyXPGhjZjKcA7WNpIGD2ngwEc/csiu+kr+O5MQT +vqRoTNoCaBZ0vrLdBORrKt03H2As2/X3oXyVtwxwhi7xOu9S98zTm/mLvg7fMbed +aFySpvXl8wo0tf97ouSHocavFwDvA5HtqRxOcT3Si2yJ9HiG5mpJoM610rCrm/b0 +1C7jcvk2xusVtyWMOvwlDbMicyF0yEqWYZL1LwsYpfSt4u5BvQF5+paMjRcCMLT5 +r3gajLQ2EBAHBXDQ9DGQilHFhiZ5shGIXsXwClTNSaa/ApzSRKft43jvRl5tcdF5 +cBxGX1HpyTfcX35pe0HfNEXgO4T0oYoKNp43zGJS4YkNKPl6I7ENPT2a/Z2B7yyQ +wHtETrtJ4A5KVpK8y7XdeReJkd5hiXSSqOMyhb5OhaRLWcsrxXiOcVTQAjeZjOVJ +6uBUcqQRBi8LjMFbvrWhsFNunLhgkR9Za/kt9JQKl7XsxXYDVBtlUrpMklZRNaBA +2CnbrlJ2Oy0wQJuK0EJWtLeIAaSHO1OWzaMWj/Nmqhexx2DgwUMFDO6bW2BvBlyH +Wyf5QBGenDPBt+U1VwV/J84XIIwc/PH72jEpSe31C4SnT8H2TsIonPru4K8H+zMR +eiFPCyEQtkA6qyI6BJyLm4SGcprSp6XEtHWRqSsjAgMBAAGjQjBAMA4GA1UdDwEB +/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTgqj8ljZ9EXME66C6u +d0yEPmcM9DANBgkqhkiG9w0BAQsFAAOCAgEAuVevuBLaV4OPaAszHQNTVfSVcOQr +PbA56/qJYv331hgELyE03fFo8NWWWt7CgKPBjcZq91l3rhVkz1t5BXdm6ozTaw3d +8VkswTOlMIAVRQdFGjEitpIAq5lNOo93r6kiyi9jyhXWx8bwPWz8HA2YEGGeEaIi +1wrykXprOQ4vMMM2SZ/g6Q8CRFA3lFV96p/2O7qUpUzpvD5RtOjKkjZUbVwlKNrd +rRT90+7iIgXr0PK3aBLXWopBGsaSpVo7Y0VPv+E6dyIvXL9G+VoDhRNCX8reU9di +taY1BMJH/5n9hN9czulegChB8n3nHpDYT3Y+gjwN/KUD+nsa2UUeYNrEjvn8K8l7 +lcUq/6qJ34IxD3L/DCfXCh5WAFAeDJDBlrXYFIW7pw0WwfgHJBu6haEaBQmAupVj +yTrsJZ9/nbqkRxWbRHDxakvWOF5D8xh+UG7pWijmZeZ3Gzr9Hb4DJqPb1OG7fpYn +Kx3upPvaJVQTA945xsMfTZDsjxtK0hzthZU4UHlG1sGQUDGpXJpuHfUzVounmdLy +yCwzk5Iwx06MZTMQZBf9JBeW0Y3COmor6xOLRPIh80oat3df1+2IpHLlOR+Vnb5n +wXARPbv0+Em34yaXOp/SX3z7wJl8OSngex2/DaeP0ik0biQVy96QXr8axGbqwua6 +OV+KmalBWQewLK8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQsw +CQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlu +bHkgUm9vdCBFMTAeFw0yMTA0MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJ +BgNVBAYTAlVTMRIwEAYDVQQKEwlDZXJ0YWlubHkxGjAYBgNVBAMTEUNlcnRhaW5s +eSBSb290IEUxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3m/4fxzf7flHh4axpMCK ++IKXgOqPyEpeKn2IaKcBYhSRJHpcnqMXfYqGITQYUBsQ3tA3SybHGWCA6TS9YBk2 +QNYphwk8kXr2vBMj3VlOBF7PyAIcGFPBMdjaIOlEjeR2o0IwQDAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4 +hevIIgcwCgYIKoZIzj0EAwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozm +ut6Dacpps6kFtZaSF4fC0urQe87YQVt8rgIwRt7qy12a7DLCZRawTDBcMPPaTnOG +BtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAw +WjELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAw +MiA0ODE0NjMwODEwMDAzNjEZMBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0x +MzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjdaMFoxCzAJBgNVBAYTAkZSMRIwEAYD +VQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYzMDgxMDAwMzYxGTAX +BgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sO +ty3tRQgXstmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9M +CiBtnyN6tMbaLOQdLNyzKNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPu +I9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8JXrJhFwLrN1CTivngqIkicuQstDuI7pm +TLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16XdG+RCYyKfHx9WzMfgIh +C59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq4NYKpkDf +ePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3Yz +IoejwpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWT +Co/1VTp2lc5ZmIoJlXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1k +JWumIWmbat10TWuXekG9qxf5kBdIjzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5 +hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp//TBt2dzhauH8XwIDAQABo4IB +GjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of +1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczov +L3d3d3cuY2VydGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilo +dHRwOi8vY3JsLmNlcnRpZ25hLmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYr +aHR0cDovL2NybC5kaGlteW90aXMuY29tL2NlcnRpZ25hcm9vdGNhLmNybDANBgkq +hkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOItOoldaDgvUSILSo3L +6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxPTGRG +HVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH6 +0BGM+RFq7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncB +lA2c5uk5jR+mUYyZDDl34bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdi +o2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1 +gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS6Cvu5zHbugRqh5jnxV/v +faci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaYtlu3zM63 +Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayh +jWZSaX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw +3kAP+HwV96LOPNdeE4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV +BAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4X +DTA3MDYyOTE1MTMwNVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQ +BgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwIQ2VydGlnbmEwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7qXOEm7RFHYeGifBZ4 +QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyHGxny +gQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbw +zBfsV1/pogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q +130yGLMLLGq/jj8UEYkgDncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2 +JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKfIrjxwo1p3Po6WAbfAgMBAAGjgbwwgbkw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQtCRZvgHyUtVF9lo53BEw +ZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJBgNVBAYT +AkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzj +AQ/JSP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG +9w0BAQUFAAOCAQEAhQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8h +bV6lUmPOEvjvKtpv6zf+EwLHyzs+ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFnc +fca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9qcEQCYsuu +HWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w +t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw +WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYT +AlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBD +QTAeFw0wNjA3MDQxNzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJP +MREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7IJUqOtdu0KBuqV5Do +0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHHrfAQ +UySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5d +RdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQ +OA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv +JoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08C +AwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0O +BBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJ +LjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecY +MnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ +44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6I +Jd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNw +i/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN +9u6wWk5JRFRYX0KD +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV +BAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04g +Uk9PVCBDQSBHMjAeFw0xNzAyMDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJ +BgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJ +R04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDF +dRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05N0Iw +vlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZ +uIt4ImfkabBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhp +n+Sc8CnTXPnGFiWeI8MgwT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKs +cpc/I1mbySKEwQdPzH/iV8oScLumZfNpdWO9lfsbl83kqK/20U6o2YpxJM02PbyW +xPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91QqhngLjYl/rNUssuHLoPj1P +rCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732jcZZroiF +DsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fx +DTvf95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgy +LcsUDFDYg2WD7rlcz8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6C +eWRgKRM+o/1Pcmqr4tTluCRVLERLiohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSCIS1mxteg4BXrzkwJ +d8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOBywaK8SJJ6ejq +kX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC +b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQl +qiCA2ClV9+BB/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0 +OJD7uNGzcgbJceaBxXntC6Z58hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+c +NywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5BiKDUyUM/FHE5r7iOZULJK2v0ZXk +ltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklWatKcsWMy5WHgUyIO +pwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tUSxfj +03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZk +PuXaTH4MNMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE +1LlSVHJ7liXMvGnjSG4N0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MX +QRBdJ3NghVdJIgc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe +Fw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMxMjdaMF4xCzAJBgNVBAYTAlRXMSMw +IQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEqMCgGA1UECwwhZVBL +SSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAH +SyZbCUNsIZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAh +ijHyl3SJCRImHJ7K2RKilTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3X +DZoTM1PRYfl61dd4s5oz9wCGzh1NlDivqOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1 +TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX12ruOzjjK9SXDrkb5wdJ +fzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0OWQqraffA +sgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uU +WH1+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLS +nT0IFaUQAS2zMnaolQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pH +dmX2Os+PYhcZewoozRrSgx4hxyy/vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJip +NiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXiZo1jDiVN1Rmy5nk3pyKdVDEC +AwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/QkqiMAwGA1UdEwQF +MAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH +ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGB +uvl2ICO1J2B01GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6Yl +PwZpVnPDimZI+ymBV3QGypzqKOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkP +JXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdVxrsStZf0X4OFunHB2WyBEXYKCrC/ +gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEPNXubrjlpC2JgQCA2 +j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+rGNm6 +5ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUB +o2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS +/jQ6fbjpKdx2qcgw+BRxgMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2z +Gp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE +W9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D +hNQ+IIX3Sj0rnP0qCglN6oH4EZw= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFajCCA1KgAwIBAgIQLd2szmKXlKFD6LDNdmpeYDANBgkqhkiG9w0BAQsFADBP +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xGzAZBgNVBAMMEkhpUEtJIFJvb3QgQ0EgLSBHMTAeFw0xOTAyMjIwOTQ2MDRa +Fw0zNzEyMzExNTU5NTlaME8xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3 +YSBUZWxlY29tIENvLiwgTHRkLjEbMBkGA1UEAwwSSGlQS0kgUm9vdCBDQSAtIEcx +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9B5/UnMyDHPkvRN0o9Qw +qNCuS9i233VHZvR85zkEHmpwINJaR3JnVfSl6J3VHiGh8Ge6zCFovkRTv4354twv +Vcg3Px+kwJyz5HdcoEb+d/oaoDjq7Zpy3iu9lFc6uux55199QmQ5eiY29yTw1S+6 +lZgRZq2XNdZ1AYDgr/SEYYwNHl98h5ZeQa/rh+r4XfEuiAU+TCK72h8q3VJGZDnz +Qs7ZngyzsHeXZJzA9KMuH5UHsBffMNsAGJZMoYFL3QRtU6M9/Aes1MU3guvklQgZ +KILSQjqj2FPseYlgSGDIcpJQ3AOPgz+yQlda22rpEZfdhSi8MEyr48KxRURHH+CK +FgeW0iEPU8DtqX7UTuybCeyvQqww1r/REEXgphaypcXTT3OUM3ECoWqj1jOXTyFj +HluP2cFeRXF3D4FdXyGarYPM+l7WjSNfGz1BryB1ZlpK9p/7qxj3ccC2HTHsOyDr +y+K49a6SsvfhhEvyovKTmiKe0xRvNlS9H15ZFblzqMF8b3ti6RZsR1pl8w4Rm0bZ +/W3c1pzAtH2lsN0/Vm+h+fbkEkj9Bn8SV7apI09bA8PgcSojt/ewsTu8mL3WmKgM +a/aOEmem8rJY5AIJEzypuxC00jBF8ez3ABHfZfjcK0NVvxaXxA/VLGGEqnKG/uY6 +fsI/fe78LxQ+5oXdUG+3Se0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQU8ncX+l6o/vY9cdVouslGDDjYr7AwDgYDVR0PAQH/BAQDAgGGMA0GCSqG +SIb3DQEBCwUAA4ICAQBQUfB13HAE4/+qddRxosuej6ip0691x1TPOhwEmSKsxBHi +7zNKpiMdDg1H2DfHb680f0+BazVP6XKlMeJ45/dOlBhbQH3PayFUhuaVevvGyuqc +SE5XCV0vrPSltJczWNWseanMX/mF+lLFjfiRFOs6DRfQUsJ748JzjkZ4Bjgs6Fza +ZsT0pPBWGTMpWmWSBUdGSquEwx4noR8RkpkndZMPvDY7l1ePJlsMu5wP1G4wB9Tc +XzZoZjmDlicmisjEOf6aIW/Vcobpf2Lll07QJNBAsNB1CI69aO4I1258EHBGG3zg +iLKecoaZAeO/n0kZtCW+VmWuF2PlHt/o/0elv+EmBYTksMCv5wiZqAxeJoBF1Pho +L5aPruJKHJwWDBNvOIf2u8g0X5IDUXlwpt/L9ZlNec1OvFefQ05rLisY+GpzjLrF +Ne85akEez3GoorKGB1s6yeHvP2UEgEcyRHCVTjFnanRbEEV16rCf0OY1/k6fi8wr +kkVbbiVghUbN0aqwdmaTd5a+g744tiROJgvM7XpWGuDpWsZkrUx6AEhEL7lAuxM+ +vhV4nYWBSipX3tUZQ9rbyltHhoMLP7YNdnhzeSJesYAfz77RP1YQmCuVh6EfnWQU +YDksswBVLuT1sw5XxJFBAJw/6KXf6vb/yPCtbVKoF6ubYfwSUTXkJf2vqmqGOQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFqTCCA5GgAwIBAgIQczswBEhb2U14LnNLyaHcZjANBgkqhkiG9w0BAQ0FADBI +MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE +LVRSVVNUIEJSIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA4NTYzMVoXDTM4MDUw +OTA4NTYzMFowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi +MCAGA1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAK7/CVmRgApKaOYkP7in5Mg6CjoWzckjYaCTcfKr +i3OPoGdlYNJUa2NRb0kz4HIHE304zQaSBylSa053bATTlfrdTIzZXcFhfUvnKLNE +gXtRr90zsWh81k5M/itoucpmacTsXld/9w3HnDY25QdgrMBM6ghs7wZ8T1soegj8 +k12b9py0i4a6Ibn08OhZWiihNIQaJZG2tY/vsvmA+vk9PBFy2OMvhnbFeSzBqZCT +Rphny4NqoFAjpzv2gTng7fC5v2Xx2Mt6++9zA84A9H3X4F07ZrjcjrqDy4d2A/wl +2ecjbwb9Z/Pg/4S8R7+1FhhGaRTMBffb00msa8yr5LULQyReS2tNZ9/WtT5PeB+U +cSTq3nD88ZP+npNa5JRal1QMNXtfbO4AHyTsA7oC9Xb0n9Sa7YUsOCIvx9gvdhFP +/Wxc6PWOJ4d/GUohR5AdeY0cW/jPSoXk7bNbjb7EZChdQcRurDhaTyN0dKkSw/bS +uREVMweR2Ds3OmMwBtHFIjYoYiMQ4EbMl6zWK11kJNXuHA7e+whadSr2Y23OC0K+ +0bpwHJwh5Q8xaRfX/Aq03u2AnMuStIv13lmiWAmlY0cL4UEyNEHZmrHZqLAbWt4N +DfTisl01gLmB1IRpkQLLddCNxbU9CZEJjxShFHR5PtbJFR2kWVki3PaKRT08EtY+ +XTIvAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUZ5Dw1t61 +GNVGKX5cq/ieCLxklRAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG +OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfYnJfcm9vdF9jYV8y +XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQA097N3U9swFrktpSHxQCF16+tI +FoE9c+CeJyrrd6kTpGoKWloUMz1oH4Guaf2Mn2VsNELZLdB/eBaxOqwjMa1ef67n +riv6uvw8l5VAk1/DLQOj7aRvU9f6QA4w9QAgLABMjDu0ox+2v5Eyq6+SmNMW5tTR +VFxDWy6u71cqqLRvpO8NVhTaIasgdp4D/Ca4nj8+AybmTNudX0KEPUUDAxxZiMrc +LmEkWqTqJwtzEr5SswrPMhfiHocaFpVIbVrg0M8JkiZmkdijYQ6qgYF/6FKC0ULn +4B0Y+qSFNueG4A3rvNTJ1jxD8V1Jbn6Bm2m1iWKPiFLY1/4nwSPFyysCu7Ff/vtD +hQNGvl3GyiEm/9cCnnRK3PgTFbGBVzbLZVzRHTF36SXDw7IyN9XxmAnkbWOACKsG +koHU6XCPpz+y7YaMgmo1yEJagtFSGkUPFaUA8JR7ZSdXOUPPfH/mvTWze/EZTN46 +ls/pdu4D58JDUjxqgejBWoC9EV2Ta/vH5mQ/u2kc6d0li690yVRAysuTEwrt+2aS +Ecr1wPrYg1UDfNPFIkZ1cGt5SAYqgpq/5usWDiJFAbzdNpQ0qTUmiteXue4Icr80 +knCDgKs4qllo3UCkGJCy89UDyibK79XH4I9TjvAA46jtn/mtd+ArY0+ew+43u3gJ +hJ65bvspmZDogNOfJA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFqTCCA5GgAwIBAgIQaSYJfoBLTKCnjHhiU19abzANBgkqhkiG9w0BAQ0FADBI +MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE +LVRSVVNUIEVWIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA5MTAzM1oXDTM4MDUw +OTA5MTAzMlowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi +MCAGA1UEAxMZRC1UUlVTVCBFViBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBANiOo4mAC7JXUtypU0w3uX9jFxPvp1sjW2l1sJkK +F8GLxNuo4MwxusLyzV3pt/gdr2rElYfXR8mV2IIEUD2BCP/kPbOx1sWy/YgJ25yE +7CUXFId/MHibaljJtnMoPDT3mfd/06b4HEV8rSyMlD/YZxBTfiLNTiVR8CUkNRFe +EMbsh2aJgWi6zCudR3Mfvc2RpHJqnKIbGKBv7FD0fUDCqDDPvXPIEysQEx6Lmqg6 +lHPTGGkKSv/BAQP/eX+1SH977ugpbzZMlWGG2Pmic4ruri+W7mjNPU0oQvlFKzIb +RlUWaqZLKfm7lVa/Rh3sHZMdwGWyH6FDrlaeoLGPaxK3YG14C8qKXO0elg6DpkiV +jTujIcSuWMYAsoS0I6SWhjW42J7YrDRJmGOVxcttSEfi8i4YHtAxq9107PncjLgc +jmgjutDzUNzPZY9zOjLHfP7KgiJPvo5iR2blzYfi6NUPGJ/lBHJLRjwQ8kTCZFZx +TnXonMkmdMV9WdEKWw9t/p51HBjGGjp82A0EzM23RWV6sY+4roRIPrN6TagD4uJ+ +ARZZaBhDM7DS3LAaQzXupdqpRlyuhoFBAUp0JuyfBr/CBTdkdXgpaP3F9ev+R/nk +hbDhezGdpn9yo7nELC7MmVcOIQxFAZRl62UJxmMiCzNJkkg8/M3OsD6Onov4/knF +NXJHAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUqvyREBuH +kV8Wub9PS5FeAByxMoAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG +OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfZXZfcm9vdF9jYV8y +XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQCTy6UfmRHsmg1fLBWTxj++EI14 +QvBukEdHjqOSMo1wj/Zbjb6JzkcBahsgIIlbyIIQbODnmaprxiqgYzWRaoUlrRc4 +pZt+UPJ26oUFKidBK7GB0aL2QHWpDsvxVUjY7NHss+jOFKE17MJeNRqrphYBBo7q +3C+jisosketSjl8MmxfPy3MHGcRqwnNU73xDUmPBEcrCRbH0O1P1aa4846XerOhU +t7KR/aypH/KH5BfGSah82ApB9PI+53c0BFLd6IHyTS9URZ0V4U/M5d40VxDJI3IX +cI1QcB9WbMy5/zpaT2N6w25lBx2Eof+pDGOJbbJAiDnXH3dotfyc1dZnaVuodNv8 +ifYbMvekJKZ2t0dT741Jj6m2g1qllpBFYfXeA08mD6iL8AOWsKwV0HFaanuU5nCT +2vFp4LJiTZ6P/4mdm13NRemUAiKN4DV/6PEEeXFsVIP4M7kFMhtYVRFP0OUnR3Hs +7dpn1mKmS00PaaLJvOwiS5THaJQXfuKOKD62xur1NGyfN4gHONuGcfrNlUhDbqNP +gofXNJhuS5N5YHVpD/Aa1VP6IQzCP+k/HxiMkl14p3ZnGbuy6n/pcAlWVqOwDAst +Nl7F6cTVg8uGF5csbBNvh1qvSaYd2804BC5f4ko1Di1L+KIkBI3Y4WNeApI02phh +XBxvWHZks/wCuPWdCg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQfMmPK4TX3+oPyWWa00tNljAKBggqhkjOPQQDAzBIMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS +VVNUIEJSIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTA5NDUwMFoXDTM1MDIxMTA5 +NDQ1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG +A1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABMbLxyjR+4T1mu9CFCDhQ2tuda38KwOE1HaTJddZO0Flax7mNCq7dPYS +zuht56vkPE4/RAiLzRZxy7+SmfSk1zxQVFKQhYN4lGdnoxwJGT11NIXe7WB9xwy0 +QVK5buXuQqOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHOREKv/ +VbNafAkl1bK6CKBrqx9tMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g +PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2JyX3Jvb3Rf +Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l +dC9DTj1ELVRSVVNUJTIwQlIlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 +c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO +PQQDAwNpADBmAjEAlJAtE/rhY/hhY+ithXhUkZy4kzg+GkHaQBZTQgjKL47xPoFW +wKrY7RjEsK70PvomAjEA8yjixtsrmfu3Ubgko6SUeho/5jbiA1czijDLgsfWFBHV +dWNbFJWcHwHP2NVypw87 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQXwJB13qHfEwDo6yWjfv/0DAKBggqhkjOPQQDAzBIMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS +VVNUIEVWIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTEwMDAwMFoXDTM1MDIxMTA5 +NTk1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG +A1UEAxMZRC1UUlVTVCBFViBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPEL3YZDIBnfl4XoIkqbz52Yv7QFJsnL46bSj8WeeHsxiamJrSc8ZRCC +/N/DnU7wMyPE0jL1HLDfMxddxfCxivnvubcUyilKwg+pf3VlSSowZ/Rk99Yad9rD +wpdhQntJraOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFH8QARY3 +OqQo5FD4pPfsazK2/umLMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g +PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2V2X3Jvb3Rf +Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l +dC9DTj1ELVRSVVNUJTIwRVYlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 +c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO +PQQDAwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CA +y/m0sRtW9XLS/BnRAjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJb +gfM0agPnIjhQW+0ZT0MW +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw +NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV +BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn +ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 +3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z +qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR +p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 +HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw +ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea +HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw +Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh +c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E +RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt +dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku +Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp +3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 +nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF +CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na +xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX +KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha +ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM +HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 +UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 +tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R +ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM +lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp +/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G +A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G +A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj +dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy +MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl +cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js +L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL +BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni +acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 +o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K +zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 +PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y +Johw1+qRzT65ysCQblrGXnRl11z+o+I= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd +AqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC +FoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi +1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq +jnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ +wI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/ +WSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy +NsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC +uvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw +IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6 +g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN +9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP +BSeOE6Fuwg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIQIZxULej27HF3+k7ow3BXlzANBgkqhkiG9w0BAQwFADBj +MQswCQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0 +eSBHbWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBSU0EgUm9vdCAy +MDIzMB4XDTIzMDMyODEyMTY0NVoXDTQ4MDMyNzIzNTk1OVowYzELMAkGA1UEBhMC +REUxJzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkG +A1UEAwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgUlNBIFJvb3QgMjAyMzCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAO01oYGA88tKaVvC+1GDrib94W7zgRJ9 +cUD/h3VCKSHtgVIs3xLBGYSJwb3FKNXVS2xE1kzbB5ZKVXrKNoIENqil/Cf2SfHV +cp6R+SPWcHu79ZvB7JPPGeplfohwoHP89v+1VmLhc2o0mD6CuKyVU/QBoCcHcqMA +U6DksquDOFczJZSfvkgdmOGjup5czQRxUX11eKvzWarE4GC+j4NSuHUaQTXtvPM6 +Y+mpFEXX5lLRbtLevOP1Czvm4MS9Q2QTps70mDdsipWol8hHD/BeEIvnHRz+sTug +BTNoBUGCwQMrAcjnj02r6LX2zWtEtefdi+zqJbQAIldNsLGyMcEWzv/9FIS3R/qy +8XDe24tsNlikfLMR0cN3f1+2JeANxdKz+bi4d9s3cXFH42AYTyS2dTd4uaNir73J +co4vzLuu2+QVUhkHM/tqty1LkCiCc/4YizWN26cEar7qwU02OxY2kTLvtkCJkUPg +8qKrBC7m8kwOFjQgrIfBLX7JZkcXFBGk8/ehJImr2BrIoVyxo/eMbcgByU/J7MT8 +rFEz0ciD0cmfHdRHNCk+y7AO+oMLKFjlKdw/fKifybYKu6boRhYPluV75Gp6SG12 +mAWl3G0eQh5C2hrgUve1g8Aae3g1LDj1H/1Joy7SWWO/gLCMk3PLNaaZlSJhZQNg ++y+TS/qanIA7AgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtqeX +gj10hZv3PJ+TmpV5dVKMbUcwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS2 +p5eCPXSFm/c8n5OalXl1UoxtRzANBgkqhkiG9w0BAQwFAAOCAgEAqMxhpr51nhVQ +pGv7qHBFfLp+sVr8WyP6Cnf4mHGCDG3gXkaqk/QeoMPhk9tLrbKmXauw1GLLXrtm +9S3ul0A8Yute1hTWjOKWi0FpkzXmuZlrYrShF2Y0pmtjxrlO8iLpWA1WQdH6DErw +M807u20hOq6OcrXDSvvpfeWxm4bu4uB9tPcy/SKE8YXJN3nptT+/XOR0so8RYgDd +GGah2XsjX/GO1WfoVNpbOms2b/mBsTNHM3dA+VKq3dSDz4V4mZqTuXNnQkYRIer+ +CqkbGmVps4+uFrb2S1ayLfmlyOw7YqPta9BO1UAJpB+Y1zqlklkg5LB9zVtzaL1t +xKITDmcZuI1CfmwMmm6gJC3VRRvcxAIU/oVbZZfKTpBQCHpCNfnqwmbU+AGuHrS+ +w6jv/naaoqYfRvaE7fzbzsQCzndILIyy7MMAo+wsVRjBfhnu4S/yrYObnqsZ38aK +L4x35bcF7DvB7L6Gs4a8wPfc5+pbrrLMtTWGS9DiP7bY+A4A7l3j941Y/8+LN+lj +X273CXE2whJdV/LItM3z7gLfEdxquVeEHVlNjM7IDiPCtyaaEBRx/pOyiriA8A4Q +ntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0o82bNSQ3+pCTE4FCxpgm +dTdmQRCsu/WU48IxK63nI1bMNSWSs1A= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN +8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ +RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 +hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 +ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM +EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 +A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy +WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ +1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 +6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT +91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml +e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p +TpPDpFQUWw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICQjCCAcmgAwIBAgIQNjqWjMlcsljN0AFdxeVXADAKBggqhkjOPQQDAzBjMQsw +CQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBH +bWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBFQ0MgUm9vdCAyMDIw +MB4XDTIwMDgyNTA3NDgyMFoXDTQ1MDgyNTIzNTk1OVowYzELMAkGA1UEBhMCREUx +JzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkGA1UE +AwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgRUNDIFJvb3QgMjAyMDB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABM6//leov9Wq9xCazbzREaK9Z0LMkOsVGJDZos0MKiXrPk/O +tdKPD/M12kOLAoC+b1EkHQ9rK8qfwm9QMuU3ILYg/4gND21Ju9sGpIeQkpT0CdDP +f8iAC8GXs7s1J8nCG6NCMEAwHQYDVR0OBBYEFONyzG6VmUex5rNhTNHLq+O6zd6f +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA +MGQCMHVSi7ekEE+uShCLsoRbQuHmKjYC2qBuGT8lv9pZMo7k+5Dck2TOrbRBR2Di +z6fLHgIwN0GMZt9Ba9aDAEH9L1r3ULRn0SyocddDypwnJJGDSA3PzfdUga/sf+Rn +27iQ7t0l +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp +Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 +MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ +bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS +7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp +0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS +B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 +BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ +LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 +DXZDjC5Ty3zfDBeWUA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBN +MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMT +HERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN +NDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs +IEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS87IE+ +ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG0 +2C+JFvuUAT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgp +wgscONyfMXdcvyej/Cestyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZM +pG2T6T867jp8nVid9E6P/DsjyG244gXazOvswzH016cpVIDPRFtMbzCe88zdH5RD +nU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnVDdXifBBiqmvwPXbzP6Po +sMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9qTXeXAaDx +Zre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cd +Lvvyz6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvX +KyY//SovcfXWJL5/MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNe +XoVPzthwiHvOAbWWl9fNff2C+MIkwcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPL +tgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4EFgQUUTMc7TZArxfTJc1paPKv +TiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN +AQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw +GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7H +PNtQOa27PShNlnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLF +O4uJ+DQtpBflF+aZfTCIITfNMBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQ +REtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/u4cnYiWB39yhL/btp/96j1EuMPik +AdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9GOUrYU9DzLjtxpdRv +/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh47a+ +p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilw +MUc/dNAUFvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WF +qUITVuwhd4GTWgzqltlJyqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCK +ovfepEWFJqgejF0pW8hL2JpqA15w8oVPbEtoL8pU9ozaMv7Da4M/OMZ+ +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c +JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP +mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+ +wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4 +VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/ +AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB +AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun +pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC +dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf +fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm +NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx +H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe ++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA +n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc +biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp +EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA +bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu +YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB +AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW +BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI +QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I +0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni +lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 +B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv +ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg +RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf +Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q +RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD +AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY +JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv +6pZjamVFkpUBtA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH +MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI +2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx +1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ +q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz +tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ +vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV +5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY +1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 +NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG +Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 +8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe +pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe +Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw +EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x +IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG +fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO +Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd +BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx +AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ +oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 +sycX +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug +RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm ++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW +PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM +xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB +Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 +hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg +EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA +FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec +nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z +eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF +hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 +Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep ++OkuE6N36B9K +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg +RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y +ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If +xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV +ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO +DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ +jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/ +CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi +EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM +fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY +uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK +chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t +9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2 +SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd ++SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc +fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa +sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N +cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N +0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie +4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI +r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 +/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm +gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa +GMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg +Fyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J +WpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB +rrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp ++ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1 +ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i +Ucw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz +PtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og +/zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH +oycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI +yV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1Ud +EwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwzJQTU7tD2 +A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL +MAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT +ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2f +BluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn +g/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl +fF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K +WWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha +B0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc +hLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR +TUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD +mbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z +ohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y +4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza +8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 +MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf +qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW +n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym +c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ +O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 +o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j +IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq +IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz +8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh +vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l +7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG +cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD +ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC +roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga +W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n +lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE ++V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV +csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd +dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg +KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM +HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 +WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 +MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR +/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu +FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR +U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c +ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR +FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k +A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw +eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl +sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp +VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q +A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ +ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD +ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI +FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv +oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg +u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP +0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf +3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl +8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ +DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN +PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ +ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV +BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu +MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy +MDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx +EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe +NcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH +PWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I +x2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe +QTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR +yyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO +QG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912 +H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ +QfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD +i/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs +nLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1 +rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI +hvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM +tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf +GopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb +lvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka ++elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal +TFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i +nSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3 +gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr +G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os +zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x +L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG +EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo +bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g +RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ +TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s +b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0 +WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS +fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB +zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq +hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB +CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD ++JbNR6iC8hZVdyR+EhCVBCyj +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYD +VQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBU +ZWNobm9sb2dpZXMgTGltaXRlZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBH +MTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgxODMwMDBaMGcxCzAJBgNVBAYTAklO +MRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVkaHJhIFRlY2hub2xv +Z2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQz +f2N4aLTNLnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO +8oG0x5ZOrRkVUkr+PHB1cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aq +d7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHWDV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhM +tTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ6DqS0hdW5TUaQBw+jSzt +Od9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrHhQIDAQAB +o0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQD +AgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31x +PaOfG1vR2vjTnGs2vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjM +wiI/aTvFthUvozXGaCocV685743QNcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6d +GNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q+Mri/Tm3R7nrft8EI6/6nAYH +6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeihU80Bv2noWgby +RQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx +iN66zB+Afko= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE +AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG +EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM +FUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC +REUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp +Nb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM +VD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+ +SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ +4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L +cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi +eowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG +A1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3 +DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j +vZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP +DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc +maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D +lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv +KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICFTCCAZugAwIBAgIQPZg7pmY9kGP3fiZXOATvADAKBggqhkjOPQQDAzBMMS4w +LAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgRUNDIFRMUyAyMDIxMQ0w +CwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTI2MjNaFw00MTA0 +MTcwOTI2MjJaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBDQSBF +Q0MgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMHYwEAYHKoZI +zj0CAQYFK4EEACIDYgAEloZYKDcKZ9Cg3iQZGeHkBQcfl+3oZIK59sRxUM6KDP/X +tXa7oWyTbIOiaG6l2b4siJVBzV3dscqDY4PMwL502eCdpO5KTlbgmClBk1IQ1SQ4 +AjJn8ZQSb+/Xxd4u/RmAo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR2 +KCXWfeBmmnoJsmo7jjPXNtNPojAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMD +aAAwZQIwW5kp85wxtolrbNa9d+F851F+uDrNozZffPc8dz7kUK2o59JZDCaOMDtu +CCrCp1rIAjEAmeMM56PDr9NJLkaCI2ZdyQAUEv049OGYa3cpetskz2VAv9LcjBHo +9H1/IISpQuQo +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFZDCCA0ygAwIBAgIQU9XP5hmTC/srBRLYwiqipDANBgkqhkiG9w0BAQwFADBM +MS4wLAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgUlNBIFRMUyAyMDIx +MQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTIxMTBaFw00 +MTA0MTcwOTIxMDlaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBD +QSBSU0EgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtoAOxHm9BYx9sKOdTSJNy/BBl01Z +4NH+VoyX8te9j2y3I49f1cTYQcvyAh5x5en2XssIKl4w8i1mx4QbZFc4nXUtVsYv +Ye+W/CBGvevUez8/fEc4BKkbqlLfEzfTFRVOvV98r61jx3ncCHvVoOX3W3WsgFWZ +kmGbzSoXfduP9LVq6hdKZChmFSlsAvFr1bqjM9xaZ6cF4r9lthawEO3NUDPJcFDs +GY6wx/J0W2tExn2WuZgIWWbeKQGb9Cpt0xU6kGpn8bRrZtkh68rZYnxGEFzedUln +nkL5/nWpo63/dgpnQOPF943HhZpZnmKaau1Fh5hnstVKPNe0OwANwI8f4UDErmwh +3El+fsqyjW22v5MvoVw+j8rtgI5Y4dtXz4U2OLJxpAmMkokIiEjxQGMYsluMWuPD +0xeqqxmjLBvk1cbiZnrXghmmOxYsL3GHX0WelXOTwkKBIROW1527k2gV+p2kHYzy +geBYBr3JtuP2iV2J+axEoctr+hbxx1A9JNr3w+SH1VbxT5Aw+kUJWdo0zuATHAR8 +ANSbhqRAvNncTFd+rrcztl524WWLZt+NyteYr842mIycg5kDcPOvdO3GDjbnvezB +c6eUWsuSZIKmAMFwoW4sKeFYV+xafJlrJaSQOoD0IJ2azsct+bJLKZWD6TWNp0lI +pw9MGZHQ9b8Q4HECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU +dEmZ0f+0emhFdcN+tNzMzjkz2ggwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB +DAUAA4ICAQAjQ1MkYlxt/T7Cz1UAbMVWiLkO3TriJQ2VSpfKgInuKs1l+NsW4AmS +4BjHeJi78+xCUvuppILXTdiK/ORO/auQxDh1MoSf/7OwKwIzNsAQkG8dnK/haZPs +o0UvFJ/1TCplQ3IM98P4lYsU84UgYt1UU90s3BiVaU+DR3BAM1h3Egyi61IxHkzJ +qM7F78PRreBrAwA0JrRUITWXAdxfG/F851X6LWh3e9NpzNMOa7pNdkTWwhWaJuyw +xfW70Xp0wmzNxbVe9kzmWy2B27O3Opee7c9GslA9hGCZcbUztVdF5kJHdWoOsAgM +rr3e97sPWD2PAzHoPYJQyi9eDF20l74gNAf0xBLh7tew2VktafcxBPTy+av5EzH4 +AXcOPUIjJsyacmdRIXrMPIWo6iFqO9taPKU0nprALN+AnCng33eU0aKAQv9qTFsR +0PXNor6uzFFcw9VUewyu1rkGd4Di7wcaaMxZUa1+XGdrudviB0JbuAEFWDlN5LuY +o7Ey7Nmj1m+UI/87tyll5gfp77YZ6ufCOB0yiJA8EytuzO+rdwY0d4RPcuSBhPm5 +dDTedk+SKlOxJTnbPP/lPqYO5Wue/9vsL3SD3460s6neFE3/MaNFcyT6lSnMEpcE +oji2jbDwN/zIIX8/syQbPYtuzE2wFg2WHYMfRsCbvUOZ58SWLs5fyQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYx +CzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQD +ExNHbG9iYWxTaWduIFJvb3QgRTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAw +MDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex +HDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkBjtjq +R+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGdd +yXqBPCCjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ +7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZkvLtoURMMA/cVi4RguYv/Uo7njLwcAjA8 ++RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc +8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke +hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI +KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg +515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO +xwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 +MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 +RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT +gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm +KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd +QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ +XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o +LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU +RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp +jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK +6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX +mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs +Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH +WD9f +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg +MB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx +MjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSNjET +MBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQssgrRI +xutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1k +ZguSgMpE3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxD +aNc9PIrFsmbVkJq3MQbFvuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJw +LnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqMPKq0pPbzlUoSB239jLKJz9CgYXfIWHSw +1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+azayOeSsJDa38O+2HBNX +k7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05OWgtH8wY2 +SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/h +bguyCLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4n +WUx2OVvq+aWh2IMP0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpY +rZxCRXluDocZXFSxZba/jJvcE+kNb7gu3GduyYsRtYQUigAZcIN5kZeR1Bonvzce +MgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNVHSMEGDAWgBSu +bAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN +nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGt +Ixg93eFyRJa0lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr61 +55wsTLxDKZmOMNOsIeDjHfrYBzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLj +vUYAGm0CuiVdjaExUd1URhxN25mW7xocBFymFe944Hn+Xds+qkxV/ZoVqW/hpvvf +cDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr3TsTjxKM4kEaSHpz +oHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB10jZp +nOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfs +pA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v +JJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R +8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4 +5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUA +MEYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYD +VQQDExNHbG9iYWxTaWduIFJvb3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMy +MDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYt +c2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08EsCVeJ +OaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQG +vGIFAha/r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud +316HCkD7rRlr+/fKYIje2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo +0q3v84RLHIf8E6M6cqJaESvWJ3En7YEtbWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSE +y132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvjK8Cd+RTyG/FWaha/LIWF +zXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD412lPFzYE ++cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCN +I/onccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzs +x2sZy/N78CsHpdlseVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqa +ByFrgY/bxFn63iLABJzjqls2k+g9vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC +4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEMBQADggIBAHx4 +7PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg +JuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti +2kM3S+LGteWygxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIk +pnnpHs6i58FZFZ8d4kuaPp92CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRF +FRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZmOUdkLG5NrmJ7v2B0GbhWrJKsFjLt +rWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qqJZ4d16GLuc1CLgSk +ZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwyeqiv5 +u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP +4vkYxboznxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6 +N3ec592kD3ZDZopD8p/7DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3 +vouXsXgxT7PntgMTzlSdriVZzH81Xwj3QEUxeCp6 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT +EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp +ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz +NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH +EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE +AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD +E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH +/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy +DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh +GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR +tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA +AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX +WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu +9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr +gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo +2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI +4uJEvlz36hz1 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs +ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw +MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj +aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp +Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg +nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 +HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N +Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN +dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 +HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G +CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU +sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 +4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg +8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 +mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYD +VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG +A1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw +WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz +IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout736G +jOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL2 +4CejQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7 +VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azTL818+FsuVbu/3ZL3pAzcMeGiAjEA/Jdm +ZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV11RZt+cRLInUue4X +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD +VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG +A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw +WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz +IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzuhXyi +QHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvR +HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D +9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8 +p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo +27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w +Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw +TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl +qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH +szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8 +Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk +MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 +wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p +aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN +VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb +C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe +QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy +h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4 +7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J +ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef +MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/ +Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT +6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ +0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm +2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb +bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlrsWNBCUaqxElqjANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTukk3LvCvpt +nfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3KgGjSY +6Dlo7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9BuXvAu +MC6C/Pq8tBcKSOWIm8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOdre7k +RXuJVfeKH2JShBKzwkCX44ofR5GmdFrS+LFjKBC4swm4VndAoiaYecb+3yXuPuWg +f9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbuak7MkogwTZq9TwtImoS1mKPV ++3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscszcTJGr61K8Yzo +dDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqjx5RW +Ir9qS34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsRnTKa +G73VululycslaVNVJ1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0kzCq +gc7dGtxRcw1PcOnlthYhGXmy5okLdWTK1au8CcEYof/UVKGFPP0UJAOyh9OktwID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEMBQADggIBAB/Kzt3H +vqGf2SdMC9wXmBFqiN495nFWcrKeGk6c1SuYJF2ba3uwM4IJvd8lRuqYnrYb/oM8 +0mJhwQTtzuDFycgTE1XnqGOtjHsB/ncw4c5omwX4Eu55MaBBRTUoCnGkJE+M3DyC +B19m3H0Q/gxhswWV7uGugQ+o+MePTagjAiZrHYNSVc61LwDKgEDg4XSsYPWHgJ2u +NmSRXbBoGOqKYcl3qJfEycel/FVL8/B/uWU9J2jQzGv6U53hkRrJXRqWbTKH7QMg +yALOWr7Z6v2yTcQvG99fevX4i8buMTolUVVnjWQye+mew4K6Ki3pHrTgSAai/Gev +HyICc/sgCq+dVEuhzf9gR7A/Xe8bVr2XIZYtCtFenTgCR2y59PYjJbigapordwj6 +xLEokCZYCDzifqrXPW+6MYgKBesntaFJ7qBFVHvmJ2WZICGoo7z7GJa7Um8M7YNR +TOlZ4iBgxcJlkoKM8xAfDoqXvneCbT+PHV28SSe9zE8P4c52hgQjxcCMElv924Sg +JPFI/2R80L5cFtHvma3AH/vLrrw4IgYmZNralw4/KBVEqE8AyvCazM90arQ+POuV +7LXTWtiBmelDGDfrs7vRWGJB82bSj6p4lVQgw1oudCvV0b4YacCs1aTPObpRhANl +6WLAYv7YTVWW4tAR+kg0Eeye7QUd5MjWHYbL +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYD +VQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgw +MTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0g +UjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wWTAT +BgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkWymOx +uYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNV +HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/ ++wpu+74zyTyjhNUwCgYIKoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147 +bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE +AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw +CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ +BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND +VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb +qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY +HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo +G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA +lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr +IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/ +0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH +k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47 +4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO +m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa +cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl +uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI +KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls +ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG +AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 +VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT +VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG +CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA +cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA +QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA +7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA +cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA +QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA +czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu +aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt +aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud +DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF +BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp +D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU +JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m +AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD +vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms +tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH +7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h +I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA +h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF +d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H +pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsx +CzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJ +WiBGTk1ULVJDTTAeFw0wODEwMjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJ +BgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBG +Tk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALpxgHpMhm5/ +yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcfqQgf +BBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAz +WHFctPVrbtQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxF +tBDXaEAUwED653cXeuYLj2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z +374jNUUeAlz+taibmSXaXvMiwzn15Cou08YfxGyqxRxqAQVKL9LFwag0Jl1mpdIC +IfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mwWsXmo8RZZUc1g16p6DUL +mbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnTtOmlcYF7 +wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peS +MKGJ47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2 +ZSysV4999AeU14ECll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMet +UqIJ5G+GR4of6ygnXYMgrwTJbFaai0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPd9xf3E6Jobd2Sn9R2gzL+H +YJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwOi8vd3d3 +LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD +nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1 +RXxlDPiyN8+sD8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYM +LVN0V2Ue1bLdI4E7pWYjJ2cJj+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf +77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrTQfv6MooqtyuGC2mDOL7Nii4LcK2N +JpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW+YJF1DngoABd15jm +fZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7Ixjp +6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp +1txyM/1d8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B +9kiABdcPUXmsEKvU7ANm5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wok +RqEIr9baRRmW1FMdW4R58MD3R++Lj8UGrp1MYp3/RgT408m2ECVAdf4WqslKYIYv +uu8wd+RU4riEmViAqhOLUTpPSPaLtrM= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQsw +CQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgw +FgYDVQRhDA9WQVRFUy1RMjgyNjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1S +Q00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4MTIyMDA5MzczM1oXDTQzMTIyMDA5 +MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQtUkNNMQ4wDAYDVQQL +DAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNBQyBS +QUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LH +sbI6GA60XYyzZl2hNPk2LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oK +Um8BA06Oi6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqGSM49BAMDA2kAMGYCMQCu +SuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoDzBOQn5IC +MQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJy +v+c= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIx +GDAWBgNVBAcTD0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxp +bXNlbCB2ZSBUZWtub2xvamlrIEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0w +KwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24gTWVya2V6aSAtIEthbXUgU00xNjA0 +BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRpZmlrYXNpIC0gU3Vy +dW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYDVQQG +EwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXll +IEJpbGltc2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklU +QUsxLTArBgNVBAsTJEthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBT +TTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11IFNNIFNTTCBLb2sgU2VydGlmaWthc2kg +LSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr3UwM6q7 +a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y86Ij5iySr +LqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INr +N3wcwv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2X +YacQuFWQfw4tJzh03+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/ +iSIzL+aFCr2lqBs23tPcLG07xxO9WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4f +AJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQUZT/HiobGPN08VFw1+DrtUgxH +V8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh +AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPf +IPP54+M638yclNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4 +lzwDGrpDxpa5RXI4s6ehlj2Re37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c +8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0jq5Rm+K37DwhuJi1/FwcJsoz7UMCf +lo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBs +MQswCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0Eg +Um9vdCBDQSAyMDIxMB4XDTIxMDIxOTEwNTUzOFoXDTQ1MDIxMzEwNTUzN1owbDEL +MAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl +YXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgUlNBIFJv +b3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIvC569l +mwVnlskNJLnQDmT8zuIkGCyEf3dRywQRNrhe7Wlxp57kJQmXZ8FHws+RFjZiPTgE +4VGC/6zStGndLuwRo0Xua2s7TL+MjaQenRG56Tj5eg4MmOIjHdFOY9TnuEFE+2uv +a9of08WRiFukiZLRgeaMOVig1mlDqa2YUlhu2wr7a89o+uOkXjpFc5gH6l8Cct4M +pbOfrqkdtx2z/IpZ525yZa31MJQjB/OCFks1mJxTuy/K5FrZx40d/JiZ+yykgmvw +Kh+OC19xXFyuQnspiYHLA6OZyoieC0AJQTPb5lh6/a6ZcMBaD9YThnEvdmn8kN3b +LW7R8pv1GmuebxWMevBLKKAiOIAkbDakO/IwkfN4E8/BPzWr8R0RI7VDIp4BkrcY +AuUR0YLbFQDMYTfBKnya4dC6s1BG7oKsnTH4+yPiAwBIcKMJJnkVU2DzOFytOOqB +AGMUuTNe3QvboEUHGjMJ+E20pwKmafTCWQWIZYVWrkvL4N48fS0ayOn7H6NhStYq +E613TBoYm5EPWNgGVMWX+Ko/IIqmhaZ39qb8HOLubpQzKoNQhArlT4b4UEV4AIHr +W2jjJo3Me1xR9BQsQL4aYB16cmEdH2MtiKrOokWQCPxrvrNQKlr9qEgYRtaQQJKQ +CoReaDH46+0N0x3GfZkYVVYnZS6NRcUk7M7jAgMBAAGjQjBAMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFApII6ZgpJIKM+qTW8VX6iVNvRLuMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAPpBIqm5iFSVmewzVjIuJndftTgfvnNAU +X15QvWiWkKQUEapobQk1OUAJ2vQJLDSle1mESSmXdMgHHkdt8s4cUCbjnj1AUz/3 +f5Z2EMVGpdAgS1D0NTsY9FVqQRtHBmg8uwkIYtlfVUKqrFOFrJVWNlar5AWMxaja +H6NpvVMPxP/cyuN+8kyIhkdGGvMA9YCRotxDQpSbIPDRzbLrLFPCU3hKTwSUQZqP +JzLB5UkZv/HywouoCjkxKLR9YjYsTewfM7Z+d21+UPCfDtcRj88YxeMn/ibvBZ3P +zzfF0HvaO7AWhAw6k9a+F9sPPg4ZeAnHqQJyIkv3N3a6dcSFA1pj1bF1BcK5vZSt +jBWZp5N99sXzqnTPBIWUmAD04vnKJGW/4GKvyMX6ssmeVkjaef2WdhW+o45WxLM0 +/L5H9MG0qPzVMIho7suuyWPEdr6sOBjhXlzPrjoiUevRi7PzKzMHVIf6tLITe7pT +BGIBnfHAT+7hOtSLIBD6Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79 +aPib8qXPMThcFarmlwDB31qlpzmq6YR/PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YW +xw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnnkf3/W9b3raYvAwtt41dU +63ZTGI0RmLo= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQsw +CQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2Vh +cmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9v +dCBDQSAyMDIxMB4XDTIxMDIxOTExMDExMFoXDTQ1MDIxMzExMDEwOVowbDELMAkG +A1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj +aCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgRUNDIFJvb3Qg +Q0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDgI/rGgltJ6rK9JOtDA4MM7 +KKrxcm1lAEeIhPyaJmuqS7psBAqIXhfyVYf8MLA04jRYVxqEU+kw2anylnTDUR9Y +STHMmE5gEYd103KUkE+bECUqqHgtvpBBWJAVcqeht6NCMEAwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQD +AgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAircJRQO9gcS3ujwLEXQNw +SaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/QwCZ61IygN +nxS2PFOiTAZpffpskcYqSUXm7LcT4Tps +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu +VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw +MTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw +JQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT +3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU ++ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp +S0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1 +bVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi +T0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL +vYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK +Vsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK +dHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT +c+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv +l7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N +iGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD +ggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH +6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt +LRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93 +nAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3 ++wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK +W2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT +AwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq +l1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG +4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ +mUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A +7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw +CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg +R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00 +MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT +ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw +EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW ++1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9 +ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI +zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW +tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1 +/q4AaOeMSQ+2b1tbFfLn +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4 +MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6 +ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD +VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j +b20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq +scIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO +xdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H +LmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX +uaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD +yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+ +JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q +rLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN +BjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L +hij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB +QFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+ +HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu +Zm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg +QTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB +BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx +MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA +A4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb +laQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56 +awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo +JNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw +LDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT +VyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk +LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb +UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/ +QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+ +naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls +QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6 +ZW5pb3dhIFMuQS4xGDAWBgNVBAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkw +NzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJBgNVBAYTAlBMMSgwJgYDVQQKDB9L +cmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYDVQQDDA9TWkFGSVIg +Uk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5QqEvN +QLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT +3PSQ1hNKDJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw +3gAeqDRHu5rr/gsUvTaE2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr6 +3fE9biCloBK0TXC5ztdyO4mTp4CEHCdJckm1/zuVnsHMyAHs6A6KCpbns6aH5db5 +BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwiieDhZNRnvDF5YTy7ykHN +XGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsF +AAOCAQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw +8PRBEew/R40/cof5O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOG +nXkZ7/e7DDWQw4rtTw/1zBLZpD67oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCP +oky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul4+vJhaAlIDf7js4MNIThPIGy +d05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6+/NNIxuZMzSg +LvWpCz/UXeHPhJ/iGcJfitYgHuNztw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD +VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 +ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G +CSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y +OTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx +FjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp +Z25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o +dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP +kd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc +cbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U +fIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7 +N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC +xkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1 ++rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM +Pcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG +SIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h +mLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk +ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 +tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c +2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t +HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNV +BAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRk +LjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJv +b3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZaFw00MjA4MjIxMjA3MDZaMHExCzAJ +BgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMg +THRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25v +IFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtv +xie+RJCxs1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+H +Wyx7xf58etqjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBSHERUI0arBeAyxr87GyZDvvzAEwDAfBgNVHSMEGDAWgBSHERUI0arB +eAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEAtVfd14pVCzbhhkT61Nlo +jbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxOsvxyqltZ ++efcMQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYD +VQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw +MTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4MjMxNjA0WjBlMQswCQYDVQQGEwJV +UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNy +b3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZR +ogPZnZH6thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYb +hGBKia/teQ87zvH2RPUBeMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBTIy5lycFIM+Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3 +FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlfXu5gKcs68tvWMoQZP3zV +L8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaReNtUjGUB +iudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBl +MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw +NAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 +IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIwNzE4MjMwMDIzWjBlMQswCQYDVQQG +EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1N +aWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZ +Nt9GkMml7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0 +ZdDMbRnMlfl7rEqUrQ7eS0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1 +HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw71VdyvD/IybLeS2v4I2wDwAW9lcfNcztm +gGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+dkC0zVJhUXAoP8XFWvLJ +jEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49FyGcohJUc +aDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaG +YaRSMLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6 +W6IYZVcSn2i51BVrlMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4K +UGsTuqwPN1q3ErWQgR5WrlcihtnJ0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH ++FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJClTUFLkqqNfs+avNJVgyeY+Q +W5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZC +LgLNFgVZJ8og6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OC +gMNPOsduET/m4xaRhPtthH80dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6 +tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk+ONVFT24bcMKpBLBaYVu32TxU5nh +SnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex/2kskZGT4d9Mozd2 +TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDyAmH3 +pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGR +xpl/j8nWZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiApp +GWSZI1b7rCoucL5mxAyE7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9 +dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKTc0QWbej09+CVgI+WXTik9KveCjCHk9hN +AHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB +RA+GsCyRxj3qrg+E +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEM +BQAwaTELMAkGA1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRG +T1JNIENvcnAuMTIwMAYDVQQDDClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4NDJaFw0zNzA4MTgyMzU5NTlaMGkx +CzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVTUyBQTEFURk9STSBD +b3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVA +iQqrDZBbUGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH +38dq6SZeWYp34+hInDEW+j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lE +HoSTGEq0n+USZGnQJoViAbbJAh2+g1G7XNr4rRVqmfeSVPc0W+m/6imBEtRTkZaz +kVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2aacp+yPOiNgSnABIqKYP +szuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4Yb8Obtoq +vC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHf +nZ3zVHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaG +YQ5fG8Ir4ozVu53BA0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo +0es+nPxdGoMuK8u180SdOqcXYZaicdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3a +CJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejyYhbLgGvtPe31HzClrkvJE+2K +AQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNVHQ4EFgQU0p+I +36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB +Af8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoN +qo0hV4/GPnrK21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatj +cu3cvuzHV+YwIHHW1xDBE1UBjCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm ++LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bxhYTeodoS76TiEJd6eN4MUZeoIUCL +hr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTgE34h5prCy8VCZLQe +lHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTHD8z7 +p/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8 +piKCk5XQA76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLR +LBT/DShycpWbXgnbiUSYqqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX +5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oGI/hGoiLtk/bdmuYqh7GYVPEi92tF4+KO +dh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmgkpzNNIaRkPpkUZ3+/uul +9XXeifdy +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG +EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3 +MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl +cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR +dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB +pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM +b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm +aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz +IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT +lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz +AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5 +VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG +ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2 +BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG +AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M +U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh +bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C ++C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC +bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F +uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 +XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQsw +CQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91 +bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwg +Um9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRaFw00MjA1MDkwOTU4MzNaMG0xCzAJ +BgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBGb3Vu +ZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2JhbCBS +b290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4ni +eUqjFqdrVCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4W +p2OQ0jnUsYd4XxiWD1AbNTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7T +rYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0EAwMDaAAwZQIwJsdpW9zV +57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtkAjEA2zQg +Mgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt +MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg +Rm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9i +YWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAwMzJaFw0zOTEyMDExNTEwMzFaMG0x +CzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBG +b3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh +bCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3 +HEokKtaXscriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGx +WuR51jIjK+FTzJlFXHtPrby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX +1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNk +u7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4oQnc/nSMbsrY9gBQHTC5P +99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvgGUpuuy9r +M2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUB +BAMCAQAwDQYJKoZIhvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrgh +cViXfa43FK8+5/ea4n32cZiZBKpDdHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5 +gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0VQreUGdNZtGn//3ZwLWoo4rO +ZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEuiHZeeevJuQHHf +aPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic +Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICODCCAb6gAwIBAgIJANZdm7N4gS7rMAoGCCqGSM49BAMDMGExCzAJBgNVBAYT +AkpQMSUwIwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMSswKQYD +VQQDEyJTZWN1cml0eSBDb21tdW5pY2F0aW9uIEVDQyBSb290Q0ExMB4XDTE2MDYx +NjA1MTUyOFoXDTM4MDExODA1MTUyOFowYTELMAkGA1UEBhMCSlAxJTAjBgNVBAoT +HFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKzApBgNVBAMTIlNlY3VyaXR5 +IENvbW11bmljYXRpb24gRUNDIFJvb3RDQTEwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AASkpW9gAwPDvTH00xecK4R1rOX9PVdu12O/5gSJko6BnOPpR27KkBLIE+Cnnfdl +dB9sELLo5OnvbYUymUSxXv3MdhDYW72ixvnWQuRXdtyQwjWpS4g8EkdtXP9JTxpK +ULGjQjBAMB0GA1UdDgQWBBSGHOf+LaVKiwj+KBH6vqNm+GBZLzAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjAVXUI9/Lbu +9zuxNuie9sRGKEkz0FhDKmMpzE2xtHqiuQ04pV1IKv3LsnNdo4gIxwwCMQDAqy0O +be0YottT6SXbVQjgUMzfRGEWgqtJsLKB7HOHeLRMsmIbEvoWTSVLY70eN9k= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl +MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe +U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX +DTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy +dXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj +YXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV +OVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr +zbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM +VAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ +hNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO +ojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw +awNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs +OPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF +coJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc +okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8 +t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy +1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/ +SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQsw +CQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T +ZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcN +MjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYG +A1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBT +ZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAR2+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccC +WvkEN/U0NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+ +6xnOQ6OjQjBAMB0GA1UdDgQWBBTRItpMWfFLXyY4qp3W7usNw/upYTAOBgNVHQ8B +Af8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNnADBkAjAn7qRa +qCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RHlAFWovgzJQxC36oCMB3q +4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21USAGKcw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT +IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw +MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N +T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR +FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J +cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW +BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm +fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv +GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID0DCCArigAwIBAgIQIKTEf93f4cdTYwcTiHdgEjANBgkqhkiG9w0BAQUFADCB +gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV +BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xMTAxMDEwMDAw +MDBaFw0zMDEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl +YXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P +RE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3 +UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI +2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8 +Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp ++2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+ +DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O +nKVIrLsm9wIDAQABo0IwQDAdBgNVHQ4EFgQUC1jli8ZMFTekQKkwqSG+RzZaVv8w +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD +ggEBAC/JxBwHO89hAgCx2SFRdXIDMLDEFh9sAIsQrK/xR9SuEDwMGvjUk2ysEDd8 +t6aDZK3N3w6HM503sMZ7OHKx8xoOo/lVem0DZgMXlUrxsXrfViEGQo+x06iF3u6X +HWLrp+cxEmbDD6ZLLkGC9/3JG6gbr+48zuOcrigHoSybJMIPIyaDMouGDx8rEkYl +Fo92kANr3ryqImhrjKGsKxE5pttwwn1y6TPn/CbxdFqR5p2ErPioBhlG5qfpqjQi +pKGfeq23sqSaM4hxAjwu1nqyH6LKwN0vEJT9s4yEIHlG1QXUEOTS22RPuFvuG8Ug +R1uUq27UlTMdphVx8fiUylQ5PsE= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB +hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV +BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5 +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT +EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR +6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X +pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC +9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV +/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf +Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z ++pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w +qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah +SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC +u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf +Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq +crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB +/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl +wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM +4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV +2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna +FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ +CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK +boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke +jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL +S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb +QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl +0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB +NVOFBkpdn627G190 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB +iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl +cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV +BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw +MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV +BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B +3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY +tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ +Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 +VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT +79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 +c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT +Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l +c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee +UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE +Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G +A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF +Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO +VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 +ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs +8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR +iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze +Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ +XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ +qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB +VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB +L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG +jjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL +MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl +eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT +JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT +Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg +VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo +I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng +o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G +A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB +zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW +RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFijCCA3KgAwIBAgIQdY39i658BwD6qSWn4cetFDANBgkqhkiG9w0BAQwFADBf +MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQD +Ey1TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYw +HhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEY +MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1Ymxp +YyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCTvtU2UnXYASOgHEdCSe5jtrch/cSV1UgrJnwUUxDa +ef0rty2k1Cz66jLdScK5vQ9IPXtamFSvnl0xdE8H/FAh3aTPaE8bEmNtJZlMKpnz +SDBh+oF8HqcIStw+KxwfGExxqjWMrfhu6DtK2eWUAtaJhBOqbchPM8xQljeSM9xf +iOefVNlI8JhD1mb9nxc4Q8UBUQvX4yMPFF1bFOdLvt30yNoDN9HWOaEhUTCDsG3X +ME6WW5HwcCSrv0WBZEMNvSE6Lzzpng3LILVCJ8zab5vuZDCQOc2TZYEhMbUjUDM3 +IuM47fgxMMxF/mL50V0yeUKH32rMVhlATc6qu/m1dkmU8Sf4kaWD5QazYw6A3OAS +VYCmO2a0OYctyPDQ0RTp5A1NDvZdV3LFOxxHVp3i1fuBYYzMTYCQNFu31xR13NgE +SJ/AwSiItOkcyqex8Va3e0lMWeUgFaiEAin6OJRpmkkGj80feRQXEgyDet4fsZfu ++Zd4KKTIRJLpfSYFplhym3kT2BFfrsU4YjRosoYwjviQYZ4ybPUHNs2iTG7sijbt +8uaZFURww3y8nDnAtOFr94MlI1fZEoDlSfB1D++N6xybVCi0ITz8fAr/73trdf+L +HaAZBav6+CuBQug4urv7qv094PPK306Xlynt8xhW6aWWrL3DkJiy4Pmi1KZHQ3xt +zwIDAQABo0IwQDAdBgNVHQ4EFgQUVnNYZJX5khqwEioEYnmhQBWIIUkwDgYDVR0P +AQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAC9c +mTz8Bl6MlC5w6tIyMY208FHVvArzZJ8HXtXBc2hkeqK5Duj5XYUtqDdFqij0lgVQ +YKlJfp/imTYpE0RHap1VIDzYm/EDMrraQKFz6oOht0SmDpkBm+S8f74TlH7Kph52 +gDY9hAaLMyZlbcp+nv4fjFg4exqDsQ+8FxG75gbMY/qB8oFM2gsQa6H61SilzwZA +Fv97fRheORKkU55+MkIQpiGRqRxOF3yEvJ+M0ejf5lG5Nkc/kLnHvALcWxxPDkjB +JYOcCj+esQMzEhonrPcibCTRAUH4WAP+JWgiH5paPHxsnnVI84HxZmduTILA7rpX +DhjvLpr3Etiga+kFpaHpaPi8TD8SHkXoUsCjvxInebnMMTzD9joiFgOgyY9mpFui +TdaBJQbpdqQACj7LzTWb4OE4y2BThihCQRxEV+ioratF4yUQvNs+ZUH7G6aXD+u5 +dHn5HrwdVw1Hr8Mvn4dGp+smWg9WY7ViYG4A++MnESLn/pmPNPW56MORcr3Ywx65 +LvKRRFHQV80MNNVIIb/bE/FmJUNS0nAiNs2fxBx1IK1jcmMGDw4nztJqDby1ORrp +0XZ60Vzk50lJLVU3aPAaOpg+VBeHVOmmJ1CJeyAvP/+/oYtKR5j/K3tJPsMpRmAY +QqszKbrAKbkTidOIijlBO8n9pu0f9GBj39ItVQGL +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz +MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv +cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz +Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO +0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao +wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj +7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS +8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT +BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg +JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3 +6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/ +3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm +D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS +CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR +3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICYDCCAgegAwIBAgIMDWpfCD8oXD5Rld9dMAoGCCqGSM49BAMCMIGRMQswCQYD +VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf +BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 +YXZlIEdsb2JhbCBFQ0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x +NzA4MjMxOTM1MTBaFw00MjA4MjMxOTM1MTBaMIGRMQswCQYDVQQGEwJVUzERMA8G +A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 +d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF +Q0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqG +SM49AwEHA0IABH77bOYj43MyCMpg5lOcunSNGLB4kFKA3TjASh3RqMyTpJcGOMoN +FWLGjgEqZZ2q3zSRLoHB5DOSMcT9CTqmP62jQzBBMA8GA1UdEwEB/wQFMAMBAf8w +DwYDVR0PAQH/BAUDAwcGADAdBgNVHQ4EFgQUo0EGrJBt0UrrdaVKEJmzsaGLSvcw +CgYIKoZIzj0EAwIDRwAwRAIgB+ZU2g6gWrKuEZ+Hxbb/ad4lvvigtwjzRM4q3wgh +DDcCIC0mA6AFvWvR9lz4ZcyGbbOcNEhjhAnFjXca4syc4XR7 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFiTCCA3GgAwIBAgIQb77arXO9CEDii02+1PdbkTANBgkqhkiG9w0BAQsFADBO +MQswCQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQD +DBxTU0wuY29tIFRMUyBSU0EgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzQyMloX +DTQ2MDgxOTE2MzQyMVowTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jw +b3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgUlNBIFJvb3QgQ0EgMjAyMjCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANCkCXJPQIgSYT41I57u9nTP +L3tYPc48DRAokC+X94xI2KDYJbFMsBFMF3NQ0CJKY7uB0ylu1bUJPiYYf7ISf5OY +t6/wNr/y7hienDtSxUcZXXTzZGbVXcdotL8bHAajvI9AI7YexoS9UcQbOcGV0ins +S657Lb85/bRi3pZ7QcacoOAGcvvwB5cJOYF0r/c0WRFXCsJbwST0MXMwgsadugL3 +PnxEX4MN8/HdIGkWCVDi1FW24IBydm5MR7d1VVm0U3TZlMZBrViKMWYPHqIbKUBO +L9975hYsLfy/7PO0+r4Y9ptJ1O4Fbtk085zx7AGL0SDGD6C1vBdOSHtRwvzpXGk3 +R2azaPgVKPC506QVzFpPulJwoxJF3ca6TvvC0PeoUidtbnm1jPx7jMEWTO6Af77w +dr5BUxIzrlo4QqvXDz5BjXYHMtWrifZOZ9mxQnUjbvPNQrL8VfVThxc7wDNY8VLS ++YCk8OjwO4s4zKTGkH8PnP2L0aPP2oOnaclQNtVcBdIKQXTbYxE3waWglksejBYS +d66UNHsef8JmAOSqg+qKkK3ONkRN0VHpvB/zagX9wHQfJRlAUW7qglFA35u5CCoG +AtUjHBPW6dvbxrB6y3snm/vg1UYk7RBLY0ulBY+6uB0rpvqR4pJSvezrZ5dtmi2f +gTIFZzL7SAg/2SW4BCUvAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0j +BBgwFoAU+y437uOEeicuzRk1sTN8/9REQrkwHQYDVR0OBBYEFPsuN+7jhHonLs0Z +NbEzfP/UREK5MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAjYlt +hEUY8U+zoO9opMAdrDC8Z2awms22qyIZZtM7QbUQnRC6cm4pJCAcAZli05bg4vsM +QtfhWsSWTVTNj8pDU/0quOr4ZcoBwq1gaAafORpR2eCNJvkLTqVTJXojpBzOCBvf +R4iyrT7gJ4eLSYwfqUdYe5byiB0YrrPRpgqU+tvT5TgKa3kSM/tKWTcWQA673vWJ +DPFs0/dRa1419dvAJuoSc06pkZCmF8NsLzjUo3KUQyxi4U5cMj29TH0ZR6LDSeeW +P4+a0zvkEdiLA9z2tmBVGKaBUfPhqBVq6+AL8BQx1rmMRTqoENjwuSfr98t67wVy +lrXEj5ZzxOhWc5y8aVFjvO9nHEMaX3cZHxj4HCUp+UmZKbaSPaKDN7EgkaibMOlq +bLQjk2UEqxHzDh1TJElTHaE/nUiSEeJ9DU/1172iWD54nR4fK/4huxoTtrEoZP2w +AgDHbICivRZQIA9ygV/MlP+7mea6kMvq+cYMwq7FGc4zoWtcu358NFcXrfA/rs3q +r5nsLFR+jM4uElZI7xc7P0peYNLcdDa8pUNjyw9bowJWCZ4kLOGGgYz+qxcs+sji +Mho6/4UIyYOf8kpIEFR3N+2ivEC+5BB09+Rbu7nzifmPQdjH5FCQNYA+HLhNkNPU +98OwoX6EyneSMSy4kLGCenROmxMmtNVQZlR4rmA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICOjCCAcCgAwIBAgIQFAP1q/s3ixdAW+JDsqXRxDAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxT +U0wuY29tIFRMUyBFQ0MgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzM0OFoXDTQ2 +MDgxOTE2MzM0N1owTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jwb3Jh +dGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgRUNDIFJvb3QgQ0EgMjAyMjB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABEUpNXP6wrgjzhR9qLFNoFs27iosU8NgCTWyJGYm +acCzldZdkkAZDsalE3D07xJRKF3nzL35PIXBz5SQySvOkkJYWWf9lCcQZIxPBLFN +SeR7T5v15wj4A4j3p8OSSxlUgaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSME +GDAWgBSJjy+j6CugFFR781a4Jl9nOAuc0DAdBgNVHQ4EFgQUiY8vo+groBRUe/NW +uCZfZzgLnNAwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2gAMGUCMFXjIlbp +15IkWE8elDIPDAI2wv2sdDJO4fscgIijzPvX6yv/N33w7deedWo1dlJF4AIxAMeN +b0Igj762TVntd00pxCAgRWSGOlDGxK0tk/UYfXLtqc/ErFc2KAhl3zx5Zn6g6g== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV +BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE +CgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMB4XDTE3MDUzMTE4MTQzN1oXDTQy +MDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4G +A1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQD +DC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvq +M0fNTPl9fb69LT3w23jhhqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssuf +OePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7wcXHswxzpY6IXFJ3vG2fThVUCAtZJycxa +4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTOZw+oz12WGQvE43LrrdF9 +HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+B6KjBSYR +aZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcA +b9ZhCBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQ +Gp8hLH94t2S42Oim9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQV +PWKchjgGAGYS5Fl2WlPAApiiECtoRHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMO +pgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+SlmJuwgUHfbSguPvuUCYHBBXtSu +UDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48+qvWBkofZ6aY +MBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV +HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa4 +9QaAJadz20ZpqJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBW +s47LCp1Jjr+kxJG7ZhcFUZh1++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5 +Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nxY/hoLVUE0fKNsKTPvDxeH3jnpaAg +cLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2GguDKBAdRUNf/ktUM +79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDzOFSz +/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXt +ll9ldDz7CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEm +Kf7GUmG6sXP/wwyc5WxqlD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKK +QbNmC1r7fSOl8hqw/96bg5Qu0T/fkreRrwU7ZcegbLHNYhLDkBvjJc40vG93drEQ +w/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi +S9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07 +mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF2jCCA8KgAwIBAgIMBfcOhtpJ80Y1LrqyMA0GCSqGSIb3DQEBCwUAMIGIMQsw +CQYDVQQGEwJVUzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28x +ITAfBgNVBAoMGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1 +c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMx +OTM0MTJaFw00MjA4MjMxOTM0MTJaMIGIMQswCQYDVQQGEwJVUzERMA8GA1UECAwI +SWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2ZSBI +b2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +ALldUShLPDeS0YLOvR29zd24q88KPuFd5dyqCblXAj7mY2Hf8g+CY66j96xz0Xzn +swuvCAAJWX/NKSqIk4cXGIDtiLK0thAfLdZfVaITXdHG6wZWiYj+rDKd/VzDBcdu +7oaJuogDnXIhhpCujwOl3J+IKMujkkkP7NAP4m1ET4BqstTnoApTAbqOl5F2brz8 +1Ws25kCI1nsvXwXoLG0R8+eyvpJETNKXpP7ScoFDB5zpET71ixpZfR9oWN0EACyW +80OzfpgZdNmcc9kYvkHHNHnZ9GLCQ7mzJ7Aiy/k9UscwR7PJPrhq4ufogXBeQotP +JqX+OsIgbrv4Fo7NDKm0G2x2EOFYeUY+VM6AqFcJNykbmROPDMjWLBz7BegIlT1l +RtzuzWniTY+HKE40Cz7PFNm73bZQmq131BnW2hqIyE4bJ3XYsgjxroMwuREOzYfw +hI0Vcnyh78zyiGG69Gm7DIwLdVcEuE4qFC49DxweMqZiNu5m4iK4BUBjECLzMx10 +coos9TkpoNPnG4CELcU9402x/RpvumUHO1jsQkUm+9jaJXLE9gCxInm943xZYkqc +BW89zubWR2OZxiRvchLIrH+QtAuRcOi35hYQcRfO3gZPSEF9NUqjifLJS3tBEW1n +twiYTOURGa5CgNz7kAXU+FDKvuStx8KU1xad5hePrzb7AgMBAAGjQjBAMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFJngGWcNYtt2s9o9uFvo/ULSMQ6HMA4GA1Ud +DwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAmHNw4rDT7TnsTGDZqRKGFx6W +0OhUKDtkLSGm+J1WE2pIPU/HPinbbViDVD2HfSMF1OQc3Og4ZYbFdada2zUFvXfe +uyk3QAUHw5RSn8pk3fEbK9xGChACMf1KaA0HZJDmHvUqoai7PF35owgLEQzxPy0Q +lG/+4jSHg9bP5Rs1bdID4bANqKCqRieCNqcVtgimQlRXtpla4gt5kNdXElE1GYhB +aCXUNxeEFfsBctyV3lImIJgm4nb1J2/6ADtKYdkNy1GTKv0WBpanI5ojSP5RvbbE +sLFUzt5sQa0WZ37b/TjNuThOssFgy50X31ieemKyJo90lZvkWx3SD92YHJtZuSPT +MaCm/zjdzyBP6VhWOmfD0faZmZ26NraAL4hHT4a/RDqA5Dccprrql5gR0IRiR2Qe +qu5AvzSxnI9O4fKSTx+O856X3vOmeWqJcU9LJxdI/uz0UA9PSX3MReO9ekDFQdxh +VicGaeVyQYHTtgGJoC86cnn+OjC/QezHYj6RS8fZMXZC+fc8Y+wmjHMMfRod6qh8 +h6jCJ3zhM0EPz8/8AKAigJ5Kp28AsEFFtyLKaEjFQqKu3R3y4G5OBVixwJAWKqQ9 +EEC+j2Jjg6mcgn0tAumDMHzLJ8n9HmYAsC7TIS+OMxZsmO0QqAfWzJPP29FpHOTK +yeC2nOnOcXHebD8WpHk= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE +BhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK +DA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz +OTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv +bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R +xFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX +qhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC +C52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3 +6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh +/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF +YD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E +JNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc +US4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8 +ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm ++Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi +M+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G +A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV +cpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc +Hadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs +PgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/ +q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0 +cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr +a6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I +H37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y +K9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu +nLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf +oYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY +Ic2wBlX7Jz9TkHCpBB5XJ7k= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICnTCCAiSgAwIBAgIMCL2Fl2yZJ6SAaEc7MAoGCCqGSM49BAMDMIGRMQswCQYD +VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf +BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 +YXZlIEdsb2JhbCBFQ0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x +NzA4MjMxOTM2NDNaFw00MjA4MjMxOTM2NDNaMIGRMQswCQYDVQQGEwJVUzERMA8G +A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 +d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF +Q0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTB2MBAGByqGSM49AgEGBSuB +BAAiA2IABGvaDXU1CDFHBa5FmVXxERMuSvgQMSOjfoPTfygIOiYaOs+Xgh+AtycJ +j9GOMMQKmw6sWASr9zZ9lCOkmwqKi6vr/TklZvFe/oyujUF5nQlgziip04pt89ZF +1PKYhDhloKNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNVHQ8BAf8EBQMDBwYAMB0G +A1UdDgQWBBRVqYSJ0sEyvRjLbKYHTsjnnb6CkDAKBggqhkjOPQQDAwNnADBkAjA3 +AZKXRRJ+oPM+rRk6ct30UJMDEr5E0k9BpIycnR+j9sKS50gU/k6bpZFXrsY3crsC +MGclCrEMXu6pY5Jv5ZAL/mYiykf9ijH3g/56vxC+GCsej/YpHpRZ744hN8tRmKVu +Sw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNDAzWhcNNDEwMjEyMTgxNDAz +WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0 +b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBS +b290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI +7Z4INcgn64mMU1jrYor+8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPg +CemB+vNH06NjMGEwHQYDVR0OBBYEFILRhXMw5zUE044CkvvlpNHEIejNMA8GA1Ud +EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD +VR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T +kdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+ +gA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx +NTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv +bSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA +VIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku +WnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP +MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX +5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ +ytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg +h5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2ln +biBHb2xkIENBIC0gRzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBF +MQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMR8wHQYDVQQDExZT +d2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUqt2/8 +76LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+ +bbqBHH5CjCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c +6bM8K8vzARO/Ws/BtQpgvd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqE +emA8atufK+ze3gE/bk3lUIbLtK/tREDFylqM2tIrfKjuvqblCqoOpd8FUrdVxyJd +MmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvRAiTysybUa9oEVeXBCsdt +MDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuendjIj3o02y +MszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69y +FGkOpeUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPi +aG59je883WX0XaxR7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxM +gI93e2CaHt+28kgeDrpOVG2Y4OGiGqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCB +qTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUWyV7 +lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64OfPAeGZe6Drn +8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov +L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe6 +45R88a7A3hfm5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczO +UYrHUDFu4Up+GC9pWbY9ZIEr44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5 +O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOfMke6UiI0HTJ6CVanfCU2qT1L2sCC +bwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6mGu6uLftIdxf+u+yv +GPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxpmo/a +77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCC +hdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3 +92qgQmwLOM7XdVAyksLfKzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEpp +Ld6leNcG2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+w +ZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+htt +Qc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFkzCCA3ugAwIBAgIUQ/oMX04bgBhE79G0TzUfRPSA7cswDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzErMCkGA1UE +AxMiU3dpc3NTaWduIFJTQSBUTFMgUm9vdCBDQSAyMDIyIC0gMTAeFw0yMjA2MDgx +MTA4MjJaFw00NzA2MDgxMTA4MjJaMFExCzAJBgNVBAYTAkNIMRUwEwYDVQQKEwxT +d2lzc1NpZ24gQUcxKzApBgNVBAMTIlN3aXNzU2lnbiBSU0EgVExTIFJvb3QgQ0Eg +MjAyMiAtIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDLKmjiC8NX +vDVjvHClO/OMPE5Xlm7DTjak9gLKHqquuN6orx122ro10JFwB9+zBvKK8i5VUXu7 +LCTLf5ImgKO0lPaCoaTo+nUdWfMHamFk4saMla+ju45vVs9xzF6BYQ1t8qsCLqSX +5XH8irCRIFucdFJtrhUnWXjyCcplDn/L9Ovn3KlMd/YrFgSVrpxxpT8q2kFC5zyE +EPThPYxr4iuRR1VPuFa+Rd4iUU1OKNlfGUEGjw5NBuBwQCMBauTLE5tzrE0USJIt +/m2n+IdreXXhvhCxqohAWVTXz8TQm0SzOGlkjIHRI36qOTw7D59Ke4LKa2/KIj4x +0LDQKhySio/YGZxH5D4MucLNvkEM+KRHBdvBFzA4OmnczcNpI/2aDwLOEGrOyvi5 +KaM2iYauC8BPY7kGWUleDsFpswrzd34unYyzJ5jSmY0lpx+Gs6ZUcDj8fV3oT4MM +0ZPlEuRU2j7yrTrePjxF8CgPBrnh25d7mUWe3f6VWQQvdT/TromZhqwUtKiE+shd +OxtYk8EXlFXIC+OCeYSf8wCENO7cMdWP8vpPlkwGqnj73mSiI80fPsWMvDdUDrta +clXvyFu1cvh43zcgTFeRc5JzrBh3Q4IgaezprClG5QtO+DdziZaKHG29777YtvTK +wP1H8K4LWCDFyB02rpeNUIMmJCn3nTsPBQIDAQABo2MwYTAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBRvjmKLk0Ow4UD2p8P98Q+4 +DxU4pTAdBgNVHQ4EFgQUb45ii5NDsOFA9qfD/fEPuA8VOKUwDQYJKoZIhvcNAQEL +BQADggIBAKwsKUF9+lz1GpUYvyypiqkkVHX1uECry6gkUSsYP2OprphWKwVDIqO3 +10aewCoSPY6WlkDfDDOLazeROpW7OSltwAJsipQLBwJNGD77+3v1dj2b9l4wBlgz +Hqp41eZUBDqyggmNzhYzWUUo8aWjlw5DI/0LIICQ/+Mmz7hkkeUFjxOgdg3XNwwQ +iJb0Pr6VvfHDffCjw3lHC1ySFWPtUnWK50Zpy1FVCypM9fJkT6lc/2cyjlUtMoIc +gC9qkfjLvH4YoiaoLqNTKIftV+Vlek4ASltOU8liNr3CjlvrzG4ngRhZi0Rjn9UM +ZfQpZX+RLOV/fuiJz48gy20HQhFRJjKKLjpHE7iNvUcNCfAWpO2Whi4Z2L6MOuhF +LhG6rlrnub+xzI/goP+4s9GFe3lmozm1O2bYQL7Pt2eLSMkZJVX8vY3PXtpOpvJp +zv1/THfQwUY1mFwjmwJFQ5Ra3bxHrSL+ul4vkSkphnsh3m5kt8sNjzdbowhq6/Td +Ao9QAwKxuDdollDruF/UKIqlIgyKhPBZLtU30WHlQnNYKoH3dtvi4k0NX/a3vgW0 +rk4N3hY9A4GzJl5LuEsAz/+MF7psYC0nhzck5npgL7XTgwSqT0N1osGDsieYK7EO +gLrAhV5Cud+xYJHT6xh+cHiudoO+cVrQkOPKwRYlZ0rwtnu64ZzZ +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx +EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT +VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 +NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT +B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF +10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz +0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh +MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH +zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc +46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2 +yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi +laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP +oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA +BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE +qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm +4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL +1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn +LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF +H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo +RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+ +nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh +15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW +6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW +nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j +wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz +aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy +KwbQBM0= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIQQAE0jMIAAAAAAAAAATzyxjANBgkqhkiG9w0BAQwFADBQ +MQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290 +IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3QgQ0EwHhcNMjIxMTIyMDY1NDI5 +WhcNNDcxMTIyMTU1OTU5WjBQMQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FO +LUNBMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3Qg +Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDG+Moe2Qkgfh1sTs6P +40czRJzHyWmqOlt47nDSkvgEs1JSHWdyKKHfi12VCv7qze33Kc7wb3+szT3vsxxF +avcokPFhV8UMxKNQXd7UtcsZyoC5dc4pztKFIuwCY8xEMCDa6pFbVuYdHNWdZsc/ +34bKS1PE2Y2yHer43CdTo0fhYcx9tbD47nORxc5zb87uEB8aBs/pJ2DFTxnk684i +JkXXYJndzk834H/nY62wuFm40AZoNWDTNq5xQwTxaWV4fPMf88oon1oglWa0zbfu +j3ikRRjpJi+NmykosaS3Om251Bw4ckVYsV7r8Cibt4LK/c/WMw+f+5eesRycnupf +Xtuq3VTpMCEobY5583WSjCb+3MX2w7DfRFlDo7YDKPYIMKoNM+HvnKkHIuNZW0CP +2oi3aQiotyMuRAlZN1vH4xfyIutuOVLF3lSnmMlLIJXcRolftBL5hSmO68gnFSDA +S9TMfAxsNAwmmyYxpjyn9tnQS6Jk/zuZQXLB4HCX8SS7K8R0IrGsayIyJNN4KsDA +oS/xUgXJP+92ZuJF2A09rZXIx4kmyA+upwMu+8Ff+iDhcK2wZSA3M2Cw1a/XDBzC +kHDXShi8fgGwsOsVHkQGzaRP6AzRwyAQ4VRlnrZR0Bp2a0JaWHY06rc3Ga4udfmW +5cFZ95RXKSWNOkyrTZpB0F8mAwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSdhWEUfMFib5do5E83QOGt4A1WNzAd +BgNVHQ4EFgQUnYVhFHzBYm+XaORPN0DhreANVjcwDQYJKoZIhvcNAQEMBQADggIB +AGSPesRiDrWIzLjHhg6hShbNcAu3p4ULs3a2D6f/CIsLJc+o1IN1KriWiLb73y0t +tGlTITVX1olNc79pj3CjYcya2x6a4CD4bLubIp1dhDGaLIrdaqHXKGnK/nZVekZn +68xDiBaiA9a5F/gZbG0jAn/xX9AKKSM70aoK7akXJlQKTcKlTfjF/biBzysseKNn +TKkHmvPfXvt89YnNdJdhEGoHK4Fa0o635yDRIG4kqIQnoVesqlVYL9zZyvpoBJ7t +RCT5dEA7IzOrg1oYJkK2bVS1FmAwbLGg+LhBoF1JSdJlBTrq/p1hvIbZv97Tujqx +f36SNI7JAG7cmL3c7IAFrQI932XtCwP39xaEBDG6k5TY8hL4iuO/Qq+n1M0RFxbI +Qh0UqEL20kCGoE8jypZFVmAGzbdVAaYBlGX+bgUJurSkquLvWL69J1bY73NxW0Qz +8ppy6rBePm6pUlvscG21h483XjyMnM7k8M4MZ0HMzvaAq07MTFb1wWFZk7Q+ptq4 +NxKfKjLji7gh7MMrZQzvIt6IKTtM1/r+t+FHvpw+PoP7UV31aPcuIYXcv/Fa4nzX +xeSDwWrruoBa3lwtcHb4yOWHh8qgnaHlIhInD0Q9HWzq1MKLL295q39QpsQZp6F6 +t5b5wR9iWqJDB0BeJsas7a5wFsWqynKKTbDPAYsDP27X +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIPAWdfJ9b+euPkrL4JWwWeMA0GCSqGSIb3DQEBCwUAMEQx +CzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UE +AwwQVGVsaWEgUm9vdCBDQSB2MjAeFw0xODExMjkxMTU1NTRaFw00MzExMjkxMTU1 +NTRaMEQxCzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZ +MBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2MjCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBALLQPwe84nvQa5n44ndp586dpAO8gm2h/oFlH0wnrI4AuhZ76zBq +AMCzdGh+sq/H1WKzej9Qyow2RCRj0jbpDIX2Q3bVTKFgcmfiKDOlyzG4OiIjNLh9 +vVYiQJ3q9HsDrWj8soFPmNB06o3lfc1jw6P23pLCWBnglrvFxKk9pXSW/q/5iaq9 +lRdU2HhE8Qx3FZLgmEKnpNaqIJLNwaCzlrI6hEKNfdWV5Nbb6WLEWLN5xYzTNTOD +n3WhUidhOPFZPY5Q4L15POdslv5e2QJltI5c0BE0312/UqeBAMN/mUWZFdUXyApT +7GPzmX3MaRKGwhfwAZ6/hLzRUssbkmbOpFPlob/E2wnW5olWK8jjfN7j/4nlNW4o +6GwLI1GpJQXrSPjdscr6bAhR77cYbETKJuFzxokGgeWKrLDiKca5JLNrRBH0pUPC +TEPlcDaMtjNXepUugqD0XBCzYYP2AgWGLnwtbNwDRm41k9V6lS/eINhbfpSQBGq6 +WT0EBXWdN6IOLj3rwaRSg/7Qa9RmjtzG6RJOHSpXqhC8fF6CfaamyfItufUXJ63R +DolUK5X6wK0dmBR4M0KGCqlztft0DbcbMBnEWg4cJ7faGND/isgFuvGqHKI3t+ZI +pEYslOqodmJHixBTB0hXbOKSTbauBcvcwUpej6w9GU7C7WB1K9vBykLVAgMBAAGj +YzBhMB8GA1UdIwQYMBaAFHKs5DN5qkWH9v2sHZ7Wxy+G2CQ5MB0GA1UdDgQWBBRy +rOQzeapFh/b9rB2e1scvhtgkOTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw +AwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAoDtZpwmUPjaE0n4vOaWWl/oRrfxn83EJ +8rKJhGdEr7nv7ZbsnGTbMjBvZ5qsfl+yqwE2foH65IRe0qw24GtixX1LDoJt0nZi +0f6X+J8wfBj5tFJ3gh1229MdqfDBmgC9bXXYfef6xzijnHDoRnkDry5023X4blMM +A8iZGok1GTzTyVR8qPAs5m4HeW9q4ebqkYJpCh3DflminmtGFZhb069GHWLIzoBS +SRE/yQQSwxN8PzuKlts8oB4KtItUsiRnDe+Cy748fdHif64W1lZYudogsYMVoe+K +TTJvQS8TUoKU1xrBeKJR3Stwbbca+few4GeXVtt8YVMJAygCQMez2P2ccGrGKMOF +6eLtGpOg3kuYooQ+BXcBlj37tCAPnHICehIv1aO6UXivKitEZU61/Qrowc15h2Er +3oBXRb9n8ZuRXqWk7FlIEA04x7D6w0RtBPV4UBySllva9bguulvP5fBqnUsvWHMt +Ty3EHD70sz+rFQ47GUGKpMFXEmZxTPpT41frYpUJnlTd0cI8Vzy9OK2YZLe4A5pT +VmBds9hCG1xLEooc6+t9xnppxyd/pPiL8uSUZodL6ZQHCRJ5irLrdATczvREWeAW +ysUsWNc8e89ihmpQfTU2Zqf7N+cox9jQraVplI/owd8k+BsHMYeB2F326CjYSlKA +rBPuUBQemMc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAw +NzEUMBIGA1UECgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJv +b3QgQ0EgdjEwHhcNMDcxMDE4MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYD +VQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwWVGVsaWFTb25lcmEgUm9vdCBDQSB2 +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+6yfwIaPzaSZVfp3F +VRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA3GV1 +7CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+X +Z75Ljo1kB1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+ +/jXh7VB7qTCNGdMJjmhnXb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs +81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxHoLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkm +dtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3F0fUTPHSiXk+TT2YqGHe +Oh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJoWjiUIMu +sDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4 +pgd7gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fs +slESl1MpWtTwEhDcTwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQ +arMCpgKIv7NHfirZ1fpoeDVNAgMBAAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYD +VR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qWDNXr+nuqF+gTEjANBgkqhkiG +9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNmzqjMDfz1mgbl +dxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx +0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1Tj +TQpgcmLNkQfWpb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBed +Y2gea+zDTYa4EzAvXUYNR0PVG6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7 +Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpcc41teyWRyu5FrgZLAMzTsVlQ2jqI +OylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOTJsjrDNYmiLbAJM+7 +vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2qReW +t88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcn +HL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx +SK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= +-----END CERTIFICATE----- diff --git a/common/certificate/mozilla.go b/common/certificate/mozilla.go index 178bcad46c..551b00c447 100644 --- a/common/certificate/mozilla.go +++ b/common/certificate/mozilla.go @@ -2,13 +2,10 @@ package certificate -import "crypto/x509" - -func newMozillaIncluded() *x509.CertPool { - pool := x509.NewCertPool() - - // Actalis Authentication Root CA - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +func mozillaIncludedPEM() string { + return ` +// Actalis Authentication Root CA +-----BEGIN CERTIFICATE----- MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 @@ -40,10 +37,10 @@ K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // TunTrust Root CA - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// TunTrust Root CA +-----BEGIN CERTIFICATE----- MIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQEL BQAwYTELMAkGA1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUg Q2VydGlmaWNhdGlvbiBFbGVjdHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJv @@ -75,10 +72,10 @@ nV2UqL1g52KAdoGDDIzMMEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQ CvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9zZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZH u/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3rAZ3r2OvEhJn7wAzMMujj d9qDRIueVSjAi1jTkD5OGwDxFa2DK5o= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Amazon Root CA 1 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Amazon Root CA 1 +-----BEGIN CERTIFICATE----- MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL @@ -97,10 +94,10 @@ N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU 5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy rqXRfboQnoZsG4q5WTP468SQvvG5 ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Amazon Root CA 2 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Amazon Root CA 2 +-----BEGIN CERTIFICATE----- MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL @@ -130,10 +127,10 @@ n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE 76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H 9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT 4PsJYGw= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Amazon Root CA 3 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Amazon Root CA 3 +-----BEGIN CERTIFICATE----- MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5 MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG @@ -144,10 +141,10 @@ QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM YyRIHN8wfdVoOw== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Amazon Root CA 4 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Amazon Root CA 4 +-----BEGIN CERTIFICATE----- MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5 MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG @@ -159,10 +156,10 @@ M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW 1KyLa2tJElMzrdfkviT8tQp21KW8EA== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Starfield Services Root Certificate Authority - G2 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Starfield Services Root Certificate Authority - G2 +-----BEGIN CERTIFICATE----- MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs @@ -185,10 +182,10 @@ qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn 0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN sSi6 ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Certum CA - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Certum CA +-----BEGIN CERTIFICATE----- MIIDDDCCAfSgAwIBAgIDAQAgMA0GCSqGSIb3DQEBBQUAMD4xCzAJBgNVBAYTAlBM MRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBD QTAeFw0wMjA2MTExMDQ2MzlaFw0yNzA2MTExMDQ2MzlaMD4xCzAJBgNVBAYTAlBM @@ -206,10 +203,10 @@ xSvgGrZgFCdsMneMvLJymM/NzD+5yCRCFNZX/OYmQ6kd5YCQzgNUKD73P9P4Te1q CjqTE5s7FCMTY5w/0YcneeVMUeMBrYVdGjux1XMQpNPyvG5k9VpWkKjHDkx0Dy5x O/fIR/RpbxXyEV6DHpx8Uq79AtoSqFlnGNu8cN2bsWntgM6JQEhqDjXKKWYVIZQs 6GAqm4VKQPNriiTsBhYscw== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Certum EC-384 CA - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Certum EC-384 CA +-----BEGIN CERTIFICATE----- MIICZTCCAeugAwIBAgIQeI8nXIESUiClBNAt3bpz9DAKBggqhkjOPQQDAzB0MQsw CQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScw JQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAXBgNVBAMT @@ -223,10 +220,10 @@ QjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI0GZnQkdjrzife81r1HfS+8 EF9LMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjADVS2m5hjEfO/J UG7BJw+ch69u1RsIGL2SKcHvlJF40jocVYli5RsJHrpka/F2tNQCMQC0QoSZ/6vn nvuRlydd3LBbMHHOXjgaatkl5+r3YZJW+OraNsKHZZYuciUvf9/DE8k= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Certum Trusted Network CA - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Certum Trusted Network CA +-----BEGIN CERTIFICATE----- MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU @@ -247,10 +244,10 @@ I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8 J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI 03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Certum Trusted Network CA 2 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Certum Trusted Network CA 2 +-----BEGIN CERTIFICATE----- MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCB gDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu QS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG @@ -283,10 +280,10 @@ b7a6bINDd82Kkhehnlt4Fj1F4jNy3eFmypnTycUm/Q1oBEauttmbjL4ZvrHG8hnj XALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P 5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi DrW5viSP ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Certum Trusted Root CA - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Certum Trusted Root CA +-----BEGIN CERTIFICATE----- MIIFwDCCA6igAwIBAgIQHr9ZULjJgDdMBvfrVU+17TANBgkqhkiG9w0BAQ0FADB6 MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu MScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxHzAdBgNV @@ -318,10 +315,10 @@ P78v3DSk+yshzWePS/Tj6tQ/50+6uaWTRRxmHyH6ZF5v4HaUMst19W7l9o/HuKTM qJZ9ZPskWkoDbGs4xugDQ5r3V7mzKWmTOPQD8rv7gmsHINFSH5pkAnuYZttcTVoP 0ISVoDwUQwbKytu4QTbaakRnh6+v40URFWkIsr4WOZckbxJF0WddCajJFdr60qZf E2Efv4WstK2tBZQIgx51F9NxO5NQI1mg7TyRVJ12AMXDuDjb ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Autoridad de Certificacion Firmaprofesional CIF A62634068 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Autoridad de Certificacion Firmaprofesional CIF A62634068 +-----BEGIN CERTIFICATE----- MIIGFDCCA/ygAwIBAgIIG3Dp0v+ubHEwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UE BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0xNDA5MjMxNTIyMDdaFw0zNjA1 @@ -355,28 +352,10 @@ CsvMo2ebKHTEm9caPARYpoKdrcd7b/+Alun4jWq9GJAd/0kakFI3ky88Al2CdgtR f9DEMnDAGf/JOC0ULGb0QkTmVXYbgBVX/8Cnp6o5qtjTcNAuuuuUavpfNIbnYrX9 ivAwhZTJryQCL2/W3Wf+47BVTwSYT6RBVuKT0Gro1vP7ZeDOdcQxWQzugsgMYDNK GbqEZycPvEJdvSRUDewdcAZfpLz6IHxV ------END CERTIFICATE-----`)) - - // FIRMAPROFESIONAL CA ROOT-A WEB - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIICejCCAgCgAwIBAgIQMZch7a+JQn81QYehZ1ZMbTAKBggqhkjOPQQDAzBuMQsw -CQYDVQQGEwJFUzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25hbCBTQTEYMBYGA1UE -YQwPVkFURVMtQTYyNjM0MDY4MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFMIENB -IFJPT1QtQSBXRUIwHhcNMjIwNDA2MDkwMTM2WhcNNDcwMzMxMDkwMTM2WjBuMQsw -CQYDVQQGEwJFUzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25hbCBTQTEYMBYGA1UE -YQwPVkFURVMtQTYyNjM0MDY4MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFMIENB -IFJPT1QtQSBXRUIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARHU+osEaR3xyrq89Zf -e9MEkVz6iMYiuYMQYneEMy3pA4jU4DP37XcsSmDq5G+tbbT4TIqk5B/K6k84Si6C -cyvHZpsKjECcfIr28jlgst7L7Ljkb+qbXbdTkBgyVcUgt5SjYzBhMA8GA1UdEwEB -/wQFMAMBAf8wHwYDVR0jBBgwFoAUk+FDY1w8ndYn81LsF7Kpryz3dvgwHQYDVR0O -BBYEFJPhQ2NcPJ3WJ/NS7Beyqa8s93b4MA4GA1UdDwEB/wQEAwIBBjAKBggqhkjO -PQQDAwNoADBlAjAdfKR7w4l1M+E7qUW/Runpod3JIha3RxEL2Jq68cgLcFBTApFw -hVmpHqTm6iMxoAACMQD94vizrxa5HnPEluPBMBnYfubDl94cT7iJLzPrSA8Z94dG -XSaQpYXFuXqUPoeovQA= ------END CERTIFICATE-----`)) - - // ANF Secure Server Root CA - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +-----END CERTIFICATE----- + +// ANF Secure Server Root CA +-----BEGIN CERTIFICATE----- MIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNV BAUTCUc2MzI4NzUxMDELMAkGA1UEBhMCRVMxJzAlBgNVBAoTHkFORiBBdXRvcmlk YWQgZGUgQ2VydGlmaWNhY2lvbjEUMBIGA1UECxMLQU5GIENBIFJhaXoxIjAgBgNV @@ -409,10 +388,10 @@ OknkJjCb5RyKqKkVMoaZkgoQI1YS4PbOTOK7vtuNknMBZi9iPrJyJ0U27U1W45eZ +PbZa/wSMVYIwFNCr5zQM378BvAxRAMU8Vjq8moNqRGyg77FGr8H6lnco4g175x2 MjxNBiLOFeXdntiP2t7SxDnlF4HPOEfrf4htWRvfn0IUrn7PqLBmZdo3r5+qPeoo tt7VMVgWglvquxl1AnMaykgaIZOQCo6ThKd9OyMYkomgjaw= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Buypass Class 2 Root CA - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Buypass Class 2 Root CA +-----BEGIN CERTIFICATE----- MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow @@ -442,10 +421,10 @@ I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h 3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Buypass Class 3 Root CA - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Buypass Class 3 Root CA +-----BEGIN CERTIFICATE----- MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow @@ -475,10 +454,10 @@ kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41 3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq 4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Certainly Root E1 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Certainly Root E1 +-----BEGIN CERTIFICATE----- MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQsw CQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlu bHkgUm9vdCBFMTAeFw0yMTA0MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJ @@ -490,10 +469,10 @@ BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4 hevIIgcwCgYIKoZIzj0EAwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozm ut6Dacpps6kFtZaSF4fC0urQe87YQVt8rgIwRt7qy12a7DLCZRawTDBcMPPaTnOG BtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Certainly Root R1 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Certainly Root R1 +-----BEGIN CERTIFICATE----- MIIFRzCCAy+gAwIBAgIRAI4P+UuQcWhlM1T01EQ5t+AwDQYJKoZIhvcNAQELBQAw PTELMAkGA1UEBhMCVVMxEjAQBgNVBAoTCUNlcnRhaW5seTEaMBgGA1UEAxMRQ2Vy dGFpbmx5IFJvb3QgUjEwHhcNMjEwNDAxMDAwMDAwWhcNNDYwNDAxMDAwMDAwWjA9 @@ -523,10 +502,10 @@ Kx3upPvaJVQTA945xsMfTZDsjxtK0hzthZU4UHlG1sGQUDGpXJpuHfUzVounmdLy yCwzk5Iwx06MZTMQZBf9JBeW0Y3COmor6xOLRPIh80oat3df1+2IpHLlOR+Vnb5n wXARPbv0+Em34yaXOp/SX3z7wJl8OSngex2/DaeP0ik0biQVy96QXr8axGbqwua6 OV+KmalBWQewLK8= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Certigna - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Certigna +-----BEGIN CERTIFICATE----- MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV BAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4X DTA3MDYyOTE1MTMwNVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQ @@ -547,10 +526,10 @@ fca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9qcEQCYsuu HWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Certigna Root CA - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Certigna Root CA +-----BEGIN CERTIFICATE----- MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAw WjELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAw MiA0ODE0NjMwODEwMDAzNjEZMBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0x @@ -585,32 +564,10 @@ faci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaYtlu3zM63 Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayh jWZSaX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw 3kAP+HwV96LOPNdeE4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0= ------END CERTIFICATE-----`)) - - // certSIGN ROOT CA - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYT -AlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBD -QTAeFw0wNjA3MDQxNzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJP -MREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCC -ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7IJUqOtdu0KBuqV5Do -0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHHrfAQ -UySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5d -RdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQ -OA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv -JoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08C -AwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0O -BBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJ -LjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecY -MnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ -44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6I -Jd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNw -i/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN -9u6wWk5JRFRYX0KD ------END CERTIFICATE-----`)) - - // certSIGN ROOT CA G2 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +-----END CERTIFICATE----- + +// certSIGN ROOT CA G2 +-----BEGIN CERTIFICATE----- MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV BAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04g Uk9PVCBDQSBHMjAeFw0xNzAyMDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJ @@ -640,10 +597,10 @@ pwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tUSxfj PuXaTH4MNMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE 1LlSVHJ7liXMvGnjSG4N0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MX QRBdJ3NghVdJIgc= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // ePKI Root Certification Authority - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// ePKI Root Certification Authority +-----BEGIN CERTIFICATE----- MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 ZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe @@ -675,10 +632,10 @@ o2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS Gp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE W9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D hNQ+IIX3Sj0rnP0qCglN6oH4EZw= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // HiPKI Root CA - G1 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// HiPKI Root CA - G1 +-----BEGIN CERTIFICATE----- MIIFajCCA1KgAwIBAgIQLd2szmKXlKFD6LDNdmpeYDANBgkqhkiG9w0BAQsFADBP MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 ZC4xGzAZBgNVBAMMEkhpUEtJIFJvb3QgQ0EgLSBHMTAeFw0xOTAyMjIwOTQ2MDRa @@ -708,10 +665,10 @@ Ne85akEez3GoorKGB1s6yeHvP2UEgEcyRHCVTjFnanRbEEV16rCf0OY1/k6fi8wr kkVbbiVghUbN0aqwdmaTd5a+g744tiROJgvM7XpWGuDpWsZkrUx6AEhEL7lAuxM+ vhV4nYWBSipX3tUZQ9rbyltHhoMLP7YNdnhzeSJesYAfz77RP1YQmCuVh6EfnWQU YDksswBVLuT1sw5XxJFBAJw/6KXf6vb/yPCtbVKoF6ubYfwSUTXkJf2vqmqGOQ== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // SecureSign Root CA12 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// SecureSign Root CA12 +-----BEGIN CERTIFICATE----- MIIDcjCCAlqgAwIBAgIUZvnHwa/swlG07VOX5uaCwysckBYwDQYJKoZIhvcNAQEL BQAwUTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28u LCBMdGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExMjAeFw0yMDA0MDgw @@ -731,10 +688,10 @@ mBClnW8Zt7vPemVV2zfrPIpyMpcemik+rY3moxtt9XUa5rBouVui7mlHJzWhhpmA 8zNL4WukJsPvdFlseqJkth5Ew1DgDzk9qTPxpfPSvWKErI4cqc1avTc7bgoitPQV 55FYxTpE05Uo2cBl6XLK0A+9H7MV2anjpEcJnuDLN/v9vZfVvhgaaaI5gdka9at/ yOPiZwud9AzqVN/Ssq+xIvEg37xEHA== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // SecureSign Root CA14 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// SecureSign Root CA14 +-----BEGIN CERTIFICATE----- MIIFcjCCA1qgAwIBAgIUZNtaDCBO6Ncpd8hQJ6JaJ90t8sswDQYJKoZIhvcNAQEM BQAwUTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28u LCBMdGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNDAeFw0yMDA0MDgw @@ -765,10 +722,10 @@ dB7h7sxaOgTdsxoEqBRjrLdHEoOabPXm6RUVkRqEGQ6UROcSjiVbgGcZ3GOTEAtl Lor6CZpO2oYofaphNdgOpygau1LgePhsumywbrmHXumZNTfxPWQrqaA0k89jL9WB 365jJ6UeTo3cKXhZ+PmhIIynJkBugnLNeLLIjzwec+fBH7/PzqUqm9tEZDKgu39c JRNItX+S ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // SecureSign Root CA15 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// SecureSign Root CA15 +-----BEGIN CERTIFICATE----- MIICIzCCAamgAwIBAgIUFhXHw9hJp75pDIqI7fBw+d23PocwCgYIKoZIzj0EAwMw UTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28uLCBM dGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNTAeFw0yMDA0MDgwODMy @@ -781,10 +738,10 @@ Af8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTrQciu/NWeUUj1vYv0hyCTQSvT 9DAKBggqhkjOPQQDAwNoADBlAjEA2S6Jfl5OpBEHvVnCB96rMjhTKkZEBhd6zlHp 4P9mLQlO4E/0BdGF9jVg3PVys0Z9AjBEmEYagoUeYWmJSwdLZrWeqrqgHkHZAXQ6 bkU6iYAZezKYVWOr62Nuk22rGwlgMU4= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // D-TRUST BR Root CA 1 2020 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// D-TRUST BR Root CA 1 2020 +-----BEGIN CERTIFICATE----- MIIC2zCCAmCgAwIBAgIQfMmPK4TX3+oPyWWa00tNljAKBggqhkjOPQQDAzBIMQsw CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS VVNUIEJSIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTA5NDUwMFoXDTM1MDIxMTA5 @@ -801,10 +758,10 @@ c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO PQQDAwNpADBmAjEAlJAtE/rhY/hhY+ithXhUkZy4kzg+GkHaQBZTQgjKL47xPoFW wKrY7RjEsK70PvomAjEA8yjixtsrmfu3Ubgko6SUeho/5jbiA1czijDLgsfWFBHV dWNbFJWcHwHP2NVypw87 ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // D-TRUST BR Root CA 2 2023 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// D-TRUST BR Root CA 2 2023 +-----BEGIN CERTIFICATE----- MIIFqTCCA5GgAwIBAgIQczswBEhb2U14LnNLyaHcZjANBgkqhkiG9w0BAQ0FADBI MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE LVRSVVNUIEJSIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA4NTYzMVoXDTM4MDUw @@ -836,10 +793,10 @@ ls/pdu4D58JDUjxqgejBWoC9EV2Ta/vH5mQ/u2kc6d0li690yVRAysuTEwrt+2aS Ecr1wPrYg1UDfNPFIkZ1cGt5SAYqgpq/5usWDiJFAbzdNpQ0qTUmiteXue4Icr80 knCDgKs4qllo3UCkGJCy89UDyibK79XH4I9TjvAA46jtn/mtd+ArY0+ew+43u3gJ hJ65bvspmZDogNOfJA== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // D-TRUST EV Root CA 1 2020 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// D-TRUST EV Root CA 1 2020 +-----BEGIN CERTIFICATE----- MIIC2zCCAmCgAwIBAgIQXwJB13qHfEwDo6yWjfv/0DAKBggqhkjOPQQDAzBIMQsw CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS VVNUIEVWIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTEwMDAwMFoXDTM1MDIxMTA5 @@ -856,10 +813,10 @@ c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO PQQDAwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CA y/m0sRtW9XLS/BnRAjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJb gfM0agPnIjhQW+0ZT0MW ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // D-TRUST EV Root CA 2 2023 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// D-TRUST EV Root CA 2 2023 +-----BEGIN CERTIFICATE----- MIIFqTCCA5GgAwIBAgIQaSYJfoBLTKCnjHhiU19abzANBgkqhkiG9w0BAQ0FADBI MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE LVRSVVNUIEVWIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA5MTAzM1oXDTM4MDUw @@ -891,10 +848,10 @@ ifYbMvekJKZ2t0dT741Jj6m2g1qllpBFYfXeA08mD6iL8AOWsKwV0HFaanuU5nCT gofXNJhuS5N5YHVpD/Aa1VP6IQzCP+k/HxiMkl14p3ZnGbuy6n/pcAlWVqOwDAst Nl7F6cTVg8uGF5csbBNvh1qvSaYd2804BC5f4ko1Di1L+KIkBI3Y4WNeApI02phh XBxvWHZks/wCuPWdCg== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // D-TRUST Root CA 3 2013 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// D-TRUST Root CA 3 2013 +-----BEGIN CERTIFICATE----- MIIEDjCCAvagAwIBAgIDD92sMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNVBAYTAkRF MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxHzAdBgNVBAMMFkQtVFJVU1QgUm9vdCBD QSAzIDIwMTMwHhcNMTMwOTIwMDgyNTUxWhcNMjgwOTIwMDgyNTUxWjBFMQswCQYD @@ -917,10 +874,10 @@ t4RVLFzI9XRKEBmLo8ftNdYJSNMOwLo5qLBGArDbxohZwr78e7Erz35ih1WWzAFv m2chlTWL+BD8cRu3SzdppjvW7IvuwbDzJcmPkn2h6sPKRL8mpXSSnON065102ctN h9j8tGlsi6BDB2B4l+nZk3zCRrybN1Kj7Yo8E6l7U0tJmhEFLAtuVqwfLoJs4Gln tQ5tLdnkwBXxP/oYcuEVbSdbLTAoK59ImmQrme/ydUlfXA== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // D-TRUST Root Class 3 CA 2 2009 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// D-TRUST Root Class 3 CA 2 2009 +-----BEGIN CERTIFICATE----- MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha @@ -944,10 +901,10 @@ o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y Johw1+qRzT65ysCQblrGXnRl11z+o+I= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // D-TRUST Root Class 3 CA 2 EV 2009 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// D-TRUST Root Class 3 CA 2 EV 2009 +-----BEGIN CERTIFICATE----- MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw @@ -971,10 +928,10 @@ nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // D-Trust SBR Root CA 1 2022 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// D-Trust SBR Root CA 1 2022 +-----BEGIN CERTIFICATE----- MIICXjCCAeOgAwIBAgIQUs/kjG2gSvc/gpcMgAmMlTAKBggqhkjOPQQDAzBJMQsw CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSMwIQYDVQQDExpELVRy dXN0IFNCUiBSb290IENBIDEgMjAyMjAeFw0yMjA3MDYxMTMwMDBaFw0zNzA3MDYx @@ -988,10 +945,10 @@ hjlodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X3Nicl9yb290X2Nh XzFfMjAyMi5jcmwwCgYIKoZIzj0EAwMDaQAwZgIxAJf53q5Lj5i1HkB/Mn1NVEPa ic3CqpI80YIec8/6TJIg+2MnxfVzPQk996dhhozzagIxAOcvfLj1JYw7OR82q431 hqIu4Xpk2mc5Av7+Mz/Zc7ZYWzr8sqTZYHh3zHmnpq5VvQ== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // D-Trust SBR Root CA 2 2022 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// D-Trust SBR Root CA 2 2022 +-----BEGIN CERTIFICATE----- MIIFrDCCA5SgAwIBAgIQVNWjlR49lbpyG5rQMSFKujANBgkqhkiG9w0BAQ0FADBJ MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSMwIQYDVQQDExpE LVRydXN0IFNCUiBSb290IENBIDIgMjAyMjAeFw0yMjA3MDcwNzMwMDBaFw0zNzA3 @@ -1023,10 +980,10 @@ JpmgVDuhEm1wYs7T+bi9IvzUmtS74jgWL7d9OcKwqQPpnM9+GI123F8Ru+tC7FAJ PlzskDHYGnK6P2kH7pg0wjSk1toT1qmE8gCGwFS6HhGw4rnEB7SR56rmMVZvsUTE KmK8ybBlnDT8DBpT3yEXu8JtoQrm8bCqRAlQSTh6XXHiMS4ZsN+VQgR9hIjOCiNn azidFt4G/ihwOKVarvyD7Q== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // T-TeleSec GlobalRoot Class 2 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// T-TeleSec GlobalRoot Class 2 +-----BEGIN CERTIFICATE----- MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl @@ -1048,10 +1005,10 @@ IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6 g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN 9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP BSeOE6Fuwg== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // T-TeleSec GlobalRoot Class 3 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// T-TeleSec GlobalRoot Class 3 +-----BEGIN CERTIFICATE----- MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl @@ -1073,10 +1030,10 @@ WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ 91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p TpPDpFQUWw== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Telekom Security SMIME ECC Root 2021 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Telekom Security SMIME ECC Root 2021 +-----BEGIN CERTIFICATE----- MIICRzCCAc2gAwIBAgIQFSrdFMkY0aRWQIamJa8HXzAKBggqhkjOPQQDAzBlMQsw CQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBH bWJIMS0wKwYDVQQDDCRUZWxla29tIFNlY3VyaXR5IFNNSU1FIEVDQyBSb290IDIw @@ -1090,10 +1047,10 @@ vZA6SzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQD AwNoADBlAjEA1rxIkodHA8dwOyW2H65GZ3N0ACdL5KUEogPfXiitbl4DyN1onLa/ lBBIlS8P/xiLAjABQDOel5dNBfJ0VAzNOf1qawnBJD9hjjiht+jXRBURYv8OYTdH S0B/Sl+yZ1pzdcI= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Telekom Security SMIME RSA Root 2023 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Telekom Security SMIME RSA Root 2023 +-----BEGIN CERTIFICATE----- MIIFtzCCA5+gAwIBAgIQDH5i9XlzO51Djotj7ZGVuDANBgkqhkiG9w0BAQwFADBl MQswCQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0 eSBHbWJIMS0wKwYDVQQDDCRUZWxla29tIFNlY3VyaXR5IFNNSU1FIFJTQSBSb290 @@ -1125,10 +1082,10 @@ NDnPpZo2qsjtebzP9s4EUwvaslAjfLw+Jq3wDkO7JsuuwkDeNx8KoFHNY522T9jG R3y82LTtnovzEeKotT7srnA+fiK7NUgXYGIUkTCjdj2mUTaLHw3dajEcpe3dlqNu cg8TTaqnqVx4+QMSGJM3RRKJPfi+yr3ZvgzZGGSnyEE+dYIhOH1l9KDUE0sHeCn5 nX7Mhz/E2i6I3eML3FpRWunZEk+eAtv3BSVR ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Telekom Security TLS ECC Root 2020 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Telekom Security TLS ECC Root 2020 +-----BEGIN CERTIFICATE----- MIICQjCCAcmgAwIBAgIQNjqWjMlcsljN0AFdxeVXADAKBggqhkjOPQQDAzBjMQsw CQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBH bWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBFQ0MgUm9vdCAyMDIw @@ -1142,10 +1099,10 @@ MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA MGQCMHVSi7ekEE+uShCLsoRbQuHmKjYC2qBuGT8lv9pZMo7k+5Dck2TOrbRBR2Di z6fLHgIwN0GMZt9Ba9aDAEH9L1r3ULRn0SyocddDypwnJJGDSA3PzfdUga/sf+Rn 27iQ7t0l ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Telekom Security TLS RSA Root 2023 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Telekom Security TLS RSA Root 2023 +-----BEGIN CERTIFICATE----- MIIFszCCA5ugAwIBAgIQIZxULej27HF3+k7ow3BXlzANBgkqhkiG9w0BAQwFADBj MQswCQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0 eSBHbWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBSU0EgUm9vdCAy @@ -1177,10 +1134,10 @@ L4x35bcF7DvB7L6Gs4a8wPfc5+pbrrLMtTWGS9DiP7bY+A4A7l3j941Y/8+LN+lj X273CXE2whJdV/LItM3z7gLfEdxquVeEHVlNjM7IDiPCtyaaEBRx/pOyiriA8A4Q ntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0o82bNSQ3+pCTE4FCxpgm dTdmQRCsu/WU48IxK63nI1bMNSWSs1A= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // DigiCert Assured ID Root CA - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// DigiCert Assured ID Root CA +-----BEGIN CERTIFICATE----- MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv @@ -1201,10 +1158,10 @@ fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe +o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // DigiCert Assured ID Root G2 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// DigiCert Assured ID Root G2 +-----BEGIN CERTIFICATE----- MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv @@ -1225,10 +1182,10 @@ lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo IhNzbM8m9Yop5w== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // DigiCert Assured ID Root G3 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// DigiCert Assured ID Root G3 +-----BEGIN CERTIFICATE----- MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg @@ -1242,10 +1199,10 @@ BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv 6pZjamVFkpUBtA== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // DigiCert Global Root CA - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// DigiCert Global Root CA +-----BEGIN CERTIFICATE----- MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD @@ -1266,10 +1223,10 @@ hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // DigiCert Global Root G2 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// DigiCert Global Root G2 +-----BEGIN CERTIFICATE----- MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH @@ -1290,10 +1247,10 @@ Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl MrY= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // DigiCert Global Root G3 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// DigiCert Global Root G3 +-----BEGIN CERTIFICATE----- MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe @@ -1307,10 +1264,10 @@ BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 sycX ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // DigiCert High Assurance EV Root CA - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// DigiCert High Assurance EV Root CA +-----BEGIN CERTIFICATE----- MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j @@ -1332,10 +1289,10 @@ hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep +OkuE6N36B9K ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // DigiCert SMIME ECC P384 Root G5 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// DigiCert SMIME ECC P384 Root G5 +-----BEGIN CERTIFICATE----- MIICHDCCAaOgAwIBAgIQBT9uoAYBcn3tP8OjtqPW7zAKBggqhkjOPQQDAzBQMQsw CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xKDAmBgNVBAMTH0Rp Z2lDZXJ0IFNNSU1FIEVDQyBQMzg0IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN @@ -1348,10 +1305,10 @@ wmQyF/7gZ5AurTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAKBggq hkjOPQQDAwNnADBkAjA3RPUygONx6/Rtz3zMkZrDbnHY0iNdkk2CQm1cYZX2kfWn CPZql+mclC2YcP0ztgkCMAc8L7lYgl4Po2Kok2fwIMNpvwMsO1CnO69BOMlSSJHW Dvu8YDB8ZD8SHkV/UT70pg== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // DigiCert SMIME RSA4096 Root G5 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// DigiCert SMIME RSA4096 Root G5 +-----BEGIN CERTIFICATE----- MIIFajCCA1KgAwIBAgIQBfa6BCODRst9XOa5W7ocVTANBgkqhkiG9w0BAQwFADBP MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJzAlBgNVBAMT HkRpZ2lDZXJ0IFNNSU1FIFJTQTQwOTYgUm9vdCBHNTAeFw0yMTAxMTUwMDAwMDBa @@ -1381,10 +1338,10 @@ CQxy5BMjYEyjyhcue2cA29DN6nofOSZXiTB3y07llUVPX/s2XD35ILU6ECVPkzJa 7sGW6OlWBLBJYU3seKidGMH/2OovVu+VK3sEXmfjVUDtOQT5C3n1aoxcD4makMfN i97bJjWhbs2zQvKiDzsMjpP/FM/895P35EEIbhlSEQ9TGXN4DM/YhYH4rVXIsJ5G Y6+cUu5cv/DAWzceCSDSPiPGoRVKDjZ+MMV5arwiiNkMUkAf3U4PZyYW0q0XHA== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // DigiCert TLS ECC P384 Root G5 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// DigiCert TLS ECC P384 Root G5 +-----BEGIN CERTIFICATE----- MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 @@ -1397,10 +1354,10 @@ B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 DXZDjC5Ty3zfDBeWUA== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // DigiCert TLS RSA4096 Root G5 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// DigiCert TLS RSA4096 Root G5 +-----BEGIN CERTIFICATE----- MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBN MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMT HERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN @@ -1430,10 +1387,10 @@ p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilw MUc/dNAUFvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WF qUITVuwhd4GTWgzqltlJyqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCK ovfepEWFJqgejF0pW8hL2JpqA15w8oVPbEtoL8pU9ozaMv7Da4M/OMZ+ ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // DigiCert Trusted Root G4 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// DigiCert Trusted Root G4 +-----BEGIN CERTIFICATE----- MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg @@ -1464,10 +1421,10 @@ cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 /YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // QuoVadis Root CA 1 G3 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// QuoVadis Root CA 1 G3 +-----BEGIN CERTIFICATE----- MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00 @@ -1497,10 +1454,10 @@ NSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj ZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp q1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt nh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // QuoVadis Root CA 2 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// QuoVadis Root CA 2 +-----BEGIN CERTIFICATE----- MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv b3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV @@ -1532,10 +1489,10 @@ mbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z ohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y 4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza 8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // QuoVadis Root CA 2 G3 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// QuoVadis Root CA 2 G3 +-----BEGIN CERTIFICATE----- MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 @@ -1565,10 +1522,10 @@ dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // QuoVadis Root CA 3 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// QuoVadis Root CA 3 +-----BEGIN CERTIFICATE----- MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv b3QgQ0EgMzAeFw0wNjExMjQxOTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNV @@ -1605,10 +1562,10 @@ zHXug/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0j Wy10QJLZYxkNc91pvGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeT mJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK 4SVhM7JZG+Ju1zdXtg2pEto= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // QuoVadis Root CA 3 G3 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// QuoVadis Root CA 3 G3 +-----BEGIN CERTIFICATE----- MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 @@ -1638,10 +1595,10 @@ u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // DIGITALSIGN GLOBAL ROOT ECDSA CA - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// DIGITALSIGN GLOBAL ROOT ECDSA CA +-----BEGIN CERTIFICATE----- MIICajCCAfCgAwIBAgIUNi2PcoiiKCfkAP8kxi3k6/qdtuEwCgYIKoZIzj0EAwMw ZDELMAkGA1UEBhMCUFQxKjAoBgNVBAoMIURpZ2l0YWxTaWduIENlcnRpZmljYWRv cmEgRGlnaXRhbDEpMCcGA1UEAwwgRElHSVRBTFNJR04gR0xPQkFMIFJPT1QgRUNE @@ -1655,10 +1612,10 @@ GDAWgBTOr0qLGnXi8TjnAvAWrV7qZNV7tDAdBgNVHQ4EFgQUzq9Kixp14vE45wLw Fq1e6mTVe7QwDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMAqIxHGc RANNjbTHvKiu2TAnNWprFmPX/OdZ4aeJG0wxmiNVRObzQyHVRydvbVcBqgIxAPuy 6uKXf1G1n0jrvG81iahkcKtXds3AxhRgyn/iggBz98w16o4km+UIWccEjHN4/g== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // DIGITALSIGN GLOBAL ROOT RSA CA - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// DIGITALSIGN GLOBAL ROOT RSA CA +-----BEGIN CERTIFICATE----- MIIFtTCCA52gAwIBAgIUXVnIyqsJV/XmtdoplARq/8XUlYcwDQYJKoZIhvcNAQEN BQAwYjELMAkGA1UEBhMCUFQxKjAoBgNVBAoMIURpZ2l0YWxTaWduIENlcnRpZmlj YWRvcmEgRGlnaXRhbDEnMCUGA1UEAwweRElHSVRBTFNJR04gR0xPQkFMIFJPT1Qg @@ -1690,10 +1647,10 @@ NrskGnkcUdH+E7v/eCzcpL4v9sVLU8+nTJlecKxZiASuZAS/e6Z6TdPod72hflAV 8+9JMIVNIVeq2yx1l62BAYeisXCdHgZaA2CxP6ZtgizUFLGBpeg9iB20cixYN4qO OJS4c92p4Lj2d6KzfFjermk6tYulGrvy2HQGnP1icyAhdrF+cJ4Z1OsXYhk4mc02 K0f+McvfueSsCNPYpuvUnn5LZKRVXSsXyQ== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // CA Disig Root R2 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// CA Disig Root R2 +-----BEGIN CERTIFICATE----- MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy @@ -1723,44 +1680,10 @@ gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL ------END CERTIFICATE-----`)) - - // GLOBALTRUST 2020 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIFgjCCA2qgAwIBAgILWku9WvtPilv6ZeUwDQYJKoZIhvcNAQELBQAwTTELMAkG -A1UEBhMCQVQxIzAhBgNVBAoTGmUtY29tbWVyY2UgbW9uaXRvcmluZyBHbWJIMRkw -FwYDVQQDExBHTE9CQUxUUlVTVCAyMDIwMB4XDTIwMDIxMDAwMDAwMFoXDTQwMDYx -MDAwMDAwMFowTTELMAkGA1UEBhMCQVQxIzAhBgNVBAoTGmUtY29tbWVyY2UgbW9u -aXRvcmluZyBHbWJIMRkwFwYDVQQDExBHTE9CQUxUUlVTVCAyMDIwMIICIjANBgkq -hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAri5WrRsc7/aVj6B3GyvTY4+ETUWiD59b -RatZe1E0+eyLinjF3WuvvcTfk0Uev5E4C64OFudBc/jbu9G4UeDLgztzOG53ig9Z -YybNpyrOVPu44sB8R85gfD+yc/LAGbaKkoc1DZAoouQVBGM+uq/ufF7MpotQsjj3 -QWPKzv9pj2gOlTblzLmMCcpL3TGQlsjMH/1WljTbjhzqLL6FLmPdqqmV0/0plRPw -yJiT2S0WR5ARg6I6IqIoV6Lr/sCMKKCmfecqQjuCgGOlYx8ZzHyyZqjC0203b+J+ -BlHZRYQfEs4kUmSFC0iAToexIiIwquuuvuAC4EDosEKAA1GqtH6qRNdDYfOiaxaJ -SaSjpCuKAsR49GiKweR6NrFvG5Ybd0mN1MkGco/PU+PcF4UgStyYJ9ORJitHHmkH -r96i5OTUawuzXnzUJIBHKWk7buis/UDr2O1xcSvy6Fgd60GXIsUf1DnQJ4+H4xj0 -4KlGDfV0OoIu0G4skaMxXDtG6nsEEFZegB31pWXogvziB4xiRfUg3kZwhqG8k9Me -dKZssCz3AwyIDMvUclOGvGBG85hqwvG/Q/lwIHfKN0F5VVJjjVsSn8VoxIidrPIw -q7ejMZdnrY8XD2zHc+0klGvIg5rQmjdJBKuxFshsSUktq6HQjJLyQUp5ISXbY9e2 -nKd+Qmn7OmMCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC -AQYwHQYDVR0OBBYEFNwuH9FhN3nkq9XVsxJxaD1qaJwiMB8GA1UdIwQYMBaAFNwu -H9FhN3nkq9XVsxJxaD1qaJwiMA0GCSqGSIb3DQEBCwUAA4ICAQCR8EICaEDuw2jA -VC/f7GLDw56KoDEoqoOOpFaWEhCGVrqXctJUMHytGdUdaG/7FELYjQ7ztdGl4wJC -XtzoRlgHNQIw4Lx0SsFDKv/bGtCwr2zD/cuz9X9tAy5ZVp0tLTWMstZDFyySCstd -6IwPS3BD0IL/qMy/pJTAvoe9iuOTe8aPmxadJ2W8esVCgmxcB9CpwYhgROmYhRZf -+I/KARDOJcP5YBugxZfD0yyIMaK9MOzQ0MAS8cE54+X1+NZK3TTN+2/BT+MAi1bi -kvcoskJ3ciNnxz8RFbLEAwW+uxF7Cr+obuf/WEPPm2eggAe2HcqtbepBEX4tdJP7 -wry+UUTF72glJ4DjyKDUEuzZpTcdN3y0kcra1LGWge9oXHYQSa9+pTeAsRxSvTOB -TI/53WXZFM2KJVj04sWDpQmQ1GwUY7VA3+vA/MRYfg0UFodUJ25W5HCEuGwyEn6C -MUO+1918oa2u1qsgEu8KwxCMSZY13At1XrFP1U80DhEgB3VDRemjEdqso5nCtnkn -4rnvyOL2NSl6dPrFf4IFYqYK6miyeUcGbvJXqBUzxvd4Sj1Ce2t+/vdG6tHrju+I -aFvowdlxfv1k7/9nR4hYJS8+hge9+6jlgqispdNpQ80xiEmEU5LAsTkbOYMBMMTy -qfrQA71yN2BWHzZ8vTmR9W0Nv3vXkg== ------END CERTIFICATE-----`)) - - // emSign ECC Root CA - C3 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +-----END CERTIFICATE----- + +// emSign ECC Root CA - C3 +-----BEGIN CERTIFICATE----- MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQG EwJVUzETMBEGA1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMx IDAeBgNVBAMTF2VtU2lnbiBFQ0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAw @@ -1773,10 +1696,10 @@ BBYEFPtaSNCAIEDyqOkAB2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB Af8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQC02C8Cif22TGK6Q04ThHK1rt0c 3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwUZOR8loMRnLDRWmFLpg9J 0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // emSign ECC Root CA - G3 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// emSign ECC Root CA - G3 +-----BEGIN CERTIFICATE----- MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g @@ -1790,10 +1713,10 @@ zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD +JbNR6iC8hZVdyR+EhCVBCyj ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // emSign Root CA - C1 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// emSign Root CA - C1 +-----BEGIN CERTIFICATE----- MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkG A1UEBhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEg SW5jMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAw @@ -1813,10 +1736,10 @@ kqNPEjE2NuLe/gDEo2APJ62gsIq1NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrG YQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9wC68AivTxEDkigcxHpvOJpkT +xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQBmIMMMAVSKeo WXzhriKi4gp6D/piq1JM4fHfyr6DDUI= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // emSign Root CA - G1 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// emSign Root CA - G1 +-----BEGIN CERTIFICATE----- MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYD VQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBU ZWNobm9sb2dpZXMgTGltaXRlZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBH @@ -1837,102 +1760,10 @@ GNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q+Mri/Tm3R7nrft8EI6/6nAYH 6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeihU80Bv2noWgby RQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx iN66zB+Afko= ------END CERTIFICATE-----`)) - - // AffirmTrust Commercial - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE -BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz -dCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL -MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp -cm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC -AQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP -Hx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr -ba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL -MeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1 -yHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr -VwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/ -nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ -KoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG -XUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj -vbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt -Z8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g -N53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC -nlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= ------END CERTIFICATE-----`)) - - // AffirmTrust Networking - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UE -BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz -dCBOZXR3b3JraW5nMB4XDTEwMDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDEL -MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp -cm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC -AQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SEHi3y -YJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbua -kCNrmreIdIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRL -QESxG9fhwoXA3hA/Pe24/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp -6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gbh+0t+nvujArjqWaJGctB+d1ENmHP4ndG -yH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNVHQ4EFgQUBx/S55zawm6i -QLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ -KoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfO -tDIuUFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzu -QY0x2+c06lkh1QF612S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZ -Lgo/bNjR9eUJtGxUAArgFU2HdW23WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4u -olu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9/ZFvgrG+CJPbFEfxojfHRZ48 -x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= ------END CERTIFICATE-----`)) - - // AffirmTrust Premium - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UE -BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVz -dCBQcmVtaXVtMB4XDTEwMDEyOTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkG -A1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1U -cnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxBLf -qV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtnBKAQ -JG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ -+jjeRFcV5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrS -s8PhaJyJ+HoAVt70VZVs+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5 -HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmdGPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d7 -70O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5Rp9EixAqnOEhss/n/fauG -V+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NIS+LI+H+S -qHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S -5u046uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4Ia -C1nEWTJ3s7xgaVY5/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TX -OwF0lkLgAOIua+rF7nKsu7/+6qqo+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYE -FJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ -BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByvMiPIs0laUZx2 -KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg -Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B -8OWycvpEgjNC6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQ -MKSOyARiqcTtNd56l+0OOF6SL5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc -0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK+4w1IX2COPKpVJEZNZOUbWo6xbLQ -u4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmVBtWVyuEklut89pMF -u+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFgIxpH -YoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8 -GKa1qF60g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaO -RtGdFNrHF+QFlozEJLUbzxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6e -KeC2uAloGRwYQw== ------END CERTIFICATE-----`)) - - // AffirmTrust Premium ECC - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMC -VVMxFDASBgNVBAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQ -cmVtaXVtIEVDQzAeFw0xMDAxMjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJ -BgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1UcnVzdDEgMB4GA1UEAwwXQWZmaXJt -VHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNMF4bFZ0D -0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQN8O9 -ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0G -A1UdDgQWBBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4G -A1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/Vs -aobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6I -flc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ== ------END CERTIFICATE-----`)) - - // Atos TrustedRoot 2011 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +-----END CERTIFICATE----- + +// Atos TrustedRoot 2011 +-----BEGIN CERTIFICATE----- MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM @@ -1952,10 +1783,10 @@ DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Atos TrustedRoot Root CA ECC G2 2020 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Atos TrustedRoot Root CA ECC G2 2020 +-----BEGIN CERTIFICATE----- MIICMTCCAbagAwIBAgIMC3MoERh0MBzvbwiEMAoGCCqGSM49BAMDMEsxCzAJBgNV BAYTAkRFMQ0wCwYDVQQKDARBdG9zMS0wKwYDVQQDDCRBdG9zIFRydXN0ZWRSb290 IFJvb3QgQ0EgRUNDIEcyIDIwMjAwHhcNMjAxMjE1MDgzOTEwWhcNNDAxMjEwMDgz @@ -1968,10 +1799,10 @@ shufvlwfjP2ztvuzDgmHMB0GA1UdDgQWBBRbH8RxbLIbn75cH4z9s7b7sw4JhzAO BgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMDaQAwZgIxAOzgmf3d5FTByx/oPijX FVlKgspTMOzrNqW5yM6TR1bIYabhbZJTlY/241VT8N165wIxALCH1RuzYPyRjYDK ohtRSzhUy6oee9flRJUWLzxEeC4luuqQ5OxS7lfsA4TzXtsWDQ== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Atos TrustedRoot Root CA ECC TLS 2021 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Atos TrustedRoot Root CA ECC TLS 2021 +-----BEGIN CERTIFICATE----- MIICFTCCAZugAwIBAgIQPZg7pmY9kGP3fiZXOATvADAKBggqhkjOPQQDAzBMMS4w LAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgRUNDIFRMUyAyMDIxMQ0w CwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTI2MjNaFw00MTA0 @@ -1984,10 +1815,10 @@ KCXWfeBmmnoJsmo7jjPXNtNPojAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMD aAAwZQIwW5kp85wxtolrbNa9d+F851F+uDrNozZffPc8dz7kUK2o59JZDCaOMDtu CCrCp1rIAjEAmeMM56PDr9NJLkaCI2ZdyQAUEv049OGYa3cpetskz2VAv9LcjBHo 9H1/IISpQuQo ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Atos TrustedRoot Root CA RSA G2 2020 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Atos TrustedRoot Root CA RSA G2 2020 +-----BEGIN CERTIFICATE----- MIIFfzCCA2egAwIBAgIMR7opRlU+FpKXsKtAMA0GCSqGSIb3DQEBDAUAMEsxCzAJ BgNVBAYTAkRFMQ0wCwYDVQQKDARBdG9zMS0wKwYDVQQDDCRBdG9zIFRydXN0ZWRS b290IFJvb3QgQ0EgUlNBIEcyIDIwMjAwHhcNMjAxMjE1MDg0MTIzWhcNNDAxMjEw @@ -2018,10 +1849,10 @@ VD190nZ12V4+MkinjPKecgz4uFi4FyOlFId1WHoAgQciOWpMlKC1otunLMGw8aOb wEz3bXDqMZ/xrn0+cyjZod/6k/CbsPDizSUgde/ifTIFyZt27su9MR75lJhLJFhW SMDeBky9pjRd7RZhY3P7GeL6W9iXddRtnmA5XpSLAizrmc5gKm4bjKdLvP025pgf ZfJ/8eOPTIBGNli2oWXLzhxEdQ== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Atos TrustedRoot Root CA RSA TLS 2021 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Atos TrustedRoot Root CA RSA TLS 2021 +-----BEGIN CERTIFICATE----- MIIFZDCCA0ygAwIBAgIQU9XP5hmTC/srBRLYwiqipDANBgkqhkiG9w0BAQwFADBM MS4wLAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgUlNBIFRMUyAyMDIx MQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTIxMTBaFw00 @@ -2051,10 +1882,10 @@ AXcOPUIjJsyacmdRIXrMPIWo6iFqO9taPKU0nprALN+AnCng33eU0aKAQv9qTFsR o7Ey7Nmj1m+UI/87tyll5gfp77YZ6ufCOB0yiJA8EytuzO+rdwY0d4RPcuSBhPm5 dDTedk+SKlOxJTnbPP/lPqYO5Wue/9vsL3SD3460s6neFE3/MaNFcyT6lSnMEpcE oji2jbDwN/zIIX8/syQbPYtuzE2wFg2WHYMfRsCbvUOZ58SWLs5fyQ== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // GlobalSign - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// GlobalSign +-----BEGIN CERTIFICATE----- MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg MB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx @@ -2085,10 +1916,26 @@ pA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v JJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R 8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4 5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- + +// GlobalSign +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc +8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke +hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI +KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg +515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO +xwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE----- - // GlobalSign - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// GlobalSign +-----BEGIN CERTIFICATE----- MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 @@ -2108,26 +1955,10 @@ jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH WD9f ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // GlobalSign - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk -MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH -bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX -DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD -QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu -MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc -8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke -hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD -VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI -KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg -515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO -xwy8p2Fp8fc74SrL+SvzZpA3 ------END CERTIFICATE-----`)) - - // GlobalSign Root CA - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// GlobalSign Root CA +-----BEGIN CERTIFICATE----- MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw @@ -2147,10 +1978,10 @@ yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // GlobalSign Root E46 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// GlobalSign Root E46 +-----BEGIN CERTIFICATE----- MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYx CzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQD ExNHbG9iYWxTaWduIFJvb3QgRTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAw @@ -2162,10 +1993,10 @@ yXqBPCCjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud DgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ 7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZkvLtoURMMA/cVi4RguYv/Uo7njLwcAjA8 +RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // GlobalSign Root R46 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// GlobalSign Root R46 +-----BEGIN CERTIFICATE----- MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUA MEYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYD VQQDExNHbG9iYWxTaWduIFJvb3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMy @@ -2195,10 +2026,10 @@ u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP 4vkYxboznxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6 N3ec592kD3ZDZopD8p/7DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3 vouXsXgxT7PntgMTzlSdriVZzH81Xwj3QEUxeCp6 ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // GlobalSign Secure Mail Root E45 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// GlobalSign Secure Mail Root E45 +-----BEGIN CERTIFICATE----- MIICITCCAaegAwIBAgIQdlP+qicdlUZd1vGe5biQCjAKBggqhkjOPQQDAzBSMQsw CQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEoMCYGA1UEAxMf R2xvYmFsU2lnbiBTZWN1cmUgTWFpbCBSb290IEU0NTAeFw0yMDAzMTgwMDAwMDBa @@ -2211,10 +2042,10 @@ DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU3xNei1/CQAL9VreUTLYe1aaxFJYw CgYIKoZIzj0EAwMDaAAwZQIwE7C+13EgPuSrnM42En1fTB8qtWlFM1/TLVqy5IjH 3go2QjJ5naZruuH5RCp7isMSAjEAoGYcToedh8ntmUwbCu4tYMM3xx3NtXKw2cbv vPL/P/BS3QjnqmR5w+RpV5EvpMt8 ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // GlobalSign Secure Mail Root R45 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// GlobalSign Secure Mail Root R45 +-----BEGIN CERTIFICATE----- MIIFcDCCA1igAwIBAgIQdlP+qExQq5+NMrUdA49X3DANBgkqhkiG9w0BAQwFADBS MQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEoMCYGA1UE AxMfR2xvYmFsU2lnbiBTZWN1cmUgTWFpbCBSb290IFI0NTAeFw0yMDAzMTgwMDAw @@ -2245,10 +2076,10 @@ l+6NJllXu3jwelAwCbBgqp9O3Mk+HjrcYpMzsDpUdG8sMUXRaxEyamh29j32ahNe JJjn6h2az3iCB2D3TRDTgZpFjZ6vm9yAx0OylWikww7oCkcVv1Qz3AHn1aYec9h6 sr8vreNVMJ7fDkG84BH1oQyoIuHjAKNOcHyS4wTRekKKdZBZ45vRTKJkvXN5m2/y s8H2PA== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Go Daddy Class 2 Certification Authority - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Go Daddy Class 2 Certification Authority +-----BEGIN CERTIFICATE----- MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh MB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE YWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3 @@ -2271,10 +2102,10 @@ TMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER dEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf ReYNnyicsbkqWletNw+vHX/bvZ8= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Go Daddy Root Certificate Authority - G2 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Go Daddy Root Certificate Authority - G2 +-----BEGIN CERTIFICATE----- MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp @@ -2296,10 +2127,10 @@ gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo 2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI 4uJEvlz36hz1 ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Starfield Class 2 Certification Authority - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Starfield Class 2 Certification Authority +-----BEGIN CERTIFICATE----- MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl MCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp U3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw @@ -2322,10 +2153,10 @@ eruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Starfield Root Certificate Authority - G2 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Starfield Root Certificate Authority - G2 +-----BEGIN CERTIFICATE----- MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs @@ -2347,10 +2178,10 @@ sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // GlobalSign - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// GlobalSign +-----BEGIN CERTIFICATE----- MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYD VQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2Jh bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgw @@ -2361,10 +2192,10 @@ uYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNV HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/ +wpu+74zyTyjhNUwCgYIKoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147 bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // GTS Root R1 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// GTS Root R1 +-----BEGIN CERTIFICATE----- MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw @@ -2394,10 +2225,10 @@ Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT 0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm 2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // GTS Root R2 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// GTS Root R2 +-----BEGIN CERTIFICATE----- MIIFVzCCAz+gAwIBAgINAgPlrsWNBCUaqxElqjANBgkqhkiG9w0BAQwFADBHMQsw CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU MBIGA1UEAxMLR1RTIFJvb3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw @@ -2427,10 +2258,10 @@ TOlZ4iBgxcJlkoKM8xAfDoqXvneCbT+PHV28SSe9zE8P4c52hgQjxcCMElv924Sg JPFI/2R80L5cFtHvma3AH/vLrrw4IgYmZNralw4/KBVEqE8AyvCazM90arQ+POuV 7LXTWtiBmelDGDfrs7vRWGJB82bSj6p4lVQgw1oudCvV0b4YacCs1aTPObpRhANl 6WLAYv7YTVWW4tAR+kg0Eeye7QUd5MjWHYbL ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // GTS Root R3 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// GTS Root R3 +-----BEGIN CERTIFICATE----- MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYD VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG A1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw @@ -2442,10 +2273,10 @@ jOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL2 BBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7 VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azTL818+FsuVbu/3ZL3pAzcMeGiAjEA/Jdm ZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV11RZt+cRLInUue4X ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // GTS Root R4 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// GTS Root R4 +-----BEGIN CERTIFICATE----- MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw @@ -2457,10 +2288,10 @@ HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D 9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8 p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Hongkong Post Root CA 3 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Hongkong Post Root CA 3 +-----BEGIN CERTIFICATE----- MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL BQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ SG9uZyBLb25nMRYwFAYDVQQKEw1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25n @@ -2493,10 +2324,10 @@ JfTzPV4e0hz5sy229zdcxsshTrD3mUcYhcErulWuBurQB7Lcq9CClnXO0lD+mefP L5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa LJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG mpv0 ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // ACCVRAIZ1 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// ACCVRAIZ1 +-----BEGIN CERTIFICATE----- MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ @@ -2539,10 +2370,10 @@ I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7 ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // AC RAIZ FNMT-RCM - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// AC RAIZ FNMT-RCM +-----BEGIN CERTIFICATE----- MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsx CzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJ WiBGTk1ULVJDTTAeFw0wODEwMjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJ @@ -2573,10 +2404,10 @@ fZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7Ixjp 9kiABdcPUXmsEKvU7ANm5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wok RqEIr9baRRmW1FMdW4R58MD3R++Lj8UGrp1MYp3/RgT408m2ECVAdf4WqslKYIYv uu8wd+RU4riEmViAqhOLUTpPSPaLtrM= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // AC RAIZ FNMT-RCM SERVIDORES SEGUROS - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// AC RAIZ FNMT-RCM SERVIDORES SEGUROS +-----BEGIN CERTIFICATE----- MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQsw CQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgw FgYDVQRhDA9WQVRFUy1RMjgyNjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1S @@ -2591,10 +2422,10 @@ VR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqGSM49BAMDA2kAMGYCMQCu SuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoDzBOQn5IC MQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJy v+c= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Staat der Nederlanden Root CA - G3 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Staat der Nederlanden Root CA - G3 +-----BEGIN CERTIFICATE----- MIIFdDCCA1ygAwIBAgIEAJiiOTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJO TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFh dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQSAtIEczMB4XDTEzMTExNDExMjg0MloX @@ -2625,10 +2456,10 @@ fsbAddJu+2gw7OyLnflJNZoaLNmzlTnVHpL3prllL+U9bTpITAjc5CgSKL59NVzq 1VmiiXTTn74eS9fGbbeIJG9gkaSChVtWQbzQRKtqE77RLFi3EjNYsjdj3BP1lB0/ QFH1T/U67cjF68IeHRaVesd+QnGTbksVtzDfqu1XhUisHWrdOWnk4Xl4vs4Fv6EM 94B7IWcnMFk= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 +-----BEGIN CERTIFICATE----- MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIx GDAWBgNVBAcTD0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxp bXNlbCB2ZSBUZWtub2xvamlrIEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0w @@ -2653,10 +2484,10 @@ IPP54+M638yclNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4 lzwDGrpDxpa5RXI4s6ehlj2Re37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c 8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0jq5Rm+K37DwhuJi1/FwcJsoz7UMCf lo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // HARICA Client ECC Root CA 2021 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// HARICA Client ECC Root CA 2021 +-----BEGIN CERTIFICATE----- MIICWjCCAeGgAwIBAgIQMWjZ2OFiVx7SGUSI5hB98DAKBggqhkjOPQQDAzBvMQsw CQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2Vh cmNoIEluc3RpdHV0aW9ucyBDQTEnMCUGA1UEAwweSEFSSUNBIENsaWVudCBFQ0Mg @@ -2670,10 +2501,10 @@ AQH/BAUwAwEB/zAdBgNVHQ4EFgQUUgjSvjKBJf31GpfsTl8au1PNkK0wDgYDVR0P AQH/BAQDAgGGMAoGCCqGSM49BAMDA2cAMGQCMEwxRUZPqOa+w3eyGhhLLYh7WOar lGtEA7AX/9+Cc0RRLP2THQZ7FNKJ7EAM7yEBLgIwL8kuWmwsHdmV4J6wuVxSfPb4 OMou8dQd8qJJopX4wVheT/5zCu8xsKsjWBOMi947 ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // HARICA Client RSA Root CA 2021 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// HARICA Client RSA Root CA 2021 +-----BEGIN CERTIFICATE----- MIIFqjCCA5KgAwIBAgIQVVL4HtsbJCyeu5YYzQIoPjANBgkqhkiG9w0BAQsFADBv MQswCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl c2VhcmNoIEluc3RpdHV0aW9ucyBDQTEnMCUGA1UEAwweSEFSSUNBIENsaWVudCBS @@ -2705,10 +2536,10 @@ ndSV9ZmqRuqurL/0FBkk6Izs4/W8BmiKKgwFXwqXdafcfsD913oY3zDROEsfsJhw v8x8c/BuxDGlpJcdrL/ObCFKvicjZ/MGVoEKkY624QMFMyzaNAhNTlAjrR+lxdR6 /uoJ7KcoYItGfLXqm91P+edrFcaIz0Pb5SfcBFZub0YV8VYt6FwMc8MjgTggy8kM ac8sqzuEYDMZUv1pFDM= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // HARICA TLS ECC Root CA 2021 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// HARICA TLS ECC Root CA 2021 +-----BEGIN CERTIFICATE----- MIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQsw CQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2Vh cmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9v @@ -2722,10 +2553,10 @@ AwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQD AgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAircJRQO9gcS3ujwLEXQNw SaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/QwCZ61IygN nxS2PFOiTAZpffpskcYqSUXm7LcT4Tps ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // HARICA TLS RSA Root CA 2021 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// HARICA TLS RSA Root CA 2021 +-----BEGIN CERTIFICATE----- MIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBs MQswCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl c2VhcmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0Eg @@ -2757,10 +2588,10 @@ BGIBnfHAT+7hOtSLIBD6Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79 aPib8qXPMThcFarmlwDB31qlpzmq6YR/PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YW xw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnnkf3/W9b3raYvAwtt41dU 63ZTGI0RmLo= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Hellenic Academic and Research Institutions ECC RootCA 2015 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Hellenic Academic and Research Institutions ECC RootCA 2015 +-----BEGIN CERTIFICATE----- MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzAN BgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl c2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hl @@ -2776,10 +2607,10 @@ MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLQi C4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaep lSTAGiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7Sof TUwJCA3sS61kFyjndc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Hellenic Academic and Research Institutions RootCA 2015 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Hellenic Academic and Research Institutions RootCA 2015 +-----BEGIN CERTIFICATE----- MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1Ix DzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5k IFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMT @@ -2813,10 +2644,10 @@ bzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVtJ94Cj8rDtSvK6evIIVM4 pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGaJI7ZjnHK e7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0 vm9qp/UsQu0yrbYhnr68 ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // IdenTrust Commercial Root CA 1 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// IdenTrust Commercial Root CA 1 +-----BEGIN CERTIFICATE----- MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw @@ -2846,10 +2677,10 @@ l1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG 4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ mUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A 7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // IdenTrust Public Sector Root CA 1 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// IdenTrust Public Sector Root CA 1 +-----BEGIN CERTIFICATE----- MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBN MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVu VHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcN @@ -2879,10 +2710,10 @@ WN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4Mhn5 tshquDDIajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhA GaQdp/lLQzfcaFpPz+vCZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv 8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // ISRG Root X1 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// ISRG Root X1 +-----BEGIN CERTIFICATE----- MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 @@ -2912,10 +2743,10 @@ oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq 4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // ISRG Root X2 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// ISRG Root X2 +-----BEGIN CERTIFICATE----- MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00 @@ -2928,10 +2759,10 @@ AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1 /q4AaOeMSQ+2b1tbFfLn ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Izenpe.com - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Izenpe.com +-----BEGIN CERTIFICATE----- MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4 MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6 ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD @@ -2964,10 +2795,10 @@ UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/ QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+ naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // SZAFIR ROOT CA2 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// SZAFIR ROOT CA2 +-----BEGIN CERTIFICATE----- MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQEL BQAwUTELMAkGA1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6 ZW5pb3dhIFMuQS4xGDAWBgNVBAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkw @@ -2987,10 +2818,10 @@ nXkZ7/e7DDWQw4rtTw/1zBLZpD67oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCP oky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul4+vJhaAlIDf7js4MNIThPIGy d05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6+/NNIxuZMzSg LvWpCz/UXeHPhJ/iGcJfitYgHuNztw== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // LAWtrust Root CA2 (4096) - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// LAWtrust Root CA2 (4096) +-----BEGIN CERTIFICATE----- MIIFmDCCA4CgAwIBAgIEVRpusTANBgkqhkiG9w0BAQsFADBDMQswCQYDVQQGEwJa QTERMA8GA1UEChMITEFXdHJ1c3QxITAfBgNVBAMTGExBV3RydXN0IFJvb3QgQ0Ey ICg0MDk2KTAgFw0yMzAyMTQwOTE5MzhaGA8yMDUzMDIxNDA5NDkzOFowQzELMAkG @@ -3021,10 +2852,10 @@ NAdigkz0fseHdA6wIR4JIIDBsxU9Rm3T8QaSP++glYocbncxtut4KQx77oKlT36k KV6eqi34jsDz/A0GhZtO3PfiCXzQFFEeerMjr/rRYSpltQHZuOMHyiR20vBKvu+G BIBCFXARaH7Xx7v+506bnJWlHEqkydAJjKrOSNIekpfXEentZsw33PXXG3SbpupC rF0y4Fj0gUf/0hLifhzcSXaWwx2fS8pcKjdbPYrROJsh2uO/RUPT4Fh3Hyg= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // e-Szigno Root CA 2017 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// e-Szigno Root CA 2017 +-----BEGIN CERTIFICATE----- MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNV BAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRk LjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJv @@ -3038,10 +2869,30 @@ A1UdDgQWBBSHERUI0arBeAyxr87GyZDvvzAEwDAfBgNVHSMEGDAWgBSHERUI0arB eAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEAtVfd14pVCzbhhkT61Nlo jbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxOsvxyqltZ +efcMQ== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Microsec e-Szigno Root CA 2009 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// e-Szigno TLS Root CA 2023 +-----BEGIN CERTIFICATE----- +MIICzzCCAjGgAwIBAgINAOhvGHvWOWuYSkmYCjAKBggqhkjOPQQDBDB1MQswCQYD +VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 +ZC4xFzAVBgNVBGEMDlZBVEhVLTIzNTg0NDk3MSIwIAYDVQQDDBllLVN6aWdubyBU +TFMgUm9vdCBDQSAyMDIzMB4XDTIzMDcxNzE0MDAwMFoXDTM4MDcxNzE0MDAwMFow +dTELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRYwFAYDVQQKDA1NaWNy +b3NlYyBMdGQuMRcwFQYDVQRhDA5WQVRIVS0yMzU4NDQ5NzEiMCAGA1UEAwwZZS1T +emlnbm8gVExTIFJvb3QgQ0EgMjAyMzCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAE +AGgP36J8PKp0iGEKjcJMpQEiFNT3YHdCnAo4YKGMZz6zY+n6kbCLS+Y53wLCMAFS +AL/fjO1ZrTJlqwlZULUZwmgcAOAFX9pQJhzDrAQixTpN7+lXWDajwRlTEArRzT/v +SzUaQ49CE0y5LBqcvjC2xN7cS53kpDzLLtmt3999Cd8ukv+ho2MwYTAPBgNVHRMB +Af8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUWYQCYlpGePVd3I8K +ECgj3NXW+0UwHwYDVR0jBBgwFoAUWYQCYlpGePVd3I8KECgj3NXW+0UwCgYIKoZI +zj0EAwQDgYsAMIGHAkIBLdqu9S54tma4n7Zwf2Z0z+yOfP7AAXmazlIC58PRDHpt +y7Ve7hekm9sEdu4pKeiv+62sUvTXK9Z3hBC9xdIoaDQCQTV2WnXzkoYI9bIeCvZl +C9p2x1L/Cx6AcCIwwzPbGO2E14vs7dOoY4G1VnxHx1YwlGhza9IuqbnZLBwpvQy6 +uWWL +-----END CERTIFICATE----- + +// Microsec e-Szigno Root CA 2009 +-----BEGIN CERTIFICATE----- MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G @@ -3064,10 +2915,10 @@ ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c 2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Microsoft ECC Root Certificate Authority 2017 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Microsoft ECC Root Certificate Authority 2017 +-----BEGIN CERTIFICATE----- MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQsw CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYD VQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw @@ -3081,10 +2932,10 @@ BTADAQH/MB0GA1UdDgQWBBTIy5lycFIM+Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3 FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlfXu5gKcs68tvWMoQZP3zV L8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaReNtUjGUB iudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Microsoft RSA Root Certificate Authority 2017 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Microsoft RSA Root Certificate Authority 2017 +-----BEGIN CERTIFICATE----- MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBl MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw NAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 @@ -3116,10 +2967,10 @@ GWSZI1b7rCoucL5mxAyE7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9 dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKTc0QWbej09+CVgI+WXTik9KveCjCHk9hN AHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB RA+GsCyRxj3qrg+E ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // NAVER Global Root Certification Authority - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// NAVER Global Root Certification Authority +-----BEGIN CERTIFICATE----- MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEM BQAwaTELMAkGA1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRG T1JNIENvcnAuMTIwMAYDVQQDDClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0 @@ -3151,10 +3002,10 @@ LBT/DShycpWbXgnbiUSYqqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX 5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oGI/hGoiLtk/bdmuYqh7GYVPEi92tF4+KO dh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmgkpzNNIaRkPpkUZ3+/uul 9XXeifdy ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // NetLock Arany (Class Gold) Főtanúsítvány - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// NetLock Arany (Class Gold) Főtanúsítvány +-----BEGIN CERTIFICATE----- MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3 MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl @@ -3177,10 +3028,10 @@ bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // OISTE Client Root ECC G1 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// OISTE Client Root ECC G1 +-----BEGIN CERTIFICATE----- MIICNDCCAbqgAwIBAgIQVOyX1ou0xAshbg6y0FPIejAKBggqhkjOPQQDAzBLMQsw CQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UEAwwY T0lTVEUgQ2xpZW50IFJvb3QgRUNDIEcxMB4XDTIzMDUzMTE0MzE0MFoXDTQ4MDUy @@ -3193,10 +3044,10 @@ Vzs5sS0AjCFmjJVpnG117Iw/+jAdBgNVHQ4EFgQUmVc7ObEtAIwhZoyVaZxtdeyM P/owDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2gAMGUCMQCW/+SCThYiW6CF GDw9Oo8gBggl5/WRNhmte7TfW2YSN3Nw7c0FKAdeCM4NQl8ZkQICMGdJh64GQR0g 0zGmqiY38SeKYQ3+mgZDpy6eJkejMhiL6F5QBfGwekh23tuhYkq6dw== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // OISTE Client Root RSA G1 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// OISTE Client Root RSA G1 +-----BEGIN CERTIFICATE----- MIIFgzCCA2ugAwIBAgIQNBdvWQGIG6ql3chIu7Q7czANBgkqhkiG9w0BAQwFADBL MQswCQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UE AwwYT0lTVEUgQ2xpZW50IFJvb3QgUlNBIEcxMB4XDTIzMDUzMTE0MjMyOVoXDTQ4 @@ -3227,10 +3078,10 @@ keWTDnWRrqMjIElk1Lo5lldw7lU0KHzWr8qpnubJAckHwdBEsYC0UVCqj/ac5Wdz ao6jC2rW/ZMMimHLvSjxX3H9uDM51krx9rJoUj5lj0OdgSQk9ihMNaf9MwqleMEE H+xJasSu1UQWpqeNf9ohlj6ouhZn1Kmh58Ka+BDZO5ruaPYvAO7Lu2aNIjiG9L9f eKnIoB1au3VQ+VILDx0CLBQa84dqd/M= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // OISTE Server Root ECC G1 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// OISTE Server Root ECC G1 +-----BEGIN CERTIFICATE----- MIICNTCCAbqgAwIBAgIQI/nD1jWvjyhLH/BU6n6XnTAKBggqhkjOPQQDAzBLMQsw CQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UEAwwY T0lTVEUgU2VydmVyIFJvb3QgRUNDIEcxMB4XDTIzMDUzMTE0NDIyOFoXDTQ4MDUy @@ -3243,10 +3094,10 @@ TYhlz/w9itWj8UnATgwQb0K0nDAdBgNVHQ4EFgQUN02IZc/8PYrVo/FJwE4MEG9C tJwwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2kAMGYCMQCpKjAd0MKfkFFR QD6VVCHNFmb3U2wIFjnQEnx/Yxvf4zgAOdktUyBFCxxgZzFDJe0CMQCSia7pXGKD YmH5LVerVrkR3SW+ak5KGoJr3M/TvEqzPNcum9v4KGm8ay3sMaE641c= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // OISTE Server Root RSA G1 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// OISTE Server Root RSA G1 +-----BEGIN CERTIFICATE----- MIIFgzCCA2ugAwIBAgIQVaXZZ5Qoxu0M+ifdWwFNGDANBgkqhkiG9w0BAQwFADBL MQswCQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UE AwwYT0lTVEUgU2VydmVyIFJvb3QgUlNBIEcxMB4XDTIzMDUzMTE0MzcxNloXDTQ4 @@ -3277,10 +3128,10 @@ YWSGRqbXVMW215AWRqWFyp464+YTFrYVI8ypKVL9AMb2kI5Wj4kI3Zaq5tNqqYY1 J8tRd/iWkx7P8nd9H0aTolkelUTFLXVksNb54Dxp6gS1HAviRkRNQzuXSXERvSS2 wq1yVAb+axj5d9spLFKebXd7Yv0PTY6YMjAwcRLWJTXjn/hvnLXrahut6hDTlhZy BiElxky8j3C7DOReIoMt0r7+hVu05L0= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // OISTE WISeKey Global Root GA CA - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// OISTE WISeKey Global Root GA CA +-----BEGIN CERTIFICATE----- MIID8TCCAtmgAwIBAgIQQT1yx/RrH4FDffHSKFTfmjANBgkqhkiG9w0BAQUFADCB ijELMAkGA1UEBhMCQ0gxEDAOBgNVBAoTB1dJU2VLZXkxGzAZBgNVBAsTEkNvcHly aWdodCAoYykgMjAwNTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNl @@ -3303,10 +3154,10 @@ vPtNsCBtQ7tgMHpnM1zFmdH4LTlSc/uMqpclXHLZCB6rTjzjgTGfA6b7wP4piFXa hNVQA7bihKOmNqoROgHhGEvWRGizPflTdISzRpFGlgC3gCy24eMQ4tui5yiPAZZi Fj4A4xylNoEYokxSdsARo27mHbrjWr42U8U+dY+GaSlYU7Wcu2+fXMUY7N0v4ZjJ /L7fCg0= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // OISTE WISeKey Global Root GB CA - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// OISTE WISeKey Global Root GB CA +-----BEGIN CERTIFICATE----- MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg Rm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9i @@ -3327,10 +3178,10 @@ gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0VQreUGdNZtGn//3ZwLWoo4rO ZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEuiHZeeevJuQHHf aPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // OISTE WISeKey Global Root GC CA - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// OISTE WISeKey Global Root GC CA +-----BEGIN CERTIFICATE----- MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQsw CQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91 bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwg @@ -3344,10 +3195,10 @@ BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7T rYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0EAwMDaAAwZQIwJsdpW9zV 57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtkAjEA2zQg Mgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9 ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Security Communication ECC RootCA1 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Security Communication ECC RootCA1 +-----BEGIN CERTIFICATE----- MIICODCCAb6gAwIBAgIJANZdm7N4gS7rMAoGCCqGSM49BAMDMGExCzAJBgNVBAYT AkpQMSUwIwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMSswKQYD VQQDEyJTZWN1cml0eSBDb21tdW5pY2F0aW9uIEVDQyBSb290Q0ExMB4XDTE2MDYx @@ -3360,10 +3211,10 @@ ULGjQjBAMB0GA1UdDgQWBBSGHOf+LaVKiwj+KBH6vqNm+GBZLzAOBgNVHQ8BAf8E BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjAVXUI9/Lbu 9zuxNuie9sRGKEkz0FhDKmMpzE2xtHqiuQ04pV1IKv3LsnNdo4gIxwwCMQDAqy0O be0YottT6SXbVQjgUMzfRGEWgqtJsLKB7HOHeLRMsmIbEvoWTSVLY70eN9k= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Security Communication RootCA2 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Security Communication RootCA2 +-----BEGIN CERTIFICATE----- MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX @@ -3383,10 +3234,10 @@ okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8 t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy 1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/ SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // AAA Certificate Services - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// AAA Certificate Services +-----BEGIN CERTIFICATE----- MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj @@ -3410,10 +3261,10 @@ Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2 G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // COMODO Certification Authority - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// COMODO Certification Authority +-----BEGIN CERTIFICATE----- MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV @@ -3437,10 +3288,10 @@ RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4 zJVSk/BwJVmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5dd BA6+C4OmF4O5MBKgxTMVBbkN+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IB ZQ== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // COMODO ECC Certification Authority - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// COMODO ECC Certification Authority +-----BEGIN CERTIFICATE----- MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT @@ -3455,10 +3306,10 @@ BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // COMODO RSA Certification Authority - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// COMODO RSA Certification Authority +-----BEGIN CERTIFICATE----- MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV @@ -3491,10 +3342,10 @@ S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl 0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB NVOFBkpdn627G190 ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Entrust Root Certification Authority - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Entrust Root Certification Authority +-----BEGIN CERTIFICATE----- MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0 Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW @@ -3520,10 +3371,10 @@ AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP 9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/ eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m 0vdXcDazv/wor3ElhVsT/h5/WrQ8 ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Entrust Root Certification Authority - EC1 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Entrust Root Certification Authority - EC1 +-----BEGIN CERTIFICATE----- MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkG A1UEBhMCVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3 d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVu @@ -3540,10 +3391,10 @@ Bz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O BBYEFLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVC R98crlOZF7ZvHH3hvxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nX hTcGtXsI/esni0qU+eH6p44mCOh8kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Entrust Root Certification Authority - G2 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Entrust Root Certification Authority - G2 +-----BEGIN CERTIFICATE----- MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50 cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs @@ -3567,10 +3418,10 @@ Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v 1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R nAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH VHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Entrust Root Certification Authority - G4 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Entrust Root Certification Authority - G4 +-----BEGIN CERTIFICATE----- MIIGSzCCBDOgAwIBAgIRANm1Q3+vqTkPAAAAAFVlrVgwDQYJKoZIhvcNAQELBQAw gb4xCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQL Ex9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykg @@ -3605,10 +3456,10 @@ b7DUUH8i119lAg2m9IUe2K4GS0qn0jFmwvjO5QimpAKWRGhXxNUzzxkvFMSUHHuk IQ6SwJAfzyBfyjs4x7dtOvPmRLgOMWuIjnDrnBdSqEGULoe256YSxXXfW8AKbnuk 5F6G+TaU33fD6Q3AOfF5u0aOq0NZJ7cguyPpVkAh7DE9ZapD8j3fcEThuk0mEDuY n/PIjhs4ViFqUZPTkcpG2om3PVODLAgfi49T3f+sHw== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Entrust.net Certification Authority (2048) - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Entrust.net Certification Authority (2048) +-----BEGIN CERTIFICATE----- MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChML RW50cnVzdC5uZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBp bmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5 @@ -3632,10 +3483,10 @@ zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5b u/8j72gZyxKTJ1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+ bYQLCIt+jerXmCHG8+c8eS9enNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/Er fF6adulZkMV8gzURZVE= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Sectigo Public Email Protection Root E46 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Sectigo Public Email Protection Root E46 +-----BEGIN CERTIFICATE----- MIICMTCCAbegAwIBAgIQbvXTp0GOoFlApzBr0kBlVjAKBggqhkjOPQQDAzBaMQsw CQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTEwLwYDVQQDEyhT ZWN0aWdvIFB1YmxpYyBFbWFpbCBQcm90ZWN0aW9uIFJvb3QgRTQ2MB4XDTIxMDMy @@ -3648,10 +3499,10 @@ HQYDVR0OBBYEFC1OjKfCI7JXqQZrPmsrifPDXkfOMA4GA1UdDwEB/wQEAwIBhjAP BgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQCSnRpZY0VYjhsW5H16 bDZIMB8rcueQMzT9JKLGBoxvOzJXWvj+xkkSU5rZELKZUXICMAUlKjMh/JPmIqLM cFUoNVaiB8QhhCMaTEyZUJmSFMtK3Fb79dOPaiz1cTr4izsDng== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Sectigo Public Email Protection Root R46 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Sectigo Public Email Protection Root R46 +-----BEGIN CERTIFICATE----- MIIFgDCCA2igAwIBAgIQHUSeuQ2DkXSu3fLriLemozANBgkqhkiG9w0BAQwFADBa MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTEwLwYDVQQD EyhTZWN0aWdvIFB1YmxpYyBFbWFpbCBQcm90ZWN0aW9uIFJvb3QgUjQ2MB4XDTIx @@ -3682,10 +3533,10 @@ IS+0AlmzLdBykLoLFHR4S8/BX1VyjlQrE876WAzTuyzZqZFh+PjxtnvevKnMkgTM S2tfc4C2Ie1QT9d2h27O39K3vWKhfVhiaEVStj/eEtvtBGmedoiqAW3ahsdgG8NS rDfsUHGAciohRQpTRzwZ643SWQTeJbDrHzVvYH3Xtca7CyeN4E1U5c8dJgFuOzXI IBKJg/DS7Vg7NJ27MfUy/THzVho= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Sectigo Public Server Authentication Root E46 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Sectigo Public Server Authentication Root E46 +-----BEGIN CERTIFICATE----- MIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQsw CQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T ZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcN @@ -3698,10 +3549,10 @@ WvkEN/U0NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+ Af8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNnADBkAjAn7qRa qCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RHlAFWovgzJQxC36oCMB3q 4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21USAGKcw== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Sectigo Public Server Authentication Root R46 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Sectigo Public Server Authentication Root R46 +-----BEGIN CERTIFICATE----- MIIFijCCA3KgAwIBAgIQdY39i658BwD6qSWn4cetFDANBgkqhkiG9w0BAQwFADBf MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQD Ey1TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYw @@ -3732,10 +3583,10 @@ dHn5HrwdVw1Hr8Mvn4dGp+smWg9WY7ViYG4A++MnESLn/pmPNPW56MORcr3Ywx65 LvKRRFHQV80MNNVIIb/bE/FmJUNS0nAiNs2fxBx1IK1jcmMGDw4nztJqDby1ORrp 0XZ60Vzk50lJLVU3aPAaOpg+VBeHVOmmJ1CJeyAvP/+/oYtKR5j/K3tJPsMpRmAY QqszKbrAKbkTidOIijlBO8n9pu0f9GBj39ItVQGL ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // USERTrust ECC Certification Authority - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// USERTrust ECC Certification Authority +-----BEGIN CERTIFICATE----- MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT @@ -3750,10 +3601,10 @@ A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // USERTrust RSA Certification Authority - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// USERTrust RSA Certification Authority +-----BEGIN CERTIFICATE----- MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV @@ -3786,10 +3637,10 @@ qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG jjxDah2nGN59PRbxYvnKkKj9 ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // SSL.com Client ECC Root CA 2022 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// SSL.com Client ECC Root CA 2022 +-----BEGIN CERTIFICATE----- MIICQDCCAcagAwIBAgIQdvhIHq7wPHAf4D8lVAGD1TAKBggqhkjOPQQDAzBRMQsw CQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSgwJgYDVQQDDB9T U0wuY29tIENsaWVudCBFQ0MgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzAzMloX @@ -3803,10 +3654,10 @@ U81SGi9dYKDDXfuyHBwwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2gAMGUC ME0HES0R+7kmwyHdcuEX/MHPFOpJznGHjtZT3BHNXVSKr9kt9IxR6rxmR+J/lYNg ZQIxAIwhTE+75bBQ35BiSebMkdv4P11xkQiOT5LJf6Zc6hN+7W3E6MMqb1wR4aXz alqaTQ== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // SSL.com Client RSA Root CA 2022 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// SSL.com Client RSA Root CA 2022 +-----BEGIN CERTIFICATE----- MIIFjzCCA3egAwIBAgIQdq/uiJMVRbZQU5uAnKTfmjANBgkqhkiG9w0BAQsFADBR MQswCQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSgwJgYDVQQD DB9TU0wuY29tIENsaWVudCBSU0EgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzEw @@ -3837,10 +3688,10 @@ miZPPiDPT0PzEIx/EGF6NsqqC33Mn0dEWa6llcaZU+MHaz1JELAY/10OhUMUS+dr OdmsupJ13UPKugGVrRqBKzrw48UvDBhNEMauwO3+BVJ/GQXLqa81CAw4IuT+VuVT Pr/k1rPZCMM91TMygSTFqeFlEbgyMzBxGEkdGkXGmhSKWDkobvPLUblJJmR4A8eR EYOpuZA0tm+qBZ6FKFeZvn8nBkliTaH8CeErRglMFJtWj0U= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // SSL.com EV Root Certification Authority ECC - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// SSL.com EV Root Certification Authority ECC +-----BEGIN CERTIFICATE----- MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T U0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp @@ -3855,10 +3706,10 @@ MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX 5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ ytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg h5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // SSL.com EV Root Certification Authority RSA R2 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// SSL.com EV Root Certification Authority RSA R2 +-----BEGIN CERTIFICATE----- MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE CgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2Vy @@ -3891,10 +3742,10 @@ QbNmC1r7fSOl8hqw/96bg5Qu0T/fkreRrwU7ZcegbLHNYhLDkBvjJc40vG93drEQ w/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi S9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07 mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // SSL.com Root Certification Authority ECC - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// SSL.com Root Certification Authority ECC +-----BEGIN CERTIFICATE----- MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T U0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0 @@ -3909,10 +3760,10 @@ EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD VR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T kdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+ gA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // SSL.com Root Certification Authority RSA - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// SSL.com Root Certification Authority RSA +-----BEGIN CERTIFICATE----- MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE BhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK DA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp @@ -3945,10 +3796,10 @@ K9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu nLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf oYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY Ic2wBlX7Jz9TkHCpBB5XJ7k= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // SSL.com TLS ECC Root CA 2022 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// SSL.com TLS ECC Root CA 2022 +-----BEGIN CERTIFICATE----- MIICOjCCAcCgAwIBAgIQFAP1q/s3ixdAW+JDsqXRxDAKBggqhkjOPQQDAzBOMQsw CQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxT U0wuY29tIFRMUyBFQ0MgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzM0OFoXDTQ2 @@ -3961,10 +3812,10 @@ GDAWgBSJjy+j6CugFFR781a4Jl9nOAuc0DAdBgNVHQ4EFgQUiY8vo+groBRUe/NW uCZfZzgLnNAwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2gAMGUCMFXjIlbp 15IkWE8elDIPDAI2wv2sdDJO4fscgIijzPvX6yv/N33w7deedWo1dlJF4AIxAMeN b0Igj762TVntd00pxCAgRWSGOlDGxK0tk/UYfXLtqc/ErFc2KAhl3zx5Zn6g6g== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // SSL.com TLS RSA Root CA 2022 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// SSL.com TLS RSA Root CA 2022 +-----BEGIN CERTIFICATE----- MIIFiTCCA3GgAwIBAgIQb77arXO9CEDii02+1PdbkTANBgkqhkiG9w0BAQsFADBO MQswCQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQD DBxTU0wuY29tIFRMUyBSU0EgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzQyMloX @@ -3995,10 +3846,10 @@ AgDHbICivRZQIA9ygV/MlP+7mea6kMvq+cYMwq7FGc4zoWtcu358NFcXrfA/rs3q r5nsLFR+jM4uElZI7xc7P0peYNLcdDa8pUNjyw9bowJWCZ4kLOGGgYz+qxcs+sji Mho6/4UIyYOf8kpIEFR3N+2ivEC+5BB09+Rbu7nzifmPQdjH5FCQNYA+HLhNkNPU 98OwoX6EyneSMSy4kLGCenROmxMmtNVQZlR4rmA= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // SwissSign Gold CA - G2 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// SwissSign Gold CA - G2 +-----BEGIN CERTIFICATE----- MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV BAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2ln biBHb2xkIENBIC0gRzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBF @@ -4030,10 +3881,10 @@ hdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3 Ld6leNcG2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+w ZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+htt Qc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // SwissSign RSA SMIME Root CA 2022 - 1 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// SwissSign RSA SMIME Root CA 2022 - 1 +-----BEGIN CERTIFICATE----- MIIFlzCCA3+gAwIBAgIURg7UAXGQoBqDLEpCECgV0mEbrTIwDQYJKoZIhvcNAQEL BQAwUzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEtMCsGA1UE AxMkU3dpc3NTaWduIFJTQSBTTUlNRSBSb290IENBIDIwMjIgLSAxMB4XDTIyMDYw @@ -4064,10 +3915,10 @@ PJPEP3ZOfmKiKcsWMN4saa+Rp+JX5TNMv9iOB6J/oTVGaUqoICn/694glVmxrk0w a9iatAMfwjjkINUO1howTGicjODtoQ+OQl3rgCoSeaYXF7SVKo40kae90ayoGkMh i97v4KxGJWUKxiuhmz4i6Bg4tSb2LMoIIN4w0a1U/dxIFZ/Np0HXNziFME8SiEM0 g9cqTdQAV1zlyvDd4ZIoKxh1vUekQhPpVlqNSl7ODnU1gHMZDywpi7uVuA== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // SwissSign RSA TLS Root CA 2022 - 1 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// SwissSign RSA TLS Root CA 2022 - 1 +-----BEGIN CERTIFICATE----- MIIFkzCCA3ugAwIBAgIUQ/oMX04bgBhE79G0TzUfRPSA7cswDQYJKoZIhvcNAQEL BQAwUTELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzErMCkGA1UE AxMiU3dpc3NTaWduIFJTQSBUTFMgUm9vdCBDQSAyMDIyIC0gMTAeFw0yMjA2MDgx @@ -4098,10 +3949,10 @@ zv1/THfQwUY1mFwjmwJFQ5Ra3bxHrSL+ul4vkSkphnsh3m5kt8sNjzdbowhq6/Td Ao9QAwKxuDdollDruF/UKIqlIgyKhPBZLtU30WHlQnNYKoH3dtvi4k0NX/a3vgW0 rk4N3hY9A4GzJl5LuEsAz/+MF7psYC0nhzck5npgL7XTgwSqT0N1osGDsieYK7EO gLrAhV5Cud+xYJHT6xh+cHiudoO+cVrQkOPKwRYlZ0rwtnu64ZzZ ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // TWCA CYBER Root CA - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// TWCA CYBER Root CA +-----BEGIN CERTIFICATE----- MIIFjTCCA3WgAwIBAgIQQAE0jMIAAAAAAAAAATzyxjANBgkqhkiG9w0BAQwFADBQ MQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290 IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3QgQ0EwHhcNMjIxMTIyMDY1NDI5 @@ -4132,10 +3983,10 @@ Qh0UqEL20kCGoE8jypZFVmAGzbdVAaYBlGX+bgUJurSkquLvWL69J1bY73NxW0Qz NxKfKjLji7gh7MMrZQzvIt6IKTtM1/r+t+FHvpw+PoP7UV31aPcuIYXcv/Fa4nzX xeSDwWrruoBa3lwtcHb4yOWHh8qgnaHlIhInD0Q9HWzq1MKLL295q39QpsQZp6F6 t5b5wR9iWqJDB0BeJsas7a5wFsWqynKKTbDPAYsDP27X ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // TWCA Global Root CA - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// TWCA Global Root CA +-----BEGIN CERTIFICATE----- MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 @@ -4165,10 +4016,10 @@ nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy KwbQBM0= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // TWCA Global Root CA G2 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// TWCA Global Root CA G2 +-----BEGIN CERTIFICATE----- MIIFlTCCA32gAwIBAgIQQAE0jMIAAAAAAAAAAZdY9DANBgkqhkiG9w0BAQwFADBU MQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290 IENBMR8wHQYDVQQDExZUV0NBIEdsb2JhbCBSb290IENBIEcyMB4XDTIyMTEyMjA2 @@ -4199,10 +4050,10 @@ UamsVsAhTqMUdAU6vOO/bT1OP16lpG0pv4RRdVOOhhr1UXAqDRxOQOH9o+OlK2eQ ksdsroW/OpsXFcqcKpPUTTkNvCAIo42IbAkNjK5EIU3JcezYJtcXni0RGDyjIn24 J1S/aMg7QsyPXk7n3MLF+mpED41WiHrfiYRsoLM+PfFlAAmI6irrQM6zXawyF67B m+nQwfVJlN2nznxaB+uuIJwXMJJpk3Lzmltxm/5q33owaY6zLtsPLN0= ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // TWCA Root Certification Authority - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// TWCA Root Certification Authority +-----BEGIN CERTIFICATE----- MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES MBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU V0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz @@ -4222,10 +4073,10 @@ XebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D lhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn aspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ YiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // Telia Root CA v2 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// Telia Root CA v2 +-----BEGIN CERTIFICATE----- MIIFdDCCA1ygAwIBAgIPAWdfJ9b+euPkrL4JWwWeMA0GCSqGSIb3DQEBCwUAMEQx CzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UE AwwQVGVsaWEgUm9vdCBDQSB2MjAeFw0xODExMjkxMTU1NTRaFw00MzExMjkxMTU1 @@ -4256,42 +4107,10 @@ Ty3EHD70sz+rFQ47GUGKpMFXEmZxTPpT41frYpUJnlTd0cI8Vzy9OK2YZLe4A5pT VmBds9hCG1xLEooc6+t9xnppxyd/pPiL8uSUZodL6ZQHCRJ5irLrdATczvREWeAW ysUsWNc8e89ihmpQfTU2Zqf7N+cox9jQraVplI/owd8k+BsHMYeB2F326CjYSlKA rBPuUBQemMc= ------END CERTIFICATE-----`)) - - // TeliaSonera Root CA v1 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAw -NzEUMBIGA1UECgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJv -b3QgQ0EgdjEwHhcNMDcxMDE4MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYD -VQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwWVGVsaWFTb25lcmEgUm9vdCBDQSB2 -MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+6yfwIaPzaSZVfp3F -VRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA3GV1 -7CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+X -Z75Ljo1kB1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+ -/jXh7VB7qTCNGdMJjmhnXb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs -81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxHoLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkm -dtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3F0fUTPHSiXk+TT2YqGHe -Oh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJoWjiUIMu -sDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4 -pgd7gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fs -slESl1MpWtTwEhDcTwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQ -arMCpgKIv7NHfirZ1fpoeDVNAgMBAAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYD -VR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qWDNXr+nuqF+gTEjANBgkqhkiG -9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNmzqjMDfz1mgbl -dxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx -0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1Tj -TQpgcmLNkQfWpb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBed -Y2gea+zDTYa4EzAvXUYNR0PVG6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7 -Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpcc41teyWRyu5FrgZLAMzTsVlQ2jqI -OylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOTJsjrDNYmiLbAJM+7 -vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2qReW -t88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcn -HL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx -SK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= ------END CERTIFICATE-----`)) - - // TrustAsia Global Root CA G3 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +-----END CERTIFICATE----- + +// TrustAsia Global Root CA G3 +-----BEGIN CERTIFICATE----- MIIFpTCCA42gAwIBAgIUZPYOZXdhaqs7tOqFhLuxibhxkw8wDQYJKoZIhvcNAQEM BQAwWjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dp ZXMsIEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHMzAe @@ -4323,10 +4142,10 @@ AX1daBli2MjN9LdyR75bl87yraKZk62Uy5P2EgmVtqvXO9A/EcswFi55gORngS1d gKGLzZlkD8Kqq7HK9W+eQ42EVJmzbsASZthwEPEGNTNDqJwuuhQxzhB/HIbjj9LV +Hfsm6vxL2PZQl/gZ4FkkfGXL/xuJvYz+NO1+MRiqzFRJQJ6+N1rZdVtTTDIZbpo FGWsJwt0ivKH ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // TrustAsia Global Root CA G4 - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// TrustAsia Global Root CA G4 +-----BEGIN CERTIFICATE----- MIICVTCCAdygAwIBAgIUTyNkuI6XY57GU4HBdk7LKnQV1tcwCgYIKoZIzj0EAwMw WjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs IEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHNDAeFw0y @@ -4340,10 +4159,10 @@ pm9xy94wHQYDVR0OBBYEFKW7SpfOsyt/pDHel4NZg6ZvccveMA4GA1UdDwEB/wQE AwIBBjAKBggqhkjOPQQDAwNnADBkAjBe8usGzEkxn0AAbbd+NvBNEU/zy4k6LHiR UKNbwMp1JvK/kF0LgoxgKJ/GcJpo5PECMFxYDlZ2z1jD1xCMuo6u47xkdUfFVZDj /bpV6wfEU6s3qe4hsiFbYI89MvHVI5TWWA== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // TrustAsia SMIME ECC Root CA - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// TrustAsia SMIME ECC Root CA +-----BEGIN CERTIFICATE----- MIICNjCCAbugAwIBAgIUWsL4KU/jfcVeHRhvO5MgH/97ui0wCgYIKoZIzj0EAwMw WjELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs IEluYy4xJDAiBgNVBAMTG1RydXN0QXNpYSBTTUlNRSBFQ0MgUm9vdCBDQTAeFw0y @@ -4356,10 +4175,10 @@ QjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFDFn5nKyDeYioKzPfiKnWTLj ZiOlMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNpADBmAjEA3TpMjaTGf+29 pcZPPv0xSyjWilbfZRZ3h037ujIIgeCeM0iLn5SG7wErlOaM1tSOAjEAn4GcsCb9 K9by9XGEnqjHiozWWBFStbgEy8xxdWPixhk42W1sGXGkFhkhk7oGRChs ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // TrustAsia SMIME RSA Root CA - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// TrustAsia SMIME RSA Root CA +-----BEGIN CERTIFICATE----- MIIFhDCCA2ygAwIBAgIUWu5x394MV4W1uzYi17h2RgJzyv8wDQYJKoZIhvcNAQEM BQAwWjELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dp ZXMsIEluYy4xJDAiBgNVBAMTG1RydXN0QXNpYSBTTUlNRSBSU0EgUm9vdCBDQTAe @@ -4390,10 +4209,10 @@ fuF0t/aBauQjrI79jzUdmnEKTypVL/4YwQD3e0iKZa9vCB1D51q4H6ToA+v9TLW0 k6gx3kOdEr3n6aTS32/8b0aj7zFOjRerG6ng+Kk0VqEO53TsqIeF2Hc1S40+bnJ8 SMwfcrNxdNQkhrzIwON5FAHO2fqBxlyz+V0MOL7O8o6NXz0l4VE5I6jqAI4Es79y oMK6g/vNpJd1IJq/p1Di3a0sH/Q/o8gx ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // TrustAsia TLS ECC Root CA - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// TrustAsia TLS ECC Root CA +-----BEGIN CERTIFICATE----- MIICMTCCAbegAwIBAgIUNnThTXxlE8msg1UloD5Sfi9QaMcwCgYIKoZIzj0EAwMw WDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs IEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgRUNDIFJvb3QgQ0EwHhcNMjQw @@ -4406,10 +4225,10 @@ DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQULIVTu7FDzTLqnqOH/qKYqKaT6RAw DgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMFRH18MtYYZI9HlaVQ01 L18N9mdsd0AaRuf4aFtOJx24mH1/k78ITcTaRTChD15KeAIxAKORh/IRM4PDwYqR OkwrULG9IpRdNYlzg8WbGf60oenUoWa2AaU2+dhoYSi3dOGiMQ== ------END CERTIFICATE-----`)) +-----END CERTIFICATE----- - // TrustAsia TLS RSA Root CA - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- +// TrustAsia TLS RSA Root CA +-----BEGIN CERTIFICATE----- MIIFgDCCA2igAwIBAgIUHBjYz+VTPyI1RlNUJDxsR9FcSpwwDQYJKoZIhvcNAQEM BQAwWDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dp ZXMsIEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgUlNBIFJvb3QgQ0EwHhcN @@ -4440,153 +4259,6 @@ XvPKnbwbPKfILfanIhHKA9jnZwqKDss1jjQ52MjqjZ9k4DewbNfFj8GQYSbbJIwe SsCI3zWQzj8C9GRh3sfIB5XeMhg6j6JCQCTl1jNdfK7vsU1P1FeQNWrcrgSXSYk0 ly4wBOeY99sLAZDBHwo/+ML+TvrbmnNzFrwFuHnYWa8G5z9nODmxfKuU4CkUpijy 323imttUQ/hHWKNddBWcwauwxzQ= ------END CERTIFICATE-----`)) - - // Secure Global CA - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBK -MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x -GTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkx -MjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3Qg -Q29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqG -SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ -iQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa -/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ -jnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI -HmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7 -sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0w -gZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF -MAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCsw -KaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsG -AQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0L -URYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO -H0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm -I50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY -iNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc -f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW ------END CERTIFICATE-----`)) - - // SecureTrust CA - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI -MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x -FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz -MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv -cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz -Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO -0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao -wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj -7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS -8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT -BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB -/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg -JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC -NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3 -6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/ -3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm -D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS -CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR -3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= ------END CERTIFICATE-----`)) - - // Trustwave Global Certification Authority - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIF2jCCA8KgAwIBAgIMBfcOhtpJ80Y1LrqyMA0GCSqGSIb3DQEBCwUAMIGIMQsw -CQYDVQQGEwJVUzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28x -ITAfBgNVBAoMGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1 -c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMx -OTM0MTJaFw00MjA4MjMxOTM0MTJaMIGIMQswCQYDVQQGEwJVUzERMA8GA1UECAwI -SWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2ZSBI -b2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZp -Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB -ALldUShLPDeS0YLOvR29zd24q88KPuFd5dyqCblXAj7mY2Hf8g+CY66j96xz0Xzn -swuvCAAJWX/NKSqIk4cXGIDtiLK0thAfLdZfVaITXdHG6wZWiYj+rDKd/VzDBcdu -7oaJuogDnXIhhpCujwOl3J+IKMujkkkP7NAP4m1ET4BqstTnoApTAbqOl5F2brz8 -1Ws25kCI1nsvXwXoLG0R8+eyvpJETNKXpP7ScoFDB5zpET71ixpZfR9oWN0EACyW -80OzfpgZdNmcc9kYvkHHNHnZ9GLCQ7mzJ7Aiy/k9UscwR7PJPrhq4ufogXBeQotP -JqX+OsIgbrv4Fo7NDKm0G2x2EOFYeUY+VM6AqFcJNykbmROPDMjWLBz7BegIlT1l -RtzuzWniTY+HKE40Cz7PFNm73bZQmq131BnW2hqIyE4bJ3XYsgjxroMwuREOzYfw -hI0Vcnyh78zyiGG69Gm7DIwLdVcEuE4qFC49DxweMqZiNu5m4iK4BUBjECLzMx10 -coos9TkpoNPnG4CELcU9402x/RpvumUHO1jsQkUm+9jaJXLE9gCxInm943xZYkqc -BW89zubWR2OZxiRvchLIrH+QtAuRcOi35hYQcRfO3gZPSEF9NUqjifLJS3tBEW1n -twiYTOURGa5CgNz7kAXU+FDKvuStx8KU1xad5hePrzb7AgMBAAGjQjBAMA8GA1Ud -EwEB/wQFMAMBAf8wHQYDVR0OBBYEFJngGWcNYtt2s9o9uFvo/ULSMQ6HMA4GA1Ud -DwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAmHNw4rDT7TnsTGDZqRKGFx6W -0OhUKDtkLSGm+J1WE2pIPU/HPinbbViDVD2HfSMF1OQc3Og4ZYbFdada2zUFvXfe -uyk3QAUHw5RSn8pk3fEbK9xGChACMf1KaA0HZJDmHvUqoai7PF35owgLEQzxPy0Q -lG/+4jSHg9bP5Rs1bdID4bANqKCqRieCNqcVtgimQlRXtpla4gt5kNdXElE1GYhB -aCXUNxeEFfsBctyV3lImIJgm4nb1J2/6ADtKYdkNy1GTKv0WBpanI5ojSP5RvbbE -sLFUzt5sQa0WZ37b/TjNuThOssFgy50X31ieemKyJo90lZvkWx3SD92YHJtZuSPT -MaCm/zjdzyBP6VhWOmfD0faZmZ26NraAL4hHT4a/RDqA5Dccprrql5gR0IRiR2Qe -qu5AvzSxnI9O4fKSTx+O856X3vOmeWqJcU9LJxdI/uz0UA9PSX3MReO9ekDFQdxh -VicGaeVyQYHTtgGJoC86cnn+OjC/QezHYj6RS8fZMXZC+fc8Y+wmjHMMfRod6qh8 -h6jCJ3zhM0EPz8/8AKAigJ5Kp28AsEFFtyLKaEjFQqKu3R3y4G5OBVixwJAWKqQ9 -EEC+j2Jjg6mcgn0tAumDMHzLJ8n9HmYAsC7TIS+OMxZsmO0QqAfWzJPP29FpHOTK -yeC2nOnOcXHebD8WpHk= ------END CERTIFICATE-----`)) - - // Trustwave Global ECC P256 Certification Authority - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIICYDCCAgegAwIBAgIMDWpfCD8oXD5Rld9dMAoGCCqGSM49BAMCMIGRMQswCQYD -VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf -BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 -YXZlIEdsb2JhbCBFQ0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x -NzA4MjMxOTM1MTBaFw00MjA4MjMxOTM1MTBaMIGRMQswCQYDVQQGEwJVUzERMA8G -A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 -d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF -Q0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqG -SM49AwEHA0IABH77bOYj43MyCMpg5lOcunSNGLB4kFKA3TjASh3RqMyTpJcGOMoN -FWLGjgEqZZ2q3zSRLoHB5DOSMcT9CTqmP62jQzBBMA8GA1UdEwEB/wQFMAMBAf8w -DwYDVR0PAQH/BAUDAwcGADAdBgNVHQ4EFgQUo0EGrJBt0UrrdaVKEJmzsaGLSvcw -CgYIKoZIzj0EAwIDRwAwRAIgB+ZU2g6gWrKuEZ+Hxbb/ad4lvvigtwjzRM4q3wgh -DDcCIC0mA6AFvWvR9lz4ZcyGbbOcNEhjhAnFjXca4syc4XR7 ------END CERTIFICATE-----`)) - - // Trustwave Global ECC P384 Certification Authority - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIICnTCCAiSgAwIBAgIMCL2Fl2yZJ6SAaEc7MAoGCCqGSM49BAMDMIGRMQswCQYD -VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf -BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 -YXZlIEdsb2JhbCBFQ0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x -NzA4MjMxOTM2NDNaFw00MjA4MjMxOTM2NDNaMIGRMQswCQYDVQQGEwJVUzERMA8G -A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 -d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF -Q0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTB2MBAGByqGSM49AgEGBSuB -BAAiA2IABGvaDXU1CDFHBa5FmVXxERMuSvgQMSOjfoPTfygIOiYaOs+Xgh+AtycJ -j9GOMMQKmw6sWASr9zZ9lCOkmwqKi6vr/TklZvFe/oyujUF5nQlgziip04pt89ZF -1PKYhDhloKNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNVHQ8BAf8EBQMDBwYAMB0G -A1UdDgQWBBRVqYSJ0sEyvRjLbKYHTsjnnb6CkDAKBggqhkjOPQQDAwNnADBkAjA3 -AZKXRRJ+oPM+rRk6ct30UJMDEr5E0k9BpIycnR+j9sKS50gU/k6bpZFXrsY3crsC -MGclCrEMXu6pY5Jv5ZAL/mYiykf9ijH3g/56vxC+GCsej/YpHpRZ744hN8tRmKVu -Sw== ------END CERTIFICATE-----`)) - - // XRamp Global Certification Authority - pool.AppendCertsFromPEM([]byte(`-----BEGIN CERTIFICATE----- -MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCB -gjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEk -MCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRY -UmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQxMTAxMTcx -NDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3 -dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2Vy -dmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB -dXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS6 -38eMpSe2OAtp87ZOqCwuIR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCP -KZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMxfoArtYzAQDsRhtDLooY2YKTVMIJt2W7Q -DxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FEzG+gSqmUsE3a56k0enI4 -qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqsAxcZZPRa -JSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNVi -PvryxS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0P -BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASs -jVy16bYbMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0 -eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQEwDQYJKoZIhvcNAQEFBQAD -ggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc/Kh4ZzXxHfAR -vbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt -qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLa -IR9NmXmd4c8nnxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSy -i6mx5O+aGtA9aZnuqCij4Tyz8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQ -O+7ETPTsJ3xCwnR8gooJybQDJbw= ------END CERTIFICATE-----`)) - return pool +-----END CERTIFICATE----- +` } diff --git a/common/certificate/mozilla.pem b/common/certificate/mozilla.pem new file mode 100644 index 0000000000..96f941b07e --- /dev/null +++ b/common/certificate/mozilla.pem @@ -0,0 +1,4256 @@ +-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE +BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w +MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 +IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC +SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1 +ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv +UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX +4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9 +KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/ +gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb +rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ +51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F +be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe +KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F +v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn +fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7 +jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz +ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt +ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL +e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70 +jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz +WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V +SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j +pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX +X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok +fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R +K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU +ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU +LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT +LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQEL +BQAwYTELMAkGA1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUg +Q2VydGlmaWNhdGlvbiBFbGVjdHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJv +b3QgQ0EwHhcNMTkwNDI2MDg1NzU2WhcNNDQwNDI2MDg1NzU2WjBhMQswCQYDVQQG +EwJUTjE3MDUGA1UECgwuQWdlbmNlIE5hdGlvbmFsZSBkZSBDZXJ0aWZpY2F0aW9u +IEVsZWN0cm9uaXF1ZTEZMBcGA1UEAwwQVHVuVHJ1c3QgUm9vdCBDQTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAMPN0/y9BFPdDCA61YguBUtB9YOCfvdZ +n56eY+hz2vYGqU8ftPkLHzmMmiDQfgbU7DTZhrx1W4eI8NLZ1KMKsmwb60ksPqxd +2JQDoOw05TDENX37Jk0bbjBU2PWARZw5rZzJJQRNmpA+TkBuimvNKWfGzC3gdOgF +VwpIUPp6Q9p+7FuaDmJ2/uqdHYVy7BG7NegfJ7/Boce7SBbdVtfMTqDhuazb1YMZ +GoXRlJfXyqNlC/M4+QKu3fZnz8k/9YosRxqZbwUN/dAdgjH8KcwAWJeRTIAAHDOF +li/LQcKLEITDCSSJH7UP2dl3RxiSlGBcx5kDPP73lad9UKGAwqmDrViWVSHbhlnU +r8a83YFuB9tgYv7sEG7aaAH0gxupPqJbI9dkxt/con3YS7qC0lH4Zr8GRuR5KiY2 +eY8fTpkdso8MDhz/yV3A/ZAQprE38806JG60hZC/gLkMjNWb1sjxVj8agIl6qeIb +MlEsPvLfe/ZdeikZjuXIvTZxi11Mwh0/rViizz1wTaZQmCXcI/m4WEEIcb9PuISg +jwBUFfyRbVinljvrS5YnzWuioYasDXxU5mZMZl+QviGaAkYt5IPCgLnPSz7ofzwB +7I9ezX/SKEIBlYrilz0QIX32nRzFNKHsLA4KUiwSVXAkPcvCFDVDXSdOvsC9qnyW +5/yeYa1E0wCXAgMBAAGjYzBhMB0GA1UdDgQWBBQGmpsfU33x9aTI04Y+oXNZtPdE +ITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFAaamx9TffH1pMjThj6hc1m0 +90QhMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAqgVutt0Vyb+z +xiD2BkewhpMl0425yAA/l/VSJ4hxyXT968pk21vvHl26v9Hr7lxpuhbI87mP0zYu +QEkHDVneixCwSQXi/5E/S7fdAo74gShczNxtr18UnH1YeA32gAm56Q6XKRm4t+v4 +FstVEuTGfbvE7Pi1HE4+Z7/FXxttbUcoqgRYYdZ2vyJ/0Adqp2RT8JeNnYA/u8EH +22Wv5psymsNUk8QcCMNE+3tjEUPRahphanltkE8pjkcFwRJpadbGNjHh/PqAulxP +xOu3Mqz4dWEX1xAZufHSCe96Qp1bWgvUxpVOKs7/B9dPfhgGiPEZtdmYu65xxBzn +dFlY7wyJz4sfdZMaBBSSSFCp61cpABbjNhzI+L/wM9VBD8TMPN3pM0MBkRArHtG5 +Xc0yGYuPjCB31yLEQtyEFpslbei0VXF/sHyz03FJuc9SpAQ/3D2gu68zngowYI7b +nV2UqL1g52KAdoGDDIzMMEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQ +CvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9zZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZH +u/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3rAZ3r2OvEhJn7wAzMMujj +d9qDRIueVSjAi1jTkD5OGwDxFa2DK5o= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj +ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM +9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw +IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 +VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L +93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm +jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA +A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI +U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs +N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv +o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU +5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy +rqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK +gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ +W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg +1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K +8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r +2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me +z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR +8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj +mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz +7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6 ++XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI +0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm +UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2 +LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY ++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS +k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl +7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm +btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl +urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+ +fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63 +n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE +76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H +9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT +4PsJYGw= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl +ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr +ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr +BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM +YyRIHN8wfdVoOw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi +9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk +M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB +MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw +CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW +1KyLa2tJElMzrdfkviT8tQp21KW8EA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs +ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 +MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD +VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy +ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy +dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p +OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2 +8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K +Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe +hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk +6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q +AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI +bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB +ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z +qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd +iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn +0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN +sSi6 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDDDCCAfSgAwIBAgIDAQAgMA0GCSqGSIb3DQEBBQUAMD4xCzAJBgNVBAYTAlBM +MRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBD +QTAeFw0wMjA2MTExMDQ2MzlaFw0yNzA2MTExMDQ2MzlaMD4xCzAJBgNVBAYTAlBM +MRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBD +QTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM6xwS7TT3zNJc4YPk/E +jG+AanPIW1H4m9LcuwBcsaD8dQPugfCI7iNS6eYVM42sLQnFdvkrOYCJ5JdLkKWo +ePhzQ3ukYbDYWMzhbGZ+nPMJXlVjhNWo7/OxLjBos8Q82KxujZlakE403Daaj4GI +ULdtlkIJ89eVgw1BS7Bqa/j8D35in2fE7SZfECYPCE/wpFcozo+47UX2bu4lXapu +Ob7kky/ZR6By6/qmW6/KUz/iDsaWVhFu9+lmqSbYf5VT7QqFiLpPKaVCjF62/IUg +AKpoC6EahQGcxEZjgoi2IrHu/qpGWX7PNSzVttpd90gzFFS269lvzs2I1qsb2pY7 +HVkCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEA +uI3O7+cUus/usESSbLQ5PqKEbq24IXfS1HeCh+YgQYHu4vgRt2PRFze+GXYkHAQa +TOs9qmdvLdTN/mUxcMUbpgIKumB7bVjCmkn+YzILa+M6wKyrO7Do0wlRjBCDxjTg +xSvgGrZgFCdsMneMvLJymM/NzD+5yCRCFNZX/OYmQ6kd5YCQzgNUKD73P9P4Te1q +CjqTE5s7FCMTY5w/0YcneeVMUeMBrYVdGjux1XMQpNPyvG5k9VpWkKjHDkx0Dy5x +O/fIR/RpbxXyEV6DHpx8Uq79AtoSqFlnGNu8cN2bsWntgM6JQEhqDjXKKWYVIZQs +6GAqm4VKQPNriiTsBhYscw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICZTCCAeugAwIBAgIQeI8nXIESUiClBNAt3bpz9DAKBggqhkjOPQQDAzB0MQsw +CQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScw +JQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAXBgNVBAMT +EENlcnR1bSBFQy0zODQgQ0EwHhcNMTgwMzI2MDcyNDU0WhcNNDMwMzI2MDcyNDU0 +WjB0MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBT +LkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAX +BgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATE +KI6rGFtqvm5kN2PkzeyrOvfMobgOgknXhimfoZTy42B4mIF4Bk3y7JoOV2CDn7Tm +Fy8as10CW4kjPMIRBSqniBMY81CE1700LCeJVf/OTOffph8oxPBUw7l8t1Ot68Kj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI0GZnQkdjrzife81r1HfS+8 +EF9LMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjADVS2m5hjEfO/J +UG7BJw+ch69u1RsIGL2SKcHvlJF40jocVYli5RsJHrpka/F2tNQCMQC0QoSZ/6vn +nvuRlydd3LBbMHHOXjgaatkl5+r3YZJW+OraNsKHZZYuciUvf9/DE8k= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM +MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D +ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU +cnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3 +WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg +Uy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw +IAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH +UV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM +TXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU +BBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM +kUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x +AcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV +HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y +sHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL +I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8 +J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY +VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI +03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCB +gDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu +QS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG +A1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMCIYDzIwMTExMDA2MDgz +OTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UEChMZ +VW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3 +b3JrIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWA +DGSdhhuWZGc/IjoedQF97/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn +0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+oCgCXhVqqndwpyeI1B+twTUrWwbNWuKFB +OJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40bRr5HMNUuctHFY9rnY3lE +fktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2puTRZCr+E +Sv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1m +o130GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02i +sx7QBlrd9pPPV3WZ9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOW +OZV7bIBaTxNyxtd9KXpEulKkKtVBRgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgez +Tv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pyehizKV/Ma5ciSixqClnrDvFAS +adgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vMBhBgu4M1t15n +3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQ +F/xlhMcQSZDe28cmk4gmb3DWAl45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTf +CVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuAL55MYIR4PSFk1vtBHxgP58l1cb29 +XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMoclm2q8KMZiYcdywm +djWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tMpkT/ +WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jb +AoJnwTnbw3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksq +P/ujmv5zMnHCnsZy4YpoJ/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Ko +b7a6bINDd82Kkhehnlt4Fj1F4jNy3eFmypnTycUm/Q1oBEauttmbjL4ZvrHG8hnj +XALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P +5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi +DrW5viSP +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFwDCCA6igAwIBAgIQHr9ZULjJgDdMBvfrVU+17TANBgkqhkiG9w0BAQ0FADB6 +MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu +MScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxHzAdBgNV +BAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwHhcNMTgwMzE2MTIxMDEzWhcNNDMw +MzE2MTIxMDEzWjB6MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEg +U3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRo +b3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQDRLY67tzbqbTeRn06TpwXkKQMlzhyC93yZ +n0EGze2jusDbCSzBfN8pfktlL5On1AFrAygYo9idBcEq2EXxkd7fO9CAAozPOA/q +p1x4EaTByIVcJdPTsuclzxFUl6s1wB52HO8AU5853BSlLCIls3Jy/I2z5T4IHhQq +NwuIPMqw9MjCoa68wb4pZ1Xi/K1ZXP69VyywkI3C7Te2fJmItdUDmj0VDT06qKhF +8JVOJVkdzZhpu9PMMsmN74H+rX2Ju7pgE8pllWeg8xn2A1bUatMn4qGtg/BKEiJ3 +HAVz4hlxQsDsdUaakFjgao4rpUYwBI4Zshfjvqm6f1bxJAPXsiEodg42MEx51UGa +mqi4NboMOvJEGyCI98Ul1z3G4z5D3Yf+xOr1Uz5MZf87Sst4WmsXXw3Hw09Omiqi +7VdNIuJGmj8PkTQkfVXjjJU30xrwCSss0smNtA0Aq2cpKNgB9RkEth2+dv5yXMSF +ytKAQd8FqKPVhJBPC/PgP5sZ0jeJP/J7UhyM9uH3PAeXjA6iWYEMspA90+NZRu0P +qafegGtaqge2Gcu8V/OXIXoMsSt0Puvap2ctTMSYnjYJdmZm/Bo/6khUHL4wvYBQ +v3y1zgD2DGHZ5yQD4OMBgQ692IU0iL2yNqh7XAjlRICMb/gv1SHKHRzQ+8S1h9E6 +Tsd2tTVItQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSM+xx1 +vALTn04uSNn5YFSqxLNP+jAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQAD +ggIBAEii1QALLtA/vBzVtVRJHlpr9OTy4EA34MwUe7nJ+jW1dReTagVphZzNTxl4 +WxmB82M+w85bj/UvXgF2Ez8sALnNllI5SW0ETsXpD4YN4fqzX4IS8TrOZgYkNCvo +zMrnadyHncI013nR03e4qllY/p0m+jiGPp2Kh2RX5Rc64vmNueMzeMGQ2Ljdt4NR +5MTMI9UGfOZR0800McD2RrsLrfw9EAUqO0qRJe6M1ISHgCq8CYyqOhNf6DR5UMEQ +GfnTKB7U0VEwKbOukGfWHwpjscWpxkIxYxeU72nLL/qMFH3EQxiJ2fAyQOaA4kZf +5ePBAFmo+eggvIksDkc0C+pXwlM2/KfUrzHN/gLldfq5Jwn58/U7yn2fqSLLiMmq +0Uc9NneoWWRrJ8/vJ8HjJLWG965+Mk2weWjROeiQWMODvA8s1pfrzgzhIMfatz7D +P78v3DSk+yshzWePS/Tj6tQ/50+6uaWTRRxmHyH6ZF5v4HaUMst19W7l9o/HuKTM +qJZ9ZPskWkoDbGs4xugDQ5r3V7mzKWmTOPQD8rv7gmsHINFSH5pkAnuYZttcTVoP +0ISVoDwUQwbKytu4QTbaakRnh6+v40URFWkIsr4WOZckbxJF0WddCajJFdr60qZf +E2Efv4WstK2tBZQIgx51F9NxO5NQI1mg7TyRVJ12AMXDuDjb +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIG3Dp0v+ubHEwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UE +BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h +cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0xNDA5MjMxNTIyMDdaFw0zNjA1 +MDUxNTIyMDdaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg +Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9 +thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM +cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG +L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i +NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h +X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b +m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy +Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja +EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T +KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF +6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh +OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMB0GA1UdDgQWBBRlzeurNR4APn7VdMAc +tHNHDhpkLzASBgNVHRMBAf8ECDAGAQH/AgEBMIGmBgNVHSAEgZ4wgZswgZgGBFUd +IAAwgY8wLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuZmlybWFwcm9mZXNpb25hbC5j +b20vY3BzMFwGCCsGAQUFBwICMFAeTgBQAGEAcwBlAG8AIABkAGUAIABsAGEAIABC +AG8AbgBhAG4AbwB2AGEAIAA0ADcAIABCAGEAcgBjAGUAbABvAG4AYQAgADAAOAAw +ADEANzAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAHSHKAIrdx9m +iWTtj3QuRhy7qPj4Cx2Dtjqn6EWKB7fgPiDL4QjbEwj4KKE1soCzC1HA01aajTNF +Sa9J8OA9B3pFE1r/yJfY0xgsfZb43aJlQ3CTkBW6kN/oGbDbLIpgD7dvlAceHabJ +hfa9NPhAeGIQcDq+fUs5gakQ1JZBu/hfHAsdCPKxsIl68veg4MSPi3i1O1ilI45P +Vf42O+AMt8oqMEEgtIDNrvx2ZnOorm7hfNoD6JQg5iKj0B+QXSBTFCZX2lSX3xZE +EAEeiGaPcjiT3SC3NL7X8e5jjkd5KAb881lFJWAiMxujX6i6KtoaPc1A6ozuBRWV +1aUsIC+nmCjuRfzxuIgALI9C2lHVnOUTaHFFQ4ueCyE8S1wF3BqfmI7avSKecs2t +CsvMo2ebKHTEm9caPARYpoKdrcd7b/+Alun4jWq9GJAd/0kakFI3ky88Al2CdgtR +5xbHV/g4+afNmyJU72OwFW1TZQNKXkqgsqeOSQBZONXH9IBk9W6VULgRfhVwOEqw +f9DEMnDAGf/JOC0ULGb0QkTmVXYbgBVX/8Cnp6o5qtjTcNAuuuuUavpfNIbnYrX9 +ivAwhZTJryQCL2/W3Wf+47BVTwSYT6RBVuKT0Gro1vP7ZeDOdcQxWQzugsgMYDNK +GbqEZycPvEJdvSRUDewdcAZfpLz6IHxV +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICejCCAgCgAwIBAgIQMZch7a+JQn81QYehZ1ZMbTAKBggqhkjOPQQDAzBuMQsw +CQYDVQQGEwJFUzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25hbCBTQTEYMBYGA1UE +YQwPVkFURVMtQTYyNjM0MDY4MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFMIENB +IFJPT1QtQSBXRUIwHhcNMjIwNDA2MDkwMTM2WhcNNDcwMzMxMDkwMTM2WjBuMQsw +CQYDVQQGEwJFUzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25hbCBTQTEYMBYGA1UE +YQwPVkFURVMtQTYyNjM0MDY4MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFMIENB +IFJPT1QtQSBXRUIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARHU+osEaR3xyrq89Zf +e9MEkVz6iMYiuYMQYneEMy3pA4jU4DP37XcsSmDq5G+tbbT4TIqk5B/K6k84Si6C +cyvHZpsKjECcfIr28jlgst7L7Ljkb+qbXbdTkBgyVcUgt5SjYzBhMA8GA1UdEwEB +/wQFMAMBAf8wHwYDVR0jBBgwFoAUk+FDY1w8ndYn81LsF7Kpryz3dvgwHQYDVR0O +BBYEFJPhQ2NcPJ3WJ/NS7Beyqa8s93b4MA4GA1UdDwEB/wQEAwIBBjAKBggqhkjO +PQQDAwNoADBlAjAdfKR7w4l1M+E7qUW/Runpod3JIha3RxEL2Jq68cgLcFBTApFw +hVmpHqTm6iMxoAACMQD94vizrxa5HnPEluPBMBnYfubDl94cT7iJLzPrSA8Z94dG +XSaQpYXFuXqUPoeovQA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNV +BAUTCUc2MzI4NzUxMDELMAkGA1UEBhMCRVMxJzAlBgNVBAoTHkFORiBBdXRvcmlk +YWQgZGUgQ2VydGlmaWNhY2lvbjEUMBIGA1UECxMLQU5GIENBIFJhaXoxIjAgBgNV +BAMTGUFORiBTZWN1cmUgU2VydmVyIFJvb3QgQ0EwHhcNMTkwOTA0MTAwMDM4WhcN +MzkwODMwMTAwMDM4WjCBhDESMBAGA1UEBRMJRzYzMjg3NTEwMQswCQYDVQQGEwJF +UzEnMCUGA1UEChMeQU5GIEF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uMRQwEgYD +VQQLEwtBTkYgQ0EgUmFpejEiMCAGA1UEAxMZQU5GIFNlY3VyZSBTZXJ2ZXIgUm9v +dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANvrayvmZFSVgpCj +cqQZAZ2cC4Ffc0m6p6zzBE57lgvsEeBbphzOG9INgxwruJ4dfkUyYA8H6XdYfp9q +yGFOtibBTI3/TO80sh9l2Ll49a2pcbnvT1gdpd50IJeh7WhM3pIXS7yr/2WanvtH +2Vdy8wmhrnZEE26cLUQ5vPnHO6RYPUG9tMJJo8gN0pcvB2VSAKduyK9o7PQUlrZX +H1bDOZ8rbeTzPvY1ZNoMHKGESy9LS+IsJJ1tk0DrtSOOMspvRdOoiXsezx76W0OL +zc2oD2rKDF65nkeP8Nm2CgtYZRczuSPkdxl9y0oukntPLxB3sY0vaJxizOBQ+OyR +p1RMVwnVdmPF6GUe7m1qzwmd+nxPrWAI/VaZDxUse6mAq4xhj0oHdkLePfTdsiQz +W7i1o0TJrH93PB0j7IKppuLIBkwC/qxcmZkLLxCKpvR/1Yd0DVlJRfbwcVw5Kda/ +SiOL9V8BY9KHcyi1Swr1+KuCLH5zJTIdC2MKF4EA/7Z2Xue0sUDKIbvVgFHlSFJn +LNJhiQcND85Cd8BEc5xEUKDbEAotlRyBr+Qc5RQe8TZBAQIvfXOn3kLMTOmJDVb3 +n5HUA8ZsyY/b2BzgQJhdZpmYgG4t/wHFzstGH6wCxkPmrqKEPMVOHj1tyRRM4y5B +u8o5vzY8KhmqQYdOpc5LMnndkEl/AgMBAAGjYzBhMB8GA1UdIwQYMBaAFJxf0Gxj +o1+TypOYCK2Mh6UsXME3MB0GA1UdDgQWBBScX9BsY6Nfk8qTmAitjIelLFzBNzAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AgEATh65isagmD9uw2nAalxJUqzLK114OMHVVISfk/CHGT0sZonrDUL8zPB1hT+L +9IBdeeUXZ701guLyPI59WzbLWoAAKfLOKyzxj6ptBZNscsdW699QIyjlRRA96Gej +rw5VD5AJYu9LWaL2U/HANeQvwSS9eS9OICI7/RogsKQOLHDtdD+4E5UGUcjohybK +pFtqFiGS3XNgnhAY3jyB6ugYw3yJ8otQPr0R4hUDqDZ9MwFsSBXXiJCZBMXM5gf0 +vPSQ7RPi6ovDj6MzD8EpTBNO2hVWcXNyglD2mjN8orGoGjR0ZVzO0eurU+AagNjq +OknkJjCb5RyKqKkVMoaZkgoQI1YS4PbOTOK7vtuNknMBZi9iPrJyJ0U27U1W45eZ +/zo1PqVUSlJZS2Db7v54EX9K3BR5YLZrZAPbFYPhor72I5dQ8AkzNqdxliXzuUJ9 +2zg/LFis6ELhDtjTO0wugumDLmsx2d1Hhk9tl5EuT+IocTUW0fJz/iUrB0ckYyfI ++PbZa/wSMVYIwFNCr5zQM378BvAxRAMU8Vjq8moNqRGyg77FGr8H6lnco4g175x2 +MjxNBiLOFeXdntiP2t7SxDnlF4HPOEfrf4htWRvfn0IUrn7PqLBmZdo3r5+qPeoo +tt7VMVgWglvquxl1AnMaykgaIZOQCo6ThKd9OyMYkomgjaw= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr +6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV +L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91 +1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx +MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ +QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB +arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr +Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi +FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS +P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN +9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz +uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h +9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s +A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t +OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo ++fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7 +KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2 +DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us +H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ +I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 +5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h +3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz +Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y +ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E +N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9 +tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX +0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c +/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X +KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY +zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS +O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D +34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP +K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3 +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv +Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj +QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV +cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS +IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2 +HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa +O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv +033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u +dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE +kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41 +3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD +u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq +4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQsw +CQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlu +bHkgUm9vdCBFMTAeFw0yMTA0MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJ +BgNVBAYTAlVTMRIwEAYDVQQKEwlDZXJ0YWlubHkxGjAYBgNVBAMTEUNlcnRhaW5s +eSBSb290IEUxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3m/4fxzf7flHh4axpMCK ++IKXgOqPyEpeKn2IaKcBYhSRJHpcnqMXfYqGITQYUBsQ3tA3SybHGWCA6TS9YBk2 +QNYphwk8kXr2vBMj3VlOBF7PyAIcGFPBMdjaIOlEjeR2o0IwQDAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4 +hevIIgcwCgYIKoZIzj0EAwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozm +ut6Dacpps6kFtZaSF4fC0urQe87YQVt8rgIwRt7qy12a7DLCZRawTDBcMPPaTnOG +BtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIRAI4P+UuQcWhlM1T01EQ5t+AwDQYJKoZIhvcNAQELBQAw +PTELMAkGA1UEBhMCVVMxEjAQBgNVBAoTCUNlcnRhaW5seTEaMBgGA1UEAxMRQ2Vy +dGFpbmx5IFJvb3QgUjEwHhcNMjEwNDAxMDAwMDAwWhcNNDYwNDAxMDAwMDAwWjA9 +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0 +YWlubHkgUm9vdCBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANA2 +1B/q3avk0bbm+yLA3RMNansiExyXPGhjZjKcA7WNpIGD2ngwEc/csiu+kr+O5MQT +vqRoTNoCaBZ0vrLdBORrKt03H2As2/X3oXyVtwxwhi7xOu9S98zTm/mLvg7fMbed +aFySpvXl8wo0tf97ouSHocavFwDvA5HtqRxOcT3Si2yJ9HiG5mpJoM610rCrm/b0 +1C7jcvk2xusVtyWMOvwlDbMicyF0yEqWYZL1LwsYpfSt4u5BvQF5+paMjRcCMLT5 +r3gajLQ2EBAHBXDQ9DGQilHFhiZ5shGIXsXwClTNSaa/ApzSRKft43jvRl5tcdF5 +cBxGX1HpyTfcX35pe0HfNEXgO4T0oYoKNp43zGJS4YkNKPl6I7ENPT2a/Z2B7yyQ +wHtETrtJ4A5KVpK8y7XdeReJkd5hiXSSqOMyhb5OhaRLWcsrxXiOcVTQAjeZjOVJ +6uBUcqQRBi8LjMFbvrWhsFNunLhgkR9Za/kt9JQKl7XsxXYDVBtlUrpMklZRNaBA +2CnbrlJ2Oy0wQJuK0EJWtLeIAaSHO1OWzaMWj/Nmqhexx2DgwUMFDO6bW2BvBlyH +Wyf5QBGenDPBt+U1VwV/J84XIIwc/PH72jEpSe31C4SnT8H2TsIonPru4K8H+zMR +eiFPCyEQtkA6qyI6BJyLm4SGcprSp6XEtHWRqSsjAgMBAAGjQjBAMA4GA1UdDwEB +/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTgqj8ljZ9EXME66C6u +d0yEPmcM9DANBgkqhkiG9w0BAQsFAAOCAgEAuVevuBLaV4OPaAszHQNTVfSVcOQr +PbA56/qJYv331hgELyE03fFo8NWWWt7CgKPBjcZq91l3rhVkz1t5BXdm6ozTaw3d +8VkswTOlMIAVRQdFGjEitpIAq5lNOo93r6kiyi9jyhXWx8bwPWz8HA2YEGGeEaIi +1wrykXprOQ4vMMM2SZ/g6Q8CRFA3lFV96p/2O7qUpUzpvD5RtOjKkjZUbVwlKNrd +rRT90+7iIgXr0PK3aBLXWopBGsaSpVo7Y0VPv+E6dyIvXL9G+VoDhRNCX8reU9di +taY1BMJH/5n9hN9czulegChB8n3nHpDYT3Y+gjwN/KUD+nsa2UUeYNrEjvn8K8l7 +lcUq/6qJ34IxD3L/DCfXCh5WAFAeDJDBlrXYFIW7pw0WwfgHJBu6haEaBQmAupVj +yTrsJZ9/nbqkRxWbRHDxakvWOF5D8xh+UG7pWijmZeZ3Gzr9Hb4DJqPb1OG7fpYn +Kx3upPvaJVQTA945xsMfTZDsjxtK0hzthZU4UHlG1sGQUDGpXJpuHfUzVounmdLy +yCwzk5Iwx06MZTMQZBf9JBeW0Y3COmor6xOLRPIh80oat3df1+2IpHLlOR+Vnb5n +wXARPbv0+Em34yaXOp/SX3z7wJl8OSngex2/DaeP0ik0biQVy96QXr8axGbqwua6 +OV+KmalBWQewLK8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV +BAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4X +DTA3MDYyOTE1MTMwNVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQ +BgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwIQ2VydGlnbmEwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7qXOEm7RFHYeGifBZ4 +QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyHGxny +gQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbw +zBfsV1/pogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q +130yGLMLLGq/jj8UEYkgDncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2 +JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKfIrjxwo1p3Po6WAbfAgMBAAGjgbwwgbkw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQtCRZvgHyUtVF9lo53BEw +ZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJBgNVBAYT +AkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzj +AQ/JSP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG +9w0BAQUFAAOCAQEAhQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8h +bV6lUmPOEvjvKtpv6zf+EwLHyzs+ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFnc +fca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9qcEQCYsuu +HWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w +t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw +WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAw +WjELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAw +MiA0ODE0NjMwODEwMDAzNjEZMBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0x +MzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjdaMFoxCzAJBgNVBAYTAkZSMRIwEAYD +VQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYzMDgxMDAwMzYxGTAX +BgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sO +ty3tRQgXstmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9M +CiBtnyN6tMbaLOQdLNyzKNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPu +I9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8JXrJhFwLrN1CTivngqIkicuQstDuI7pm +TLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16XdG+RCYyKfHx9WzMfgIh +C59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq4NYKpkDf +ePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3Yz +IoejwpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWT +Co/1VTp2lc5ZmIoJlXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1k +JWumIWmbat10TWuXekG9qxf5kBdIjzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5 +hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp//TBt2dzhauH8XwIDAQABo4IB +GjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of +1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczov +L3d3d3cuY2VydGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilo +dHRwOi8vY3JsLmNlcnRpZ25hLmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYr +aHR0cDovL2NybC5kaGlteW90aXMuY29tL2NlcnRpZ25hcm9vdGNhLmNybDANBgkq +hkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOItOoldaDgvUSILSo3L +6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxPTGRG +HVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH6 +0BGM+RFq7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncB +lA2c5uk5jR+mUYyZDDl34bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdi +o2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1 +gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS6Cvu5zHbugRqh5jnxV/v +faci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaYtlu3zM63 +Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayh +jWZSaX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw +3kAP+HwV96LOPNdeE4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYT +AlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBD +QTAeFw0wNjA3MDQxNzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJP +MREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7IJUqOtdu0KBuqV5Do +0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHHrfAQ +UySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5d +RdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQ +OA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv +JoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08C +AwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0O +BBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJ +LjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecY +MnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ +44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6I +Jd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNw +i/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN +9u6wWk5JRFRYX0KD +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV +BAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04g +Uk9PVCBDQSBHMjAeFw0xNzAyMDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJ +BgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJ +R04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDF +dRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05N0Iw +vlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZ +uIt4ImfkabBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhp +n+Sc8CnTXPnGFiWeI8MgwT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKs +cpc/I1mbySKEwQdPzH/iV8oScLumZfNpdWO9lfsbl83kqK/20U6o2YpxJM02PbyW +xPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91QqhngLjYl/rNUssuHLoPj1P +rCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732jcZZroiF +DsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fx +DTvf95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgy +LcsUDFDYg2WD7rlcz8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6C +eWRgKRM+o/1Pcmqr4tTluCRVLERLiohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSCIS1mxteg4BXrzkwJ +d8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOBywaK8SJJ6ejq +kX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC +b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQl +qiCA2ClV9+BB/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0 +OJD7uNGzcgbJceaBxXntC6Z58hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+c +NywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5BiKDUyUM/FHE5r7iOZULJK2v0ZXk +ltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklWatKcsWMy5WHgUyIO +pwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tUSxfj +03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZk +PuXaTH4MNMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE +1LlSVHJ7liXMvGnjSG4N0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MX +QRBdJ3NghVdJIgc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe +Fw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMxMjdaMF4xCzAJBgNVBAYTAlRXMSMw +IQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEqMCgGA1UECwwhZVBL +SSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAH +SyZbCUNsIZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAh +ijHyl3SJCRImHJ7K2RKilTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3X +DZoTM1PRYfl61dd4s5oz9wCGzh1NlDivqOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1 +TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX12ruOzjjK9SXDrkb5wdJ +fzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0OWQqraffA +sgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uU +WH1+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLS +nT0IFaUQAS2zMnaolQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pH +dmX2Os+PYhcZewoozRrSgx4hxyy/vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJip +NiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXiZo1jDiVN1Rmy5nk3pyKdVDEC +AwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/QkqiMAwGA1UdEwQF +MAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH +ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGB +uvl2ICO1J2B01GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6Yl +PwZpVnPDimZI+ymBV3QGypzqKOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkP +JXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdVxrsStZf0X4OFunHB2WyBEXYKCrC/ +gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEPNXubrjlpC2JgQCA2 +j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+rGNm6 +5ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUB +o2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS +/jQ6fbjpKdx2qcgw+BRxgMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2z +Gp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE +W9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D +hNQ+IIX3Sj0rnP0qCglN6oH4EZw= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFajCCA1KgAwIBAgIQLd2szmKXlKFD6LDNdmpeYDANBgkqhkiG9w0BAQsFADBP +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xGzAZBgNVBAMMEkhpUEtJIFJvb3QgQ0EgLSBHMTAeFw0xOTAyMjIwOTQ2MDRa +Fw0zNzEyMzExNTU5NTlaME8xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3 +YSBUZWxlY29tIENvLiwgTHRkLjEbMBkGA1UEAwwSSGlQS0kgUm9vdCBDQSAtIEcx +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9B5/UnMyDHPkvRN0o9Qw +qNCuS9i233VHZvR85zkEHmpwINJaR3JnVfSl6J3VHiGh8Ge6zCFovkRTv4354twv +Vcg3Px+kwJyz5HdcoEb+d/oaoDjq7Zpy3iu9lFc6uux55199QmQ5eiY29yTw1S+6 +lZgRZq2XNdZ1AYDgr/SEYYwNHl98h5ZeQa/rh+r4XfEuiAU+TCK72h8q3VJGZDnz +Qs7ZngyzsHeXZJzA9KMuH5UHsBffMNsAGJZMoYFL3QRtU6M9/Aes1MU3guvklQgZ +KILSQjqj2FPseYlgSGDIcpJQ3AOPgz+yQlda22rpEZfdhSi8MEyr48KxRURHH+CK +FgeW0iEPU8DtqX7UTuybCeyvQqww1r/REEXgphaypcXTT3OUM3ECoWqj1jOXTyFj +HluP2cFeRXF3D4FdXyGarYPM+l7WjSNfGz1BryB1ZlpK9p/7qxj3ccC2HTHsOyDr +y+K49a6SsvfhhEvyovKTmiKe0xRvNlS9H15ZFblzqMF8b3ti6RZsR1pl8w4Rm0bZ +/W3c1pzAtH2lsN0/Vm+h+fbkEkj9Bn8SV7apI09bA8PgcSojt/ewsTu8mL3WmKgM +a/aOEmem8rJY5AIJEzypuxC00jBF8ez3ABHfZfjcK0NVvxaXxA/VLGGEqnKG/uY6 +fsI/fe78LxQ+5oXdUG+3Se0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQU8ncX+l6o/vY9cdVouslGDDjYr7AwDgYDVR0PAQH/BAQDAgGGMA0GCSqG +SIb3DQEBCwUAA4ICAQBQUfB13HAE4/+qddRxosuej6ip0691x1TPOhwEmSKsxBHi +7zNKpiMdDg1H2DfHb680f0+BazVP6XKlMeJ45/dOlBhbQH3PayFUhuaVevvGyuqc +SE5XCV0vrPSltJczWNWseanMX/mF+lLFjfiRFOs6DRfQUsJ748JzjkZ4Bjgs6Fza +ZsT0pPBWGTMpWmWSBUdGSquEwx4noR8RkpkndZMPvDY7l1ePJlsMu5wP1G4wB9Tc +XzZoZjmDlicmisjEOf6aIW/Vcobpf2Lll07QJNBAsNB1CI69aO4I1258EHBGG3zg +iLKecoaZAeO/n0kZtCW+VmWuF2PlHt/o/0elv+EmBYTksMCv5wiZqAxeJoBF1Pho +L5aPruJKHJwWDBNvOIf2u8g0X5IDUXlwpt/L9ZlNec1OvFefQ05rLisY+GpzjLrF +Ne85akEez3GoorKGB1s6yeHvP2UEgEcyRHCVTjFnanRbEEV16rCf0OY1/k6fi8wr +kkVbbiVghUbN0aqwdmaTd5a+g744tiROJgvM7XpWGuDpWsZkrUx6AEhEL7lAuxM+ +vhV4nYWBSipX3tUZQ9rbyltHhoMLP7YNdnhzeSJesYAfz77RP1YQmCuVh6EfnWQU +YDksswBVLuT1sw5XxJFBAJw/6KXf6vb/yPCtbVKoF6ubYfwSUTXkJf2vqmqGOQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUZvnHwa/swlG07VOX5uaCwysckBYwDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28u +LCBMdGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExMjAeFw0yMDA0MDgw +NTM2NDZaFw00MDA0MDgwNTM2NDZaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpD +eWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBS +b290IENBMTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6OcE3emhF +KxS06+QT61d1I02PJC0W6K6OyX2kVzsqdiUzg2zqMoqUm048luT9Ub+ZyZN+v/mt +p7JIKwccJ/VMvHASd6SFVLX9kHrko+RRWAPNEHl57muTH2SOa2SroxPjcf59q5zd +J1M3s6oYwlkm7Fsf0uZlfO+TvdhYXAvA42VvPMfKWeP+bl+sg779XSVOKik71gur +FzJ4pOE+lEa+Ym6b3kaosRbnhW70CEBFEaCeVESE99g2zvVQR9wsMJvuwPWW0v4J +hscGWa5Pro4RmHvzC1KqYiaqId+OJTN5lxZJjfU+1UefNzFJM3IFTQy2VYzxV4+K +h9GtxRESOaCtAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBRXNPN0zwRL1SXm8UC2LEzZLemgrTANBgkqhkiG9w0BAQsF +AAOCAQEAPrvbFxbS8hQBICw4g0utvsqFepq2m2um4fylOqyttCg6r9cBg0krY6Ld +mmQOmFxv3Y67ilQiLUoT865AQ9tPkbeGGuwAtEGBpE/6aouIs3YIcipJQMPTw4WJ +mBClnW8Zt7vPemVV2zfrPIpyMpcemik+rY3moxtt9XUa5rBouVui7mlHJzWhhpmA +8zNL4WukJsPvdFlseqJkth5Ew1DgDzk9qTPxpfPSvWKErI4cqc1avTc7bgoitPQV +55FYxTpE05Uo2cBl6XLK0A+9H7MV2anjpEcJnuDLN/v9vZfVvhgaaaI5gdka9at/ +yOPiZwud9AzqVN/Ssq+xIvEg37xEHA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFcjCCA1qgAwIBAgIUZNtaDCBO6Ncpd8hQJ6JaJ90t8sswDQYJKoZIhvcNAQEM +BQAwUTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28u +LCBMdGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNDAeFw0yMDA0MDgw +NzA2MTlaFw00NTA0MDgwNzA2MTlaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpD +eWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBS +b290IENBMTQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDF0nqh1oq/ +FjHQmNE6lPxauG4iwWL3pwon71D2LrGeaBLwbCRjOfHw3xDG3rdSINVSW0KZnvOg +vlIfX8xnbacuUKLBl422+JX1sLrcneC+y9/3OPJH9aaakpUqYllQC6KxNedlsmGy +6pJxaeQp8E+BgQQ8sqVb1MWoWWd7VRxJq3qdwudzTe/NCcLEVxLbAQ4jeQkHO6Lo +/IrPj8BGJJw4J+CDnRugv3gVEOuGTgpa/d/aLIJ+7sr2KeH6caH3iGicnPCNvg9J +kdjqOvn90Ghx2+m1K06Ckm9mH+Dw3EzsytHqunQG+bOEkJTRX45zGRBdAuVwpcAQ +0BB8b8VYSbSwbprafZX1zNoCr7gsfXmPvkPx+SgojQlD+Ajda8iLLCSxjVIHvXib +y8posqTdDEx5YMaZ0ZPxMBoH064iwurO8YQJzOAUbn8/ftKChazcqRZOhaBgy/ac +18izju3Gm5h1DVXoX+WViwKkrkMpKBGk5hIwAUt1ax5mnXkvpXYvHUC0bcl9eQjs +0Wq2XSqypWa9a4X0dFbD9ed1Uigspf9mR6XU/v6eVL9lfgHWMI+lNpyiUBzuOIAB +SMbHdPTGrMNASRZhdCyvjG817XsYAFs2PJxQDcqSMxDxJklt33UkN4Ii1+iW/RVL +ApY+B3KVfqs9TC7XyvDf4Fg/LS8EmjijAQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUBpOjCl4oaTeqYR3r6/wtbyPk +86AwDQYJKoZIhvcNAQEMBQADggIBAJaAcgkGfpzMkwQWu6A6jZJOtxEaCnFxEM0E +rX+lRVAQZk5KQaID2RFPeje5S+LGjzJmdSX7684/AykmjbgWHfYfM25I5uj4V7Ib +ed87hwriZLoAymzvftAj63iP/2SbNDefNWWipAA9EiOWWF3KY4fGoweITedpdopT +zfFP7ELyk+OZpDc8h7hi2/DsHzc/N19DzFGdtfCXwreFamgLRB7lUe6TzktuhsHS +DCRZNhqfLJGP4xjblJUK7ZGqDpncllPjYYPGFrojutzdfhrGe0K22VoF3Jpf1d+4 +2kd92jjbrDnVHmtsKheMYc2xbXIBw8MgAGJoFjHVdqqGuw6qnsb58Nn4DSEC5MUo +FlkRudlpcyqSeLiSV5sI8jrlL5WwWLdrIBRtFO8KvH7YVdiI2i/6GaX7i+B/OfVy +K4XELKzvGUWSTLNhB9xNH27SgRNcmvMSZ4PPmz+Ln52kuaiWA3rF7iDeM9ovnhp6 +dB7h7sxaOgTdsxoEqBRjrLdHEoOabPXm6RUVkRqEGQ6UROcSjiVbgGcZ3GOTEAtl +Lor6CZpO2oYofaphNdgOpygau1LgePhsumywbrmHXumZNTfxPWQrqaA0k89jL9WB +365jJ6UeTo3cKXhZ+PmhIIynJkBugnLNeLLIjzwec+fBH7/PzqUqm9tEZDKgu39c +JRNItX+S +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICIzCCAamgAwIBAgIUFhXHw9hJp75pDIqI7fBw+d23PocwCgYIKoZIzj0EAwMw +UTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28uLCBM +dGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNTAeFw0yMDA0MDgwODMy +NTZaFw00NTA0MDgwODMyNTZaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpDeWJl +cnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBSb290 +IENBMTUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQLUHSNZDKZmbPSYAi4Io5GdCx4 +wCtELW1fHcmuS1Iggz24FG1Th2CeX2yF2wYUleDHKP+dX+Sq8bOLbe1PL0vJSpSR +ZHX+AezB2Ot6lHhWGENfa4HL9rzatAy2KZMIaY+jQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTrQciu/NWeUUj1vYv0hyCTQSvT +9DAKBggqhkjOPQQDAwNoADBlAjEA2S6Jfl5OpBEHvVnCB96rMjhTKkZEBhd6zlHp +4P9mLQlO4E/0BdGF9jVg3PVys0Z9AjBEmEYagoUeYWmJSwdLZrWeqrqgHkHZAXQ6 +bkU6iYAZezKYVWOr62Nuk22rGwlgMU4= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQfMmPK4TX3+oPyWWa00tNljAKBggqhkjOPQQDAzBIMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS +VVNUIEJSIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTA5NDUwMFoXDTM1MDIxMTA5 +NDQ1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG +A1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABMbLxyjR+4T1mu9CFCDhQ2tuda38KwOE1HaTJddZO0Flax7mNCq7dPYS +zuht56vkPE4/RAiLzRZxy7+SmfSk1zxQVFKQhYN4lGdnoxwJGT11NIXe7WB9xwy0 +QVK5buXuQqOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHOREKv/ +VbNafAkl1bK6CKBrqx9tMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g +PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2JyX3Jvb3Rf +Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l +dC9DTj1ELVRSVVNUJTIwQlIlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 +c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO +PQQDAwNpADBmAjEAlJAtE/rhY/hhY+ithXhUkZy4kzg+GkHaQBZTQgjKL47xPoFW +wKrY7RjEsK70PvomAjEA8yjixtsrmfu3Ubgko6SUeho/5jbiA1czijDLgsfWFBHV +dWNbFJWcHwHP2NVypw87 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFqTCCA5GgAwIBAgIQczswBEhb2U14LnNLyaHcZjANBgkqhkiG9w0BAQ0FADBI +MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE +LVRSVVNUIEJSIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA4NTYzMVoXDTM4MDUw +OTA4NTYzMFowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi +MCAGA1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAK7/CVmRgApKaOYkP7in5Mg6CjoWzckjYaCTcfKr +i3OPoGdlYNJUa2NRb0kz4HIHE304zQaSBylSa053bATTlfrdTIzZXcFhfUvnKLNE +gXtRr90zsWh81k5M/itoucpmacTsXld/9w3HnDY25QdgrMBM6ghs7wZ8T1soegj8 +k12b9py0i4a6Ibn08OhZWiihNIQaJZG2tY/vsvmA+vk9PBFy2OMvhnbFeSzBqZCT +Rphny4NqoFAjpzv2gTng7fC5v2Xx2Mt6++9zA84A9H3X4F07ZrjcjrqDy4d2A/wl +2ecjbwb9Z/Pg/4S8R7+1FhhGaRTMBffb00msa8yr5LULQyReS2tNZ9/WtT5PeB+U +cSTq3nD88ZP+npNa5JRal1QMNXtfbO4AHyTsA7oC9Xb0n9Sa7YUsOCIvx9gvdhFP +/Wxc6PWOJ4d/GUohR5AdeY0cW/jPSoXk7bNbjb7EZChdQcRurDhaTyN0dKkSw/bS +uREVMweR2Ds3OmMwBtHFIjYoYiMQ4EbMl6zWK11kJNXuHA7e+whadSr2Y23OC0K+ +0bpwHJwh5Q8xaRfX/Aq03u2AnMuStIv13lmiWAmlY0cL4UEyNEHZmrHZqLAbWt4N +DfTisl01gLmB1IRpkQLLddCNxbU9CZEJjxShFHR5PtbJFR2kWVki3PaKRT08EtY+ +XTIvAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUZ5Dw1t61 +GNVGKX5cq/ieCLxklRAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG +OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfYnJfcm9vdF9jYV8y +XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQA097N3U9swFrktpSHxQCF16+tI +FoE9c+CeJyrrd6kTpGoKWloUMz1oH4Guaf2Mn2VsNELZLdB/eBaxOqwjMa1ef67n +riv6uvw8l5VAk1/DLQOj7aRvU9f6QA4w9QAgLABMjDu0ox+2v5Eyq6+SmNMW5tTR +VFxDWy6u71cqqLRvpO8NVhTaIasgdp4D/Ca4nj8+AybmTNudX0KEPUUDAxxZiMrc +LmEkWqTqJwtzEr5SswrPMhfiHocaFpVIbVrg0M8JkiZmkdijYQ6qgYF/6FKC0ULn +4B0Y+qSFNueG4A3rvNTJ1jxD8V1Jbn6Bm2m1iWKPiFLY1/4nwSPFyysCu7Ff/vtD +hQNGvl3GyiEm/9cCnnRK3PgTFbGBVzbLZVzRHTF36SXDw7IyN9XxmAnkbWOACKsG +koHU6XCPpz+y7YaMgmo1yEJagtFSGkUPFaUA8JR7ZSdXOUPPfH/mvTWze/EZTN46 +ls/pdu4D58JDUjxqgejBWoC9EV2Ta/vH5mQ/u2kc6d0li690yVRAysuTEwrt+2aS +Ecr1wPrYg1UDfNPFIkZ1cGt5SAYqgpq/5usWDiJFAbzdNpQ0qTUmiteXue4Icr80 +knCDgKs4qllo3UCkGJCy89UDyibK79XH4I9TjvAA46jtn/mtd+ArY0+ew+43u3gJ +hJ65bvspmZDogNOfJA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQXwJB13qHfEwDo6yWjfv/0DAKBggqhkjOPQQDAzBIMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS +VVNUIEVWIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTEwMDAwMFoXDTM1MDIxMTA5 +NTk1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG +A1UEAxMZRC1UUlVTVCBFViBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPEL3YZDIBnfl4XoIkqbz52Yv7QFJsnL46bSj8WeeHsxiamJrSc8ZRCC +/N/DnU7wMyPE0jL1HLDfMxddxfCxivnvubcUyilKwg+pf3VlSSowZ/Rk99Yad9rD +wpdhQntJraOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFH8QARY3 +OqQo5FD4pPfsazK2/umLMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g +PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2V2X3Jvb3Rf +Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l +dC9DTj1ELVRSVVNUJTIwRVYlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 +c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO +PQQDAwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CA +y/m0sRtW9XLS/BnRAjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJb +gfM0agPnIjhQW+0ZT0MW +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFqTCCA5GgAwIBAgIQaSYJfoBLTKCnjHhiU19abzANBgkqhkiG9w0BAQ0FADBI +MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE +LVRSVVNUIEVWIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA5MTAzM1oXDTM4MDUw +OTA5MTAzMlowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi +MCAGA1UEAxMZRC1UUlVTVCBFViBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBANiOo4mAC7JXUtypU0w3uX9jFxPvp1sjW2l1sJkK +F8GLxNuo4MwxusLyzV3pt/gdr2rElYfXR8mV2IIEUD2BCP/kPbOx1sWy/YgJ25yE +7CUXFId/MHibaljJtnMoPDT3mfd/06b4HEV8rSyMlD/YZxBTfiLNTiVR8CUkNRFe +EMbsh2aJgWi6zCudR3Mfvc2RpHJqnKIbGKBv7FD0fUDCqDDPvXPIEysQEx6Lmqg6 +lHPTGGkKSv/BAQP/eX+1SH977ugpbzZMlWGG2Pmic4ruri+W7mjNPU0oQvlFKzIb +RlUWaqZLKfm7lVa/Rh3sHZMdwGWyH6FDrlaeoLGPaxK3YG14C8qKXO0elg6DpkiV +jTujIcSuWMYAsoS0I6SWhjW42J7YrDRJmGOVxcttSEfi8i4YHtAxq9107PncjLgc +jmgjutDzUNzPZY9zOjLHfP7KgiJPvo5iR2blzYfi6NUPGJ/lBHJLRjwQ8kTCZFZx +TnXonMkmdMV9WdEKWw9t/p51HBjGGjp82A0EzM23RWV6sY+4roRIPrN6TagD4uJ+ +ARZZaBhDM7DS3LAaQzXupdqpRlyuhoFBAUp0JuyfBr/CBTdkdXgpaP3F9ev+R/nk +hbDhezGdpn9yo7nELC7MmVcOIQxFAZRl62UJxmMiCzNJkkg8/M3OsD6Onov4/knF +NXJHAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUqvyREBuH +kV8Wub9PS5FeAByxMoAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG +OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfZXZfcm9vdF9jYV8y +XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQCTy6UfmRHsmg1fLBWTxj++EI14 +QvBukEdHjqOSMo1wj/Zbjb6JzkcBahsgIIlbyIIQbODnmaprxiqgYzWRaoUlrRc4 +pZt+UPJ26oUFKidBK7GB0aL2QHWpDsvxVUjY7NHss+jOFKE17MJeNRqrphYBBo7q +3C+jisosketSjl8MmxfPy3MHGcRqwnNU73xDUmPBEcrCRbH0O1P1aa4846XerOhU +t7KR/aypH/KH5BfGSah82ApB9PI+53c0BFLd6IHyTS9URZ0V4U/M5d40VxDJI3IX +cI1QcB9WbMy5/zpaT2N6w25lBx2Eof+pDGOJbbJAiDnXH3dotfyc1dZnaVuodNv8 +ifYbMvekJKZ2t0dT741Jj6m2g1qllpBFYfXeA08mD6iL8AOWsKwV0HFaanuU5nCT +2vFp4LJiTZ6P/4mdm13NRemUAiKN4DV/6PEEeXFsVIP4M7kFMhtYVRFP0OUnR3Hs +7dpn1mKmS00PaaLJvOwiS5THaJQXfuKOKD62xur1NGyfN4gHONuGcfrNlUhDbqNP +gofXNJhuS5N5YHVpD/Aa1VP6IQzCP+k/HxiMkl14p3ZnGbuy6n/pcAlWVqOwDAst +Nl7F6cTVg8uGF5csbBNvh1qvSaYd2804BC5f4ko1Di1L+KIkBI3Y4WNeApI02phh +XBxvWHZks/wCuPWdCg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEDjCCAvagAwIBAgIDD92sMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxHzAdBgNVBAMMFkQtVFJVU1QgUm9vdCBD +QSAzIDIwMTMwHhcNMTMwOTIwMDgyNTUxWhcNMjgwOTIwMDgyNTUxWjBFMQswCQYD +VQQGEwJERTEVMBMGA1UECgwMRC1UcnVzdCBHbWJIMR8wHQYDVQQDDBZELVRSVVNU +IFJvb3QgQ0EgMyAyMDEzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +xHtCkoIf7O1UmI4SwMoJ35NuOpNcG+QQd55OaYhs9uFp8vabomGxvQcgdJhl8Ywm +CM2oNcqANtFjbehEeoLDbF7eu+g20sRoNoyfMr2EIuDcwu4QRjltr5M5rofmw7wJ +ySxrZ1vZm3Z1TAvgu8XXvD558l++0ZBX+a72Zl8xv9Ntj6e6SvMjZbu376Ml1wrq +WLbviPr6ebJSWNXwrIyhUXQplapRO5AyA58ccnSQ3j3tYdLl4/1kR+W5t0qp9x+u +loYErC/jpIF3t1oW/9gPP/a3eMykr/pbPBJbqFKJcu+I89VEgYaVI5973bzZNO98 +lDyqwEHC451QGsDkGSL8swIDAQABo4IBBTCCAQEwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUP5DIfccVb/Mkj6nDL0uiDyGyL+cwDgYDVR0PAQH/BAQDAgEGMIG+ +BgNVHR8EgbYwgbMwdKByoHCGbmxkYXA6Ly9kaXJlY3RvcnkuZC10cnVzdC5uZXQv +Q049RC1UUlVTVCUyMFJvb3QlMjBDQSUyMDMlMjAyMDEzLE89RC1UcnVzdCUyMEdt +YkgsQz1ERT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MDugOaA3hjVodHRwOi8v +Y3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2FfM18yMDEzLmNybDAN +BgkqhkiG9w0BAQsFAAOCAQEADlkOWOR0SCNEzzQhtZwUGq2aS7eziG1cqRdw8Cqf +jXv5e4X6xznoEAiwNStfzwLS05zICx7uBVSuN5MECX1sj8J0vPgclL4xAUAt8yQg +t4RVLFzI9XRKEBmLo8ftNdYJSNMOwLo5qLBGArDbxohZwr78e7Erz35ih1WWzAFv +m2chlTWL+BD8cRu3SzdppjvW7IvuwbDzJcmPkn2h6sPKRL8mpXSSnON065102ctN +h9j8tGlsi6BDB2B4l+nZk3zCRrybN1Kj7Yo8E6l7U0tJmhEFLAtuVqwfLoJs4Gln +tQ5tLdnkwBXxP/oYcuEVbSdbLTAoK59ImmQrme/ydUlfXA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha +ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM +HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 +UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 +tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R +ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM +lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp +/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G +A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G +A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj +dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy +MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl +cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js +L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL +BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni +acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 +o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K +zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 +PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y +Johw1+qRzT65ysCQblrGXnRl11z+o+I= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw +NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV +BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn +ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 +3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z +qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR +p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 +HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw +ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea +HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw +Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh +c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E +RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt +dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku +Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp +3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 +nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF +CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na +xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX +KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICXjCCAeOgAwIBAgIQUs/kjG2gSvc/gpcMgAmMlTAKBggqhkjOPQQDAzBJMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSMwIQYDVQQDExpELVRy +dXN0IFNCUiBSb290IENBIDEgMjAyMjAeFw0yMjA3MDYxMTMwMDBaFw0zNzA3MDYx +MTI5NTlaMEkxCzAJBgNVBAYTAkRFMRUwEwYDVQQKEwxELVRydXN0IEdtYkgxIzAh +BgNVBAMTGkQtVHJ1c3QgU0JSIFJvb3QgQ0EgMSAyMDIyMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAEWZM59oxJZijXYQzIq38Moy3foqR8kito1S5+HkDLtGhJfxKhq39X +nxkuYy5b/mZxDDMPud5rxIjDse/sOUDjlqvb5XuuH9z5r0aaakYGL8c3ZIsXYv6W +w6LuhOCwlzm8o4GPMIGMMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFPEpox4B +Eh09dVZNx1B8xRmqDxi3MA4GA1UdDwEB/wQEAwIBBjBKBgNVHR8EQzBBMD+gPaA7 +hjlodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X3Nicl9yb290X2Nh +XzFfMjAyMi5jcmwwCgYIKoZIzj0EAwMDaQAwZgIxAJf53q5Lj5i1HkB/Mn1NVEPa +ic3CqpI80YIec8/6TJIg+2MnxfVzPQk996dhhozzagIxAOcvfLj1JYw7OR82q431 +hqIu4Xpk2mc5Av7+Mz/Zc7ZYWzr8sqTZYHh3zHmnpq5VvQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFrDCCA5SgAwIBAgIQVNWjlR49lbpyG5rQMSFKujANBgkqhkiG9w0BAQ0FADBJ +MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSMwIQYDVQQDExpE +LVRydXN0IFNCUiBSb290IENBIDIgMjAyMjAeFw0yMjA3MDcwNzMwMDBaFw0zNzA3 +MDcwNzI5NTlaMEkxCzAJBgNVBAYTAkRFMRUwEwYDVQQKEwxELVRydXN0IEdtYkgx +IzAhBgNVBAMTGkQtVHJ1c3QgU0JSIFJvb3QgQ0EgMiAyMDIyMIICIjANBgkqhkiG +9w0BAQEFAAOCAg8AMIICCgKCAgEAryy8jjaM62SvUWrWbjxekTrqmsPKbPuqJ55k +IqlA37koRVrsU2EWKJjCiqR1eFCE3fogSJIHZUE1ZlESdGGdBwaFOTFXeyg/1Zyl +7FrpHEsnn84nBvM39VLYETMWQTof9WN4ZWOGyb/IAQQfbu7i7KwM7oKS4vYaDT85 ++Z1lk634uQXBPfg3gVbDoP4F7OCUFjojFgTapgqThXJtYTuhjUXW43++Fb02hAj2 +C4NrJqqiveCw56rgrmfE04KlDKmk8DN5DVA/8O+QPSS5f9IgbOqX87+c3EfeCWG9 +lHmVWgJ2NWDERyIN93ZjA9PG+4PGXaut7WklKwNbTSUAQeOMhxdSqOAFK0NNFBPK +5z9DIrw3pHXx9r867zIeru5YhpByugSsQEjvXMR4p6mPJ1rLeuxY8sIIWJBtTQOF +eXEVBQ5OPvnfDwX3XxRIViENM5KxrIzlGP6/D+7gBKq9IfJYtlyJCosYCSIaszXG +ZsL1MxWZgOAI+ZYvE4zu2reIxOk3tddq1zqETatwjNNOFFWgohD8ZNpn6PHLM93J +moqPli9Ygdn4mgBDzJD7VXb7huM3ASgMb/TpWU0Vd1FCSsw0uIBDUIHvV6UT26eU +eQ9Lyn4Xfa+jIWTocVVWjwawR+xZD11wWywWQvCGnnXea01ImITiVxi2nIKZZTqL +gHhXDEkCAwEAAaOBjzCBjDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRds4CU +G+WGv2i6FDSk9u5t8t3f5zAOBgNVHQ8BAf8EBAMCAQYwSgYDVR0fBEMwQTA/oD2g +O4Y5aHR0cDovL2NybC5kLXRydXN0Lm5ldC9jcmwvZC10cnVzdF9zYnJfcm9vdF9j +YV8yXzIwMjIuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQA0VC5YGFbNSr2X0/V9K9yv +D1HhTbwhS5P0AEQTBxALJRg+SFmW96Hhk5B4Zho9I+siqwGmjgxRM+ZtjDHurKQB +cDlI3sdmLGsNy3Ofh5LpPkcfuO8v7rdWjEiJ8DinFTmy7sA/F6RzAgicvAaKpMK3 +YWH5w9vE0Hp8Yd6xWJH13WVMLwv46z217Yq+dxy6WQISZnHlmCfODj2vUaJF+YL7 +WqWUcPeLhMNMZSWbe+IfMHCzQI467r3052jFnckpR3EOk8i1SE71ZrsHiHFpa3tI +jm/wEcS0yXAUmCC97afqAdpupZsS/j5EMLPw63VSwPTD+ncmpHeCLW/zKB5OlfAw +94n4LKJQW/K+Mn5sVNtyySpa4By2C9hSmlmh47ABJ8WgFlBm3OuubfSbWz2EbVuH +56mJu2644JtTicD/LkAaiUQuGENnOOR8cl/ZoyklQUE9HHcbZKjDVe5jcWZig/R/ +JpmgVDuhEm1wYs7T+bi9IvzUmtS74jgWL7d9OcKwqQPpnM9+GI123F8Ru+tC7FAJ +PlzskDHYGnK6P2kH7pg0wjSk1toT1qmE8gCGwFS6HhGw4rnEB7SR56rmMVZvsUTE +KmK8ybBlnDT8DBpT3yEXu8JtoQrm8bCqRAlQSTh6XXHiMS4ZsN+VQgR9hIjOCiNn +azidFt4G/ihwOKVarvyD7Q== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd +AqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC +FoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi +1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq +jnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ +wI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/ +WSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy +NsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC +uvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw +IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6 +g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN +9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP +BSeOE6Fuwg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN +8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ +RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 +hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 +ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM +EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 +A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy +WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ +1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 +6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT +91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml +e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p +TpPDpFQUWw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICRzCCAc2gAwIBAgIQFSrdFMkY0aRWQIamJa8HXzAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBH +bWJIMS0wKwYDVQQDDCRUZWxla29tIFNlY3VyaXR5IFNNSU1FIEVDQyBSb290IDIw +MjEwHhcNMjEwMzE4MTEwODMwWhcNNDYwMzE3MjM1OTU5WjBlMQswCQYDVQQGEwJE +RTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBHbWJIMS0wKwYD +VQQDDCRUZWxla29tIFNlY3VyaXR5IFNNSU1FIEVDQyBSb290IDIwMjEwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAASwGY+ia7XHzQ8wmTcMw2Bb8fEnIFU9wJKLq1ehb3OD +IcJDEwxeiarHBTV5k2KQ1l0TH9F6oLyeEKdmfEYKsFdsv+ZUOTghbBJccczTWl9t +t6eG37Pf7sLniUGWNfYvSrWjQjBAMB0GA1UdDgQWBBQrywEMY8NTEqWoV6/QnIP7 +vZA6SzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQD +AwNoADBlAjEA1rxIkodHA8dwOyW2H65GZ3N0ACdL5KUEogPfXiitbl4DyN1onLa/ +lBBIlS8P/xiLAjABQDOel5dNBfJ0VAzNOf1qawnBJD9hjjiht+jXRBURYv8OYTdH +S0B/Sl+yZ1pzdcI= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFtzCCA5+gAwIBAgIQDH5i9XlzO51Djotj7ZGVuDANBgkqhkiG9w0BAQwFADBl +MQswCQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0 +eSBHbWJIMS0wKwYDVQQDDCRUZWxla29tIFNlY3VyaXR5IFNNSU1FIFJTQSBSb290 +IDIwMjMwHhcNMjMwMzI4MTIwOTIyWhcNNDgwMzI3MjM1OTU5WjBlMQswCQYDVQQG +EwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBHbWJIMS0w +KwYDVQQDDCRUZWxla29tIFNlY3VyaXR5IFNNSU1FIFJTQSBSb290IDIwMjMwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDvxQ6LvjLSZ0f/Ckxnsyq/yMPF +keu1xx6R4WaoiItVIIAfUV53l54ZClzHazchfAM2AfSIJdmoLkGq/Ngm4JZAYnmu +V54DOBocsncUPumhctDk4DfRF0btUFx6WMX4K/d1L8+BnlostzqsoFmYBFEM/0nF +UP0e00eFSzNPoje1rwSaJzKdVtU/VWHji2+uUf6X/mkH+mJbJuYUeRWlEziuXze+ +lErWDYAWaaSRsjpJmHWdRhCKXHp/hKXorx7Hq7NaRrWjS/WmIzYARrHbBbYbzp56 +Mlya1XLDnYZNK4TTHrWI2hB4nCLDOyO16xMHvW9T7Jvsm9Nl9QcJ412nmbV+ho7V +Av+3hQnjRxTdlmYYNN4I1d/LGJliCyvsAF1SRNPGlvwyViWRz80ZO5U5PgKHmWO2 +1T40eg8RdYG8fQTKYLQoddcCUd1SAC7H/YnxXPPLpCcSOI+7+4nw5MQ4LL6CoHFh +YpGPSAwvK6mw8csQBOd0vzeQ708qQzWXEsYqcA3eLFVHeWMp9cofagZSHK4tJCKD +Iq/QqjC3Kh//ZSNYZZPIjn1AEDGGeNlVyzww8N5RKgA20idFX9jooSE9fkZWOylF +8R0FCc62QzDcRZAQMEyka4aLPz0vMZFx7ya59r6dsGzfEe5YP0N5hjmA8SYXB5jw +maowLENZFM7t4kAThQIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FJrOrCrsAfplcN6XnfHSAIylo2S7MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgw +FoAUms6sKuwB+mVw3ped8dIAjKWjZLswDQYJKoZIhvcNAQEMBQADggIBAONQ/fVA +FiIJljoNqe+B5y4y8KHxSV57iA0Ecte+Z6i6He5Qu3JuetG7DHIwRsjV1wISFplO +Ht9alu6Pkb6uhvgQd6XEbkdhwPIm2U9haAVIdQgVpaF71biziXnm7fHzYQCGey4x +/qNc+Hk9tFuIe+Ajuw2hF/rLaA2Yd3EI4m1DdGvENsWUQaQA1lctmYqLIBIVAjIO +0knsgUjFaidS17JzVVOWPJ5PTLWg0E9X0GcoSGS+xri67GTPyHvFaucq5llXttbU +1sBnXNmeKAlAv/OpNTFlYAPLGWyClQMeXz/hvepJceVbtwtHFhsgiW2UmQx+iGwd +DfS3IRpZl6zL6L4XH5V8U5uvUFKqjQsur1rXYPIqaSq57lRwGKq99aE/0t2hYxkA ++KcM66N58nBZo/iiEgPsE//kAoY218HDpLXUpMI3RbaUcD3FveujFR3jNnoVaSpW +NDnPpZo2qsjtebzP9s4EUwvaslAjfLw+Jq3wDkO7JsuuwkDeNx8KoFHNY522T9jG +R3y82LTtnovzEeKotT7srnA+fiK7NUgXYGIUkTCjdj2mUTaLHw3dajEcpe3dlqNu +cg8TTaqnqVx4+QMSGJM3RRKJPfi+yr3ZvgzZGGSnyEE+dYIhOH1l9KDUE0sHeCn5 +nX7Mhz/E2i6I3eML3FpRWunZEk+eAtv3BSVR +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICQjCCAcmgAwIBAgIQNjqWjMlcsljN0AFdxeVXADAKBggqhkjOPQQDAzBjMQsw +CQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBH +bWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBFQ0MgUm9vdCAyMDIw +MB4XDTIwMDgyNTA3NDgyMFoXDTQ1MDgyNTIzNTk1OVowYzELMAkGA1UEBhMCREUx +JzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkGA1UE +AwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgRUNDIFJvb3QgMjAyMDB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABM6//leov9Wq9xCazbzREaK9Z0LMkOsVGJDZos0MKiXrPk/O +tdKPD/M12kOLAoC+b1EkHQ9rK8qfwm9QMuU3ILYg/4gND21Ju9sGpIeQkpT0CdDP +f8iAC8GXs7s1J8nCG6NCMEAwHQYDVR0OBBYEFONyzG6VmUex5rNhTNHLq+O6zd6f +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA +MGQCMHVSi7ekEE+uShCLsoRbQuHmKjYC2qBuGT8lv9pZMo7k+5Dck2TOrbRBR2Di +z6fLHgIwN0GMZt9Ba9aDAEH9L1r3ULRn0SyocddDypwnJJGDSA3PzfdUga/sf+Rn +27iQ7t0l +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIQIZxULej27HF3+k7ow3BXlzANBgkqhkiG9w0BAQwFADBj +MQswCQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0 +eSBHbWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBSU0EgUm9vdCAy +MDIzMB4XDTIzMDMyODEyMTY0NVoXDTQ4MDMyNzIzNTk1OVowYzELMAkGA1UEBhMC +REUxJzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkG +A1UEAwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgUlNBIFJvb3QgMjAyMzCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAO01oYGA88tKaVvC+1GDrib94W7zgRJ9 +cUD/h3VCKSHtgVIs3xLBGYSJwb3FKNXVS2xE1kzbB5ZKVXrKNoIENqil/Cf2SfHV +cp6R+SPWcHu79ZvB7JPPGeplfohwoHP89v+1VmLhc2o0mD6CuKyVU/QBoCcHcqMA +U6DksquDOFczJZSfvkgdmOGjup5czQRxUX11eKvzWarE4GC+j4NSuHUaQTXtvPM6 +Y+mpFEXX5lLRbtLevOP1Czvm4MS9Q2QTps70mDdsipWol8hHD/BeEIvnHRz+sTug +BTNoBUGCwQMrAcjnj02r6LX2zWtEtefdi+zqJbQAIldNsLGyMcEWzv/9FIS3R/qy +8XDe24tsNlikfLMR0cN3f1+2JeANxdKz+bi4d9s3cXFH42AYTyS2dTd4uaNir73J +co4vzLuu2+QVUhkHM/tqty1LkCiCc/4YizWN26cEar7qwU02OxY2kTLvtkCJkUPg +8qKrBC7m8kwOFjQgrIfBLX7JZkcXFBGk8/ehJImr2BrIoVyxo/eMbcgByU/J7MT8 +rFEz0ciD0cmfHdRHNCk+y7AO+oMLKFjlKdw/fKifybYKu6boRhYPluV75Gp6SG12 +mAWl3G0eQh5C2hrgUve1g8Aae3g1LDj1H/1Joy7SWWO/gLCMk3PLNaaZlSJhZQNg ++y+TS/qanIA7AgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtqeX +gj10hZv3PJ+TmpV5dVKMbUcwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS2 +p5eCPXSFm/c8n5OalXl1UoxtRzANBgkqhkiG9w0BAQwFAAOCAgEAqMxhpr51nhVQ +pGv7qHBFfLp+sVr8WyP6Cnf4mHGCDG3gXkaqk/QeoMPhk9tLrbKmXauw1GLLXrtm +9S3ul0A8Yute1hTWjOKWi0FpkzXmuZlrYrShF2Y0pmtjxrlO8iLpWA1WQdH6DErw +M807u20hOq6OcrXDSvvpfeWxm4bu4uB9tPcy/SKE8YXJN3nptT+/XOR0so8RYgDd +GGah2XsjX/GO1WfoVNpbOms2b/mBsTNHM3dA+VKq3dSDz4V4mZqTuXNnQkYRIer+ +CqkbGmVps4+uFrb2S1ayLfmlyOw7YqPta9BO1UAJpB+Y1zqlklkg5LB9zVtzaL1t +xKITDmcZuI1CfmwMmm6gJC3VRRvcxAIU/oVbZZfKTpBQCHpCNfnqwmbU+AGuHrS+ +w6jv/naaoqYfRvaE7fzbzsQCzndILIyy7MMAo+wsVRjBfhnu4S/yrYObnqsZ38aK +L4x35bcF7DvB7L6Gs4a8wPfc5+pbrrLMtTWGS9DiP7bY+A4A7l3j941Y/8+LN+lj +X273CXE2whJdV/LItM3z7gLfEdxquVeEHVlNjM7IDiPCtyaaEBRx/pOyiriA8A4Q +ntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0o82bNSQ3+pCTE4FCxpgm +dTdmQRCsu/WU48IxK63nI1bMNSWSs1A= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c +JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP +mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+ +wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4 +VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/ +AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB +AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun +pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC +dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf +fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm +NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx +H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe ++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA +n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc +biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp +EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA +bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu +YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB +AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW +BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI +QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I +0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni +lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 +B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv +ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg +RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf +Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q +RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD +AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY +JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv +6pZjamVFkpUBtA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH +MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI +2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx +1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ +q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz +tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ +vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV +5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY +1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 +NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG +Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 +8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe +pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe +Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw +EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x +IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG +fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO +Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd +BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx +AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ +oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 +sycX +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug +RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm ++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW +PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM +xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB +Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 +hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg +EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA +FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec +nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z +eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF +hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 +Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep ++OkuE6N36B9K +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICHDCCAaOgAwIBAgIQBT9uoAYBcn3tP8OjtqPW7zAKBggqhkjOPQQDAzBQMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xKDAmBgNVBAMTH0Rp +Z2lDZXJ0IFNNSU1FIEVDQyBQMzg0IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN +NDYwMTE0MjM1OTU5WjBQMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs +IEluYy4xKDAmBgNVBAMTH0RpZ2lDZXJ0IFNNSU1FIEVDQyBQMzg0IFJvb3QgRzUw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQWnVXlttT7+2drGtShqtJ3lT6I5QeftnBm +ICikiOxwNa+zMv83E0qevAED3oTBuMbmZUeJ8hNVv82lHghgf61/6GGSKc8JR14L +HMAfpL/yW7yY75lMzHBrtrrQKB2/vgSjQjBAMB0GA1UdDgQWBBRzemuW20IHi1Jm +wmQyF/7gZ5AurTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAKBggq +hkjOPQQDAwNnADBkAjA3RPUygONx6/Rtz3zMkZrDbnHY0iNdkk2CQm1cYZX2kfWn +CPZql+mclC2YcP0ztgkCMAc8L7lYgl4Po2Kok2fwIMNpvwMsO1CnO69BOMlSSJHW +Dvu8YDB8ZD8SHkV/UT70pg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFajCCA1KgAwIBAgIQBfa6BCODRst9XOa5W7ocVTANBgkqhkiG9w0BAQwFADBP +MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJzAlBgNVBAMT +HkRpZ2lDZXJ0IFNNSU1FIFJTQTQwOTYgUm9vdCBHNTAeFw0yMTAxMTUwMDAwMDBa +Fw00NjAxMTQyMzU5NTlaME8xCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2Vy +dCwgSW5jLjEnMCUGA1UEAxMeRGlnaUNlcnQgU01JTUUgUlNBNDA5NiBSb290IEc1 +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4Gpb2fj5fey1e+9f3Vw0 +2Npd0ctldashfFsA1IJvRYVBiqkSAnIy8BT1A3W7Y5dJD0CZCxoeVqfS0OGr3eUE +G+MfFBICiPWggAn2J5pQ8LrjouCsahSRtWs4EHqiMeGRG7e58CtbyHcJdrdRxDYK +mVNURCW3CTWGFwVWkz1BtwLXYh+KkhGH6hFt6ggR3LF4SEmS9rRRgHgj2P7hVho6 +kBNWNInV4pWLX96yzPs/OLeF9+qevy6hLi9NfWoRLjag/xEIBJVV4Bs7Z5OplFXq +Mu0GOn/Cf+OtEyfRNEGzMMO/tIj4A4Kk3z6reHegWZNx593rAAR7zEg5KOAeoxVp +yDayoQuX31XW75GcpPYW91EK7gMjkdwE/+DdOPYiAwDCB3EaEsnXRiqUG83Wuxvu +v75NUFiwC80wdin1z+W2ai92sLBpatBtZRg1fpO8chfBVULNL8Ilu/T9HaFkIlRd +4p5yQYRucZbqRQe2XnpKhp1zZHc4A9IPU6VVIMRN/2hvVanq3XHkT9mFo3xOKQKe +CwnyGlPMAKbd0TT2DcEwsZwCZKw17aWwKbHSlTMP0iAzvewjS/IZ+dqYZOQsMR8u +4Y0cBJUoTYxYzUvlc4KGjOyo1nlc+2S73AxMKPYXr+Jo1haGmNv8AdwxuvicDvko +Rkrh/ZYGRXkRaBdlXIsmh1sCAwEAAaNCMEAwHQYDVR0OBBYEFNGj1FcdT1XbdUxc +Qp5jFs60xjsfMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MA0GCSqG +SIb3DQEBDAUAA4ICAQAHpwreU7ua63C/sjaQzeSnuPEM5F1aHXhl/Mm4HiMRV3xp +NW0B/1NQvwcOuscBP1gqlHUDqxwLI9wbih43PR1Yj3PZsypv3xCgWwynyrB/uSSi +ATUy5V5GQevYf3PnQumkUSZ3gQqo6w8KUJ1+iiBn/AuOOhHTxYxgGNlLsfzU8bRJ +Tq6H4dH7dqFf8wbPl5YM6Z51gVxTDSL8NuZJbnTbAIWNfCKgjvsQTNRiE1vvS3Im +i/xOio/+lxBTxXiLQmQbX+CJ/bsJf1DgVIUmEWodZflJKdx8Nt/7PffSrO4yjW6m +fTmcRcTKDfU7tHlTpS9Wx1HFikxkXZBDI45rTBd4zOi/9TvkqEjPrZsM3zJK09kS +jiN4DS2vn6+ePAnClwDtOmkccT8539OPxGb17zaUD/PdkraWX5Cm3XOqpiCUlCVq +CQxy5BMjYEyjyhcue2cA29DN6nofOSZXiTB3y07llUVPX/s2XD35ILU6ECVPkzJa +7sGW6OlWBLBJYU3seKidGMH/2OovVu+VK3sEXmfjVUDtOQT5C3n1aoxcD4makMfN +i97bJjWhbs2zQvKiDzsMjpP/FM/895P35EEIbhlSEQ9TGXN4DM/YhYH4rVXIsJ5G +Y6+cUu5cv/DAWzceCSDSPiPGoRVKDjZ+MMV5arwiiNkMUkAf3U4PZyYW0q0XHA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp +Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 +MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ +bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS +7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp +0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS +B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 +BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ +LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 +DXZDjC5Ty3zfDBeWUA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBN +MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMT +HERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN +NDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs +IEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS87IE+ +ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG0 +2C+JFvuUAT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgp +wgscONyfMXdcvyej/Cestyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZM +pG2T6T867jp8nVid9E6P/DsjyG244gXazOvswzH016cpVIDPRFtMbzCe88zdH5RD +nU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnVDdXifBBiqmvwPXbzP6Po +sMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9qTXeXAaDx +Zre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cd +Lvvyz6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvX +KyY//SovcfXWJL5/MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNe +XoVPzthwiHvOAbWWl9fNff2C+MIkwcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPL +tgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4EFgQUUTMc7TZArxfTJc1paPKv +TiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN +AQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw +GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7H +PNtQOa27PShNlnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLF +O4uJ+DQtpBflF+aZfTCIITfNMBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQ +REtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/u4cnYiWB39yhL/btp/96j1EuMPik +AdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9GOUrYU9DzLjtxpdRv +/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh47a+ +p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilw +MUc/dNAUFvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WF +qUITVuwhd4GTWgzqltlJyqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCK +ovfepEWFJqgejF0pW8hL2JpqA15w8oVPbEtoL8pU9ozaMv7Da4M/OMZ+ +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg +RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y +ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If +xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV +ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO +DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ +jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/ +CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi +EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM +fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY +uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK +chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t +9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2 +SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd ++SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc +fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa +sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N +cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N +0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie +4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI +r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 +/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm +gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00 +MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV +wedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe +rNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341 +68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh +4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp +UhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o +abw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc +3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G +KubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt +hfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO +Tk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt +zCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD +ggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC +MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2 +cDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN +qXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5 +YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv +b2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2 +8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k +NSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj +ZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp +q1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt +nh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa +GMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg +Fyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J +WpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB +rrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp ++ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1 +ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i +Ucw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz +PtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og +/zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH +oycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI +yV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1Ud +EwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwzJQTU7tD2 +A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL +MAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT +ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2f +BluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn +g/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl +fF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K +WWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha +B0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc +hLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR +TUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD +mbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z +ohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y +4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza +8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 +MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf +qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW +n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym +c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ +O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 +o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j +IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq +IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz +8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh +vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l +7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG +cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD +ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC +roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga +W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n +lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE ++V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV +csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd +dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg +KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM +HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 +WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMzAeFw0wNjExMjQxOTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDM +V0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNggDhoB +4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUr +H556VOijKTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd +8lyyBTNvijbO0BNO/79KDDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9Cabwv +vWhDFlaJKjdhkf2mrk7AyxRllDdLkgbvBNDInIjbC3uBr7E9KsRlOni27tyAsdLT +mZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwpp5ijJUMv7/FfJuGITfhe +btfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8nT8KKdjc +T5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDt +WAEXMJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZ +c6tsgLjoC2SToJyMGf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A +4iLItLRkT9a6fUg+qGkM17uGcclzuD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYD +VR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHTBgkrBgEEAb5YAAMwgcUwgZMG +CCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmljYXRlIGNvbnN0 +aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0 +aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVu +dC4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2Nw +czALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4G +A1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4ywLQoUmkRzBFMQswCQYDVQQGEwJC +TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UEAxMSUXVvVmFkaXMg +Um9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZVqyM0 +7ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSem +d1o417+shvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd ++LJ2w/w4E6oM3kJpK27zPOuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B +4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadN +t54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp8kokUvd0/bpO5qgdAm6x +DYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBCbjPsMZ57 +k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6s +zHXug/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0j +Wy10QJLZYxkNc91pvGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeT +mJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK +4SVhM7JZG+Ju1zdXtg2pEto= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 +MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR +/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu +FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR +U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c +ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR +FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k +A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw +eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl +sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp +VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q +A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ +ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD +ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI +FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv +oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg +u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP +0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf +3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl +8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ +DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN +PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ +ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICajCCAfCgAwIBAgIUNi2PcoiiKCfkAP8kxi3k6/qdtuEwCgYIKoZIzj0EAwMw +ZDELMAkGA1UEBhMCUFQxKjAoBgNVBAoMIURpZ2l0YWxTaWduIENlcnRpZmljYWRv +cmEgRGlnaXRhbDEpMCcGA1UEAwwgRElHSVRBTFNJR04gR0xPQkFMIFJPT1QgRUNE +U0EgQ0EwHhcNMjEwMTIxMTEwNzUwWhcNNDYwMTE1MTEwNzUwWjBkMQswCQYDVQQG +EwJQVDEqMCgGA1UECgwhRGlnaXRhbFNpZ24gQ2VydGlmaWNhZG9yYSBEaWdpdGFs +MSkwJwYDVQQDDCBESUdJVEFMU0lHTiBHTE9CQUwgUk9PVCBFQ0RTQSBDQTB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABG4Lo6szTRzqSuj8BI0UoH3wCCxfg6uT0dJ7utdJ +fY/sElBf1LnL5fD5M2MfyVfsQNgRC5foUhbMKY70BoYeONw9V8Tuqr3IVAQmWicT +UUc9Hx8ajqiVpDPQzEfMbbj8SKNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSME +GDAWgBTOr0qLGnXi8TjnAvAWrV7qZNV7tDAdBgNVHQ4EFgQUzq9Kixp14vE45wLw +Fq1e6mTVe7QwDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMAqIxHGc +RANNjbTHvKiu2TAnNWprFmPX/OdZ4aeJG0wxmiNVRObzQyHVRydvbVcBqgIxAPuy +6uKXf1G1n0jrvG81iahkcKtXds3AxhRgyn/iggBz98w16o4km+UIWccEjHN4/g== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFtTCCA52gAwIBAgIUXVnIyqsJV/XmtdoplARq/8XUlYcwDQYJKoZIhvcNAQEN +BQAwYjELMAkGA1UEBhMCUFQxKjAoBgNVBAoMIURpZ2l0YWxTaWduIENlcnRpZmlj +YWRvcmEgRGlnaXRhbDEnMCUGA1UEAwweRElHSVRBTFNJR04gR0xPQkFMIFJPT1Qg +UlNBIENBMB4XDTIxMDEyMTEwNTAzNFoXDTQ2MDExNTEwNTAzNFowYjELMAkGA1UE +BhMCUFQxKjAoBgNVBAoMIURpZ2l0YWxTaWduIENlcnRpZmljYWRvcmEgRGlnaXRh +bDEnMCUGA1UEAwweRElHSVRBTFNJR04gR0xPQkFMIFJPT1QgUlNBIENBMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyIe2ONMc8N4S+IPHxIriibi0Inp4 ++AxmUWh2NwrVT8JaCLgWXPdyAQk3hIEqVGvXktBs+qinQxI06w7bNw8p/ooxUULo +S5yQqMgsEdP9oCl+zt6U9oLgWLRORSXxIvI90w97VBrcMrbWUU5+QbRXuCzGuQ4u +ylfx1cjTWOel6UIRrtMgJZRp14/Kog3D058HaD8V0mcuU/12gpsLc6kpDZ4RkxQI +mOyeVBJKVqIGFexrbC6SYC6GDa6CH1FN47IH1xAZVyL2qWlEhPPZPaAGv8yIfn/1 +zlulwipqdELqb6b/+Wix0F+9kdJVbzNXTB6d5OKLwYVloOBqnAAAiJLdWAgW8nAx +qBzh3r1OcenWvn61oVrDTfe/m72UpP31qlOTRskmAQRwxKBxus4lZvuRflVw7kkK +TWJ/wlCacvIYZ53pRag0hOj4gfbRWiIeB087s3/dEaVz3L6pGTppqW0bMuKJqqUn +C1p+dOIPZDldfly5wRf8x41eyewk7dLyP3qERTcCvj5rWcTmWxZtwKqeqrVZLixw +VZzMmZaYJFTRjtrKtBG0t3BDH2+QCyCgqHYTZdvbI1p1S6ELMXcK7n1oYRoTjOpR +flxWo1dMXaHrE2W/VBTM8+7c1+w8l/J4Vrjfclxw/M4G3Z/SBzHv51KRns2618AY +RAcxZUkyaRNK648CAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAW +gBS1Nrw8jBqrLPZZGS2DFNqTJRXWhjAdBgNVHQ4EFgQUtTa8PIwaqyz2WRktgxTa +kyUV1oYwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBDQUAA4ICAQAU+zElODH4 +ygiyI3Y4rfjTWfXMtFcl4US+fvwW7K76Jp9PZxZKVvD97ccZATSOkFot1oBc7HHS +gSWCHgBx35rR1R0iu9Gl82IPtOvcJHP+plbNmhTFBDUWMaIH66UA4rb4X3L9P2FJ +jt5+TTjXeh50N2xR3L4ABLg4FPMgwe2bpyP9DUKEHX/yc8PQeGPxn+zXW+nxvmyg +SwOejWnhFNqIEIEjU//aVCsLxrmWlQQYRvN7qJfYW2ik5DgcDkXlmNMJrppe7LN5 +DTly8vSUnQ6eYCLmqPZMhc0HgjpoOc09X+M49LavO2tKn2BRRaJAAuWqDOM+0XjU +onScJroFmihwSj6mC9AdSfC6+K5BEH6kBxK9qM8pPVe7x/FDRwA+rnAYWiB7Ccs6 +OnCA5UxgmMEVwR1K98jwm+FyreddaFgLBLGMvJ+3+26LWwRV++sjVdd4UNoly74n +NrskGnkcUdH+E7v/eCzcpL4v9sVLU8+nTJlecKxZiASuZAS/e6Z6TdPod72hflAV +8+9JMIVNIVeq2yx1l62BAYeisXCdHgZaA2CxP6ZtgizUFLGBpeg9iB20cixYN4qO +OJS4c92p4Lj2d6KzfFjermk6tYulGrvy2HQGnP1icyAhdrF+cJ4Z1OsXYhk4mc02 +K0f+McvfueSsCNPYpuvUnn5LZKRVXSsXyQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV +BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu +MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy +MDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx +EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe +NcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH +PWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I +x2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe +QTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR +yyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO +QG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912 +H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ +QfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD +i/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs +nLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1 +rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI +hvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM +tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf +GopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb +lvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka ++elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal +TFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i +nSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3 +gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr +G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os +zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x +L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFgjCCA2qgAwIBAgILWku9WvtPilv6ZeUwDQYJKoZIhvcNAQELBQAwTTELMAkG +A1UEBhMCQVQxIzAhBgNVBAoTGmUtY29tbWVyY2UgbW9uaXRvcmluZyBHbWJIMRkw +FwYDVQQDExBHTE9CQUxUUlVTVCAyMDIwMB4XDTIwMDIxMDAwMDAwMFoXDTQwMDYx +MDAwMDAwMFowTTELMAkGA1UEBhMCQVQxIzAhBgNVBAoTGmUtY29tbWVyY2UgbW9u +aXRvcmluZyBHbWJIMRkwFwYDVQQDExBHTE9CQUxUUlVTVCAyMDIwMIICIjANBgkq +hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAri5WrRsc7/aVj6B3GyvTY4+ETUWiD59b +RatZe1E0+eyLinjF3WuvvcTfk0Uev5E4C64OFudBc/jbu9G4UeDLgztzOG53ig9Z +YybNpyrOVPu44sB8R85gfD+yc/LAGbaKkoc1DZAoouQVBGM+uq/ufF7MpotQsjj3 +QWPKzv9pj2gOlTblzLmMCcpL3TGQlsjMH/1WljTbjhzqLL6FLmPdqqmV0/0plRPw +yJiT2S0WR5ARg6I6IqIoV6Lr/sCMKKCmfecqQjuCgGOlYx8ZzHyyZqjC0203b+J+ +BlHZRYQfEs4kUmSFC0iAToexIiIwquuuvuAC4EDosEKAA1GqtH6qRNdDYfOiaxaJ +SaSjpCuKAsR49GiKweR6NrFvG5Ybd0mN1MkGco/PU+PcF4UgStyYJ9ORJitHHmkH +r96i5OTUawuzXnzUJIBHKWk7buis/UDr2O1xcSvy6Fgd60GXIsUf1DnQJ4+H4xj0 +4KlGDfV0OoIu0G4skaMxXDtG6nsEEFZegB31pWXogvziB4xiRfUg3kZwhqG8k9Me +dKZssCz3AwyIDMvUclOGvGBG85hqwvG/Q/lwIHfKN0F5VVJjjVsSn8VoxIidrPIw +q7ejMZdnrY8XD2zHc+0klGvIg5rQmjdJBKuxFshsSUktq6HQjJLyQUp5ISXbY9e2 +nKd+Qmn7OmMCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFNwuH9FhN3nkq9XVsxJxaD1qaJwiMB8GA1UdIwQYMBaAFNwu +H9FhN3nkq9XVsxJxaD1qaJwiMA0GCSqGSIb3DQEBCwUAA4ICAQCR8EICaEDuw2jA +VC/f7GLDw56KoDEoqoOOpFaWEhCGVrqXctJUMHytGdUdaG/7FELYjQ7ztdGl4wJC +XtzoRlgHNQIw4Lx0SsFDKv/bGtCwr2zD/cuz9X9tAy5ZVp0tLTWMstZDFyySCstd +6IwPS3BD0IL/qMy/pJTAvoe9iuOTe8aPmxadJ2W8esVCgmxcB9CpwYhgROmYhRZf ++I/KARDOJcP5YBugxZfD0yyIMaK9MOzQ0MAS8cE54+X1+NZK3TTN+2/BT+MAi1bi +kvcoskJ3ciNnxz8RFbLEAwW+uxF7Cr+obuf/WEPPm2eggAe2HcqtbepBEX4tdJP7 +wry+UUTF72glJ4DjyKDUEuzZpTcdN3y0kcra1LGWge9oXHYQSa9+pTeAsRxSvTOB +TI/53WXZFM2KJVj04sWDpQmQ1GwUY7VA3+vA/MRYfg0UFodUJ25W5HCEuGwyEn6C +MUO+1918oa2u1qsgEu8KwxCMSZY13At1XrFP1U80DhEgB3VDRemjEdqso5nCtnkn +4rnvyOL2NSl6dPrFf4IFYqYK6miyeUcGbvJXqBUzxvd4Sj1Ce2t+/vdG6tHrju+I +aFvowdlxfv1k7/9nR4hYJS8+hge9+6jlgqispdNpQ80xiEmEU5LAsTkbOYMBMMTy +qfrQA71yN2BWHzZ8vTmR9W0Nv3vXkg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQG +EwJVUzETMBEGA1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMx +IDAeBgNVBAMTF2VtU2lnbiBFQ0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAw +MFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln +biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQDExdlbVNpZ24gRUND +IFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd6bci +MK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4Ojavti +sIGJAnB9SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0O +BBYEFPtaSNCAIEDyqOkAB2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB +Af8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQC02C8Cif22TGK6Q04ThHK1rt0c +3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwUZOR8loMRnLDRWmFLpg9J +0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG +EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo +bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g +RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ +TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s +b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0 +WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS +fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB +zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq +hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB +CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD ++JbNR6iC8hZVdyR+EhCVBCyj +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkG +A1UEBhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEg +SW5jMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAw +MFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln +biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNpZ24gUm9v +dCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+upufGZ +BczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZ +HdPIWoU/Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH +3DspVpNqs8FqOp099cGXOFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvH +GPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4VI5b2P/AgNBbeCsbEBEV5f6f9vtKppa+c +xSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleoomslMuoaJuvimUnzYnu3Yy1 +aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+XJGFehiq +TbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87 +/kOXSTKZEhVb3xEp/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4 +kqNPEjE2NuLe/gDEo2APJ62gsIq1NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrG +YQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9wC68AivTxEDkigcxHpvOJpkT ++xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQBmIMMMAVSKeo +WXzhriKi4gp6D/piq1JM4fHfyr6DDUI= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYD +VQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBU +ZWNobm9sb2dpZXMgTGltaXRlZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBH +MTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgxODMwMDBaMGcxCzAJBgNVBAYTAklO +MRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVkaHJhIFRlY2hub2xv +Z2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQz +f2N4aLTNLnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO +8oG0x5ZOrRkVUkr+PHB1cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aq +d7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHWDV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhM +tTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ6DqS0hdW5TUaQBw+jSzt +Od9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrHhQIDAQAB +o0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQD +AgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31x +PaOfG1vR2vjTnGs2vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjM +wiI/aTvFthUvozXGaCocV685743QNcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6d +GNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q+Mri/Tm3R7nrft8EI6/6nAYH +6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeihU80Bv2noWgby +RQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx +iN66zB+Afko= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP +Hx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr +ba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL +MeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1 +yHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr +VwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/ +nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG +XUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj +vbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt +Z8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g +N53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC +nlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBOZXR3b3JraW5nMB4XDTEwMDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SEHi3y +YJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbua +kCNrmreIdIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRL +QESxG9fhwoXA3hA/Pe24/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp +6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gbh+0t+nvujArjqWaJGctB+d1ENmHP4ndG +yH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNVHQ4EFgQUBx/S55zawm6i +QLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfO +tDIuUFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzu +QY0x2+c06lkh1QF612S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZ +Lgo/bNjR9eUJtGxUAArgFU2HdW23WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4u +olu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9/ZFvgrG+CJPbFEfxojfHRZ48 +x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVz +dCBQcmVtaXVtMB4XDTEwMDEyOTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkG +A1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1U +cnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxBLf +qV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtnBKAQ +JG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ ++jjeRFcV5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrS +s8PhaJyJ+HoAVt70VZVs+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5 +HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmdGPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d7 +70O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5Rp9EixAqnOEhss/n/fauG +V+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NIS+LI+H+S +qHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S +5u046uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4Ia +C1nEWTJ3s7xgaVY5/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TX +OwF0lkLgAOIua+rF7nKsu7/+6qqo+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYE +FJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByvMiPIs0laUZx2 +KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg +Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B +8OWycvpEgjNC6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQ +MKSOyARiqcTtNd56l+0OOF6SL5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc +0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK+4w1IX2COPKpVJEZNZOUbWo6xbLQ +u4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmVBtWVyuEklut89pMF +u+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFgIxpH +YoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8 +GKa1qF60g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaO +RtGdFNrHF+QFlozEJLUbzxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6e +KeC2uAloGRwYQw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMC +VVMxFDASBgNVBAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQ +cmVtaXVtIEVDQzAeFw0xMDAxMjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJ +BgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1UcnVzdDEgMB4GA1UEAwwXQWZmaXJt +VHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNMF4bFZ0D +0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQN8O9 +ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0G +A1UdDgQWBBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/Vs +aobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6I +flc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE +AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG +EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM +FUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC +REUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp +Nb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM +VD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+ +SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ +4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L +cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi +eowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG +A1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3 +DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j +vZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP +DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc +maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D +lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv +KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICMTCCAbagAwIBAgIMC3MoERh0MBzvbwiEMAoGCCqGSM49BAMDMEsxCzAJBgNV +BAYTAkRFMQ0wCwYDVQQKDARBdG9zMS0wKwYDVQQDDCRBdG9zIFRydXN0ZWRSb290 +IFJvb3QgQ0EgRUNDIEcyIDIwMjAwHhcNMjAxMjE1MDgzOTEwWhcNNDAxMjEwMDgz +OTA5WjBLMQswCQYDVQQGEwJERTENMAsGA1UECgwEQXRvczEtMCsGA1UEAwwkQXRv +cyBUcnVzdGVkUm9vdCBSb290IENBIEVDQyBHMiAyMDIwMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAEyFyAyk7CKB9XvzjmYSP80KlblhYWwwxeFaWQCf84KLR6HgrWUyrB +u5BAdDfpgeiNL2gBNXxSLtj0WLMRHFvZhxiTkS3sndpsnm2ESPzCiQXrmBMCAWxT +Hg5JY1hHsa/Co2MwYTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFFsfxHFs +shufvlwfjP2ztvuzDgmHMB0GA1UdDgQWBBRbH8RxbLIbn75cH4z9s7b7sw4JhzAO +BgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMDaQAwZgIxAOzgmf3d5FTByx/oPijX +FVlKgspTMOzrNqW5yM6TR1bIYabhbZJTlY/241VT8N165wIxALCH1RuzYPyRjYDK +ohtRSzhUy6oee9flRJUWLzxEeC4luuqQ5OxS7lfsA4TzXtsWDQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICFTCCAZugAwIBAgIQPZg7pmY9kGP3fiZXOATvADAKBggqhkjOPQQDAzBMMS4w +LAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgRUNDIFRMUyAyMDIxMQ0w +CwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTI2MjNaFw00MTA0 +MTcwOTI2MjJaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBDQSBF +Q0MgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMHYwEAYHKoZI +zj0CAQYFK4EEACIDYgAEloZYKDcKZ9Cg3iQZGeHkBQcfl+3oZIK59sRxUM6KDP/X +tXa7oWyTbIOiaG6l2b4siJVBzV3dscqDY4PMwL502eCdpO5KTlbgmClBk1IQ1SQ4 +AjJn8ZQSb+/Xxd4u/RmAo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR2 +KCXWfeBmmnoJsmo7jjPXNtNPojAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMD +aAAwZQIwW5kp85wxtolrbNa9d+F851F+uDrNozZffPc8dz7kUK2o59JZDCaOMDtu +CCrCp1rIAjEAmeMM56PDr9NJLkaCI2ZdyQAUEv049OGYa3cpetskz2VAv9LcjBHo +9H1/IISpQuQo +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFfzCCA2egAwIBAgIMR7opRlU+FpKXsKtAMA0GCSqGSIb3DQEBDAUAMEsxCzAJ +BgNVBAYTAkRFMQ0wCwYDVQQKDARBdG9zMS0wKwYDVQQDDCRBdG9zIFRydXN0ZWRS +b290IFJvb3QgQ0EgUlNBIEcyIDIwMjAwHhcNMjAxMjE1MDg0MTIzWhcNNDAxMjEw +MDg0MTIyWjBLMQswCQYDVQQGEwJERTENMAsGA1UECgwEQXRvczEtMCsGA1UEAwwk +QXRvcyBUcnVzdGVkUm9vdCBSb290IENBIFJTQSBHMiAyMDIwMIICIjANBgkqhkiG +9w0BAQEFAAOCAg8AMIICCgKCAgEAljGFSqoPMv554UOHnPsjt45/DVS9x2KTd+Qc +NQR2owOLIu7EhN2lk25uso4JA+tRFjEXqmkVGA5ndCNe6pp9tTk+PYKpa+H+qRyw +rVpNTHiDQYvP8h1impgEnGPpq2X+SB0kZQdHPrmRLumdm38aNak0sLflcDPvSnJR +tge/YD8qn51U3/PXlElRA1pAqWjdEVlc+HamvFBSEO2s7JXg1INrSdoKT5mD3jKD +SINnlbJ+54GFPc2C98oC7W2IXQiNuDW/KmkwmbtL0UHbRaCTmVGBkDYIqoq26I+z +y+7lRg1ydfVJbOGify+87YSmN+7ewk85Tvae8MnRmzCdSW3h2v8SEIzW5Zl7BbZ9 +sAnHpPiyHDmVOTP0Nc4lYnuwXyDzy234bFIUZESP08ipdgflr3GZLS0EJUh2r8Pn +zEPyB7xKJCQ33fpulAlvTF4BtP5U7COWpV7dhv/pRirx6NzspT2vb6oOD7R1+j4I +uSZFT2aGTLwZuOHVNe6ChMjTqxLnzXMzYnf0F8u9NHYqBc6V5Xh5S56wjfk8WDiR +6l6HOMC3Qv2qTIcjrQQgsX52Qtq7tha6V8iOE/p11QhMrziRqu+P+p9JLlR8Clax +evrETi/Uo/oWitCV5Zem/8P8fA5HWPN/B3sS3Fc/LeOhTVtSTDOHmagJe2x+DvLP +VkKe6wUCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBQgJfMH +/adv8ZbukRBpzJrvfchoeDAdBgNVHQ4EFgQUICXzB/2nb/GW7pEQacya733IaHgw +DgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBDAUAA4ICAQAkK06Y8h0X7dl2JrYw +M+hpRaFRS1LYejowtuQS6r+fTOAEpPY1xv6hMPdThZKtVAVXX5LlKt42J557E0fJ +anWv/PM35wz1PQFztWlR+L1Z0boL+Lq6ZCdDs3yDlYrnnhOW129KlkFJiw4grRbG +96aHW4gSiYuJyhLSVq8iASFG6auYP6eI3uTLKpp1Gfo5XgkF1wMyGrgXUQjHAEB9 +9L74DFn0aXZu06RYW14mc+RCVQZeeEAP0zif7yZRcHSR8XdiAejZy+uh3zkyHbtr +/XH+68+l5hT9AIATxpoASLCZBemugEj7CT9RFLW552BNTcovgSHuUgxletz1iUlM +MJI0WIAyWbEN/yRhD+cKQtB7vPiOJ0c/cJ0n2bYGPaW7y16Prg5Tx5xqbztMD6NA +cKiaB87UblsHotLiVLa9bzNyY61RmOGPdvFqBzgl/vZizl/bY8Jume8G3LneGRro +VD190nZ12V4+MkinjPKecgz4uFi4FyOlFId1WHoAgQciOWpMlKC1otunLMGw8aOb +wEz3bXDqMZ/xrn0+cyjZod/6k/CbsPDizSUgde/ifTIFyZt27su9MR75lJhLJFhW +SMDeBky9pjRd7RZhY3P7GeL6W9iXddRtnmA5XpSLAizrmc5gKm4bjKdLvP025pgf +ZfJ/8eOPTIBGNli2oWXLzhxEdQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFZDCCA0ygAwIBAgIQU9XP5hmTC/srBRLYwiqipDANBgkqhkiG9w0BAQwFADBM +MS4wLAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgUlNBIFRMUyAyMDIx +MQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTIxMTBaFw00 +MTA0MTcwOTIxMDlaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBD +QSBSU0EgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtoAOxHm9BYx9sKOdTSJNy/BBl01Z +4NH+VoyX8te9j2y3I49f1cTYQcvyAh5x5en2XssIKl4w8i1mx4QbZFc4nXUtVsYv +Ye+W/CBGvevUez8/fEc4BKkbqlLfEzfTFRVOvV98r61jx3ncCHvVoOX3W3WsgFWZ +kmGbzSoXfduP9LVq6hdKZChmFSlsAvFr1bqjM9xaZ6cF4r9lthawEO3NUDPJcFDs +GY6wx/J0W2tExn2WuZgIWWbeKQGb9Cpt0xU6kGpn8bRrZtkh68rZYnxGEFzedUln +nkL5/nWpo63/dgpnQOPF943HhZpZnmKaau1Fh5hnstVKPNe0OwANwI8f4UDErmwh +3El+fsqyjW22v5MvoVw+j8rtgI5Y4dtXz4U2OLJxpAmMkokIiEjxQGMYsluMWuPD +0xeqqxmjLBvk1cbiZnrXghmmOxYsL3GHX0WelXOTwkKBIROW1527k2gV+p2kHYzy +geBYBr3JtuP2iV2J+axEoctr+hbxx1A9JNr3w+SH1VbxT5Aw+kUJWdo0zuATHAR8 +ANSbhqRAvNncTFd+rrcztl524WWLZt+NyteYr842mIycg5kDcPOvdO3GDjbnvezB +c6eUWsuSZIKmAMFwoW4sKeFYV+xafJlrJaSQOoD0IJ2azsct+bJLKZWD6TWNp0lI +pw9MGZHQ9b8Q4HECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU +dEmZ0f+0emhFdcN+tNzMzjkz2ggwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB +DAUAA4ICAQAjQ1MkYlxt/T7Cz1UAbMVWiLkO3TriJQ2VSpfKgInuKs1l+NsW4AmS +4BjHeJi78+xCUvuppILXTdiK/ORO/auQxDh1MoSf/7OwKwIzNsAQkG8dnK/haZPs +o0UvFJ/1TCplQ3IM98P4lYsU84UgYt1UU90s3BiVaU+DR3BAM1h3Egyi61IxHkzJ +qM7F78PRreBrAwA0JrRUITWXAdxfG/F851X6LWh3e9NpzNMOa7pNdkTWwhWaJuyw +xfW70Xp0wmzNxbVe9kzmWy2B27O3Opee7c9GslA9hGCZcbUztVdF5kJHdWoOsAgM +rr3e97sPWD2PAzHoPYJQyi9eDF20l74gNAf0xBLh7tew2VktafcxBPTy+av5EzH4 +AXcOPUIjJsyacmdRIXrMPIWo6iFqO9taPKU0nprALN+AnCng33eU0aKAQv9qTFsR +0PXNor6uzFFcw9VUewyu1rkGd4Di7wcaaMxZUa1+XGdrudviB0JbuAEFWDlN5LuY +o7Ey7Nmj1m+UI/87tyll5gfp77YZ6ufCOB0yiJA8EytuzO+rdwY0d4RPcuSBhPm5 +dDTedk+SKlOxJTnbPP/lPqYO5Wue/9vsL3SD3460s6neFE3/MaNFcyT6lSnMEpcE +oji2jbDwN/zIIX8/syQbPYtuzE2wFg2WHYMfRsCbvUOZ58SWLs5fyQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg +MB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx +MjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSNjET +MBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQssgrRI +xutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1k +ZguSgMpE3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxD +aNc9PIrFsmbVkJq3MQbFvuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJw +LnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqMPKq0pPbzlUoSB239jLKJz9CgYXfIWHSw +1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+azayOeSsJDa38O+2HBNX +k7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05OWgtH8wY2 +SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/h +bguyCLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4n +WUx2OVvq+aWh2IMP0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpY +rZxCRXluDocZXFSxZba/jJvcE+kNb7gu3GduyYsRtYQUigAZcIN5kZeR1Bonvzce +MgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNVHSMEGDAWgBSu +bAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN +nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGt +Ixg93eFyRJa0lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr61 +55wsTLxDKZmOMNOsIeDjHfrYBzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLj +vUYAGm0CuiVdjaExUd1URhxN25mW7xocBFymFe944Hn+Xds+qkxV/ZoVqW/hpvvf +cDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr3TsTjxKM4kEaSHpz +oHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB10jZp +nOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfs +pA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v +JJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R +8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4 +5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 +MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 +RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT +gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm +KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd +QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ +XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o +LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU +RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp +jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK +6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX +mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs +Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH +WD9f +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc +8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke +hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI +KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg +515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO +xwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG +A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv +b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw +MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i +YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT +aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ +jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp +xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp +1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG +snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ +U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8 +9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B +AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz +yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE +38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP +AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad +DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME +HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYx +CzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQD +ExNHbG9iYWxTaWduIFJvb3QgRTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAw +MDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex +HDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkBjtjq +R+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGdd +yXqBPCCjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ +7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZkvLtoURMMA/cVi4RguYv/Uo7njLwcAjA8 ++RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUA +MEYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYD +VQQDExNHbG9iYWxTaWduIFJvb3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMy +MDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYt +c2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08EsCVeJ +OaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQG +vGIFAha/r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud +316HCkD7rRlr+/fKYIje2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo +0q3v84RLHIf8E6M6cqJaESvWJ3En7YEtbWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSE +y132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvjK8Cd+RTyG/FWaha/LIWF +zXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD412lPFzYE ++cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCN +I/onccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzs +x2sZy/N78CsHpdlseVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqa +ByFrgY/bxFn63iLABJzjqls2k+g9vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC +4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEMBQADggIBAHx4 +7PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg +JuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti +2kM3S+LGteWygxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIk +pnnpHs6i58FZFZ8d4kuaPp92CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRF +FRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZmOUdkLG5NrmJ7v2B0GbhWrJKsFjLt +rWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qqJZ4d16GLuc1CLgSk +ZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwyeqiv5 +u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP +4vkYxboznxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6 +N3ec592kD3ZDZopD8p/7DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3 +vouXsXgxT7PntgMTzlSdriVZzH81Xwj3QEUxeCp6 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICITCCAaegAwIBAgIQdlP+qicdlUZd1vGe5biQCjAKBggqhkjOPQQDAzBSMQsw +CQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEoMCYGA1UEAxMf +R2xvYmFsU2lnbiBTZWN1cmUgTWFpbCBSb290IEU0NTAeFw0yMDAzMTgwMDAwMDBa +Fw00NTAzMTgwMDAwMDBaMFIxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxT +aWduIG52LXNhMSgwJgYDVQQDEx9HbG9iYWxTaWduIFNlY3VyZSBNYWlsIFJvb3Qg +RTQ1MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE+XmLgUc3iZY/RUlQfxomC5Myfi7A +wKcImsNuj5s+CyLsN1O3b4qwvCc3S22pRjvZH/+loUS7LXO/nkEHXFObUQg6Wrtv +OMcWkXjCShNpHYLfWi8AiJaiLhx0+Z1+ZjeKo0IwQDAOBgNVHQ8BAf8EBAMCAYYw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU3xNei1/CQAL9VreUTLYe1aaxFJYw +CgYIKoZIzj0EAwMDaAAwZQIwE7C+13EgPuSrnM42En1fTB8qtWlFM1/TLVqy5IjH +3go2QjJ5naZruuH5RCp7isMSAjEAoGYcToedh8ntmUwbCu4tYMM3xx3NtXKw2cbv +vPL/P/BS3QjnqmR5w+RpV5EvpMt8 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFcDCCA1igAwIBAgIQdlP+qExQq5+NMrUdA49X3DANBgkqhkiG9w0BAQwFADBS +MQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEoMCYGA1UE +AxMfR2xvYmFsU2lnbiBTZWN1cmUgTWFpbCBSb290IFI0NTAeFw0yMDAzMTgwMDAw +MDBaFw00NTAzMTgwMDAwMDBaMFIxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i +YWxTaWduIG52LXNhMSgwJgYDVQQDEx9HbG9iYWxTaWduIFNlY3VyZSBNYWlsIFJv +b3QgUjQ1MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA3HnMbQb5bbvg +VgRsf+B1zC0FSehL3FTsW3eVcr9/Yp2FqYokUF9T5dt0b6QpWxMqCa2axS/C93Y7 +oUVGqkPmJP4rsG8ycBlGWnkmL/w9fV9ky1fMYWGo2ZVu45Wgbn9HEhjW7wPJ+4r6 +mr2CFalVd0sRT1nga8Nx8wzYVNWBaD4TuRUuh4o8RCc2YiRu+CwFcjBhvUKRI8Sd +JafZVJoUozGtgHkMp2NsmKOsV0czH2WW4dDSNdr5cfehpiW1QV3fPmDY0fafpfK4 +zBOqj/mybuGDLZPdPoUa3eixXCYBy0mF/PzS1H+FYoZ0+cvsNSKiDDCPO6t561by ++kLz7fkfRYlAKa3qknTqUv1WtCvaou11wm6rzlKQS/be8EmPmkjUiBltRebMjLnd +ZGBgAkD4uc+8WOs9hbnGCtOcB2aPxxg5I0bhPB6jL1Bhkgs9K2zxo0c4V5GrDY/G +nU0E0iZSXOWl/SotFioBaeepfeE2t7Eqxdmxjb25i87Mi6E+C0jNUJU0xNgIWdhr +JvS+9dQiFwBXya6bBDAznwv731aiyW5Udtqxl2InWQ8RiiIbZJY/qPG3JEqNPFN8 +bYN2PbImSHP1RBYBLQkqjhaWUNBzBl27IkiCTApGWj+A/1zy8pqsLAjg1urwEjiB +T6YQ7UarzBacC89kppkChURnRq39TecCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgGG +MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFKCTFShu7o8IsjXGnmJ5dKexDit7 +MA0GCSqGSIb3DQEBDAUAA4ICAQBFCvjRXKxigdAE17b/V1GJCwzL3iRlN/urnu1m +9OoMGWmJuBmxMFa02fb3vsaul8tF9hGMOjBkTMGfWcBGQggGR2QXeOCVBwbWjKKs +qdk/03tWT/zEhyjftisWI8CfH1vj1kReIk8jBIw1FrV5B4ZcL5fi9ghkptzbqIrj +pHt3DdEpkyggtFOjS05f3sH2dSP8Hzx4T3AxeC+iNVRxBKzIxG3D9pGx/s3uRG6B +9kDFPioBv6tMsQM/DRHkD9Ik4yKIm59fRz1RSeAJN34XITF2t2dxSChLJdcQ6J9h +WRbFPjJOHwzOo8wP5McRByIvOAjdW5frQmxZmpruetCd38XbCUMuCqoZPWvoajB6 +V+a/s2o5qY/j8U9laLa9nyiPoRZaCVA6Mi4dL0QRQqYA5jGY/y2hD+akYFbPedey +Ttew+m4MVyPHzh+lsUxtGUmeDn9wj3E/WCifdd1h4Dq3Obbul9Q1UfuLSWDIPGau +l+6NJllXu3jwelAwCbBgqp9O3Mk+HjrcYpMzsDpUdG8sMUXRaxEyamh29j32ahNe +JJjn6h2az3iCB2D3TRDTgZpFjZ6vm9yAx0OylWikww7oCkcVv1Qz3AHn1aYec9h6 +sr8vreNVMJ7fDkG84BH1oQyoIuHjAKNOcHyS4wTRekKKdZBZ45vRTKJkvXN5m2/y +s8H2PA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh +MB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE +YWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3 +MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo +ZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg +MiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN +ADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA +PVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w +wdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi +EqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY +avx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+ +YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE +sNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h +/t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5 +IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD +ggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy +OO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P +TMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ +HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER +dEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf +ReYNnyicsbkqWletNw+vHX/bvZ8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT +EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp +ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz +NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH +EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE +AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD +E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH +/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy +DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh +GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR +tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA +AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX +WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu +9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr +gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo +2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI +4uJEvlz36hz1 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl +MCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp +U3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw +NjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE +ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp +ZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3 +DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf +8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN ++lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0 +X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa +K4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA +1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G +A1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR +zt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0 +YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD +bGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w +DQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3 +L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D +eruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl +xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp +VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY +WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs +ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw +MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj +aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp +Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg +nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 +HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N +Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN +dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 +HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G +CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU +sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 +4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg +8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 +mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYD +VQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgw +MTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0g +UjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wWTAT +BgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkWymOx +uYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNV +HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/ ++wpu+74zyTyjhNUwCgYIKoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147 +bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo +27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w +Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw +TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl +qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH +szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8 +Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk +MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 +wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p +aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN +VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb +C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe +QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy +h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4 +7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J +ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef +MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/ +Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT +6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ +0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm +2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb +bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlrsWNBCUaqxElqjANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTukk3LvCvpt +nfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3KgGjSY +6Dlo7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9BuXvAu +MC6C/Pq8tBcKSOWIm8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOdre7k +RXuJVfeKH2JShBKzwkCX44ofR5GmdFrS+LFjKBC4swm4VndAoiaYecb+3yXuPuWg +f9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbuak7MkogwTZq9TwtImoS1mKPV ++3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscszcTJGr61K8Yzo +dDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqjx5RW +Ir9qS34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsRnTKa +G73VululycslaVNVJ1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0kzCq +gc7dGtxRcw1PcOnlthYhGXmy5okLdWTK1au8CcEYof/UVKGFPP0UJAOyh9OktwID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEMBQADggIBAB/Kzt3H +vqGf2SdMC9wXmBFqiN495nFWcrKeGk6c1SuYJF2ba3uwM4IJvd8lRuqYnrYb/oM8 +0mJhwQTtzuDFycgTE1XnqGOtjHsB/ncw4c5omwX4Eu55MaBBRTUoCnGkJE+M3DyC +B19m3H0Q/gxhswWV7uGugQ+o+MePTagjAiZrHYNSVc61LwDKgEDg4XSsYPWHgJ2u +NmSRXbBoGOqKYcl3qJfEycel/FVL8/B/uWU9J2jQzGv6U53hkRrJXRqWbTKH7QMg +yALOWr7Z6v2yTcQvG99fevX4i8buMTolUVVnjWQye+mew4K6Ki3pHrTgSAai/Gev +HyICc/sgCq+dVEuhzf9gR7A/Xe8bVr2XIZYtCtFenTgCR2y59PYjJbigapordwj6 +xLEokCZYCDzifqrXPW+6MYgKBesntaFJ7qBFVHvmJ2WZICGoo7z7GJa7Um8M7YNR +TOlZ4iBgxcJlkoKM8xAfDoqXvneCbT+PHV28SSe9zE8P4c52hgQjxcCMElv924Sg +JPFI/2R80L5cFtHvma3AH/vLrrw4IgYmZNralw4/KBVEqE8AyvCazM90arQ+POuV +7LXTWtiBmelDGDfrs7vRWGJB82bSj6p4lVQgw1oudCvV0b4YacCs1aTPObpRhANl +6WLAYv7YTVWW4tAR+kg0Eeye7QUd5MjWHYbL +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYD +VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG +A1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw +WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz +IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout736G +jOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL2 +4CejQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7 +VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azTL818+FsuVbu/3ZL3pAzcMeGiAjEA/Jdm +ZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV11RZt+cRLInUue4X +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD +VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG +A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw +WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz +IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzuhXyi +QHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvR +HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D +9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8 +p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL +BQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ +SG9uZyBLb25nMRYwFAYDVQQKEw1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25n +a29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2MDMwMjI5NDZaFw00MjA2MDMwMjI5 +NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtvbmcxEjAQBgNVBAcT +CUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMXSG9u +Z2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCziNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFO +dem1p+/l6TWZ5Mwc50tfjTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mI +VoBc+L0sPOFMV4i707mV78vH9toxdCim5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV +9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOesL4jpNrcyCse2m5FHomY +2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj0mRiikKY +vLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+Tt +bNe/JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZb +x39ri1UbSsUgYT2uy1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+ +l2oBlKN8W4UdKjk60FSh0Tlxnf0h+bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YK +TE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsGxVd7GYYKecsAyVKvQv83j+Gj +Hno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwIDAQABo2MwYTAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e +i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEw +DQYJKoZIhvcNAQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG +7BJ8dNVI0lkUmcDrudHr9EgwW62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCk +MpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWldy8joRTnU+kLBEUx3XZL7av9YROXr +gZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov+BS5gLNdTaqX4fnk +GMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDceqFS +3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJm +Ozj/2ZQw9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+ +l6mc1X5VTMbeRRAc6uk7nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6c +JfTzPV4e0hz5sy229zdcxsshTrD3mUcYhcErulWuBurQB7Lcq9CClnXO0lD+mefP +L5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa +LJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG +mpv0 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE +AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw +CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ +BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND +VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb +qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY +HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo +G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA +lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr +IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/ +0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH +k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47 +4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO +m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa +cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl +uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI +KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls +ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG +AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 +VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT +VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG +CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA +cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA +QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA +7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA +cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA +QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA +czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu +aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt +aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud +DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF +BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp +D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU +JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m +AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD +vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms +tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH +7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h +I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA +h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF +d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H +pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsx +CzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJ +WiBGTk1ULVJDTTAeFw0wODEwMjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJ +BgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBG +Tk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALpxgHpMhm5/ +yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcfqQgf +BBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAz +WHFctPVrbtQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxF +tBDXaEAUwED653cXeuYLj2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z +374jNUUeAlz+taibmSXaXvMiwzn15Cou08YfxGyqxRxqAQVKL9LFwag0Jl1mpdIC +IfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mwWsXmo8RZZUc1g16p6DUL +mbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnTtOmlcYF7 +wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peS +MKGJ47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2 +ZSysV4999AeU14ECll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMet +UqIJ5G+GR4of6ygnXYMgrwTJbFaai0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPd9xf3E6Jobd2Sn9R2gzL+H +YJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwOi8vd3d3 +LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD +nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1 +RXxlDPiyN8+sD8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYM +LVN0V2Ue1bLdI4E7pWYjJ2cJj+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf +77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrTQfv6MooqtyuGC2mDOL7Nii4LcK2N +JpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW+YJF1DngoABd15jm +fZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7Ixjp +6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp +1txyM/1d8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B +9kiABdcPUXmsEKvU7ANm5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wok +RqEIr9baRRmW1FMdW4R58MD3R++Lj8UGrp1MYp3/RgT408m2ECVAdf4WqslKYIYv +uu8wd+RU4riEmViAqhOLUTpPSPaLtrM= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQsw +CQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgw +FgYDVQRhDA9WQVRFUy1RMjgyNjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1S +Q00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4MTIyMDA5MzczM1oXDTQzMTIyMDA5 +MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQtUkNNMQ4wDAYDVQQL +DAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNBQyBS +QUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LH +sbI6GA60XYyzZl2hNPk2LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oK +Um8BA06Oi6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqGSM49BAMDA2kAMGYCMQCu +SuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoDzBOQn5IC +MQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJy +v+c= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIEAJiiOTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJO +TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFh +dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQSAtIEczMB4XDTEzMTExNDExMjg0MloX +DTI4MTExMzIzMDAwMFowWjELMAkGA1UEBhMCTkwxHjAcBgNVBAoMFVN0YWF0IGRl +ciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5lZGVybGFuZGVuIFJv +b3QgQ0EgLSBHMzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL4yolQP +cPssXFnrbMSkUeiFKrPMSjTysF/zDsccPVMeiAho2G89rcKezIJnByeHaHE6n3WW +IkYFsO2tx1ueKt6c/DrGlaf1F2cY5y9JCAxcz+bMNO14+1Cx3Gsy8KL+tjzk7FqX +xz8ecAgwoNzFs21v0IJyEavSgWhZghe3eJJg+szeP4TrjTgzkApyI/o1zCZxMdFy +KJLZWyNtZrVtB0LrpjPOktvA9mxjeM3KTj215VKb8b475lRgsGYeCasH/lSJEULR +9yS6YHgamPfJEf0WwTUaVHXvQ9Plrk7O53vDxk5hUUurmkVLoR9BvUhTFXFkC4az +5S6+zqQbwSmEorXLCCN2QyIkHxcE1G6cxvx/K2Ya7Irl1s9N9WMJtxU51nus6+N8 +6U78dULI7ViVDAZCopz35HCz33JvWjdAidiFpNfxC95DGdRKWCyMijmev4SH8RY7 +Ngzp07TKbBlBUgmhHbBqv4LvcFEhMtwFdozL92TkA1CvjJFnq8Xy7ljY3r735zHP +bMk7ccHViLVlvMDoFxcHErVc0qsgk7TmgoNwNsXNo42ti+yjwUOH5kPiNL6VizXt +BznaqB16nzaeErAMZRKQFWDZJkBE41ZgpRDUajz9QdwOWke275dhdU/Z/seyHdTt +XUmzqWrLZoQT1Vyg3N9udwbRcXXIV2+vD3dbAgMBAAGjQjBAMA8GA1UdEwEB/wQF +MAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRUrfrHkleuyjWcLhL75Lpd +INyUVzANBgkqhkiG9w0BAQsFAAOCAgEAMJmdBTLIXg47mAE6iqTnB/d6+Oea31BD +U5cqPco8R5gu4RV78ZLzYdqQJRZlwJ9UXQ4DO1t3ApyEtg2YXzTdO2PCwyiBwpwp +LiniyMMB8jPqKqrMCQj3ZWfGzd/TtiunvczRDnBfuCPRy5FOCvTIeuXZYzbB1N/8 +Ipf3YF3qKS9Ysr1YvY2WTxB1v0h7PVGHoTx0IsL8B3+A3MSs/mrBcDCw6Y5p4ixp +gZQJut3+TcCDjJRYwEYgr5wfAvg1VUkvRtTA8KCWAg8zxXHzniN9lLf9OtMJgwYh +/WA9rjLA0u6NpvDntIJ8CsxwyXmA+P5M9zWEGYox+wrZ13+b8KKaa8MFSu1BYBQw +0aoRQm7TIwIEC8Zl3d1Sd9qBa7Ko+gE4uZbqKmxnl4mUnrzhVNXkanjvSr0rmj1A +fsbAddJu+2gw7OyLnflJNZoaLNmzlTnVHpL3prllL+U9bTpITAjc5CgSKL59NVzq +4BZ+Extq1z7XnvwtdbLBFNUjA9tbbws+eC8N3jONFrdI54OagQ97wUNNVQQXOEpR +1VmiiXTTn74eS9fGbbeIJG9gkaSChVtWQbzQRKtqE77RLFi3EjNYsjdj3BP1lB0/ +QFH1T/U67cjF68IeHRaVesd+QnGTbksVtzDfqu1XhUisHWrdOWnk4Xl4vs4Fv6EM +94B7IWcnMFk= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIx +GDAWBgNVBAcTD0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxp +bXNlbCB2ZSBUZWtub2xvamlrIEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0w +KwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24gTWVya2V6aSAtIEthbXUgU00xNjA0 +BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRpZmlrYXNpIC0gU3Vy +dW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYDVQQG +EwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXll +IEJpbGltc2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklU +QUsxLTArBgNVBAsTJEthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBT +TTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11IFNNIFNTTCBLb2sgU2VydGlmaWthc2kg +LSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr3UwM6q7 +a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y86Ij5iySr +LqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INr +N3wcwv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2X +YacQuFWQfw4tJzh03+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/ +iSIzL+aFCr2lqBs23tPcLG07xxO9WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4f +AJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQUZT/HiobGPN08VFw1+DrtUgxH +V8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh +AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPf +IPP54+M638yclNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4 +lzwDGrpDxpa5RXI4s6ehlj2Re37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c +8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0jq5Rm+K37DwhuJi1/FwcJsoz7UMCf +lo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICWjCCAeGgAwIBAgIQMWjZ2OFiVx7SGUSI5hB98DAKBggqhkjOPQQDAzBvMQsw +CQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2Vh +cmNoIEluc3RpdHV0aW9ucyBDQTEnMCUGA1UEAwweSEFSSUNBIENsaWVudCBFQ0Mg +Um9vdCBDQSAyMDIxMB4XDTIxMDIxOTExMDMzNFoXDTQ1MDIxMzExMDMzM1owbzEL +MAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl +YXJjaCBJbnN0aXR1dGlvbnMgQ0ExJzAlBgNVBAMMHkhBUklDQSBDbGllbnQgRUND +IFJvb3QgQ0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABAcYrZWWlNBcD4L3 +KkD6AsnJPTamowRqwW2VAYhgElRsXKIrbhM6iJUMHCaGNkqJGbcY3jvoqFAfyt9b +v0mAFdvjMOEdWscqigEH/m0sNO8oKJe8wflXhpWLNc+eWtFolaNCMEAwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUUgjSvjKBJf31GpfsTl8au1PNkK0wDgYDVR0P +AQH/BAQDAgGGMAoGCCqGSM49BAMDA2cAMGQCMEwxRUZPqOa+w3eyGhhLLYh7WOar +lGtEA7AX/9+Cc0RRLP2THQZ7FNKJ7EAM7yEBLgIwL8kuWmwsHdmV4J6wuVxSfPb4 +OMou8dQd8qJJopX4wVheT/5zCu8xsKsjWBOMi947 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFqjCCA5KgAwIBAgIQVVL4HtsbJCyeu5YYzQIoPjANBgkqhkiG9w0BAQsFADBv +MQswCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDQTEnMCUGA1UEAwweSEFSSUNBIENsaWVudCBS +U0EgUm9vdCBDQSAyMDIxMB4XDTIxMDIxOTEwNTg0NloXDTQ1MDIxMzEwNTg0NVow +bzELMAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBS +ZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ0ExJzAlBgNVBAMMHkhBUklDQSBDbGllbnQg +UlNBIFJvb3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +AIHbV0KQLHQ19Pi4dBlNqwlad0WBc2KwNZ/40LczAIcTtparDlQSMAe8m7dI19EZ +g66O2KnxqQCEsIxenugMj1Rpv/bUCE8mcP4YQWMaszKLQPgHq1cx8MYWdmeatN0v +8tFrxdCShJFxbg8uY+kfU6TdUhPMCYMpgQzFU3VEsQ5nUxjQwx+IS5+UJLQpvLvo +Tv1v0hUdSdyNcPIRGiBRVRG6iG/E91B51qox4oQ9XjLIdypQceULL+m26u+rCjM5 +Dv2PpWdDgo6YaQkJG0DNOGdH6snsl3ES3iT1cjzR90NMJveQsonpRUtVPTEFekHi +lbpDwBfFtoU9GY1kcPNbrM2f0yl1h0uVZ2qm+NHdvJCGiUMpqTdb9V2wJlpTQnaQ +K8+eVmwrVM9cmmXfW4tIYDh8+8ULz3YEYwIzKn31g2fn+sZD/SsP1CYvd6QywSTq +ZJ2/szhxMUTyR7iiZkGh+5t7vMdGanW/WqKM6GpEwbiWtcAyCC17dDVzssrG/q8R +chj258jCz6Uq6nvWWeh8oLJqQAlpDqWW29EAufGIbjbwiLKd8VLyw3y/MIk8Cmn5 +IqRl4ZvgdMaxhZeWLK6Uj1CmORIfvkfygXjTdTaefVogl+JSrpmfxnybZvP+2M/u +vZcGHS2F3D42U5Z7ILroyOGtlmI+EXyzAISep0xxq0o3AgMBAAGjQjBAMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFKDWBz1eJPd7oEQuJFINGaorBJGnMA4GA1Ud +DwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEADUf5CWYxUux57sKo8mg+7ZZF +yzqmmGM/6itNTgPQHILhy9Pl1qtbZyi8nf4MmQqAVafOGyNhDbBX8P7gyr7mkNuD +LL6DjvR5tv7QDUKnWB9p6oH1BaX+RmjrbHjJ4Orn5t4xxdLVLIJjKJ1dqBp+iObn +K/Es1dAFntwtvTdm1ASip62/OsKoO63/jZ0z4LmahKGHH3b0gnTXDvkwSD5biD6q +XGvWLwzojnPCGJGDObZmWtAfYCddTeP2Og1mUJx4e6vzExCuDy+r6GSzGCCdRjVk +JXPqmxBcWDWJsUZIp/Ss1B2eW8yppRoTTyRQqtkbbbFA+53dWHTEwm8UcuzbNZ+4 +VHVFw6bIGig1Oq5l8qmYzq9byTiMMTt/zNyW/eJb1tBZ9Ha6C8tPgxDHQNAdYOkq +5UhYdwxFab4ZcQQk4uMkH0rIwT6Z9ZaYOEgloRWwG9fihBhb9nE1mmh7QMwYXAwk +ndSV9ZmqRuqurL/0FBkk6Izs4/W8BmiKKgwFXwqXdafcfsD913oY3zDROEsfsJhw +v8x8c/BuxDGlpJcdrL/ObCFKvicjZ/MGVoEKkY624QMFMyzaNAhNTlAjrR+lxdR6 +/uoJ7KcoYItGfLXqm91P+edrFcaIz0Pb5SfcBFZub0YV8VYt6FwMc8MjgTggy8kM +ac8sqzuEYDMZUv1pFDM= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQsw +CQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2Vh +cmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9v +dCBDQSAyMDIxMB4XDTIxMDIxOTExMDExMFoXDTQ1MDIxMzExMDEwOVowbDELMAkG +A1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj +aCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgRUNDIFJvb3Qg +Q0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDgI/rGgltJ6rK9JOtDA4MM7 +KKrxcm1lAEeIhPyaJmuqS7psBAqIXhfyVYf8MLA04jRYVxqEU+kw2anylnTDUR9Y +STHMmE5gEYd103KUkE+bECUqqHgtvpBBWJAVcqeht6NCMEAwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQD +AgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAircJRQO9gcS3ujwLEXQNw +SaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/QwCZ61IygN +nxS2PFOiTAZpffpskcYqSUXm7LcT4Tps +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBs +MQswCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0Eg +Um9vdCBDQSAyMDIxMB4XDTIxMDIxOTEwNTUzOFoXDTQ1MDIxMzEwNTUzN1owbDEL +MAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl +YXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgUlNBIFJv +b3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIvC569l +mwVnlskNJLnQDmT8zuIkGCyEf3dRywQRNrhe7Wlxp57kJQmXZ8FHws+RFjZiPTgE +4VGC/6zStGndLuwRo0Xua2s7TL+MjaQenRG56Tj5eg4MmOIjHdFOY9TnuEFE+2uv +a9of08WRiFukiZLRgeaMOVig1mlDqa2YUlhu2wr7a89o+uOkXjpFc5gH6l8Cct4M +pbOfrqkdtx2z/IpZ525yZa31MJQjB/OCFks1mJxTuy/K5FrZx40d/JiZ+yykgmvw +Kh+OC19xXFyuQnspiYHLA6OZyoieC0AJQTPb5lh6/a6ZcMBaD9YThnEvdmn8kN3b +LW7R8pv1GmuebxWMevBLKKAiOIAkbDakO/IwkfN4E8/BPzWr8R0RI7VDIp4BkrcY +AuUR0YLbFQDMYTfBKnya4dC6s1BG7oKsnTH4+yPiAwBIcKMJJnkVU2DzOFytOOqB +AGMUuTNe3QvboEUHGjMJ+E20pwKmafTCWQWIZYVWrkvL4N48fS0ayOn7H6NhStYq +E613TBoYm5EPWNgGVMWX+Ko/IIqmhaZ39qb8HOLubpQzKoNQhArlT4b4UEV4AIHr +W2jjJo3Me1xR9BQsQL4aYB16cmEdH2MtiKrOokWQCPxrvrNQKlr9qEgYRtaQQJKQ +CoReaDH46+0N0x3GfZkYVVYnZS6NRcUk7M7jAgMBAAGjQjBAMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFApII6ZgpJIKM+qTW8VX6iVNvRLuMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAPpBIqm5iFSVmewzVjIuJndftTgfvnNAU +X15QvWiWkKQUEapobQk1OUAJ2vQJLDSle1mESSmXdMgHHkdt8s4cUCbjnj1AUz/3 +f5Z2EMVGpdAgS1D0NTsY9FVqQRtHBmg8uwkIYtlfVUKqrFOFrJVWNlar5AWMxaja +H6NpvVMPxP/cyuN+8kyIhkdGGvMA9YCRotxDQpSbIPDRzbLrLFPCU3hKTwSUQZqP +JzLB5UkZv/HywouoCjkxKLR9YjYsTewfM7Z+d21+UPCfDtcRj88YxeMn/ibvBZ3P +zzfF0HvaO7AWhAw6k9a+F9sPPg4ZeAnHqQJyIkv3N3a6dcSFA1pj1bF1BcK5vZSt +jBWZp5N99sXzqnTPBIWUmAD04vnKJGW/4GKvyMX6ssmeVkjaef2WdhW+o45WxLM0 +/L5H9MG0qPzVMIho7suuyWPEdr6sOBjhXlzPrjoiUevRi7PzKzMHVIf6tLITe7pT +BGIBnfHAT+7hOtSLIBD6Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79 +aPib8qXPMThcFarmlwDB31qlpzmq6YR/PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YW +xw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnnkf3/W9b3raYvAwtt41dU +63ZTGI0RmLo= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzAN +BgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hl +bGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgRUNDIFJv +b3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEwMzcxMlowgaoxCzAJ +BgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmljIEFj +YWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5 +MUQwQgYDVQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0 +dXRpb25zIEVDQyBSb290Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKg +QehLgoRc4vgxEZmGZE4JJS+dQS8KrjVPdJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJa +jq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoKVlp8aQuqgAkkbH7BRqNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLQi +C4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaep +lSTAGiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7Sof +TUwJCA3sS61kFyjndc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1Ix +DzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5k +IFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMT +N0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9v +dENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAxMTIxWjCBpjELMAkG +A1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNh +ZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkx +QDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 +dGlvbnMgUm9vdENBIDIwMTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQDC+Kk/G4n8PDwEXT2QNrCROnk8ZlrvbTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA +4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+ehiGsxr/CL0BgzuNtFajT0 +AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+6PAQZe10 +4S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06C +ojXdFPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV +9Cz82XBST3i4vTwri5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrD +gfgXy5I2XdGj2HUb4Ysn6npIQf1FGQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6 +Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2fu/Z8VFRfS0myGlZYeCsargq +NhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9muiNX6hME6wGko +LfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc +Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVd +ctA4GGqd83EkVAswDQYJKoZIhvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0I +XtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+D1hYc2Ryx+hFjtyp8iY/xnmMsVMI +M4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrMd/K4kPFox/la/vot +9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+yd+2V +Z5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/ea +j8GsGsVn82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnh +X9izjFk0WaSrT2y7HxjbdavYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQ +l033DlZdwJVqwjbDG2jJ9SrcR5q+ss7FJej6A7na+RZukYT1HCjI/CbM1xyQVqdf +bzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVtJ94Cj8rDtSvK6evIIVM4 +pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGaJI7ZjnHK +e7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0 +vm9qp/UsQu0yrbYhnr68 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu +VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw +MTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw +JQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT +3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU ++ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp +S0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1 +bVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi +T0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL +vYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK +Vsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK +dHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT +c+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv +l7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N +iGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD +ggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH +6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt +LRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93 +nAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3 ++wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK +W2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT +AwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq +l1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG +4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ +mUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A +7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBN +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVu +VHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcN +MzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0 +MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTyP4o7 +ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGy +RBb06tD6Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlS +bdsHyo+1W/CD80/HLaXIrcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF +/YTLNiCBWS2ab21ISGHKTN9T0a9SvESfqy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R +3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoSmJxZZoY+rfGwyj4GD3vw +EUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFnol57plzy +9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9V +GxyhLrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ +2fjXctscvG29ZV/viDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsV +WaFHVCkugyhfHMKiq3IXAAaOReyL4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gD +W/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMwDQYJKoZIhvcN +AQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj +t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHV +DRDtfULAj+7AmgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9 +TaDKQGXSc3z1i9kKlT/YPyNtGtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8G +lwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFtm6/n6J91eEyrRjuazr8FGF1NFTwW +mhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMxNRF4eKLg6TCMf4Df +WN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4Mhn5 ++bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJ +tshquDDIajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhA +GaQdp/lLQzfcaFpPz+vCZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv +8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw +CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg +R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00 +MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT +ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw +EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW ++1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9 +ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI +zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW +tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1 +/q4AaOeMSQ+2b1tbFfLn +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4 +MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6 +ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD +VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j +b20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq +scIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO +xdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H +LmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX +uaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD +yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+ +JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q +rLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN +BjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L +hij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB +QFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+ +HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu +Zm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg +QTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB +BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx +MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA +A4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb +laQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56 +awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo +JNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw +LDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT +VyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk +LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb +UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/ +QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+ +naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls +QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6 +ZW5pb3dhIFMuQS4xGDAWBgNVBAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkw +NzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJBgNVBAYTAlBMMSgwJgYDVQQKDB9L +cmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYDVQQDDA9TWkFGSVIg +Uk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5QqEvN +QLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT +3PSQ1hNKDJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw +3gAeqDRHu5rr/gsUvTaE2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr6 +3fE9biCloBK0TXC5ztdyO4mTp4CEHCdJckm1/zuVnsHMyAHs6A6KCpbns6aH5db5 +BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwiieDhZNRnvDF5YTy7ykHN +XGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsF +AAOCAQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw +8PRBEew/R40/cof5O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOG +nXkZ7/e7DDWQw4rtTw/1zBLZpD67oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCP +oky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul4+vJhaAlIDf7js4MNIThPIGy +d05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6+/NNIxuZMzSg +LvWpCz/UXeHPhJ/iGcJfitYgHuNztw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFmDCCA4CgAwIBAgIEVRpusTANBgkqhkiG9w0BAQsFADBDMQswCQYDVQQGEwJa +QTERMA8GA1UEChMITEFXdHJ1c3QxITAfBgNVBAMTGExBV3RydXN0IFJvb3QgQ0Ey +ICg0MDk2KTAgFw0yMzAyMTQwOTE5MzhaGA8yMDUzMDIxNDA5NDkzOFowQzELMAkG +A1UEBhMCWkExETAPBgNVBAoTCExBV3RydXN0MSEwHwYDVQQDExhMQVd0cnVzdCBS +b290IENBMiAoNDA5NikwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDM +F8srQ7ps+cmTimUNEkzsJxS3E3ng1NUtGFbx+eoqEBZObETHamVG85qJNdGH+DOJ +L4gJGpIQkZDBa58Obn8mihNdGKxoAQ0QeGVw2I6PhFqXMBjQEQ5KjVIQpYErUSj1 +Y8S27ECzAeWtd73lOO+8jbPdGaB7DY2022r7JTNa+pGvxHFFMPiIKXvLv9W6JwSO +3bIA98pcmTUU6v11BhUIu8pXaPs/+7Q0c2PR1ePIOFppfWp6RAwNik7tkh0Qjzsi +LLbf7cXG8Il5VGVeXxu9j33fubft6+TFB9FnPJU7kf5CelJAgATSOVdL9JJ9/5vv +5Z3JCbKREjimKQg7ruvKzO1N504hAQf8bzLOaYyEUsZ36icwCt6lrzAraB+s1Owh +rSJJds4PwvIHKvlqEoOaOwSuGXr+oYYk+kFeJXxArCe24yk2bzXiV9AZWN//ZPbD +AUl22yu+vLlPFArVG1gh9hwuAHz4lLXLNxoU5DK5FtRg7AWqXzL6aiMSrNQQu9Ki +grRLDotwJ6rWB8FniPqEwwjJioTI0jdygQ+NFkrk1zVRpTgPjIRLlTbA9ded4F2P +q5HuAAi5nVIf7PiZu3lWsUna0uXYYYtbr/CrN8V7Go6Gvn7FexUeYWjoC4eLc0mh +F3N+KXiOyuBBL3VzdKKXOn/3LnQJuExgi0Y2GRAtnQIDAQABo4GRMIGOMA8GA1Ud +EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMCsGA1UdEAQkMCKADzIwMjMwMjE0 +MDkxOTM4WoEPMjA1MzAyMTQwOTQ5MzhaMB8GA1UdIwQYMBaAFNfWVmJcPxeB5nNE +KfVRBe8LYDesMB0GA1UdDgQWBBTX1lZiXD8XgeZzRCn1UQXvC2A3rDANBgkqhkiG +9w0BAQsFAAOCAgEASZwp/j3snkV/qz48/iNvNz53p1P/eJ/8SUSAV2acbtp5/81F +rUyTv7VZxukQt+X4jPuHxR6L2LM/ApYKu4qO79e0wIMgOJdZRWT89ncT8gnXocg4 +dAjq+UhM+h8EnLT/7G5WNnKTbJU+LF/eDwurycwVPhaPZvyyELih0bTewGMZzO9T +qnU2IoslH7+byNfBX+ymNwmqe2K89iIt8dZY3Yy7UvQLp3apensajdytmoFiLoYF +kHJHL6HJZ4SwDWywuJsWt9CZFC+cEpsjqI2mQx7p5S3leKcfZJRktneyqFz7Casp +6x5tddH20MWlwx2fHvMaLbLIH+UoCm7zX/3a5iOhdpBcS5gBgizuRy0CGl9/NMVp +tXKtPvPPnm34KegRJyvgWQsbYetKymmlpNXNURuUjnnN3/audF2xLBuGU/7RMAZB +NAdigkz0fseHdA6wIR4JIIDBsxU9Rm3T8QaSP++glYocbncxtut4KQx77oKlT36k +KV6eqi34jsDz/A0GhZtO3PfiCXzQFFEeerMjr/rRYSpltQHZuOMHyiR20vBKvu+G +BIBCFXARaH7Xx7v+506bnJWlHEqkydAJjKrOSNIekpfXEentZsw33PXXG3SbpupC +rF0y4Fj0gUf/0hLifhzcSXaWwx2fS8pcKjdbPYrROJsh2uO/RUPT4Fh3Hyg= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNV +BAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRk +LjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJv +b3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZaFw00MjA4MjIxMjA3MDZaMHExCzAJ +BgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMg +THRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25v +IFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtv +xie+RJCxs1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+H +Wyx7xf58etqjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBSHERUI0arBeAyxr87GyZDvvzAEwDAfBgNVHSMEGDAWgBSHERUI0arB +eAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEAtVfd14pVCzbhhkT61Nlo +jbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxOsvxyqltZ ++efcMQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICzzCCAjGgAwIBAgINAOhvGHvWOWuYSkmYCjAKBggqhkjOPQQDBDB1MQswCQYD +VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 +ZC4xFzAVBgNVBGEMDlZBVEhVLTIzNTg0NDk3MSIwIAYDVQQDDBllLVN6aWdubyBU +TFMgUm9vdCBDQSAyMDIzMB4XDTIzMDcxNzE0MDAwMFoXDTM4MDcxNzE0MDAwMFow +dTELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRYwFAYDVQQKDA1NaWNy +b3NlYyBMdGQuMRcwFQYDVQRhDA5WQVRIVS0yMzU4NDQ5NzEiMCAGA1UEAwwZZS1T +emlnbm8gVExTIFJvb3QgQ0EgMjAyMzCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAE +AGgP36J8PKp0iGEKjcJMpQEiFNT3YHdCnAo4YKGMZz6zY+n6kbCLS+Y53wLCMAFS +AL/fjO1ZrTJlqwlZULUZwmgcAOAFX9pQJhzDrAQixTpN7+lXWDajwRlTEArRzT/v +SzUaQ49CE0y5LBqcvjC2xN7cS53kpDzLLtmt3999Cd8ukv+ho2MwYTAPBgNVHRMB +Af8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUWYQCYlpGePVd3I8K +ECgj3NXW+0UwHwYDVR0jBBgwFoAUWYQCYlpGePVd3I8KECgj3NXW+0UwCgYIKoZI +zj0EAwQDgYsAMIGHAkIBLdqu9S54tma4n7Zwf2Z0z+yOfP7AAXmazlIC58PRDHpt +y7Ve7hekm9sEdu4pKeiv+62sUvTXK9Z3hBC9xdIoaDQCQTV2WnXzkoYI9bIeCvZl +C9p2x1L/Cx6AcCIwwzPbGO2E14vs7dOoY4G1VnxHx1YwlGhza9IuqbnZLBwpvQy6 +uWWL +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD +VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 +ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G +CSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y +OTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx +FjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp +Z25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o +dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP +kd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc +cbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U +fIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7 +N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC +xkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1 ++rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM +Pcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG +SIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h +mLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk +ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 +tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c +2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t +HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYD +VQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw +MTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4MjMxNjA0WjBlMQswCQYDVQQGEwJV +UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNy +b3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZR +ogPZnZH6thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYb +hGBKia/teQ87zvH2RPUBeMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBTIy5lycFIM+Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3 +FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlfXu5gKcs68tvWMoQZP3zV +L8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaReNtUjGUB +iudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBl +MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw +NAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 +IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIwNzE4MjMwMDIzWjBlMQswCQYDVQQG +EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1N +aWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZ +Nt9GkMml7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0 +ZdDMbRnMlfl7rEqUrQ7eS0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1 +HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw71VdyvD/IybLeS2v4I2wDwAW9lcfNcztm +gGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+dkC0zVJhUXAoP8XFWvLJ +jEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49FyGcohJUc +aDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaG +YaRSMLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6 +W6IYZVcSn2i51BVrlMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4K +UGsTuqwPN1q3ErWQgR5WrlcihtnJ0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH ++FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJClTUFLkqqNfs+avNJVgyeY+Q +W5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZC +LgLNFgVZJ8og6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OC +gMNPOsduET/m4xaRhPtthH80dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6 +tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk+ONVFT24bcMKpBLBaYVu32TxU5nh +SnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex/2kskZGT4d9Mozd2 +TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDyAmH3 +pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGR +xpl/j8nWZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiApp +GWSZI1b7rCoucL5mxAyE7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9 +dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKTc0QWbej09+CVgI+WXTik9KveCjCHk9hN +AHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB +RA+GsCyRxj3qrg+E +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEM +BQAwaTELMAkGA1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRG +T1JNIENvcnAuMTIwMAYDVQQDDClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4NDJaFw0zNzA4MTgyMzU5NTlaMGkx +CzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVTUyBQTEFURk9STSBD +b3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVA +iQqrDZBbUGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH +38dq6SZeWYp34+hInDEW+j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lE +HoSTGEq0n+USZGnQJoViAbbJAh2+g1G7XNr4rRVqmfeSVPc0W+m/6imBEtRTkZaz +kVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2aacp+yPOiNgSnABIqKYP +szuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4Yb8Obtoq +vC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHf +nZ3zVHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaG +YQ5fG8Ir4ozVu53BA0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo +0es+nPxdGoMuK8u180SdOqcXYZaicdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3a +CJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejyYhbLgGvtPe31HzClrkvJE+2K +AQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNVHQ4EFgQU0p+I +36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB +Af8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoN +qo0hV4/GPnrK21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatj +cu3cvuzHV+YwIHHW1xDBE1UBjCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm ++LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bxhYTeodoS76TiEJd6eN4MUZeoIUCL +hr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTgE34h5prCy8VCZLQe +lHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTHD8z7 +p/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8 +piKCk5XQA76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLR +LBT/DShycpWbXgnbiUSYqqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX +5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oGI/hGoiLtk/bdmuYqh7GYVPEi92tF4+KO +dh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmgkpzNNIaRkPpkUZ3+/uul +9XXeifdy +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG +EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3 +MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl +cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR +dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB +pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM +b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm +aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz +IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT +lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz +AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5 +VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG +ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2 +BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG +AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M +U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh +bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C ++C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC +bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F +uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 +XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICNDCCAbqgAwIBAgIQVOyX1ou0xAshbg6y0FPIejAKBggqhkjOPQQDAzBLMQsw +CQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UEAwwY +T0lTVEUgQ2xpZW50IFJvb3QgRUNDIEcxMB4XDTIzMDUzMTE0MzE0MFoXDTQ4MDUy +NDE0MzEzOVowSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5kYXRp +b24xITAfBgNVBAMMGE9JU1RFIENsaWVudCBSb290IEVDQyBHMTB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABIhOaB/Jnr46BFsVwzX0zFDFCK04bqg80gK6zKsl/XVA/WcZ +nxsKXfbLFnv5XB6C3BVE1Jw8bWGTRfRPz2K53z5TjZrUSt6Iqgum8dRh1h501Riy +xU1M74B77A3rgzlUlqNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSZ +Vzs5sS0AjCFmjJVpnG117Iw/+jAdBgNVHQ4EFgQUmVc7ObEtAIwhZoyVaZxtdeyM +P/owDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2gAMGUCMQCW/+SCThYiW6CF +GDw9Oo8gBggl5/WRNhmte7TfW2YSN3Nw7c0FKAdeCM4NQl8ZkQICMGdJh64GQR0g +0zGmqiY38SeKYQ3+mgZDpy6eJkejMhiL6F5QBfGwekh23tuhYkq6dw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIQNBdvWQGIG6ql3chIu7Q7czANBgkqhkiG9w0BAQwFADBL +MQswCQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UE +AwwYT0lTVEUgQ2xpZW50IFJvb3QgUlNBIEcxMB4XDTIzMDUzMTE0MjMyOVoXDTQ4 +MDUyNDE0MjMyOFowSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5k +YXRpb24xITAfBgNVBAMMGE9JU1RFIENsaWVudCBSb290IFJTQSBHMTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBALpP/v5UE7WEPLzg0zHxHW7cxFNx+uQ5 +UUN2fZIfgX8Aa0HC5trcGE1sF1lwCTNi7GmILbDdWflhYGBW8ba07+uH0BP+w89v +j345WFGziQKOVJUeIl+rKAVDJ/hF9AlCJpT+vRN4u5HyEBCcDWd82mQg63owGrpI +DXhUKpkxNKvLpmrnDGc5ZqQmqCco5/PmPHPkK8xvMS4TdGHLaObSM85SvH5lJFoh +gTFDqrKc0RjnYTxSr4CJ6TRG3vlNmVptHb3GJdGTVY74J5JDOoyVRUDjiRinhsFZ +mMrbJhwTwIyBuZiwrWmtbhjje2JB9a02/gu0eyBfn6lu+ZmCElLSisRUeLR890Gb +A+cHXrPCuUlkZ5IWxGCQDrCCfTOt0Dbq0XZrfIhHmKwb+bRQjGGBadgx8436PvL1 +S6/Owx3vXygb6xjWoFhSMr5Cb81JlyLBcLnT42BP3oOCoE4wvXNTwr0X/aDAmI/q +DhcH5kOVIE7bEaj549O4J0cMJ9sS64FVzHXbn9MXQ8T764oobemvRFBaQ/vxOeKT +UM+Y/ESWWDilpe1Fw1JCBafv5TykrD3n1qlWBaqww6cZ5OU911dEbZQRH8pwyPy5 +TMxBWoN0U5B4z9bULk+xqk0u9dEIWzpk78inqHph7Oym1YhOtlTUWJHCJWSRvAoU +PZIUmrULBukvAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU +KYIlNQo6vpIr5AkD5OyPjThyOcswHQYDVR0OBBYEFCmCJTUKOr6SK+QJA+Tsj404 +cjnLMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQwFAAOCAgEAbSOGwv/14MjA +VYpgMcyXQ0dwQ9Pj7FL608Ke+4kyGspGk08Elyvb0JyEDZUHQlT+72kh35IDLo83 +ISN3qXc3bKDErpynWDlKFZdiRoNRIO0/wqPxw2In0KwTHv48Uh2Q1WPxqV7qf+fn +65ZaUezUqRvjDJRmrMuIkkm+c1yK4Gq8poHNs1zUI5LITfkgjHCUS2ht8o8ebDX3 +6F/U170gN1Jm/yu7SWa3cagsX3MPB5LnTl+lBtvJijyXxULqfQ+BG1frngwP/6Mn +IElTprM6TMttMDXa8vCa/lDfbVwkPU13an2GX0zQ4aa0rgQTAZDxgGiEB5SCB4Pr +keWTDnWRrqMjIElk1Lo5lldw7lU0KHzWr8qpnubJAckHwdBEsYC0UVCqj/ac5Wdz +0BvqgzUXL1DG3lbHu6MDy+KhGOj4zlEGo9IDQGEap2dXg/zRErkoqtpOa9Wc2IU3 +2r0i1zRZnBqmznjWlHgHBg+xkyGgSccQngquUXca+XGQw62YD4opamABqk+tIAMt +ao6jC2rW/ZMMimHLvSjxX3H9uDM51krx9rJoUj5lj0OdgSQk9ihMNaf9MwqleMEE +H+xJasSu1UQWpqeNf9ohlj6ouhZn1Kmh58Ka+BDZO5ruaPYvAO7Lu2aNIjiG9L9f +eKnIoB1au3VQ+VILDx0CLBQa84dqd/M= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICNTCCAbqgAwIBAgIQI/nD1jWvjyhLH/BU6n6XnTAKBggqhkjOPQQDAzBLMQsw +CQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UEAwwY +T0lTVEUgU2VydmVyIFJvb3QgRUNDIEcxMB4XDTIzMDUzMTE0NDIyOFoXDTQ4MDUy +NDE0NDIyN1owSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5kYXRp +b24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IEVDQyBHMTB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABBcv+hK8rBjzCvRE1nZCnrPoH7d5qVi2+GXROiFPqOujvqQy +cvO2Ackr/XeFblPdreqqLiWStukhEaivtUwL85Zgmjvn6hp4LrQ95SjeHIC6XG4N +2xml4z+cKrhAS93mT6NjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBQ3 +TYhlz/w9itWj8UnATgwQb0K0nDAdBgNVHQ4EFgQUN02IZc/8PYrVo/FJwE4MEG9C +tJwwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2kAMGYCMQCpKjAd0MKfkFFR +QD6VVCHNFmb3U2wIFjnQEnx/Yxvf4zgAOdktUyBFCxxgZzFDJe0CMQCSia7pXGKD +YmH5LVerVrkR3SW+ak5KGoJr3M/TvEqzPNcum9v4KGm8ay3sMaE641c= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIQVaXZZ5Qoxu0M+ifdWwFNGDANBgkqhkiG9w0BAQwFADBL +MQswCQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UE +AwwYT0lTVEUgU2VydmVyIFJvb3QgUlNBIEcxMB4XDTIzMDUzMTE0MzcxNloXDTQ4 +MDUyNDE0MzcxNVowSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5k +YXRpb24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IFJTQSBHMTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAKqu9KuCz/vlNwvn1ZatkOhLKdxVYOPM +vLO8LZK55KN68YG0nnJyQ98/qwsmtO57Gmn7KNByXEptaZnwYx4M0rH/1ow00O7b +rEi56rAUjtgHqSSY3ekJvqgiG1k50SeH3BzN+Puz6+mTeO0Pzjd8JnduodgsIUzk +ik/HEzxux9UTl7Ko2yRpg1bTacuCErudG/L4NPKYKyqOBGf244ehHa1uzjZ0Dl4z +O8vbUZeUapU8zhhabkvG/AePLhq5SvdkNCncpo1Q4Y2LS+VIG24ugBA/5J8bZT8R +tOpXaZ+0AOuFJJkk9SGdl6r7NH8CaxWQrbueWhl/pIzY+m0o/DjH40ytas7ZTpOS +jswMZ78LS5bOZmdTaMsXEY5Z96ycG7mOaES3GK/m5Q9l3JUJsJMStR8+lKXHiHUh +sd4JJCpM4rzsTGdHwimIuQq6+cF0zowYJmXa92/GjHtoXAvuY8BeS/FOzJ8vD+Ho +mnqT8eDI278n5mUpezbgMxVz8p1rhAhoKzYHKyfMeNhqhw5HdPSqoBNdZH702xSu ++zrkL8Fl47l6QGzwBrd7KJvX4V84c5Ss2XCTLdyEr0YconosP4EmQufU2MVshGYR +i3drVByjtdgQ8K4p92cIiBdcuJd5z+orKu5YM+Vt6SmqZQENghPsJQtdLEByFSnT +kCz3GkPVavBpAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU +8snBDw1jALvsRQ5KH7WxszbNDo0wHQYDVR0OBBYEFPLJwQ8NYwC77EUOSh+1sbM2 +zQ6NMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQwFAAOCAgEANGd5sjrG5T33 +I3K5Ce+SrScfoE4KsvXaFwyihdJ+klH9FWXXXGtkFu6KRcoMQzZENdl//nk6HOjG +5D1rd9QhEOP28yBOqb6J8xycqd+8MDoX0TJD0KqKchxRKEzdNsjkLWd9kYccnbz8 +qyiWXmFcuCIzGEgWUOrKL+mlSdx/PKQZvDatkuK59EvV6wit53j+F8Bdh3foZ3dP +AGav9LEDOr4SfEE15fSmG0eLy3n31r8Xbk5l8PjaV8GUgeV6Vg27Rn9vkf195hfk +gSe7BYhW3SCl95gtkRlpMV+bMPKZrXJAlszYd2abtNUOshD+FKrDgHGdPY3ofRRs +YWSGRqbXVMW215AWRqWFyp464+YTFrYVI8ypKVL9AMb2kI5Wj4kI3Zaq5tNqqYY1 +9tVFeEJKRvwDyF7YZvZFZSS0vod7VSCd9521Kvy5YhnLbDuv0204bKt7ph6N/Ome +/msVuduCmsuY33OhkKCgxeDoAaijFJzIwZqsFVAzje18KotzlUBDJvyBpCpfOZC3 +J8tRd/iWkx7P8nd9H0aTolkelUTFLXVksNb54Dxp6gS1HAviRkRNQzuXSXERvSS2 +wq1yVAb+axj5d9spLFKebXd7Yv0PTY6YMjAwcRLWJTXjn/hvnLXrahut6hDTlhZy +BiElxky8j3C7DOReIoMt0r7+hVu05L0= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID8TCCAtmgAwIBAgIQQT1yx/RrH4FDffHSKFTfmjANBgkqhkiG9w0BAQUFADCB +ijELMAkGA1UEBhMCQ0gxEDAOBgNVBAoTB1dJU2VLZXkxGzAZBgNVBAsTEkNvcHly +aWdodCAoYykgMjAwNTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNl +ZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQSBDQTAeFw0w +NTEyMTExNjAzNDRaFw0zNzEyMTExNjA5NTFaMIGKMQswCQYDVQQGEwJDSDEQMA4G +A1UEChMHV0lTZUtleTEbMBkGA1UECxMSQ29weXJpZ2h0IChjKSAyMDA1MSIwIAYD +VQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBX +SVNlS2V5IEdsb2JhbCBSb290IEdBIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAy0+zAJs9Nt350UlqaxBJH+zYK7LG+DKBKUOVTJoZIyEVRd7jyBxR +VVuuk+g3/ytr6dTqvirdqFEr12bDYVxgAsj1znJ7O7jyTmUIms2kahnBAbtzptf2 +w93NvKSLtZlhuAGio9RN1AU9ka34tAhxZK9w8RxrfvbDd50kc3vkDIzh2TbhmYsF +mQvtRTEJysIA2/dyoJaqlYfQjse2YXMNdmaM3Bu0Y6Kff5MTMPGhJ9vZ/yxViJGg +4E8HsChWjBgbl0SOid3gF27nKu+POQoxhILYQBRJLnpB5Kf+42TMwVlxSywhp1t9 +4B3RLoGbw9ho972WG6xwsRYUC9tguSYBBQIDAQABo1EwTzALBgNVHQ8EBAMCAYYw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUswN+rja8sHnR3JQmthG+IbJphpQw +EAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBAEuh/wuHbrP5wUOx +SPMowB0uyQlB+pQAHKSkq0lPjz0e701vvbyk9vImMMkQyh2I+3QZH4VFvbBsUfk2 +ftv1TDI6QU9bR8/oCy22xBmddMVHxjtqD6wU2zz0c5ypBd8A3HR4+vg1YFkCExh8 +vPtNsCBtQ7tgMHpnM1zFmdH4LTlSc/uMqpclXHLZCB6rTjzjgTGfA6b7wP4piFXa +hNVQA7bihKOmNqoROgHhGEvWRGizPflTdISzRpFGlgC3gCy24eMQ4tui5yiPAZZi +Fj4A4xylNoEYokxSdsARo27mHbrjWr42U8U+dY+GaSlYU7Wcu2+fXMUY7N0v4ZjJ +/L7fCg0= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt +MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg +Rm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9i +YWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAwMzJaFw0zOTEyMDExNTEwMzFaMG0x +CzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBG +b3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh +bCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3 +HEokKtaXscriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGx +WuR51jIjK+FTzJlFXHtPrby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX +1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNk +u7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4oQnc/nSMbsrY9gBQHTC5P +99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvgGUpuuy9r +M2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUB +BAMCAQAwDQYJKoZIhvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrgh +cViXfa43FK8+5/ea4n32cZiZBKpDdHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5 +gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0VQreUGdNZtGn//3ZwLWoo4rO +ZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEuiHZeeevJuQHHf +aPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic +Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQsw +CQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91 +bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwg +Um9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRaFw00MjA1MDkwOTU4MzNaMG0xCzAJ +BgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBGb3Vu +ZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2JhbCBS +b290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4ni +eUqjFqdrVCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4W +p2OQ0jnUsYd4XxiWD1AbNTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7T +rYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0EAwMDaAAwZQIwJsdpW9zV +57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtkAjEA2zQg +Mgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICODCCAb6gAwIBAgIJANZdm7N4gS7rMAoGCCqGSM49BAMDMGExCzAJBgNVBAYT +AkpQMSUwIwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMSswKQYD +VQQDEyJTZWN1cml0eSBDb21tdW5pY2F0aW9uIEVDQyBSb290Q0ExMB4XDTE2MDYx +NjA1MTUyOFoXDTM4MDExODA1MTUyOFowYTELMAkGA1UEBhMCSlAxJTAjBgNVBAoT +HFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKzApBgNVBAMTIlNlY3VyaXR5 +IENvbW11bmljYXRpb24gRUNDIFJvb3RDQTEwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AASkpW9gAwPDvTH00xecK4R1rOX9PVdu12O/5gSJko6BnOPpR27KkBLIE+Cnnfdl +dB9sELLo5OnvbYUymUSxXv3MdhDYW72ixvnWQuRXdtyQwjWpS4g8EkdtXP9JTxpK +ULGjQjBAMB0GA1UdDgQWBBSGHOf+LaVKiwj+KBH6vqNm+GBZLzAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjAVXUI9/Lbu +9zuxNuie9sRGKEkz0FhDKmMpzE2xtHqiuQ04pV1IKv3LsnNdo4gIxwwCMQDAqy0O +be0YottT6SXbVQjgUMzfRGEWgqtJsLKB7HOHeLRMsmIbEvoWTSVLY70eN9k= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl +MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe +U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX +DTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy +dXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj +YXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV +OVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr +zbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM +VAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ +hNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO +ojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw +awNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs +OPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF +coJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc +okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8 +t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy +1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/ +SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj +YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM +GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua +BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe +3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4 +YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR +rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm +ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU +oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v +QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t +b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF +AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q +GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz +Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2 +G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi +l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 +smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB +gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV +BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw +MDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl +YXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P +RE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3 +UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI +2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8 +Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp ++2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+ +DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O +nKVIrLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW +/zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6g +PKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9u +QXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5t3iuHXIY +SdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv +IC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ +RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4 +zJVSk/BwJVmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5dd +BA6+C4OmF4O5MBKgxTMVBbkN+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IB +ZQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT +IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw +MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N +T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR +FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J +cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW +BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm +fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv +GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB +hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV +BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5 +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT +EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR +6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X +pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC +9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV +/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf +Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z ++pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w +qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah +SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC +u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf +Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq +crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB +/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl +wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM +4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV +2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna +FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ +CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK +boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke +jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL +S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb +QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl +0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB +NVOFBkpdn627G190 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0 +Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW +KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl +cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw +NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw +NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy +ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV +BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo +Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4 +4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9 +KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI +rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi +94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB +sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi +gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo +kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE +vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA +A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t +O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua +AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP +9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/ +eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m +0vdXcDazv/wor3ElhVsT/h5/WrQ8 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkG +A1UEBhMCVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3 +d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVu +dHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEzMDEGA1UEAxMq +RW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRUMxMB4XDTEy +MTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYwFAYD +VQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0 +L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0g +Zm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBD +ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEVDMTB2MBAGByqGSM49AgEGBSuBBAAi +A2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHyAsWfoPZb1YsGGYZPUxBt +ByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef9eNi1KlH +Bz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O +BBYEFLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVC +R98crlOZF7ZvHH3hvxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nX +hTcGtXsI/esni0qU+eH6p44mCOh8kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50 +cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs +IEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVz +dCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcy +NTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVu +dHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwt +dGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0 +aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/T +RU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWN +cCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hW +wcKUs/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1 +U1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0 +jaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2fkBJmqzAN +BgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/ +jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ +Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v +1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R +nAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH +VHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGSzCCBDOgAwIBAgIRANm1Q3+vqTkPAAAAAFVlrVgwDQYJKoZIhvcNAQELBQAw +gb4xCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQL +Ex9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykg +MjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMjAw +BgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0 +MB4XDTE1MDUyNzExMTExNloXDTM3MTIyNzExNDExNlowgb4xCzAJBgNVBAYTAlVT +MRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1 +c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJ +bmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3Qg +Um9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0MIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAsewsQu7i0TD/pZJH4i3DumSXbcr3DbVZwbPLqGgZ +2K+EbTBwXX7zLtJTmeH+H17ZSK9dE43b/2MzTdMAArzE+NEGCJR5WIoV3imz/f3E +T+iq4qA7ec2/a0My3dl0ELn39GjUu9CH1apLiipvKgS1sqbHoHrmSKvS0VnM1n4j +5pds8ELl3FFLFUHtSUrJ3hCX1nbB76W1NhSXNdh4IjVS70O92yfbYVaCNNzLiGAM +C1rlLAHGVK/XqsEQe9IFWrhAnoanw5CGAlZSCXqc0ieCU0plUmr1POeo8pyvi73T +DtTUXm6Hnmo9RR3RXRv06QqsYJn7ibT/mCzPfB3pAqoEmh643IhuJbNsZvc8kPNX +wbMv9W3y+8qh+CmdRouzavbmZwe+LGcKKh9asj5XxNMhIWNlUpEbsZmOeX7m640A +2Vqq6nPopIICR5b+W45UYaPrL0swsIsjdXJ8ITzI9vF01Bx7owVV7rtNOzK+mndm +nqxpkCIHH2E6lr7lmk/MBTwoWdPBDFSoWWG9yHJM6Nyfh3+9nEg2XpWjDrk4JFX8 +dWbrAuMINClKxuMrLzOg2qOGpRKX/YAr2hRC45K9PvJdXmd0LhyIRyk0X+IyqJwl +N4y6mACXi0mWHv0liqzc2thddG5msP9E36EYxr5ILzeUePiVSj9/E15dWf10hkNj +c0kCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFJ84xFYjwznooHFs6FRM5Og6sb9nMA0GCSqGSIb3DQEBCwUAA4ICAQAS +5UKme4sPDORGpbZgQIeMJX6tuGguW8ZAdjwD+MlZ9POrYs4QjbRaZIxowLByQzTS +Gwv2LFPSypBLhmb8qoMi9IsabyZIrHZ3CL/FmFz0Jomee8O5ZDIBf9PD3Vht7LGr +hFV0d4QEJ1JrhkzO3bll/9bGXp+aEJlLdWr+aumXIOTkdnrG0CSqkM0gkLpHZPt/ +B7NTeLUKYvJzQ85BK4FqLoUWlFPUa19yIqtRLULVAJyZv967lDtX/Zr1hstWO1uI +AeV8KEsD+UmDfLJ/fOPtjqF/YFOOVZ1QNBIPt5d7bIdKROf1beyAN/BYGW5KaHbw +H5Lk6rWS02FREAutp9lfx1/cH6NcjKF+m7ee01ZvZl4HliDtC3T7Zk6LERXpgUl+ +b7DUUH8i119lAg2m9IUe2K4GS0qn0jFmwvjO5QimpAKWRGhXxNUzzxkvFMSUHHuk +2fCfDrGA4tGeEWSpiBE6doLlYsKA2KSD7ZPvfC+QsDJMlhVoSFLUmQjAJOgc47Ol +IQ6SwJAfzyBfyjs4x7dtOvPmRLgOMWuIjnDrnBdSqEGULoe256YSxXXfW8AKbnuk +5F6G+TaU33fD6Q3AOfF5u0aOq0NZJ7cguyPpVkAh7DE9ZapD8j3fcEThuk0mEDuY +n/PIjhs4ViFqUZPTkcpG2om3PVODLAgfi49T3f+sHw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChML +RW50cnVzdC5uZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBp +bmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5 +IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNVBAMTKkVudHJ1c3QubmV0IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQxNzUwNTFaFw0yOTA3 +MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3d3d3 +LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxp +YWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEG +A1UEAxMqRW50cnVzdC5uZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgp +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArU1LqRKGsuqjIAcVFmQq +K0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOLGp18EzoOH1u3Hs/lJBQe +sYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSrhRSGlVuX +MlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVT +XTzWnLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/ +HoZdenoVve8AjhUiVBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH +4QIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJKoZIhvcNAQEFBQADggEBADub +j1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPyT/4xmf3IDExo +U8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf +zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5b +u/8j72gZyxKTJ1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+ +bYQLCIt+jerXmCHG8+c8eS9enNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/Er +fF6adulZkMV8gzURZVE= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICMTCCAbegAwIBAgIQbvXTp0GOoFlApzBr0kBlVjAKBggqhkjOPQQDAzBaMQsw +CQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTEwLwYDVQQDEyhT +ZWN0aWdvIFB1YmxpYyBFbWFpbCBQcm90ZWN0aW9uIFJvb3QgRTQ2MB4XDTIxMDMy +MjAwMDAwMFoXDTQ2MDMyMTIzNTk1OVowWjELMAkGA1UEBhMCR0IxGDAWBgNVBAoT +D1NlY3RpZ28gTGltaXRlZDExMC8GA1UEAxMoU2VjdGlnbyBQdWJsaWMgRW1haWwg +UHJvdGVjdGlvbiBSb290IEU0NjB2MBAGByqGSM49AgEGBSuBBAAiA2IABLinUpT1 +PgWwG/YfsdN+ueQFZlSAzmylaH3kU1LbgvrEht9DePfIrRa8P3gyy2vTSdZE5bN+ +n3umxizy4rbTibCaPEvOiUvGxss6SWAPRrxtTnqcyZuFewq2sEfCiOPU0aNCMEAw +HQYDVR0OBBYEFC1OjKfCI7JXqQZrPmsrifPDXkfOMA4GA1UdDwEB/wQEAwIBhjAP +BgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQCSnRpZY0VYjhsW5H16 +bDZIMB8rcueQMzT9JKLGBoxvOzJXWvj+xkkSU5rZELKZUXICMAUlKjMh/JPmIqLM +cFUoNVaiB8QhhCMaTEyZUJmSFMtK3Fb79dOPaiz1cTr4izsDng== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFgDCCA2igAwIBAgIQHUSeuQ2DkXSu3fLriLemozANBgkqhkiG9w0BAQwFADBa +MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTEwLwYDVQQD +EyhTZWN0aWdvIFB1YmxpYyBFbWFpbCBQcm90ZWN0aW9uIFJvb3QgUjQ2MB4XDTIx +MDMyMjAwMDAwMFoXDTQ2MDMyMTIzNTk1OVowWjELMAkGA1UEBhMCR0IxGDAWBgNV +BAoTD1NlY3RpZ28gTGltaXRlZDExMC8GA1UEAxMoU2VjdGlnbyBQdWJsaWMgRW1h +aWwgUHJvdGVjdGlvbiBSb290IFI0NjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC +AgoCggIBAJHlG/qqbTcrdccuXxSl2yyXtixGj2nZ7JYt8x1avtMdI+ZoCf9KEXMa +rmefdprS5+y42V8r+SZWUa92nan8F+8yCtAjPLosT0eD7J0FaEJeBuDV6CtoSJey ++vOkcTV9NJsXi39NDdvcTwVMlGK/NfovyKccZtlxX+XmWlXKq/S4dxlFUEVOSqvb +nmbBGbc3QshWpUAS+TPoOEU6xoSjAo4vJLDDQYUHSZzP3NHyJm/tMxwzZypFN9mF +ZSIasbUQUglrA8YfcD2RxH2QPe1m+JD/JeDtkqKLMSmtnBJmeGOdV+z7C96O3IvL +Oql39Lrl7DiMi+YTZqdpWMOCGhrN8Z/YU5JOSX2pRefxQyFatz5AzWOJz9m/x1AL +4bzniJatntQX2l3P4JH9phDUuQOBm2ms+4SogTXrG+tobHxgPsPfybSudB1Ird1u +EYbhKmo2Fq7IzrzbWPxAk0DYjlOXwqwiOOWIMbMuoe/s4EIN6v+TVkoGpJtMAmhk +j1ZQwYEF/cvbxdcV8mu1dsOj+TLOyrVKqRt9Gdx/x2p+ley2uI39lUqcoytti/Fw +5UcrAFzkuZ7U+NlYKdDL4ChibK6cYuLMvDaTQfXv/kZilbBXSnQsR1Ipnd2ioU9C +wpLOLVBSXowKoffYncX4/TaHTlf9aKFfmYMc8LXd6JLTZUBVypaFAgMBAAGjQjBA +MB0GA1UdDgQWBBSn15V360rDJ82TvjdMJoQhFH1dmDAOBgNVHQ8BAf8EBAMCAYYw +DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQwFAAOCAgEANNLxFfOTAdRyi/Cr +CB8TPHO0sKvoeNlsupqvJuwQgOUNUzHd4/qMUSIkMze4GH46+ljoNOWM4KEfCUHS +Nz/Mywk1Qojp/BHXz0KqpHC2ccFTvcV0r8QiJGPPYoJ9yctRwYiQbVtcvvuZqLq2 +hrDpZgvlG2uv6iuGp9+oI0yWP09XQhgVg0Pxhia3KgPOC53opWgejG+9heMbUY/n +Fy8r0NZ4wi3dcojUZZ76mdR+55cKkgGapamEOgwqdD0zGMiH9+ik9YZCOf1rdSn8 +AAasoqUaVI7pUEkXZq9LBC2blIClVKuMVxdEnw/WaGRytEseAcfZm5TZg5mvEgUR +o5gi0vJXyiT5ujgVEki6Yzv8i5V41nIHVszN/J0c0MVkO2M0zwSZircweXq28sbV +2VR6hwt+TveE7BTziBYS8dWuChoJ7oat5av9rsMpeXTDAV8Rm991mcZK95uPbEns +IS+0AlmzLdBykLoLFHR4S8/BX1VyjlQrE876WAzTuyzZqZFh+PjxtnvevKnMkgTM +S2tfc4C2Ie1QT9d2h27O39K3vWKhfVhiaEVStj/eEtvtBGmedoiqAW3ahsdgG8NS +rDfsUHGAciohRQpTRzwZ643SWQTeJbDrHzVvYH3Xtca7CyeN4E1U5c8dJgFuOzXI +IBKJg/DS7Vg7NJ27MfUy/THzVho= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQsw +CQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T +ZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcN +MjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYG +A1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBT +ZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAR2+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccC +WvkEN/U0NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+ +6xnOQ6OjQjBAMB0GA1UdDgQWBBTRItpMWfFLXyY4qp3W7usNw/upYTAOBgNVHQ8B +Af8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNnADBkAjAn7qRa +qCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RHlAFWovgzJQxC36oCMB3q +4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21USAGKcw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFijCCA3KgAwIBAgIQdY39i658BwD6qSWn4cetFDANBgkqhkiG9w0BAQwFADBf +MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQD +Ey1TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYw +HhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEY +MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1Ymxp +YyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCTvtU2UnXYASOgHEdCSe5jtrch/cSV1UgrJnwUUxDa +ef0rty2k1Cz66jLdScK5vQ9IPXtamFSvnl0xdE8H/FAh3aTPaE8bEmNtJZlMKpnz +SDBh+oF8HqcIStw+KxwfGExxqjWMrfhu6DtK2eWUAtaJhBOqbchPM8xQljeSM9xf +iOefVNlI8JhD1mb9nxc4Q8UBUQvX4yMPFF1bFOdLvt30yNoDN9HWOaEhUTCDsG3X +ME6WW5HwcCSrv0WBZEMNvSE6Lzzpng3LILVCJ8zab5vuZDCQOc2TZYEhMbUjUDM3 +IuM47fgxMMxF/mL50V0yeUKH32rMVhlATc6qu/m1dkmU8Sf4kaWD5QazYw6A3OAS +VYCmO2a0OYctyPDQ0RTp5A1NDvZdV3LFOxxHVp3i1fuBYYzMTYCQNFu31xR13NgE +SJ/AwSiItOkcyqex8Va3e0lMWeUgFaiEAin6OJRpmkkGj80feRQXEgyDet4fsZfu ++Zd4KKTIRJLpfSYFplhym3kT2BFfrsU4YjRosoYwjviQYZ4ybPUHNs2iTG7sijbt +8uaZFURww3y8nDnAtOFr94MlI1fZEoDlSfB1D++N6xybVCi0ITz8fAr/73trdf+L +HaAZBav6+CuBQug4urv7qv094PPK306Xlynt8xhW6aWWrL3DkJiy4Pmi1KZHQ3xt +zwIDAQABo0IwQDAdBgNVHQ4EFgQUVnNYZJX5khqwEioEYnmhQBWIIUkwDgYDVR0P +AQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAC9c +mTz8Bl6MlC5w6tIyMY208FHVvArzZJ8HXtXBc2hkeqK5Duj5XYUtqDdFqij0lgVQ +YKlJfp/imTYpE0RHap1VIDzYm/EDMrraQKFz6oOht0SmDpkBm+S8f74TlH7Kph52 +gDY9hAaLMyZlbcp+nv4fjFg4exqDsQ+8FxG75gbMY/qB8oFM2gsQa6H61SilzwZA +Fv97fRheORKkU55+MkIQpiGRqRxOF3yEvJ+M0ejf5lG5Nkc/kLnHvALcWxxPDkjB +JYOcCj+esQMzEhonrPcibCTRAUH4WAP+JWgiH5paPHxsnnVI84HxZmduTILA7rpX +DhjvLpr3Etiga+kFpaHpaPi8TD8SHkXoUsCjvxInebnMMTzD9joiFgOgyY9mpFui +TdaBJQbpdqQACj7LzTWb4OE4y2BThihCQRxEV+ioratF4yUQvNs+ZUH7G6aXD+u5 +dHn5HrwdVw1Hr8Mvn4dGp+smWg9WY7ViYG4A++MnESLn/pmPNPW56MORcr3Ywx65 +LvKRRFHQV80MNNVIIb/bE/FmJUNS0nAiNs2fxBx1IK1jcmMGDw4nztJqDby1ORrp +0XZ60Vzk50lJLVU3aPAaOpg+VBeHVOmmJ1CJeyAvP/+/oYtKR5j/K3tJPsMpRmAY +QqszKbrAKbkTidOIijlBO8n9pu0f9GBj39ItVQGL +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL +MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl +eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT +JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT +Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg +VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo +I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng +o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G +A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB +zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW +RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB +iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl +cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV +BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw +MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV +BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B +3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY +tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ +Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 +VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT +79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 +c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT +Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l +c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee +UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE +Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G +A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF +Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO +VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 +ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs +8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR +iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze +Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ +XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ +qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB +VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB +L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG +jjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBK +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +GTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkx +MjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3Qg +Q29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ +iQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa +/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ +jnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI +HmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7 +sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0w +gZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCsw +KaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsG +AQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0L +URYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO +H0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm +I50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY +iNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc +f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz +MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv +cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz +Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO +0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao +wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj +7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS +8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT +BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg +JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3 +6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/ +3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm +D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS +CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR +3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICQDCCAcagAwIBAgIQdvhIHq7wPHAf4D8lVAGD1TAKBggqhkjOPQQDAzBRMQsw +CQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSgwJgYDVQQDDB9T +U0wuY29tIENsaWVudCBFQ0MgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzAzMloX +DTQ2MDgxOTE2MzAzMVowUTELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jw +b3JhdGlvbjEoMCYGA1UEAwwfU1NMLmNvbSBDbGllbnQgRUNDIFJvb3QgQ0EgMjAy +MjB2MBAGByqGSM49AgEGBSuBBAAiA2IABC1Tfp+LPrM2ulDizOvcuiaK04wGP2cP +7/UX5dSumkYqQQEHaedncfHCAzbG8CtSjs8UkmikPnBREmmNeKKCyikUwOSUIrJE +kmBvyASkZ9Wi0PPQ1+qOPA+60kBHkDTufaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAf +BgNVHSMEGDAWgBS3/i1ixYFTzVIaL11goMNd+7IcHDAdBgNVHQ4EFgQUt/4tYsWB +U81SGi9dYKDDXfuyHBwwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2gAMGUC +ME0HES0R+7kmwyHdcuEX/MHPFOpJznGHjtZT3BHNXVSKr9kt9IxR6rxmR+J/lYNg +ZQIxAIwhTE+75bBQ35BiSebMkdv4P11xkQiOT5LJf6Zc6hN+7W3E6MMqb1wR4aXz +alqaTQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFjzCCA3egAwIBAgIQdq/uiJMVRbZQU5uAnKTfmjANBgkqhkiG9w0BAQsFADBR +MQswCQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSgwJgYDVQQD +DB9TU0wuY29tIENsaWVudCBSU0EgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzEw +N1oXDTQ2MDgxOTE2MzEwNlowUTELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBD +b3Jwb3JhdGlvbjEoMCYGA1UEAwwfU1NMLmNvbSBDbGllbnQgUlNBIFJvb3QgQ0Eg +MjAyMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALhY20Yw+8k/48jw +ATM04tpIqBjpIG6a1wHh1SmPMLQjauTLYrC+4p8gvT5UoDlox4Y3ZnQGBu90K9rc +n4SpUi+Q0u5+fPulIq1vcEZnlj0p1KO7VnsUBFnBIWNEHrIfElyQh2UNiPYeiCLi +Y1S78zb41n/c2v8pNanGbg5pWz/YvoKHFXBdsMdcEg9jpjjNz3O5ww6JJjcbP2Ic +MmnRm9n/VZAx3rFj3c/FdHf874ghU78AMRomLAAwpV9s4+T2AIrKmIecdAN6i2bs +fv2jjzUlXHils6T7PW2pivBsiIKL/UrQb+TXo7SONEk4vs5F5dIcyl7CNxSLzWZW +Mzed5WvsQ5JkoELadW/AFez5ab00uYp7+hb7Vf5SIOgEBFZWZfU3RJjIikbpt6y4 +6L5ijlQ2W/c7cL9d7i26X95CGYbwf4vrCMvYvuoOQkKgNnNXF+0y6tCN6Acbm5no +xJpiBA5I9zwSuvdYwZqM6cewIzZWNB3LbNq6B4Qd/dGsn+bCie/DuWwYs2mHV1+1 +DDhbpyEkKjunNJGetFTqKE/TwaOL5OYr1fKdv5thACLd1ktEHz9dVv7enHjMmVuq +5L2620NLrUwmTKNNNIpsdDYT22L8m7IFgf+uPwzN9hui9DnnyvVMXPtUdzWAWsAS +oRMBM2c9nYGhqfWFJFiIeOf042hVAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8w +HwYDVR0jBBgwFoAU8DhClDSpPAB/Uu45pfdLDbxqfSMwHQYDVR0OBBYEFPA4QpQ0 +qTwAf1LuOaX3Sw28an0jMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOC +AgEAmU/b8OrWEfoq/cirbeQOc2LSQp8V/nxwUj9kh4IxP0VALuEinwZmKfyW0y2N +tjjH2fMnwVkpoIz2cyQPKCLXTmHdE93bnzJSk/tPzOo4PJhqA6sWryHRQq59RSvq +xM+KWZ+CcHY6+GImyRCXWEAkpC25LymAJ+GJa3LKSQhxN1MF8YDO00IC0vzC0ZQG +7gfi9oPif5/nu1bDW7/dlZMJHiTBzybNraSuwrRp56q17TeU6d3RY4VrmnpKVnbc +GYUo1OTGpNi4lkF30LRZ8UYFh4cCH2m5ghjQQ9km2hpnqNZ1durybQ5C/4gmom6E +/n5iG/DGPe3AHGrHkda4ADdJm7mEBaHNbjHWROpTi7pTmB2hkIrphfgb8pNYw8jc +miZPPiDPT0PzEIx/EGF6NsqqC33Mn0dEWa6llcaZU+MHaz1JELAY/10OhUMUS+dr +00q1smBh3GlJAiNd6JJxw5yfRWd5HtwyhrqqVTxkbzK1EEAV3nJAeOBucLtu6wno +OdmsupJ13UPKugGVrRqBKzrw48UvDBhNEMauwO3+BVJ/GQXLqa81CAw4IuT+VuVT +Pr/k1rPZCMM91TMygSTFqeFlEbgyMzBxGEkdGkXGmhSKWDkobvPLUblJJmR4A8eR +EYOpuZA0tm+qBZ6FKFeZvn8nBkliTaH8CeErRglMFJtWj0U= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx +NTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv +bSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA +VIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku +WnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP +MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX +5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ +ytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg +h5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV +BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE +CgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMB4XDTE3MDUzMTE4MTQzN1oXDTQy +MDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4G +A1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQD +DC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvq +M0fNTPl9fb69LT3w23jhhqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssuf +OePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7wcXHswxzpY6IXFJ3vG2fThVUCAtZJycxa +4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTOZw+oz12WGQvE43LrrdF9 +HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+B6KjBSYR +aZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcA +b9ZhCBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQ +Gp8hLH94t2S42Oim9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQV +PWKchjgGAGYS5Fl2WlPAApiiECtoRHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMO +pgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+SlmJuwgUHfbSguPvuUCYHBBXtSu +UDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48+qvWBkofZ6aY +MBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV +HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa4 +9QaAJadz20ZpqJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBW +s47LCp1Jjr+kxJG7ZhcFUZh1++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5 +Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nxY/hoLVUE0fKNsKTPvDxeH3jnpaAg +cLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2GguDKBAdRUNf/ktUM +79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDzOFSz +/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXt +ll9ldDz7CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEm +Kf7GUmG6sXP/wwyc5WxqlD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKK +QbNmC1r7fSOl8hqw/96bg5Qu0T/fkreRrwU7ZcegbLHNYhLDkBvjJc40vG93drEQ +w/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi +S9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07 +mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNDAzWhcNNDEwMjEyMTgxNDAz +WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0 +b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBS +b290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI +7Z4INcgn64mMU1jrYor+8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPg +CemB+vNH06NjMGEwHQYDVR0OBBYEFILRhXMw5zUE044CkvvlpNHEIejNMA8GA1Ud +EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD +VR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T +kdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+ +gA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE +BhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK +DA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz +OTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv +bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R +xFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX +qhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC +C52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3 +6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh +/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF +YD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E +JNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc +US4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8 +ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm ++Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi +M+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G +A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV +cpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc +Hadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs +PgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/ +q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0 +cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr +a6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I +H37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y +K9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu +nLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf +oYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY +Ic2wBlX7Jz9TkHCpBB5XJ7k= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICOjCCAcCgAwIBAgIQFAP1q/s3ixdAW+JDsqXRxDAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxT +U0wuY29tIFRMUyBFQ0MgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzM0OFoXDTQ2 +MDgxOTE2MzM0N1owTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jwb3Jh +dGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgRUNDIFJvb3QgQ0EgMjAyMjB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABEUpNXP6wrgjzhR9qLFNoFs27iosU8NgCTWyJGYm +acCzldZdkkAZDsalE3D07xJRKF3nzL35PIXBz5SQySvOkkJYWWf9lCcQZIxPBLFN +SeR7T5v15wj4A4j3p8OSSxlUgaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSME +GDAWgBSJjy+j6CugFFR781a4Jl9nOAuc0DAdBgNVHQ4EFgQUiY8vo+groBRUe/NW +uCZfZzgLnNAwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2gAMGUCMFXjIlbp +15IkWE8elDIPDAI2wv2sdDJO4fscgIijzPvX6yv/N33w7deedWo1dlJF4AIxAMeN +b0Igj762TVntd00pxCAgRWSGOlDGxK0tk/UYfXLtqc/ErFc2KAhl3zx5Zn6g6g== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFiTCCA3GgAwIBAgIQb77arXO9CEDii02+1PdbkTANBgkqhkiG9w0BAQsFADBO +MQswCQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQD +DBxTU0wuY29tIFRMUyBSU0EgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzQyMloX +DTQ2MDgxOTE2MzQyMVowTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jw +b3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgUlNBIFJvb3QgQ0EgMjAyMjCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANCkCXJPQIgSYT41I57u9nTP +L3tYPc48DRAokC+X94xI2KDYJbFMsBFMF3NQ0CJKY7uB0ylu1bUJPiYYf7ISf5OY +t6/wNr/y7hienDtSxUcZXXTzZGbVXcdotL8bHAajvI9AI7YexoS9UcQbOcGV0ins +S657Lb85/bRi3pZ7QcacoOAGcvvwB5cJOYF0r/c0WRFXCsJbwST0MXMwgsadugL3 +PnxEX4MN8/HdIGkWCVDi1FW24IBydm5MR7d1VVm0U3TZlMZBrViKMWYPHqIbKUBO +L9975hYsLfy/7PO0+r4Y9ptJ1O4Fbtk085zx7AGL0SDGD6C1vBdOSHtRwvzpXGk3 +R2azaPgVKPC506QVzFpPulJwoxJF3ca6TvvC0PeoUidtbnm1jPx7jMEWTO6Af77w +dr5BUxIzrlo4QqvXDz5BjXYHMtWrifZOZ9mxQnUjbvPNQrL8VfVThxc7wDNY8VLS ++YCk8OjwO4s4zKTGkH8PnP2L0aPP2oOnaclQNtVcBdIKQXTbYxE3waWglksejBYS +d66UNHsef8JmAOSqg+qKkK3ONkRN0VHpvB/zagX9wHQfJRlAUW7qglFA35u5CCoG +AtUjHBPW6dvbxrB6y3snm/vg1UYk7RBLY0ulBY+6uB0rpvqR4pJSvezrZ5dtmi2f +gTIFZzL7SAg/2SW4BCUvAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0j +BBgwFoAU+y437uOEeicuzRk1sTN8/9REQrkwHQYDVR0OBBYEFPsuN+7jhHonLs0Z +NbEzfP/UREK5MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAjYlt +hEUY8U+zoO9opMAdrDC8Z2awms22qyIZZtM7QbUQnRC6cm4pJCAcAZli05bg4vsM +QtfhWsSWTVTNj8pDU/0quOr4ZcoBwq1gaAafORpR2eCNJvkLTqVTJXojpBzOCBvf +R4iyrT7gJ4eLSYwfqUdYe5byiB0YrrPRpgqU+tvT5TgKa3kSM/tKWTcWQA673vWJ +DPFs0/dRa1419dvAJuoSc06pkZCmF8NsLzjUo3KUQyxi4U5cMj29TH0ZR6LDSeeW +P4+a0zvkEdiLA9z2tmBVGKaBUfPhqBVq6+AL8BQx1rmMRTqoENjwuSfr98t67wVy +lrXEj5ZzxOhWc5y8aVFjvO9nHEMaX3cZHxj4HCUp+UmZKbaSPaKDN7EgkaibMOlq +bLQjk2UEqxHzDh1TJElTHaE/nUiSEeJ9DU/1172iWD54nR4fK/4huxoTtrEoZP2w +AgDHbICivRZQIA9ygV/MlP+7mea6kMvq+cYMwq7FGc4zoWtcu358NFcXrfA/rs3q +r5nsLFR+jM4uElZI7xc7P0peYNLcdDa8pUNjyw9bowJWCZ4kLOGGgYz+qxcs+sji +Mho6/4UIyYOf8kpIEFR3N+2ivEC+5BB09+Rbu7nzifmPQdjH5FCQNYA+HLhNkNPU +98OwoX6EyneSMSy4kLGCenROmxMmtNVQZlR4rmA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF2jCCA8KgAwIBAgIMBfcOhtpJ80Y1LrqyMA0GCSqGSIb3DQEBCwUAMIGIMQsw +CQYDVQQGEwJVUzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28x +ITAfBgNVBAoMGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1 +c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMx +OTM0MTJaFw00MjA4MjMxOTM0MTJaMIGIMQswCQYDVQQGEwJVUzERMA8GA1UECAwI +SWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2ZSBI +b2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +ALldUShLPDeS0YLOvR29zd24q88KPuFd5dyqCblXAj7mY2Hf8g+CY66j96xz0Xzn +swuvCAAJWX/NKSqIk4cXGIDtiLK0thAfLdZfVaITXdHG6wZWiYj+rDKd/VzDBcdu +7oaJuogDnXIhhpCujwOl3J+IKMujkkkP7NAP4m1ET4BqstTnoApTAbqOl5F2brz8 +1Ws25kCI1nsvXwXoLG0R8+eyvpJETNKXpP7ScoFDB5zpET71ixpZfR9oWN0EACyW +80OzfpgZdNmcc9kYvkHHNHnZ9GLCQ7mzJ7Aiy/k9UscwR7PJPrhq4ufogXBeQotP +JqX+OsIgbrv4Fo7NDKm0G2x2EOFYeUY+VM6AqFcJNykbmROPDMjWLBz7BegIlT1l +RtzuzWniTY+HKE40Cz7PFNm73bZQmq131BnW2hqIyE4bJ3XYsgjxroMwuREOzYfw +hI0Vcnyh78zyiGG69Gm7DIwLdVcEuE4qFC49DxweMqZiNu5m4iK4BUBjECLzMx10 +coos9TkpoNPnG4CELcU9402x/RpvumUHO1jsQkUm+9jaJXLE9gCxInm943xZYkqc +BW89zubWR2OZxiRvchLIrH+QtAuRcOi35hYQcRfO3gZPSEF9NUqjifLJS3tBEW1n +twiYTOURGa5CgNz7kAXU+FDKvuStx8KU1xad5hePrzb7AgMBAAGjQjBAMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFJngGWcNYtt2s9o9uFvo/ULSMQ6HMA4GA1Ud +DwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAmHNw4rDT7TnsTGDZqRKGFx6W +0OhUKDtkLSGm+J1WE2pIPU/HPinbbViDVD2HfSMF1OQc3Og4ZYbFdada2zUFvXfe +uyk3QAUHw5RSn8pk3fEbK9xGChACMf1KaA0HZJDmHvUqoai7PF35owgLEQzxPy0Q +lG/+4jSHg9bP5Rs1bdID4bANqKCqRieCNqcVtgimQlRXtpla4gt5kNdXElE1GYhB +aCXUNxeEFfsBctyV3lImIJgm4nb1J2/6ADtKYdkNy1GTKv0WBpanI5ojSP5RvbbE +sLFUzt5sQa0WZ37b/TjNuThOssFgy50X31ieemKyJo90lZvkWx3SD92YHJtZuSPT +MaCm/zjdzyBP6VhWOmfD0faZmZ26NraAL4hHT4a/RDqA5Dccprrql5gR0IRiR2Qe +qu5AvzSxnI9O4fKSTx+O856X3vOmeWqJcU9LJxdI/uz0UA9PSX3MReO9ekDFQdxh +VicGaeVyQYHTtgGJoC86cnn+OjC/QezHYj6RS8fZMXZC+fc8Y+wmjHMMfRod6qh8 +h6jCJ3zhM0EPz8/8AKAigJ5Kp28AsEFFtyLKaEjFQqKu3R3y4G5OBVixwJAWKqQ9 +EEC+j2Jjg6mcgn0tAumDMHzLJ8n9HmYAsC7TIS+OMxZsmO0QqAfWzJPP29FpHOTK +yeC2nOnOcXHebD8WpHk= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICYDCCAgegAwIBAgIMDWpfCD8oXD5Rld9dMAoGCCqGSM49BAMCMIGRMQswCQYD +VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf +BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 +YXZlIEdsb2JhbCBFQ0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x +NzA4MjMxOTM1MTBaFw00MjA4MjMxOTM1MTBaMIGRMQswCQYDVQQGEwJVUzERMA8G +A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 +d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF +Q0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqG +SM49AwEHA0IABH77bOYj43MyCMpg5lOcunSNGLB4kFKA3TjASh3RqMyTpJcGOMoN +FWLGjgEqZZ2q3zSRLoHB5DOSMcT9CTqmP62jQzBBMA8GA1UdEwEB/wQFMAMBAf8w +DwYDVR0PAQH/BAUDAwcGADAdBgNVHQ4EFgQUo0EGrJBt0UrrdaVKEJmzsaGLSvcw +CgYIKoZIzj0EAwIDRwAwRAIgB+ZU2g6gWrKuEZ+Hxbb/ad4lvvigtwjzRM4q3wgh +DDcCIC0mA6AFvWvR9lz4ZcyGbbOcNEhjhAnFjXca4syc4XR7 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICnTCCAiSgAwIBAgIMCL2Fl2yZJ6SAaEc7MAoGCCqGSM49BAMDMIGRMQswCQYD +VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf +BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 +YXZlIEdsb2JhbCBFQ0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x +NzA4MjMxOTM2NDNaFw00MjA4MjMxOTM2NDNaMIGRMQswCQYDVQQGEwJVUzERMA8G +A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 +d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF +Q0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTB2MBAGByqGSM49AgEGBSuB +BAAiA2IABGvaDXU1CDFHBa5FmVXxERMuSvgQMSOjfoPTfygIOiYaOs+Xgh+AtycJ +j9GOMMQKmw6sWASr9zZ9lCOkmwqKi6vr/TklZvFe/oyujUF5nQlgziip04pt89ZF +1PKYhDhloKNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNVHQ8BAf8EBQMDBwYAMB0G +A1UdDgQWBBRVqYSJ0sEyvRjLbKYHTsjnnb6CkDAKBggqhkjOPQQDAwNnADBkAjA3 +AZKXRRJ+oPM+rRk6ct30UJMDEr5E0k9BpIycnR+j9sKS50gU/k6bpZFXrsY3crsC +MGclCrEMXu6pY5Jv5ZAL/mYiykf9ijH3g/56vxC+GCsej/YpHpRZ744hN8tRmKVu +Sw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCB +gjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEk +MCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRY +UmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQxMTAxMTcx +NDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3 +dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2Vy +dmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS6 +38eMpSe2OAtp87ZOqCwuIR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCP +KZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMxfoArtYzAQDsRhtDLooY2YKTVMIJt2W7Q +DxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FEzG+gSqmUsE3a56k0enI4 +qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqsAxcZZPRa +JSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNVi +PvryxS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0P +BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASs +jVy16bYbMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0 +eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQEwDQYJKoZIhvcNAQEFBQAD +ggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc/Kh4ZzXxHfAR +vbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt +qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLa +IR9NmXmd4c8nnxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSy +i6mx5O+aGtA9aZnuqCij4Tyz8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQ +O+7ETPTsJ3xCwnR8gooJybQDJbw= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2ln +biBHb2xkIENBIC0gRzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBF +MQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMR8wHQYDVQQDExZT +d2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUqt2/8 +76LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+ +bbqBHH5CjCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c +6bM8K8vzARO/Ws/BtQpgvd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqE +emA8atufK+ze3gE/bk3lUIbLtK/tREDFylqM2tIrfKjuvqblCqoOpd8FUrdVxyJd +MmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvRAiTysybUa9oEVeXBCsdt +MDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuendjIj3o02y +MszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69y +FGkOpeUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPi +aG59je883WX0XaxR7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxM +gI93e2CaHt+28kgeDrpOVG2Y4OGiGqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCB +qTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUWyV7 +lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64OfPAeGZe6Drn +8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov +L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe6 +45R88a7A3hfm5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczO +UYrHUDFu4Up+GC9pWbY9ZIEr44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5 +O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOfMke6UiI0HTJ6CVanfCU2qT1L2sCC +bwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6mGu6uLftIdxf+u+yv +GPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxpmo/a +77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCC +hdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3 +92qgQmwLOM7XdVAyksLfKzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEpp +Ld6leNcG2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+w +ZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+htt +Qc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFlzCCA3+gAwIBAgIURg7UAXGQoBqDLEpCECgV0mEbrTIwDQYJKoZIhvcNAQEL +BQAwUzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEtMCsGA1UE +AxMkU3dpc3NTaWduIFJTQSBTTUlNRSBSb290IENBIDIwMjIgLSAxMB4XDTIyMDYw +ODEwNTMxM1oXDTQ3MDYwODEwNTMxM1owUzELMAkGA1UEBhMCQ0gxFTATBgNVBAoT +DFN3aXNzU2lnbiBBRzEtMCsGA1UEAxMkU3dpc3NTaWduIFJTQSBTTUlNRSBSb290 +IENBIDIwMjIgLSAxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1Pv6 +P4aimXAJOsnWoU4Bzka1LSRIDUXprMka1zKApObTytbyKTfsmizWgc7mG52xD0Hf +WNNfqqB5WQuMrfnF+Rz7w+k1QHTDwQzLZ/ucXgwj+dAv+kyCRRy19R/4GW7ak7dO +aIN+Yi0djJUfcNnOWowhXai+CKlWbdn3uZCZxzvXvZ4uyWdXLiHO8DKD+wQB+beC +RA2yy3oJoUg+T8ALahsb7M8dnn8GkKwoBQuo5lQ7oqcsOROZqPs06/XwvQHYiBHI +rroZAkkC3IostL1hYOydeFxqiy8Xhl7yT5MAa13FsqmlGOrmbX5XBfsH/Lx8oUOx +ZhyoZ/urN/aqqrh6Qfc51YyfrnI2J+RixkOZ8aFB6f+Jnw9Jr8kUBhcnZDkNpbQq +W+w8+5/FX8Y7XSYZ8oQpuJVECVL9bDDQYo8opYGWK5QvJnXkCYwK3zjzfl04joKa +jNyers4SQjoi8jWNT9IayEkzC/o2P/8sa2ogcUzNrRA/aTKEjlzuU4hE4t3MAzCS +hnmQKkt1+1JixPRvTffbI6EY3UVTF5pjJEiJIs1+mwEcgCgDj1sr+h/jfBm95o+x +QHag8sc3sjKUEDLNpxOX8TssejQie3Q6QOKvgvjBwXj8X+Q1f8D0TPBMsuqHA3Il +WYMqCKRR3s/uqOfoQD+I8DarCU7YoKh/8+EJ27kCAwEAAaNjMGEwDwYDVR0TAQH/ +BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHwYDVR0jBBgwFoAUzC6tiYyD40CjJWml +6pJ90jc6x8YwHQYDVR0OBBYEFMwurYmMg+NAoyVppeqSfdI3OsfGMA0GCSqGSIb3 +DQEBCwUAA4ICAQAAB2YWpe3Hub+8yJGtWO1eEgWz9kabe+SEEC8HsVpeMm5tAPBe +x5piOYdN5Dzzvva6alNshG0H1GHKZ2a+mz5FMJ1R0tdaQq6dkg4jq9AVlD6omsqb +7cHCXyGjmYD8uaZhDlCAgCfH6H2g1JR6mAPn7kKL81JQXO++sHZaHAmhv4PAHnZl +0CVBW2mRk3f5jEvwLNubBgAXg/palLSGie+8CgsS+AZN0nPikThduWpLT6ev2iYl +kiMafB8nDZGE7xdy9kbrazs3qdTVmmO6XnmMKrWbojS1zJYn+XkIPH9t4P983MUm +r8OhemkW3Yc1c8ZrMWtWAS1PmdnuyuHQg962hecW+NGuM0j7Gs9dX4qEYXQHbxmw +USGyoQSxe1OP76JFrR+Y3flqBGyqNsWvjOopSUrn/1ezxjwRSRgX5maF4egj8osO +PJPEP3ZOfmKiKcsWMN4saa+Rp+JX5TNMv9iOB6J/oTVGaUqoICn/694glVmxrk0w +a9iatAMfwjjkINUO1howTGicjODtoQ+OQl3rgCoSeaYXF7SVKo40kae90ayoGkMh +i97v4KxGJWUKxiuhmz4i6Bg4tSb2LMoIIN4w0a1U/dxIFZ/Np0HXNziFME8SiEM0 +g9cqTdQAV1zlyvDd4ZIoKxh1vUekQhPpVlqNSl7ODnU1gHMZDywpi7uVuA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFkzCCA3ugAwIBAgIUQ/oMX04bgBhE79G0TzUfRPSA7cswDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzErMCkGA1UE +AxMiU3dpc3NTaWduIFJTQSBUTFMgUm9vdCBDQSAyMDIyIC0gMTAeFw0yMjA2MDgx +MTA4MjJaFw00NzA2MDgxMTA4MjJaMFExCzAJBgNVBAYTAkNIMRUwEwYDVQQKEwxT +d2lzc1NpZ24gQUcxKzApBgNVBAMTIlN3aXNzU2lnbiBSU0EgVExTIFJvb3QgQ0Eg +MjAyMiAtIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDLKmjiC8NX +vDVjvHClO/OMPE5Xlm7DTjak9gLKHqquuN6orx122ro10JFwB9+zBvKK8i5VUXu7 +LCTLf5ImgKO0lPaCoaTo+nUdWfMHamFk4saMla+ju45vVs9xzF6BYQ1t8qsCLqSX +5XH8irCRIFucdFJtrhUnWXjyCcplDn/L9Ovn3KlMd/YrFgSVrpxxpT8q2kFC5zyE +EPThPYxr4iuRR1VPuFa+Rd4iUU1OKNlfGUEGjw5NBuBwQCMBauTLE5tzrE0USJIt +/m2n+IdreXXhvhCxqohAWVTXz8TQm0SzOGlkjIHRI36qOTw7D59Ke4LKa2/KIj4x +0LDQKhySio/YGZxH5D4MucLNvkEM+KRHBdvBFzA4OmnczcNpI/2aDwLOEGrOyvi5 +KaM2iYauC8BPY7kGWUleDsFpswrzd34unYyzJ5jSmY0lpx+Gs6ZUcDj8fV3oT4MM +0ZPlEuRU2j7yrTrePjxF8CgPBrnh25d7mUWe3f6VWQQvdT/TromZhqwUtKiE+shd +OxtYk8EXlFXIC+OCeYSf8wCENO7cMdWP8vpPlkwGqnj73mSiI80fPsWMvDdUDrta +clXvyFu1cvh43zcgTFeRc5JzrBh3Q4IgaezprClG5QtO+DdziZaKHG29777YtvTK +wP1H8K4LWCDFyB02rpeNUIMmJCn3nTsPBQIDAQABo2MwYTAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBRvjmKLk0Ow4UD2p8P98Q+4 +DxU4pTAdBgNVHQ4EFgQUb45ii5NDsOFA9qfD/fEPuA8VOKUwDQYJKoZIhvcNAQEL +BQADggIBAKwsKUF9+lz1GpUYvyypiqkkVHX1uECry6gkUSsYP2OprphWKwVDIqO3 +10aewCoSPY6WlkDfDDOLazeROpW7OSltwAJsipQLBwJNGD77+3v1dj2b9l4wBlgz +Hqp41eZUBDqyggmNzhYzWUUo8aWjlw5DI/0LIICQ/+Mmz7hkkeUFjxOgdg3XNwwQ +iJb0Pr6VvfHDffCjw3lHC1ySFWPtUnWK50Zpy1FVCypM9fJkT6lc/2cyjlUtMoIc +gC9qkfjLvH4YoiaoLqNTKIftV+Vlek4ASltOU8liNr3CjlvrzG4ngRhZi0Rjn9UM +ZfQpZX+RLOV/fuiJz48gy20HQhFRJjKKLjpHE7iNvUcNCfAWpO2Whi4Z2L6MOuhF +LhG6rlrnub+xzI/goP+4s9GFe3lmozm1O2bYQL7Pt2eLSMkZJVX8vY3PXtpOpvJp +zv1/THfQwUY1mFwjmwJFQ5Ra3bxHrSL+ul4vkSkphnsh3m5kt8sNjzdbowhq6/Td +Ao9QAwKxuDdollDruF/UKIqlIgyKhPBZLtU30WHlQnNYKoH3dtvi4k0NX/a3vgW0 +rk4N3hY9A4GzJl5LuEsAz/+MF7psYC0nhzck5npgL7XTgwSqT0N1osGDsieYK7EO +gLrAhV5Cud+xYJHT6xh+cHiudoO+cVrQkOPKwRYlZ0rwtnu64ZzZ +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIQQAE0jMIAAAAAAAAAATzyxjANBgkqhkiG9w0BAQwFADBQ +MQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290 +IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3QgQ0EwHhcNMjIxMTIyMDY1NDI5 +WhcNNDcxMTIyMTU1OTU5WjBQMQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FO +LUNBMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3Qg +Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDG+Moe2Qkgfh1sTs6P +40czRJzHyWmqOlt47nDSkvgEs1JSHWdyKKHfi12VCv7qze33Kc7wb3+szT3vsxxF +avcokPFhV8UMxKNQXd7UtcsZyoC5dc4pztKFIuwCY8xEMCDa6pFbVuYdHNWdZsc/ +34bKS1PE2Y2yHer43CdTo0fhYcx9tbD47nORxc5zb87uEB8aBs/pJ2DFTxnk684i +JkXXYJndzk834H/nY62wuFm40AZoNWDTNq5xQwTxaWV4fPMf88oon1oglWa0zbfu +j3ikRRjpJi+NmykosaS3Om251Bw4ckVYsV7r8Cibt4LK/c/WMw+f+5eesRycnupf +Xtuq3VTpMCEobY5583WSjCb+3MX2w7DfRFlDo7YDKPYIMKoNM+HvnKkHIuNZW0CP +2oi3aQiotyMuRAlZN1vH4xfyIutuOVLF3lSnmMlLIJXcRolftBL5hSmO68gnFSDA +S9TMfAxsNAwmmyYxpjyn9tnQS6Jk/zuZQXLB4HCX8SS7K8R0IrGsayIyJNN4KsDA +oS/xUgXJP+92ZuJF2A09rZXIx4kmyA+upwMu+8Ff+iDhcK2wZSA3M2Cw1a/XDBzC +kHDXShi8fgGwsOsVHkQGzaRP6AzRwyAQ4VRlnrZR0Bp2a0JaWHY06rc3Ga4udfmW +5cFZ95RXKSWNOkyrTZpB0F8mAwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSdhWEUfMFib5do5E83QOGt4A1WNzAd +BgNVHQ4EFgQUnYVhFHzBYm+XaORPN0DhreANVjcwDQYJKoZIhvcNAQEMBQADggIB +AGSPesRiDrWIzLjHhg6hShbNcAu3p4ULs3a2D6f/CIsLJc+o1IN1KriWiLb73y0t +tGlTITVX1olNc79pj3CjYcya2x6a4CD4bLubIp1dhDGaLIrdaqHXKGnK/nZVekZn +68xDiBaiA9a5F/gZbG0jAn/xX9AKKSM70aoK7akXJlQKTcKlTfjF/biBzysseKNn +TKkHmvPfXvt89YnNdJdhEGoHK4Fa0o635yDRIG4kqIQnoVesqlVYL9zZyvpoBJ7t +RCT5dEA7IzOrg1oYJkK2bVS1FmAwbLGg+LhBoF1JSdJlBTrq/p1hvIbZv97Tujqx +f36SNI7JAG7cmL3c7IAFrQI932XtCwP39xaEBDG6k5TY8hL4iuO/Qq+n1M0RFxbI +Qh0UqEL20kCGoE8jypZFVmAGzbdVAaYBlGX+bgUJurSkquLvWL69J1bY73NxW0Qz +8ppy6rBePm6pUlvscG21h483XjyMnM7k8M4MZ0HMzvaAq07MTFb1wWFZk7Q+ptq4 +NxKfKjLji7gh7MMrZQzvIt6IKTtM1/r+t+FHvpw+PoP7UV31aPcuIYXcv/Fa4nzX +xeSDwWrruoBa3lwtcHb4yOWHh8qgnaHlIhInD0Q9HWzq1MKLL295q39QpsQZp6F6 +t5b5wR9iWqJDB0BeJsas7a5wFsWqynKKTbDPAYsDP27X +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx +EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT +VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 +NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT +B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF +10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz +0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh +MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH +zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc +46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2 +yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi +laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP +oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA +BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE +qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm +4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL +1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn +LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF +H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo +RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+ +nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh +15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW +6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW +nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j +wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz +aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy +KwbQBM0= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFlTCCA32gAwIBAgIQQAE0jMIAAAAAAAAAAZdY9DANBgkqhkiG9w0BAQwFADBU +MQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290 +IENBMR8wHQYDVQQDExZUV0NBIEdsb2JhbCBSb290IENBIEcyMB4XDTIyMTEyMjA2 +NDIyMVoXDTQ3MTEyMjE1NTk1OVowVDELMAkGA1UEBhMCVFcxEjAQBgNVBAoTCVRB +SVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEfMB0GA1UEAxMWVFdDQSBHbG9iYWwg +Um9vdCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKoO1SCS +Aa2C+QwIkTRrihbQRhb/A7jYjeqTNPv/K739bqrcm/KGgVX1iRzEjXVqWHiREx4C +E3A9774K5wCPuDHldMUwvv991pnlwkKjzyHWswh/kdVh5qKVEA3vXpcLSTjVIrDX +i1lvnzWbf9KRzHp/u6Cf3lUz9kuNCup9CcB53L1E4v4c52QhKM8ESuK0v4Z5KrsO +k8mPXqwwOVKQB7nqnCZCFMRnRv7RGmihPlAZoyYKJymQwva063OaeB7hmPRlDDUh +BvgL3mLlTcGzXdm5+mGXKuPqx0RVJJL+Eqc/xHfgLQKBB9X7feYQnjq0qO/s+1Dq +Nc/MfrtCuURsUum/KnIfP96bcOncWsU7u7/wWYWvL8GwFHkFrHWfJfURJwZgIcdt +Zb6oiZzlrEbf+F1EA41gvfexDcwv70FUL+5rlblOfDTfO/l3nX3NBz0cBjMSgOxy +nPItgtrVO8TH+QTDZAJ89TVgp7RGKS4b76VYgC56iVE4Njz9oXe4gDDQit6NpzQm +7CO7GFUYNkXu7QEGqk2/ZAzKmJcaMQJm+HhoW4jfCajnm/o0bXAcIa0Ii/Khtqx2 +ar/xgCUAvjweTa65PLaVY71rfkcSkFVFEY3sFx/BvieBk1djaQAmd4vDWeV70Q1E +8qjw94WaBffCLnCak4XYlZAxkFSm7AufN0UPAgMBAAGjYzBhMA4GA1UdDwEB/wQE +AwIBBjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFJKM1DbRW0dTxHENhN1k +KvU2ZEDnMB0GA1UdDgQWBBSSjNQ20VtHU8RxDYTdZCr1NmRA5zANBgkqhkiG9w0B +AQwFAAOCAgEAJfxL2pC02nXnQTqB0ab+oGrzGHFiaiQIi6l6TclVzs8QKC4EGZYF +z10CICo7s1U/Ac1CzbJ37f9183x325alz4xnBvSkm3L2IUkJmKMyXndaYwnvYkOX +Aji16jwYUGj8WVvZedTx5FZIE1bY03ELXniUOBFF+gUX9Q51HmJSYUa6LhmthrSI +D7FQ5kAANBqVnZPgUfnUVUbplTwlhi6X1wExGETsHGDpfWmvMviXQCUkto0aVTzF +t/e8BlI7cTBwPnEXfvFmBF5dvIoxQ6aSHXtU0qU2i2+N1l7a1MMuHd85VWCCMJ4n +/46A3WNMplU12NAzqYBtPl6dzKhngGb6mVcMUsoZdbA4NVUqgcWMHlbXX5DyINja +4GZx6bJ4q2e5JG5rNnL8b439f3I5KGdSkQUfV2XSo6cNYfqh59U1RpXJBof2MOwy +UamsVsAhTqMUdAU6vOO/bT1OP16lpG0pv4RRdVOOhhr1UXAqDRxOQOH9o+OlK2eQ +ksdsroW/OpsXFcqcKpPUTTkNvCAIo42IbAkNjK5EIU3JcezYJtcXni0RGDyjIn24 +J1S/aMg7QsyPXk7n3MLF+mpED41WiHrfiYRsoLM+PfFlAAmI6irrQM6zXawyF67B +m+nQwfVJlN2nznxaB+uuIJwXMJJpk3Lzmltxm/5q33owaY6zLtsPLN0= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES +MBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU +V0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz +WhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO +LUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE +AcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH +K3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX +RfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z +rX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx +3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq +hkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC +MErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls +XebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D +lhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn +aspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ +YiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIPAWdfJ9b+euPkrL4JWwWeMA0GCSqGSIb3DQEBCwUAMEQx +CzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UE +AwwQVGVsaWEgUm9vdCBDQSB2MjAeFw0xODExMjkxMTU1NTRaFw00MzExMjkxMTU1 +NTRaMEQxCzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZ +MBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2MjCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBALLQPwe84nvQa5n44ndp586dpAO8gm2h/oFlH0wnrI4AuhZ76zBq +AMCzdGh+sq/H1WKzej9Qyow2RCRj0jbpDIX2Q3bVTKFgcmfiKDOlyzG4OiIjNLh9 +vVYiQJ3q9HsDrWj8soFPmNB06o3lfc1jw6P23pLCWBnglrvFxKk9pXSW/q/5iaq9 +lRdU2HhE8Qx3FZLgmEKnpNaqIJLNwaCzlrI6hEKNfdWV5Nbb6WLEWLN5xYzTNTOD +n3WhUidhOPFZPY5Q4L15POdslv5e2QJltI5c0BE0312/UqeBAMN/mUWZFdUXyApT +7GPzmX3MaRKGwhfwAZ6/hLzRUssbkmbOpFPlob/E2wnW5olWK8jjfN7j/4nlNW4o +6GwLI1GpJQXrSPjdscr6bAhR77cYbETKJuFzxokGgeWKrLDiKca5JLNrRBH0pUPC +TEPlcDaMtjNXepUugqD0XBCzYYP2AgWGLnwtbNwDRm41k9V6lS/eINhbfpSQBGq6 +WT0EBXWdN6IOLj3rwaRSg/7Qa9RmjtzG6RJOHSpXqhC8fF6CfaamyfItufUXJ63R +DolUK5X6wK0dmBR4M0KGCqlztft0DbcbMBnEWg4cJ7faGND/isgFuvGqHKI3t+ZI +pEYslOqodmJHixBTB0hXbOKSTbauBcvcwUpej6w9GU7C7WB1K9vBykLVAgMBAAGj +YzBhMB8GA1UdIwQYMBaAFHKs5DN5qkWH9v2sHZ7Wxy+G2CQ5MB0GA1UdDgQWBBRy +rOQzeapFh/b9rB2e1scvhtgkOTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw +AwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAoDtZpwmUPjaE0n4vOaWWl/oRrfxn83EJ +8rKJhGdEr7nv7ZbsnGTbMjBvZ5qsfl+yqwE2foH65IRe0qw24GtixX1LDoJt0nZi +0f6X+J8wfBj5tFJ3gh1229MdqfDBmgC9bXXYfef6xzijnHDoRnkDry5023X4blMM +A8iZGok1GTzTyVR8qPAs5m4HeW9q4ebqkYJpCh3DflminmtGFZhb069GHWLIzoBS +SRE/yQQSwxN8PzuKlts8oB4KtItUsiRnDe+Cy748fdHif64W1lZYudogsYMVoe+K +TTJvQS8TUoKU1xrBeKJR3Stwbbca+few4GeXVtt8YVMJAygCQMez2P2ccGrGKMOF +6eLtGpOg3kuYooQ+BXcBlj37tCAPnHICehIv1aO6UXivKitEZU61/Qrowc15h2Er +3oBXRb9n8ZuRXqWk7FlIEA04x7D6w0RtBPV4UBySllva9bguulvP5fBqnUsvWHMt +Ty3EHD70sz+rFQ47GUGKpMFXEmZxTPpT41frYpUJnlTd0cI8Vzy9OK2YZLe4A5pT +VmBds9hCG1xLEooc6+t9xnppxyd/pPiL8uSUZodL6ZQHCRJ5irLrdATczvREWeAW +ysUsWNc8e89ihmpQfTU2Zqf7N+cox9jQraVplI/owd8k+BsHMYeB2F326CjYSlKA +rBPuUBQemMc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAw +NzEUMBIGA1UECgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJv +b3QgQ0EgdjEwHhcNMDcxMDE4MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYD +VQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwWVGVsaWFTb25lcmEgUm9vdCBDQSB2 +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+6yfwIaPzaSZVfp3F +VRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA3GV1 +7CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+X +Z75Ljo1kB1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+ +/jXh7VB7qTCNGdMJjmhnXb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs +81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxHoLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkm +dtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3F0fUTPHSiXk+TT2YqGHe +Oh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJoWjiUIMu +sDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4 +pgd7gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fs +slESl1MpWtTwEhDcTwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQ +arMCpgKIv7NHfirZ1fpoeDVNAgMBAAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYD +VR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qWDNXr+nuqF+gTEjANBgkqhkiG +9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNmzqjMDfz1mgbl +dxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx +0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1Tj +TQpgcmLNkQfWpb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBed +Y2gea+zDTYa4EzAvXUYNR0PVG6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7 +Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpcc41teyWRyu5FrgZLAMzTsVlQ2jqI +OylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOTJsjrDNYmiLbAJM+7 +vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2qReW +t88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcn +HL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx +SK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFpTCCA42gAwIBAgIUZPYOZXdhaqs7tOqFhLuxibhxkw8wDQYJKoZIhvcNAQEM +BQAwWjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dp +ZXMsIEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHMzAe +Fw0yMTA1MjAwMjEwMTlaFw00NjA1MTkwMjEwMTlaMFoxCzAJBgNVBAYTAkNOMSUw +IwYDVQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDDBtU +cnVzdEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzMwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQDAMYJhkuSUGwoqZdC+BqmHO1ES6nBBruL7dOoKjbmzTNyPtxNS +T1QY4SxzlZHFZjtqz6xjbYdT8PfxObegQ2OwxANdV6nnRM7EoYNl9lA+sX4WuDqK +AtCWHwDNBSHvBm3dIZwZQ0WhxeiAysKtQGIXBsaqvPPW5vxQfmZCHzyLpnl5hkA1 +nyDvP+uLRx+PjsXUjrYsyUQE49RDdT/VP68czH5GX6zfZBCK70bwkPAPLfSIC7Ep +qq+FqklYqL9joDiR5rPmd2jE+SoZhLsO4fWvieylL1AgdB4SQXMeJNnKziyhWTXA +yB1GJ2Faj/lN03J5Zh6fFZAhLf3ti1ZwA0pJPn9pMRJpxx5cynoTi+jm9WAPzJMs +hH/x/Gr8m0ed262IPfN2dTPXS6TIi/n1Q1hPy8gDVI+lhXgEGvNz8teHHUGf59gX +zhqcD0r83ERoVGjiQTz+LISGNzzNPy+i2+f3VANfWdP3kXjHi3dqFuVJhZBFcnAv +kV34PmVACxmZySYgWmjBNb9Pp1Hx2BErW+Canig7CjoKH8GB5S7wprlppYiU5msT +f9FkPz2ccEblooV7WIQn3MSAPmeamseaMQ4w7OYXQJXZRe0Blqq/DPNL0WP3E1jA +uPP6Z92bfW1K/zJMtSU7/xxnD4UiWQWRkUF3gdCFTIcQcf+eQxuulXUtgQIDAQAB +o2MwYTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFEDk5PIj7zjKsK5Xf/Ih +MBY027ySMB0GA1UdDgQWBBRA5OTyI+84yrCuV3/yITAWNNu8kjAOBgNVHQ8BAf8E +BAMCAQYwDQYJKoZIhvcNAQEMBQADggIBACY7UeFNOPMyGLS0XuFlXsSUT9SnYaP4 +wM8zAQLpw6o1D/GUE3d3NZ4tVlFEbuHGLige/9rsR82XRBf34EzC4Xx8MnpmyFq2 +XFNFV1pF1AWZLy4jVe5jaN/TG3inEpQGAHUNcoTpLrxaatXeL1nHo+zSh2bbt1S1 +JKv0Q3jbSwTEb93mPmY+KfJLaHEih6D4sTNjduMNhXJEIlU/HHzp/LgV6FL6qj6j +ITk1dImmasI5+njPtqzn59ZW/yOSLlALqbUHM/Q4X6RJpstlcHboCoWASzY9M/eV +VHUl2qzEc4Jl6VL1XP04lQJqaTDFHApXB64ipCz5xUG3uOyfT0gA+QEEVcys+TIx +xHWVBqB/0Y0n3bOppHKH/lmLmnp0Ft0WpWIp6zqW3IunaFnT63eROfjXy9mPX1on +AX1daBli2MjN9LdyR75bl87yraKZk62Uy5P2EgmVtqvXO9A/EcswFi55gORngS1d +7XB4tmBZrOFdRWOPyN9yaFvqHbgB8X7754qz41SgOAngPN5C8sLtLpvzHzW2Ntjj +gKGLzZlkD8Kqq7HK9W+eQ42EVJmzbsASZthwEPEGNTNDqJwuuhQxzhB/HIbjj9LV ++Hfsm6vxL2PZQl/gZ4FkkfGXL/xuJvYz+NO1+MRiqzFRJQJ6+N1rZdVtTTDIZbpo +FGWsJwt0ivKH +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICVTCCAdygAwIBAgIUTyNkuI6XY57GU4HBdk7LKnQV1tcwCgYIKoZIzj0EAwMw +WjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs +IEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHNDAeFw0y +MTA1MjAwMjEwMjJaFw00NjA1MTkwMjEwMjJaMFoxCzAJBgNVBAYTAkNOMSUwIwYD +VQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDDBtUcnVz +dEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATx +s8045CVD5d4ZCbuBeaIVXxVjAd7Cq92zphtnS4CDr5nLrBfbK5bKfFJV4hrhPVbw +LxYI+hW8m7tH5j/uqOFMjPXTNvk4XatwmkcN4oFBButJ+bAp3TPsUKV/eSm4IJij +YzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUpbtKl86zK3+kMd6Xg1mD +pm9xy94wHQYDVR0OBBYEFKW7SpfOsyt/pDHel4NZg6ZvccveMA4GA1UdDwEB/wQE +AwIBBjAKBggqhkjOPQQDAwNnADBkAjBe8usGzEkxn0AAbbd+NvBNEU/zy4k6LHiR +UKNbwMp1JvK/kF0LgoxgKJ/GcJpo5PECMFxYDlZ2z1jD1xCMuo6u47xkdUfFVZDj +/bpV6wfEU6s3qe4hsiFbYI89MvHVI5TWWA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICNjCCAbugAwIBAgIUWsL4KU/jfcVeHRhvO5MgH/97ui0wCgYIKoZIzj0EAwMw +WjELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs +IEluYy4xJDAiBgNVBAMTG1RydXN0QXNpYSBTTUlNRSBFQ0MgUm9vdCBDQTAeFw0y +NDA1MTUwNTQxNTlaFw00NDA1MTUwNTQxNThaMFoxCzAJBgNVBAYTAkNOMSUwIwYD +VQQKExxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDExtUcnVz +dEFzaWEgU01JTUUgRUNDIFJvb3QgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATN +2fsnvWnshsmQQ7FwF5SnyXcjOj8jZdMcox0eQlQg69BCu1m5i6zyho1Ljh2qliIj +OXZtkpvrIst6Q6Jz/XNLwiUPKrFpxv9F36k8lYC7qR5Kky/sHB2I9BGSN583mHKj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFDFn5nKyDeYioKzPfiKnWTLj +ZiOlMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNpADBmAjEA3TpMjaTGf+29 +pcZPPv0xSyjWilbfZRZ3h037ujIIgeCeM0iLn5SG7wErlOaM1tSOAjEAn4GcsCb9 +K9by9XGEnqjHiozWWBFStbgEy8xxdWPixhk42W1sGXGkFhkhk7oGRChs +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFhDCCA2ygAwIBAgIUWu5x394MV4W1uzYi17h2RgJzyv8wDQYJKoZIhvcNAQEM +BQAwWjELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dp +ZXMsIEluYy4xJDAiBgNVBAMTG1RydXN0QXNpYSBTTUlNRSBSU0EgUm9vdCBDQTAe +Fw0yNDA1MTUwNTQyMDFaFw00NDA1MTUwNTQyMDBaMFoxCzAJBgNVBAYTAkNOMSUw +IwYDVQQKExxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDExtU +cnVzdEFzaWEgU01JTUUgUlNBIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQCYlZytPFlz05N2pkhUphyIckxN4YL/GhMfUN6M2ZBC0byZ0zej +13E6yt1eG5BhQm6PQAFzfR8xutQdbgTSqpCESjMKRJ9aGR+0bi1o/K/An0oQEr8+ +gsKCsC/nkG+QZBCD7Ow2lAx8T+ACDT2HeUJNAOUwrnAfFf36z1IlNk15ILvxEJjg +YIfJ9XgMIu0C5hFs8ZtakRF0htD+eJKWBMOY78Zwr6mQqhb2Iu3Y+kYoceLJCMBQ +vHajui2W8hH5pL0QVvgnbStLZIjcF13PAAiKkq4azBLX3/AQKPPNOuo6Eowb52EJ +Q2rkOOn+dDnbzQo7w09T1q5x1TiDhx/O50zzEVWH37ev9+sahhBtqO1I3TLQ26oq +C3J3KXf9eug/eCAqaL7ebwjmtYVHgDf5cZaLpZhWl3wRZRaO1M7YJ9T5WsWnjbvR +Nw2lq2Vd2nSTiF7bdfZ/m8KasW0IAgyYSrvNMK92NQKFViNRCUAJBffwPR7CyHoa +usVBFbkNdrS0pLhF/Y2jOz0DKs2zlX80e92hT9k6/yf1DcIBnP9ZdVoayefS/X9P +D7X+DTzmoNb7tXZctDBNED/+4utaDrFPT1B+CDMCkVcySYmnQBBQF2ufY7qyslaY +dvT/cukEnNSnTE/2Oh9aVDFvy7oyrfhtr0XHe2NE38L9eOhKirB0dRbejwIDAQAB +o0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSAGqpDwcl/ixqWRbw9u2tI +UmxbqzAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEMBQADggIBACp1gaGCIOp/ +Vq4JMJcQePTZQBRSpO5qf/AJKNYQY+BOe8kxxwilF+uvhuKXB0+pDqKFzO2kgIEd +WlMGPEwaqbeEhs989YUKcJnQ7TaRjed3Ls6EnCiGLSU1jEwB5n3bYV3id4TTAdFi +3QyiCmSk/PDtOkjyOew11qF6F3Hs09LsuCb7rRVwVkrPZMC5YFv35s2gwgMr+bLl +2rqlNxzYjdp5dCpn5KJ6xyyNpcFqgWzM9ak5aiJ9ouIIzemT27rLH3V3nveYrxTk +O6BMp3LntV5TScz/klfxWSsJuulSk8APRQth1mxZcwvY+QEv2gNPNxz034NeC0Gg +sXw5AKFs0Ni0kXIrGz+imtHE3yvVyJV9hM12G9zkJMY/FSI9hadCK+1+cVlhSMI9 +kWNAfCmzgBYKJfwYYA5TrQ4qzvxBOs2x5GprzDltyE1luKqTiHhuDwKL4JaOdB/Q +fuF0t/aBauQjrI79jzUdmnEKTypVL/4YwQD3e0iKZa9vCB1D51q4H6ToA+v9TLW0 +k6gx3kOdEr3n6aTS32/8b0aj7zFOjRerG6ng+Kk0VqEO53TsqIeF2Hc1S40+bnJ8 +SMwfcrNxdNQkhrzIwON5FAHO2fqBxlyz+V0MOL7O8o6NXz0l4VE5I6jqAI4Es79y +oMK6g/vNpJd1IJq/p1Di3a0sH/Q/o8gx +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICMTCCAbegAwIBAgIUNnThTXxlE8msg1UloD5Sfi9QaMcwCgYIKoZIzj0EAwMw +WDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs +IEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgRUNDIFJvb3QgQ0EwHhcNMjQw +NTE1MDU0MTU2WhcNNDQwNTE1MDU0MTU1WjBYMQswCQYDVQQGEwJDTjElMCMGA1UE +ChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1c3RB +c2lhIFRMUyBFQ0MgUm9vdCBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLh/pVs/ +AT598IhtrimY4ZtcU5nb9wj/1WrgjstEpvDBjL1P1M7UiFPoXlfXTr4sP/MSpwDp +guMqWzJ8S5sUKZ74LYO1644xST0mYekdcouJtgq7nDM1D9rs3qlKH8kzsaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQULIVTu7FDzTLqnqOH/qKYqKaT6RAw +DgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMFRH18MtYYZI9HlaVQ01 +L18N9mdsd0AaRuf4aFtOJx24mH1/k78ITcTaRTChD15KeAIxAKORh/IRM4PDwYqR +OkwrULG9IpRdNYlzg8WbGf60oenUoWa2AaU2+dhoYSi3dOGiMQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFgDCCA2igAwIBAgIUHBjYz+VTPyI1RlNUJDxsR9FcSpwwDQYJKoZIhvcNAQEM +BQAwWDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dp +ZXMsIEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgUlNBIFJvb3QgQ0EwHhcN +MjQwNTE1MDU0MTU3WhcNNDQwNTE1MDU0MTU2WjBYMQswCQYDVQQGEwJDTjElMCMG +A1UEChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1 +c3RBc2lhIFRMUyBSU0EgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC +AgoCggIBAMMWuBtqpERz5dZO9LnPWwvB0ZqB9WOwj0PBuwhaGnrhB3YmH49pVr7+ +NmDQDIPNlOrnxS1cLwUWAp4KqC/lYCZUlviYQB2srp10Zy9U+5RjmOMmSoPGlbYJ +Q1DNDX3eRA5gEk9bNb2/mThtfWza4mhzH/kxpRkQcwUqwzIZheo0qt1CHjCNP561 +HmHVb70AcnKtEj+qpklz8oYVlQwQX1Fkzv93uMltrOXVmPGZLmzjyUT5tUMnCE32 +ft5EebuyjBza00tsLtbDeLdM1aTk2tyKjg7/D8OmYCYozza/+lcK7Fs/6TAWe8Tb +xNRkoDD75f0dcZLdKY9BWN4ArTr9PXwaqLEX8E40eFgl1oUh63kd0Nyrz2I8sMeX +i9bQn9P+PN7F4/w6g3CEIR0JwqH8uyghZVNgepBtljhb//HXeltt08lwSUq6HTrQ +UNoyIBnkiz/r1RYmNzz7dZ6wB3C4FGB33PYPXFIKvF1tjVEK2sUYyJtt3LCDs3+j +TnhMmCWr8n4uIF6CFabW2I+s5c0yhsj55NqJ4js+k8UTav/H9xj8Z7XvGCxUq0DT +bE3txci3OE9kxJRMT6DNrqXGJyV1J23G2pyOsAWZ1SgRxSHUuPzHlqtKZFlhaxP8 +S8ySpg+kUb8OWJDZgoM5pl+z+m6Ss80zDoWo8SnTq1mt1tve1CuBAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLgHkXlcBvRG/XtZylomkadFK/hT +MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQwFAAOCAgEAIZtqBSBdGBanEqT3 +Rz/NyjuujsCCztxIJXgXbODgcMTWltnZ9r96nBO7U5WS/8+S4PPFJzVXqDuiGev4 +iqME3mmL5Dw8veWv0BIb5Ylrc5tvJQJLkIKvQMKtuppgJFqBTQUYo+IzeXoLH5Pt +7DlK9RME7I10nYEKqG/odv6LTytpEoYKNDbdgptvT+Bz3Ul/KD7JO6NXBNiT2Twp +2xIQaOHEibgGIOcberyxk2GaGUARtWqFVwHxtlotJnMnlvm5P1vQiJ3koP26TpUJ +g3933FEFlJ0gcXax7PqJtZwuhfG5WyRasQmr2soaB82G39tp27RIGAAtvKLEiUUj +pQ7hRGU+isFqMB3iYPg6qocJQrmBktwliJiJ8Xw18WLK7nn4GS/+X/jbh87qqA8M +pugLoDzga5SYnH+tBuYc6kIQX+ImFTw3OffXvO645e8D7r0i+yiGNFjEWn9hongP +XvPKnbwbPKfILfanIhHKA9jnZwqKDss1jjQ52MjqjZ9k4DewbNfFj8GQYSbbJIwe +SsCI3zWQzj8C9GRh3sfIB5XeMhg6j6JCQCTl1jNdfK7vsU1P1FeQNWrcrgSXSYk0 +ly4wBOeY99sLAZDBHwo/+ML+TvrbmnNzFrwFuHnYWa8G5z9nODmxfKuU4CkUpijy +323imttUQ/hHWKNddBWcwauwxzQ= +-----END CERTIFICATE----- diff --git a/common/certificate/store.go b/common/certificate/store.go index 353bc8182c..0e0c25b107 100644 --- a/common/certificate/store.go +++ b/common/certificate/store.go @@ -25,6 +25,7 @@ type Store struct { storeType string systemPool *x509.CertPool currentPool *x509.CertPool + currentPEM []string certificate string certificatePaths []string certificateDirectoryPaths []string @@ -125,15 +126,41 @@ func (s *Store) Pool() *x509.CertPool { return s.currentPool } +func (s *Store) StoreKind() string { + return s.storeType +} + +func (s *Store) CurrentPEM() []string { + s.access.RLock() + defer s.access.RUnlock() + return append([]string(nil), s.currentPEM...) +} + func (s *Store) update() error { currentPool, err := s.newBasePool() + var currentPEM []string if err != nil { return err } + switch s.storeType { + case C.CertificateStoreMozilla: + pemContent := mozillaIncludedPEM() + if !currentPool.AppendCertsFromPEM([]byte(pemContent)) { + return E.New("invalid Mozilla included certificate PEM") + } + currentPEM = append(currentPEM, pemContent) + case C.CertificateStoreChrome: + pemContent := chromeIncludedPEM() + if !currentPool.AppendCertsFromPEM([]byte(pemContent)) { + return E.New("invalid Chrome included certificate PEM") + } + currentPEM = append(currentPEM, pemContent) + } if s.certificate != "" { if !currentPool.AppendCertsFromPEM([]byte(s.certificate)) { return E.New("invalid certificate PEM strings") } + currentPEM = append(currentPEM, s.certificate) } for _, path := range s.certificatePaths { pemContent, err := os.ReadFile(path) @@ -143,6 +170,7 @@ func (s *Store) update() error { if !currentPool.AppendCertsFromPEM(pemContent) { return E.New("invalid certificate PEM file: ", path) } + currentPEM = append(currentPEM, string(pemContent)) } var firstErr error for _, directoryPath := range s.certificateDirectoryPaths { @@ -155,8 +183,8 @@ func (s *Store) update() error { } for _, directoryEntry := range directoryEntries { pemContent, err := os.ReadFile(filepath.Join(directoryPath, directoryEntry.Name())) - if err == nil { - currentPool.AppendCertsFromPEM(pemContent) + if err == nil && currentPool.AppendCertsFromPEM(pemContent) { + currentPEM = append(currentPEM, string(pemContent)) } } } @@ -166,6 +194,7 @@ func (s *Store) update() error { s.access.Lock() defer s.access.Unlock() s.currentPool = currentPool + s.currentPEM = currentPEM return nil } @@ -176,10 +205,8 @@ func (s *Store) newBasePool() (*x509.CertPool, error) { return x509.NewCertPool(), nil } return s.systemPool.Clone(), nil - case C.CertificateStoreMozilla: - return newMozillaIncluded(), nil - case C.CertificateStoreChrome: - return newChromeIncluded(), nil + case C.CertificateStoreMozilla, C.CertificateStoreChrome: + return x509.NewCertPool(), nil case C.CertificateStoreNone: return x509.NewCertPool(), nil default: diff --git a/common/dialer/detour.go b/common/dialer/detour.go index 5c0b552ba8..dc1777022c 100644 --- a/common/dialer/detour.go +++ b/common/dialer/detour.go @@ -19,6 +19,7 @@ type DirectDialer interface { type DetourDialer struct { outboundManager adapter.OutboundManager detour string + defaultOutbound bool legacyDNSDialer bool dialer N.Dialer initOnce sync.Once @@ -33,6 +34,13 @@ func NewDetour(outboundManager adapter.OutboundManager, detour string, legacyDNS } } +func NewDefaultOutboundDetour(outboundManager adapter.OutboundManager) N.Dialer { + return &DetourDialer{ + outboundManager: outboundManager, + defaultOutbound: true, + } +} + func InitializeDetour(dialer N.Dialer) error { detourDialer, isDetour := common.Cast[*DetourDialer](dialer) if !isDetour { @@ -47,12 +55,18 @@ func (d *DetourDialer) Dialer() (N.Dialer, error) { } func (d *DetourDialer) init() { - dialer, loaded := d.outboundManager.Outbound(d.detour) - if !loaded { - d.initErr = E.New("outbound detour not found: ", d.detour) - return + var dialer adapter.Outbound + if d.detour != "" { + var loaded bool + dialer, loaded = d.outboundManager.Outbound(d.detour) + if !loaded { + d.initErr = E.New("outbound detour not found: ", d.detour) + return + } + } else { + dialer = d.outboundManager.Default() } - if !d.legacyDNSDialer { + if !d.defaultOutbound && !d.legacyDNSDialer { if directDialer, isDirect := dialer.(DirectDialer); isDirect { if directDialer.IsEmpty() { d.initErr = E.New("detour to an empty direct outbound makes no sense") diff --git a/common/dialer/dialer.go b/common/dialer/dialer.go index ca6f905fe0..08257a04a7 100644 --- a/common/dialer/dialer.go +++ b/common/dialer/dialer.go @@ -25,6 +25,7 @@ type Options struct { NewDialer bool LegacyDNSDialer bool DirectOutbound bool + DefaultOutbound bool } // TODO: merge with NewWithOptions @@ -42,19 +43,26 @@ func NewWithOptions(options Options) (N.Dialer, error) { dialer N.Dialer err error ) + hasDetour := dialOptions.Detour != "" || options.DefaultOutbound if dialOptions.Detour != "" { outboundManager := service.FromContext[adapter.OutboundManager](options.Context) if outboundManager == nil { return nil, E.New("missing outbound manager") } dialer = NewDetour(outboundManager, dialOptions.Detour, options.LegacyDNSDialer) + } else if options.DefaultOutbound { + outboundManager := service.FromContext[adapter.OutboundManager](options.Context) + if outboundManager == nil { + return nil, E.New("missing outbound manager") + } + dialer = NewDefaultOutboundDetour(outboundManager) } else { dialer, err = NewDefault(options.Context, dialOptions) if err != nil { return nil, err } } - if options.RemoteIsDomain && (dialOptions.Detour == "" || options.ResolverOnDetour || dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "") { + if options.RemoteIsDomain && (!hasDetour || options.ResolverOnDetour || dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "") { networkManager := service.FromContext[adapter.NetworkManager](options.Context) dnsTransport := service.FromContext[adapter.DNSTransportManager](options.Context) var defaultOptions adapter.NetworkOptions diff --git a/common/httpclient/apple_transport_darwin.go b/common/httpclient/apple_transport_darwin.go new file mode 100644 index 0000000000..b9174009b0 --- /dev/null +++ b/common/httpclient/apple_transport_darwin.go @@ -0,0 +1,423 @@ +//go:build darwin && cgo + +package httpclient + +/* +#cgo CFLAGS: -x objective-c -fobjc-arc +#cgo LDFLAGS: -framework Foundation -framework Security + +#include +#include "apple_transport_darwin.h" +*/ +import "C" + +import ( + "bytes" + "context" + "crypto/sha256" + "fmt" + "io" + "net" + "net/http" + "strings" + "sync" + "sync/atomic" + "time" + "unsafe" + + "github.com/sagernet/sing-box/common/proxybridge" + boxTLS "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/ntp" +) + +const applePinnedHashSize = sha256.Size + +func verifyApplePinnedPublicKeySHA256(flatHashes []byte, leafCertificate []byte) error { + if len(flatHashes)%applePinnedHashSize != 0 { + return E.New("invalid pinned public key list") + } + knownHashes := make([][]byte, 0, len(flatHashes)/applePinnedHashSize) + for offset := 0; offset < len(flatHashes); offset += applePinnedHashSize { + knownHashes = append(knownHashes, append([]byte(nil), flatHashes[offset:offset+applePinnedHashSize]...)) + } + return boxTLS.VerifyPublicKeySHA256(knownHashes, [][]byte{leafCertificate}) +} + +//export box_apple_http_verify_public_key_sha256 +func box_apple_http_verify_public_key_sha256(knownHashValues *C.uint8_t, knownHashValuesLen C.size_t, leafCert *C.uint8_t, leafCertLen C.size_t) *C.char { + flatHashes := C.GoBytes(unsafe.Pointer(knownHashValues), C.int(knownHashValuesLen)) + leafCertificate := C.GoBytes(unsafe.Pointer(leafCert), C.int(leafCertLen)) + err := verifyApplePinnedPublicKeySHA256(flatHashes, leafCertificate) + if err == nil { + return nil + } + return C.CString(err.Error()) +} + +type appleSessionConfig struct { + serverName string + minVersion uint16 + maxVersion uint16 + insecure bool + anchorPEM string + anchorOnly bool + pinnedPublicKeySHA256s []byte +} + +type appleTransportShared struct { + logger logger.ContextLogger + bridge *proxybridge.Bridge + config appleSessionConfig + timeFunc func() time.Time + refs atomic.Int32 +} + +type appleTransport struct { + shared *appleTransportShared + access sync.Mutex + session *C.box_apple_http_session_t + closed bool +} + +func newAppleTransport(ctx context.Context, logger logger.ContextLogger, rawDialer N.Dialer, options option.HTTPClientOptions) (innerTransport, error) { + sessionConfig, err := newAppleSessionConfig(ctx, options) + if err != nil { + return nil, err + } + bridge, err := proxybridge.New(ctx, logger, "apple http proxy", rawDialer) + if err != nil { + return nil, err + } + shared := &appleTransportShared{ + logger: logger, + bridge: bridge, + config: sessionConfig, + timeFunc: ntp.TimeFuncFromContext(ctx), + } + shared.refs.Store(1) + session, err := shared.newSession() + if err != nil { + bridge.Close() + return nil, err + } + return &appleTransport{ + shared: shared, + session: session, + }, nil +} + +func newAppleSessionConfig(ctx context.Context, options option.HTTPClientOptions) (appleSessionConfig, error) { + version := options.Version + if version == 0 { + version = 2 + } + switch version { + case 2: + case 1: + return appleSessionConfig{}, E.New("HTTP/1.1 is unsupported in Apple HTTP engine") + case 3: + return appleSessionConfig{}, E.New("HTTP/3 is unsupported in Apple HTTP engine") + default: + return appleSessionConfig{}, E.New("unknown HTTP version: ", version) + } + if options.DisableVersionFallback { + return appleSessionConfig{}, E.New("disable_version_fallback is unsupported in Apple HTTP engine") + } + if options.HTTP2Options != (option.HTTP2Options{}) { + return appleSessionConfig{}, E.New("HTTP/2 options are unsupported in Apple HTTP engine") + } + if options.HTTP3Options != (option.QUICOptions{}) { + return appleSessionConfig{}, E.New("QUIC options are unsupported in Apple HTTP engine") + } + + tlsOptions := common.PtrValueOrDefault(options.TLS) + if tlsOptions.Engine != "" { + return appleSessionConfig{}, E.New("tls.engine is unsupported in Apple HTTP engine") + } + if len(tlsOptions.ALPN) > 0 { + return appleSessionConfig{}, E.New("tls.alpn is unsupported in Apple HTTP engine") + } + validated, err := boxTLS.ValidateAppleTLSOptions(ctx, tlsOptions, "Apple HTTP engine") + if err != nil { + return appleSessionConfig{}, err + } + + config := appleSessionConfig{ + serverName: tlsOptions.ServerName, + minVersion: validated.MinVersion, + maxVersion: validated.MaxVersion, + insecure: tlsOptions.Insecure || len(tlsOptions.CertificatePublicKeySHA256) > 0, + anchorPEM: validated.AnchorPEM, + anchorOnly: validated.AnchorOnly, + } + if len(tlsOptions.CertificatePublicKeySHA256) > 0 { + config.pinnedPublicKeySHA256s = make([]byte, 0, len(tlsOptions.CertificatePublicKeySHA256)*applePinnedHashSize) + for _, hashValue := range tlsOptions.CertificatePublicKeySHA256 { + if len(hashValue) != applePinnedHashSize { + return appleSessionConfig{}, E.New("invalid certificate_public_key_sha256 length: ", len(hashValue)) + } + config.pinnedPublicKeySHA256s = append(config.pinnedPublicKeySHA256s, hashValue...) + } + } + return config, nil +} + +func (s *appleTransportShared) retain() { + s.refs.Add(1) +} + +func (s *appleTransportShared) release() error { + if s.refs.Add(-1) == 0 { + return s.bridge.Close() + } + return nil +} + +func (s *appleTransportShared) newSession() (*C.box_apple_http_session_t, error) { + cProxyHost := C.CString("127.0.0.1") + defer C.free(unsafe.Pointer(cProxyHost)) + cProxyUsername := C.CString(s.bridge.Username()) + defer C.free(unsafe.Pointer(cProxyUsername)) + cProxyPassword := C.CString(s.bridge.Password()) + defer C.free(unsafe.Pointer(cProxyPassword)) + var cAnchorPEM *C.char + if s.config.anchorPEM != "" { + cAnchorPEM = C.CString(s.config.anchorPEM) + defer C.free(unsafe.Pointer(cAnchorPEM)) + } + var pinnedPointer *C.uint8_t + if len(s.config.pinnedPublicKeySHA256s) > 0 { + pinnedPointer = (*C.uint8_t)(C.CBytes(s.config.pinnedPublicKeySHA256s)) + defer C.free(unsafe.Pointer(pinnedPointer)) + } + cConfig := C.box_apple_http_session_config_t{ + proxy_host: cProxyHost, + proxy_port: C.int(s.bridge.Port()), + proxy_username: cProxyUsername, + proxy_password: cProxyPassword, + min_tls_version: C.uint16_t(s.config.minVersion), + max_tls_version: C.uint16_t(s.config.maxVersion), + insecure: C.bool(s.config.insecure), + anchor_pem: cAnchorPEM, + anchor_pem_len: C.size_t(len(s.config.anchorPEM)), + anchor_only: C.bool(s.config.anchorOnly), + pinned_public_key_sha256: pinnedPointer, + pinned_public_key_sha256_len: C.size_t(len(s.config.pinnedPublicKeySHA256s)), + } + var cErr *C.char + session := C.box_apple_http_session_create(&cConfig, &cErr) + if session != nil { + return session, nil + } + return nil, appleCStringError(cErr, "create Apple HTTP session") +} + +func (t *appleTransport) RoundTrip(request *http.Request) (*http.Response, error) { + if requestRequiresHTTP1(request) { + return nil, E.New("HTTP upgrade requests are unsupported in Apple HTTP engine") + } + if request.URL == nil { + return nil, E.New("missing request URL") + } + switch request.URL.Scheme { + case "http", "https": + default: + return nil, E.New("unsupported URL scheme: ", request.URL.Scheme) + } + if request.URL.Scheme == "https" && t.shared.config.serverName != "" && !strings.EqualFold(t.shared.config.serverName, request.URL.Hostname()) { + return nil, E.New("tls.server_name is unsupported in Apple HTTP engine unless it matches request host") + } + var body []byte + if request.Body != nil && request.Body != http.NoBody { + defer request.Body.Close() + var err error + body, err = io.ReadAll(request.Body) + if err != nil { + return nil, err + } + } + headerKeys, headerValues := flattenRequestHeaders(request) + cMethod := C.CString(request.Method) + defer C.free(unsafe.Pointer(cMethod)) + cURL := C.CString(request.URL.String()) + defer C.free(unsafe.Pointer(cURL)) + cHeaderKeys := make([]*C.char, len(headerKeys)) + cHeaderValues := make([]*C.char, len(headerValues)) + defer func() { + for _, ptr := range cHeaderKeys { + C.free(unsafe.Pointer(ptr)) + } + for _, ptr := range cHeaderValues { + C.free(unsafe.Pointer(ptr)) + } + }() + for index, value := range headerKeys { + cHeaderKeys[index] = C.CString(value) + } + for index, value := range headerValues { + cHeaderValues[index] = C.CString(value) + } + var headerKeysPointer **C.char + var headerValuesPointer **C.char + if len(cHeaderKeys) > 0 { + pointerArraySize := C.size_t(len(cHeaderKeys)) * C.size_t(unsafe.Sizeof((*C.char)(nil))) + headerKeysPointer = (**C.char)(C.malloc(pointerArraySize)) + defer C.free(unsafe.Pointer(headerKeysPointer)) + headerValuesPointer = (**C.char)(C.malloc(pointerArraySize)) + defer C.free(unsafe.Pointer(headerValuesPointer)) + copy(unsafe.Slice(headerKeysPointer, len(cHeaderKeys)), cHeaderKeys) + copy(unsafe.Slice(headerValuesPointer, len(cHeaderValues)), cHeaderValues) + } + var bodyPointer *C.uint8_t + if len(body) > 0 { + bodyPointer = (*C.uint8_t)(C.CBytes(body)) + defer C.free(unsafe.Pointer(bodyPointer)) + } + var ( + hasVerifyTime bool + verifyTimeUnixMilli int64 + ) + if t.shared.timeFunc != nil { + hasVerifyTime = true + verifyTimeUnixMilli = t.shared.timeFunc().UnixMilli() + } + cRequest := C.box_apple_http_request_t{ + method: cMethod, + url: cURL, + header_keys: (**C.char)(headerKeysPointer), + header_values: (**C.char)(headerValuesPointer), + header_count: C.size_t(len(cHeaderKeys)), + body: bodyPointer, + body_len: C.size_t(len(body)), + has_verify_time: C.bool(hasVerifyTime), + verify_time_unix_millis: C.int64_t(verifyTimeUnixMilli), + } + var cErr *C.char + var task *C.box_apple_http_task_t + t.access.Lock() + if t.session == nil { + t.access.Unlock() + return nil, net.ErrClosed + } + // Keep the session attached until NSURLSession has created the task. + task = C.box_apple_http_session_send_async(t.session, &cRequest, &cErr) + t.access.Unlock() + if task == nil { + return nil, appleCStringError(cErr, "create Apple HTTP request") + } + cancelDone := make(chan struct{}) + cancelExit := make(chan struct{}) + go func() { + defer close(cancelExit) + select { + case <-request.Context().Done(): + C.box_apple_http_task_cancel(task) + case <-cancelDone: + } + }() + cResponse := C.box_apple_http_task_wait(task, &cErr) + close(cancelDone) + <-cancelExit + C.box_apple_http_task_close(task) + if cResponse == nil { + err := appleCStringError(cErr, "Apple HTTP request failed") + if request.Context().Err() != nil { + return nil, request.Context().Err() + } + return nil, err + } + defer C.box_apple_http_response_free(cResponse) + return parseAppleHTTPResponse(request, cResponse), nil +} + +func (t *appleTransport) CloseIdleConnections() { + t.access.Lock() + if t.closed { + t.access.Unlock() + return + } + t.access.Unlock() + newSession, err := t.shared.newSession() + if err != nil { + t.shared.logger.Error(E.Cause(err, "reset Apple HTTP session")) + return + } + t.access.Lock() + if t.closed { + t.access.Unlock() + C.box_apple_http_session_close(newSession) + return + } + oldSession := t.session + t.session = newSession + t.access.Unlock() + C.box_apple_http_session_retire(oldSession) +} + +func (t *appleTransport) Close() error { + t.access.Lock() + if t.closed { + t.access.Unlock() + return nil + } + t.closed = true + session := t.session + t.session = nil + t.access.Unlock() + C.box_apple_http_session_close(session) + return t.shared.release() +} + +func flattenRequestHeaders(request *http.Request) ([]string, []string) { + var ( + keys []string + values []string + ) + for key, headerValues := range request.Header { + for _, value := range headerValues { + keys = append(keys, key) + values = append(values, value) + } + } + if request.Host != "" { + keys = append(keys, "Host") + values = append(values, request.Host) + } + return keys, values +} + +func parseAppleHTTPResponse(request *http.Request, response *C.box_apple_http_response_t) *http.Response { + headers := make(http.Header) + headerKeys := unsafe.Slice(response.header_keys, int(response.header_count)) + headerValues := unsafe.Slice(response.header_values, int(response.header_count)) + for index := range headerKeys { + headers.Add(C.GoString(headerKeys[index]), C.GoString(headerValues[index])) + } + body := bytes.NewReader(C.GoBytes(unsafe.Pointer(response.body), C.int(response.body_len))) + // NSURLSession's completion-handler API does not expose the negotiated protocol; + // callers that read Response.Proto will see HTTP/1.1 even when the wire was HTTP/2. + return &http.Response{ + StatusCode: int(response.status_code), + Status: fmt.Sprintf("%d %s", int(response.status_code), http.StatusText(int(response.status_code))), + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: headers, + Body: io.NopCloser(body), + ContentLength: int64(body.Len()), + Request: request, + } +} + +func appleCStringError(cErr *C.char, message string) error { + if cErr == nil { + return E.New(message) + } + defer C.free(unsafe.Pointer(cErr)) + return E.New(message, ": ", C.GoString(cErr)) +} diff --git a/common/httpclient/apple_transport_darwin.h b/common/httpclient/apple_transport_darwin.h new file mode 100644 index 0000000000..26d6a77bcf --- /dev/null +++ b/common/httpclient/apple_transport_darwin.h @@ -0,0 +1,71 @@ +#include +#include +#include + +typedef struct box_apple_http_session box_apple_http_session_t; +typedef struct box_apple_http_task box_apple_http_task_t; + +typedef struct box_apple_http_session_config { + const char *proxy_host; + int proxy_port; + const char *proxy_username; + const char *proxy_password; + uint16_t min_tls_version; + uint16_t max_tls_version; + bool insecure; + const char *anchor_pem; + size_t anchor_pem_len; + bool anchor_only; + const uint8_t *pinned_public_key_sha256; + size_t pinned_public_key_sha256_len; +} box_apple_http_session_config_t; + +typedef struct box_apple_http_request { + const char *method; + const char *url; + const char **header_keys; + const char **header_values; + size_t header_count; + const uint8_t *body; + size_t body_len; + bool has_verify_time; + int64_t verify_time_unix_millis; +} box_apple_http_request_t; + +typedef struct box_apple_http_response { + int status_code; + char **header_keys; + char **header_values; + size_t header_count; + uint8_t *body; + size_t body_len; + char *error; +} box_apple_http_response_t; + +box_apple_http_session_t *box_apple_http_session_create( + const box_apple_http_session_config_t *config, + char **error_out +); +void box_apple_http_session_retire(box_apple_http_session_t *session); +void box_apple_http_session_close(box_apple_http_session_t *session); + +box_apple_http_task_t *box_apple_http_session_send_async( + box_apple_http_session_t *session, + const box_apple_http_request_t *request, + char **error_out +); +box_apple_http_response_t *box_apple_http_task_wait( + box_apple_http_task_t *task, + char **error_out +); +void box_apple_http_task_cancel(box_apple_http_task_t *task); +void box_apple_http_task_close(box_apple_http_task_t *task); + +void box_apple_http_response_free(box_apple_http_response_t *response); + +char *box_apple_http_verify_public_key_sha256( + uint8_t *known_hash_values, + size_t known_hash_values_len, + uint8_t *leaf_cert, + size_t leaf_cert_len +); diff --git a/common/httpclient/apple_transport_darwin.m b/common/httpclient/apple_transport_darwin.m new file mode 100644 index 0000000000..d7c09350cf --- /dev/null +++ b/common/httpclient/apple_transport_darwin.m @@ -0,0 +1,398 @@ +#import "apple_transport_darwin.h" + +#import +#import +#import +#import +#import +#import + +typedef struct box_apple_http_session { + void *handle; +} box_apple_http_session_t; + +typedef struct box_apple_http_task { + void *task; + void *done_semaphore; + box_apple_http_response_t *response; + char *error; +} box_apple_http_task_t; + +static NSString *const box_apple_http_verify_time_key = @"sing-box.verify-time"; + +static void box_set_error_string(char **error_out, NSString *message) { + if (error_out == NULL || *error_out != NULL) { + return; + } + const char *utf8 = [message UTF8String]; + *error_out = strdup(utf8 != NULL ? utf8 : "unknown error"); +} + +static void box_set_error_from_nserror(char **error_out, NSError *error) { + if (error == nil) { + box_set_error_string(error_out, @"unknown error"); + return; + } + box_set_error_string(error_out, error.localizedDescription ?: error.description); +} + +static NSArray *box_parse_certificates_from_pem(const char *pem, size_t pem_len) { + if (pem == NULL || pem_len == 0) { + return @[]; + } + NSString *content = [[NSString alloc] initWithBytes:pem length:pem_len encoding:NSUTF8StringEncoding]; + if (content == nil) { + return @[]; + } + NSString *beginMarker = @"-----BEGIN CERTIFICATE-----"; + NSString *endMarker = @"-----END CERTIFICATE-----"; + NSMutableArray *certificates = [NSMutableArray array]; + NSUInteger searchFrom = 0; + while (searchFrom < content.length) { + NSRange beginRange = [content rangeOfString:beginMarker options:0 range:NSMakeRange(searchFrom, content.length - searchFrom)]; + if (beginRange.location == NSNotFound) { + break; + } + NSUInteger bodyStart = beginRange.location + beginRange.length; + NSRange endRange = [content rangeOfString:endMarker options:0 range:NSMakeRange(bodyStart, content.length - bodyStart)]; + if (endRange.location == NSNotFound) { + break; + } + NSString *base64Section = [content substringWithRange:NSMakeRange(bodyStart, endRange.location - bodyStart)]; + NSArray *components = [base64Section componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + NSString *base64Content = [components componentsJoinedByString:@""]; + NSData *der = [[NSData alloc] initWithBase64EncodedString:base64Content options:0]; + if (der != nil) { + SecCertificateRef certificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)der); + if (certificate != NULL) { + [certificates addObject:(__bridge id)certificate]; + CFRelease(certificate); + } + } + searchFrom = endRange.location + endRange.length; + } + return certificates; +} + +static bool box_evaluate_trust(SecTrustRef trustRef, NSArray *anchors, bool anchor_only, NSDate *verifyDate) { + if (trustRef == NULL) { + return false; + } + if (verifyDate != nil && SecTrustSetVerifyDate(trustRef, (__bridge CFDateRef)verifyDate) != errSecSuccess) { + return false; + } + if (anchors.count > 0 || anchor_only) { + CFMutableArrayRef anchorArray = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks); + for (id certificate in anchors) { + CFArrayAppendValue(anchorArray, (__bridge const void *)certificate); + } + SecTrustSetAnchorCertificates(trustRef, anchorArray); + SecTrustSetAnchorCertificatesOnly(trustRef, anchor_only); + CFRelease(anchorArray); + } + CFErrorRef error = NULL; + bool result = SecTrustEvaluateWithError(trustRef, &error); + if (error != NULL) { + CFRelease(error); + } + return result; +} + +static NSDate *box_apple_http_verify_date_for_request(NSURLRequest *request) { + if (request == nil) { + return nil; + } + id value = [NSURLProtocol propertyForKey:box_apple_http_verify_time_key inRequest:request]; + if (![value isKindOfClass:[NSNumber class]]) { + return nil; + } + return [NSDate dateWithTimeIntervalSince1970:[(NSNumber *)value longLongValue] / 1000.0]; +} + +static box_apple_http_response_t *box_create_response(NSHTTPURLResponse *httpResponse, NSData *data) { + box_apple_http_response_t *response = calloc(1, sizeof(box_apple_http_response_t)); + response->status_code = (int)httpResponse.statusCode; + NSDictionary *headers = httpResponse.allHeaderFields; + response->header_count = headers.count; + if (response->header_count > 0) { + response->header_keys = calloc(response->header_count, sizeof(char *)); + response->header_values = calloc(response->header_count, sizeof(char *)); + NSUInteger index = 0; + for (id key in headers) { + NSString *keyString = [[key description] copy]; + NSString *valueString = [[headers[key] description] copy]; + response->header_keys[index] = strdup(keyString.UTF8String ?: ""); + response->header_values[index] = strdup(valueString.UTF8String ?: ""); + index++; + } + } + if (data.length > 0) { + response->body_len = data.length; + response->body = malloc(data.length); + memcpy(response->body, data.bytes, data.length); + } + return response; +} + +@interface BoxAppleHTTPSessionDelegate : NSObject +@property(nonatomic, assign) BOOL insecure; +@property(nonatomic, assign) BOOL anchorOnly; +@property(nonatomic, strong) NSArray *anchors; +@property(nonatomic, strong) NSData *pinnedPublicKeyHashes; +@end + +@implementation BoxAppleHTTPSessionDelegate + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task +willPerformHTTPRedirection:(NSHTTPURLResponse *)response + newRequest:(NSURLRequest *)request + completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler { + completionHandler(nil); +} + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task +didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge + completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler { + if (![challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { + completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); + return; + } + SecTrustRef trustRef = challenge.protectionSpace.serverTrust; + if (trustRef == NULL) { + completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); + return; + } + NSDate *verifyDate = box_apple_http_verify_date_for_request(task.currentRequest ?: task.originalRequest); + BOOL needsCustomHandling = self.insecure || self.anchorOnly || self.anchors.count > 0 || self.pinnedPublicKeyHashes.length > 0 || verifyDate != nil; + if (!needsCustomHandling) { + completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); + return; + } + BOOL ok = YES; + if (!self.insecure) { + ok = box_evaluate_trust(trustRef, self.anchors, self.anchorOnly, verifyDate); + } + if (ok && self.pinnedPublicKeyHashes.length > 0) { + CFArrayRef certificateChain = SecTrustCopyCertificateChain(trustRef); + SecCertificateRef leafCertificate = NULL; + if (certificateChain != NULL && CFArrayGetCount(certificateChain) > 0) { + leafCertificate = (SecCertificateRef)CFArrayGetValueAtIndex(certificateChain, 0); + } + if (leafCertificate == NULL) { + ok = NO; + } else { + NSData *leafData = CFBridgingRelease(SecCertificateCopyData(leafCertificate)); + char *pinError = box_apple_http_verify_public_key_sha256( + (uint8_t *)self.pinnedPublicKeyHashes.bytes, + self.pinnedPublicKeyHashes.length, + (uint8_t *)leafData.bytes, + leafData.length + ); + if (pinError != NULL) { + free(pinError); + ok = NO; + } + } + if (certificateChain != NULL) { + CFRelease(certificateChain); + } + } + if (!ok) { + completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); + return; + } + completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:trustRef]); +} + +@end + +@interface BoxAppleHTTPSessionHandle : NSObject +@property(nonatomic, strong) NSURLSession *session; +@property(nonatomic, strong) BoxAppleHTTPSessionDelegate *delegate; +@end + +@implementation BoxAppleHTTPSessionHandle +@end + +box_apple_http_session_t *box_apple_http_session_create( + const box_apple_http_session_config_t *config, + char **error_out +) { + @autoreleasepool { + NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration ephemeralSessionConfiguration]; + sessionConfig.URLCache = nil; + sessionConfig.HTTPCookieStorage = nil; + sessionConfig.URLCredentialStorage = nil; + sessionConfig.HTTPShouldSetCookies = NO; + if (config != NULL && config->proxy_host != NULL && config->proxy_port > 0) { + NSMutableDictionary *proxyDictionary = [NSMutableDictionary dictionary]; + proxyDictionary[(__bridge NSString *)kCFStreamPropertySOCKSProxyHost] = [NSString stringWithUTF8String:config->proxy_host]; + proxyDictionary[(__bridge NSString *)kCFStreamPropertySOCKSProxyPort] = @(config->proxy_port); + proxyDictionary[(__bridge NSString *)kCFStreamPropertySOCKSVersion] = (__bridge NSString *)kCFStreamSocketSOCKSVersion5; + if (config->proxy_username != NULL) { + proxyDictionary[(__bridge NSString *)kCFStreamPropertySOCKSUser] = [NSString stringWithUTF8String:config->proxy_username]; + } + if (config->proxy_password != NULL) { + proxyDictionary[(__bridge NSString *)kCFStreamPropertySOCKSPassword] = [NSString stringWithUTF8String:config->proxy_password]; + } + sessionConfig.connectionProxyDictionary = proxyDictionary; + } + if (config != NULL && config->min_tls_version != 0) { + sessionConfig.TLSMinimumSupportedProtocolVersion = (tls_protocol_version_t)config->min_tls_version; + } + if (config != NULL && config->max_tls_version != 0) { + sessionConfig.TLSMaximumSupportedProtocolVersion = (tls_protocol_version_t)config->max_tls_version; + } + BoxAppleHTTPSessionDelegate *delegate = [[BoxAppleHTTPSessionDelegate alloc] init]; + if (config != NULL) { + delegate.insecure = config->insecure; + delegate.anchorOnly = config->anchor_only; + delegate.anchors = box_parse_certificates_from_pem(config->anchor_pem, config->anchor_pem_len); + if (config->pinned_public_key_sha256 != NULL && config->pinned_public_key_sha256_len > 0) { + delegate.pinnedPublicKeyHashes = [NSData dataWithBytes:config->pinned_public_key_sha256 length:config->pinned_public_key_sha256_len]; + } + } + NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:delegate delegateQueue:nil]; + if (session == nil) { + box_set_error_string(error_out, @"create URLSession"); + return NULL; + } + BoxAppleHTTPSessionHandle *handle = [[BoxAppleHTTPSessionHandle alloc] init]; + handle.session = session; + handle.delegate = delegate; + box_apple_http_session_t *sessionHandle = calloc(1, sizeof(box_apple_http_session_t)); + sessionHandle->handle = (__bridge_retained void *)handle; + return sessionHandle; + } +} + +void box_apple_http_session_retire(box_apple_http_session_t *session) { + if (session == NULL || session->handle == NULL) { + return; + } + BoxAppleHTTPSessionHandle *handle = (__bridge_transfer BoxAppleHTTPSessionHandle *)session->handle; + [handle.session finishTasksAndInvalidate]; + free(session); +} + +void box_apple_http_session_close(box_apple_http_session_t *session) { + if (session == NULL || session->handle == NULL) { + return; + } + BoxAppleHTTPSessionHandle *handle = (__bridge_transfer BoxAppleHTTPSessionHandle *)session->handle; + [handle.session invalidateAndCancel]; + free(session); +} + +box_apple_http_task_t *box_apple_http_session_send_async( + box_apple_http_session_t *session, + const box_apple_http_request_t *request, + char **error_out +) { + @autoreleasepool { + if (session == NULL || session->handle == NULL || request == NULL || request->method == NULL || request->url == NULL) { + box_set_error_string(error_out, @"invalid apple HTTP request"); + return NULL; + } + BoxAppleHTTPSessionHandle *handle = (__bridge BoxAppleHTTPSessionHandle *)session->handle; + NSURL *requestURL = [NSURL URLWithString:[NSString stringWithUTF8String:request->url]]; + if (requestURL == nil) { + box_set_error_string(error_out, @"invalid request URL"); + return NULL; + } + NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:requestURL]; + urlRequest.HTTPMethod = [NSString stringWithUTF8String:request->method]; + for (size_t index = 0; index < request->header_count; index++) { + const char *key = request->header_keys[index]; + const char *value = request->header_values[index]; + if (key == NULL || value == NULL) { + continue; + } + [urlRequest addValue:[NSString stringWithUTF8String:value] forHTTPHeaderField:[NSString stringWithUTF8String:key]]; + } + if (request->body != NULL && request->body_len > 0) { + urlRequest.HTTPBody = [NSData dataWithBytes:request->body length:request->body_len]; + } + if (request->has_verify_time) { + [NSURLProtocol setProperty:@(request->verify_time_unix_millis) forKey:box_apple_http_verify_time_key inRequest:urlRequest]; + } + box_apple_http_task_t *task = calloc(1, sizeof(box_apple_http_task_t)); + dispatch_semaphore_t doneSemaphore = dispatch_semaphore_create(0); + task->done_semaphore = (__bridge_retained void *)doneSemaphore; + NSURLSessionDataTask *dataTask = [handle.session dataTaskWithRequest:urlRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + if (error != nil) { + box_set_error_from_nserror(&task->error, error); + } else if (![response isKindOfClass:[NSHTTPURLResponse class]]) { + box_set_error_string(&task->error, @"unexpected HTTP response type"); + } else { + task->response = box_create_response((NSHTTPURLResponse *)response, data ?: [NSData data]); + } + dispatch_semaphore_signal((__bridge dispatch_semaphore_t)task->done_semaphore); + }]; + if (dataTask == nil) { + box_set_error_string(error_out, @"create data task"); + box_apple_http_task_close(task); + return NULL; + } + task->task = (__bridge_retained void *)dataTask; + [dataTask resume]; + return task; + } +} + +box_apple_http_response_t *box_apple_http_task_wait( + box_apple_http_task_t *task, + char **error_out +) { + if (task == NULL || task->done_semaphore == NULL) { + box_set_error_string(error_out, @"invalid apple HTTP task"); + return NULL; + } + dispatch_semaphore_wait((__bridge dispatch_semaphore_t)task->done_semaphore, DISPATCH_TIME_FOREVER); + if (task->error != NULL) { + box_set_error_string(error_out, [NSString stringWithUTF8String:task->error]); + return NULL; + } + return task->response; +} + +void box_apple_http_task_cancel(box_apple_http_task_t *task) { + if (task == NULL || task->task == NULL) { + return; + } + NSURLSessionTask *nsTask = (__bridge NSURLSessionTask *)task->task; + [nsTask cancel]; +} + +void box_apple_http_task_close(box_apple_http_task_t *task) { + if (task == NULL) { + return; + } + if (task->task != NULL) { + __unused NSURLSessionTask *nsTask = (__bridge_transfer NSURLSessionTask *)task->task; + task->task = NULL; + } + if (task->done_semaphore != NULL) { + __unused dispatch_semaphore_t doneSemaphore = (__bridge_transfer dispatch_semaphore_t)task->done_semaphore; + task->done_semaphore = NULL; + } + free(task->error); + free(task); +} + +void box_apple_http_response_free(box_apple_http_response_t *response) { + if (response == NULL) { + return; + } + for (size_t index = 0; index < response->header_count; index++) { + free(response->header_keys[index]); + free(response->header_values[index]); + } + free(response->header_keys); + free(response->header_values); + free(response->body); + free(response->error); + free(response); +} diff --git a/common/httpclient/apple_transport_darwin_test.go b/common/httpclient/apple_transport_darwin_test.go new file mode 100644 index 0000000000..47c7de6dd4 --- /dev/null +++ b/common/httpclient/apple_transport_darwin_test.go @@ -0,0 +1,855 @@ +//go:build darwin && cgo + +package httpclient + +import ( + "bytes" + "context" + "crypto/sha256" + stdtls "crypto/tls" + "crypto/x509" + "errors" + "io" + "net" + "net/http" + "net/http/httptest" + "net/url" + "slices" + "strconv" + "strings" + "testing" + "time" + + "github.com/sagernet/sing-box/adapter" + boxTLS "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/route" + "github.com/sagernet/sing/common/json/badoption" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" +) + +const appleHTTPTestTimeout = 5 * time.Second + +const appleHTTPRecoveryLoops = 5 + +type appleHTTPTestDialer struct { + dialer net.Dialer + listener net.ListenConfig + hostMap map[string]string +} + +type appleHTTPObservedRequest struct { + method string + body string + host string + values []string + protoMajor int +} + +type appleHTTPTestServer struct { + server *httptest.Server + baseURL string + dialHost string + certificate stdtls.Certificate + certificatePEM string + publicKeyHash []byte +} + +func TestNewAppleSessionConfig(t *testing.T) { + serverCertificate, serverCertificatePEM := newAppleHTTPTestCertificate(t, "localhost") + serverHash := certificatePublicKeySHA256(t, serverCertificate.Certificate[0]) + otherHash := bytes.Repeat([]byte{0x7f}, applePinnedHashSize) + + testCases := []struct { + name string + options option.HTTPClientOptions + check func(t *testing.T, config appleSessionConfig) + wantErr string + }{ + { + name: "success with certificate anchors", + options: option.HTTPClientOptions{ + Version: 2, + DialerOptions: option.DialerOptions{ + ConnectTimeout: badoption.Duration(2 * time.Second), + }, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "localhost", + MinVersion: "1.2", + MaxVersion: "1.3", + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }, + }, + }, + check: func(t *testing.T, config appleSessionConfig) { + t.Helper() + if config.serverName != "localhost" { + t.Fatalf("unexpected server name: %q", config.serverName) + } + if config.minVersion != stdtls.VersionTLS12 { + t.Fatalf("unexpected min version: %x", config.minVersion) + } + if config.maxVersion != stdtls.VersionTLS13 { + t.Fatalf("unexpected max version: %x", config.maxVersion) + } + if config.insecure { + t.Fatal("unexpected insecure flag") + } + if !config.anchorOnly { + t.Fatal("expected anchor_only") + } + if !strings.Contains(config.anchorPEM, "BEGIN CERTIFICATE") { + t.Fatalf("unexpected anchor pem: %q", config.anchorPEM) + } + if len(config.pinnedPublicKeySHA256s) != 0 { + t.Fatalf("unexpected pinned hashes: %d", len(config.pinnedPublicKeySHA256s)) + } + }, + }, + { + name: "success with flattened pins", + options: option.HTTPClientOptions{ + Version: 2, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + Insecure: true, + CertificatePublicKeySHA256: badoption.Listable[[]byte]{serverHash, otherHash}, + }, + }, + }, + check: func(t *testing.T, config appleSessionConfig) { + t.Helper() + if !config.insecure { + t.Fatal("expected insecure flag") + } + if len(config.pinnedPublicKeySHA256s) != 2*applePinnedHashSize { + t.Fatalf("unexpected flattened pin length: %d", len(config.pinnedPublicKeySHA256s)) + } + if !bytes.Equal(config.pinnedPublicKeySHA256s[:applePinnedHashSize], serverHash) { + t.Fatal("unexpected first pin") + } + if !bytes.Equal(config.pinnedPublicKeySHA256s[applePinnedHashSize:], otherHash) { + t.Fatal("unexpected second pin") + } + if config.anchorPEM != "" { + t.Fatalf("unexpected anchor pem: %q", config.anchorPEM) + } + if config.anchorOnly { + t.Fatal("unexpected anchor_only") + } + }, + }, + { + name: "http11 unsupported", + options: option.HTTPClientOptions{Version: 1}, + wantErr: "HTTP/1.1 is unsupported in Apple HTTP engine", + }, + { + name: "http3 unsupported", + options: option.HTTPClientOptions{Version: 3}, + wantErr: "HTTP/3 is unsupported in Apple HTTP engine", + }, + { + name: "unknown version", + options: option.HTTPClientOptions{Version: 9}, + wantErr: "unknown HTTP version: 9", + }, + { + name: "disable version fallback unsupported", + options: option.HTTPClientOptions{ + DisableVersionFallback: true, + }, + wantErr: "disable_version_fallback is unsupported in Apple HTTP engine", + }, + { + name: "http2 options unsupported", + options: option.HTTPClientOptions{ + HTTP2Options: option.HTTP2Options{ + IdleTimeout: badoption.Duration(time.Second), + }, + }, + wantErr: "HTTP/2 options are unsupported in Apple HTTP engine", + }, + { + name: "quic options unsupported", + options: option.HTTPClientOptions{ + HTTP3Options: option.QUICOptions{ + InitialPacketSize: 1200, + }, + }, + wantErr: "QUIC options are unsupported in Apple HTTP engine", + }, + { + name: "tls engine unsupported", + options: option.HTTPClientOptions{ + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{Engine: "go"}, + }, + }, + wantErr: "tls.engine is unsupported in Apple HTTP engine", + }, + { + name: "disable sni unsupported", + options: option.HTTPClientOptions{ + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{DisableSNI: true}, + }, + }, + wantErr: "disable_sni is unsupported in Apple HTTP engine", + }, + { + name: "alpn unsupported", + options: option.HTTPClientOptions{ + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + ALPN: badoption.Listable[string]{"h2"}, + }, + }, + }, + wantErr: "tls.alpn is unsupported in Apple HTTP engine", + }, + { + name: "cipher suites unsupported", + options: option.HTTPClientOptions{ + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + CipherSuites: badoption.Listable[string]{"TLS_AES_128_GCM_SHA256"}, + }, + }, + }, + wantErr: "cipher_suites is unsupported in Apple HTTP engine", + }, + { + name: "curve preferences unsupported", + options: option.HTTPClientOptions{ + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + CurvePreferences: badoption.Listable[option.CurvePreference]{option.CurvePreference(option.X25519)}, + }, + }, + }, + wantErr: "curve_preferences is unsupported in Apple HTTP engine", + }, + { + name: "client certificate unsupported", + options: option.HTTPClientOptions{ + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + ClientCertificate: badoption.Listable[string]{"client-certificate"}, + ClientKey: badoption.Listable[string]{"client-key"}, + }, + }, + }, + wantErr: "client certificate is unsupported in Apple HTTP engine", + }, + { + name: "tls fragment unsupported", + options: option.HTTPClientOptions{ + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{Fragment: true}, + }, + }, + wantErr: "tls fragment is unsupported in Apple HTTP engine", + }, + { + name: "ktls unsupported", + options: option.HTTPClientOptions{ + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{KernelTx: true}, + }, + }, + wantErr: "ktls is unsupported in Apple HTTP engine", + }, + { + name: "ech unsupported", + options: option.HTTPClientOptions{ + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + ECH: &option.OutboundECHOptions{Enabled: true}, + }, + }, + }, + wantErr: "ech is unsupported in Apple HTTP engine", + }, + { + name: "utls unsupported", + options: option.HTTPClientOptions{ + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + UTLS: &option.OutboundUTLSOptions{Enabled: true}, + }, + }, + }, + wantErr: "utls is unsupported in Apple HTTP engine", + }, + { + name: "reality unsupported", + options: option.HTTPClientOptions{ + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Reality: &option.OutboundRealityOptions{Enabled: true}, + }, + }, + }, + wantErr: "reality is unsupported in Apple HTTP engine", + }, + { + name: "pin and certificate conflict", + options: option.HTTPClientOptions{ + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Certificate: badoption.Listable[string]{serverCertificatePEM}, + CertificatePublicKeySHA256: badoption.Listable[[]byte]{serverHash}, + }, + }, + }, + wantErr: "certificate_public_key_sha256 is conflict with certificate or certificate_path", + }, + { + name: "invalid min version", + options: option.HTTPClientOptions{ + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{MinVersion: "bogus"}, + }, + }, + wantErr: "parse min_version", + }, + { + name: "invalid max version", + options: option.HTTPClientOptions{ + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{MaxVersion: "bogus"}, + }, + }, + wantErr: "parse max_version", + }, + { + name: "invalid pin length", + options: option.HTTPClientOptions{ + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + CertificatePublicKeySHA256: badoption.Listable[[]byte]{{0x01, 0x02}}, + }, + }, + }, + wantErr: "invalid certificate_public_key_sha256 length: 2", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + config, err := newAppleSessionConfig(context.Background(), testCase.options) + if testCase.wantErr != "" { + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), testCase.wantErr) { + t.Fatalf("unexpected error: %v", err) + } + return + } + if err != nil { + t.Fatal(err) + } + if testCase.check != nil { + testCase.check(t, config) + } + }) + } +} + +func TestAppleTransportVerifyPublicKeySHA256(t *testing.T) { + serverCertificate, _ := newAppleHTTPTestCertificate(t, "localhost") + goodHash := certificatePublicKeySHA256(t, serverCertificate.Certificate[0]) + badHash := append([]byte(nil), goodHash...) + badHash[0] ^= 0xff + + err := verifyApplePinnedPublicKeySHA256(goodHash, serverCertificate.Certificate[0]) + if err != nil { + t.Fatalf("expected correct pin to succeed: %v", err) + } + + err = verifyApplePinnedPublicKeySHA256(badHash, serverCertificate.Certificate[0]) + if err == nil { + t.Fatal("expected incorrect pin to fail") + } + if !strings.Contains(err.Error(), "unrecognized remote public key") { + t.Fatalf("unexpected pin mismatch error: %v", err) + } + + err = verifyApplePinnedPublicKeySHA256(goodHash[:applePinnedHashSize-1], serverCertificate.Certificate[0]) + if err == nil { + t.Fatal("expected malformed pin list to fail") + } + if !strings.Contains(err.Error(), "invalid pinned public key list") { + t.Fatalf("unexpected malformed pin error: %v", err) + } +} + +func TestAppleTransportRoundTripHTTPS(t *testing.T) { + requests := make(chan appleHTTPObservedRequest, 1) + server := startAppleHTTPTestServer(t, func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + t.Error(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + requests <- appleHTTPObservedRequest{ + method: r.Method, + body: string(body), + host: r.Host, + values: append([]string(nil), r.Header.Values("X-Test")...), + protoMajor: r.ProtoMajor, + } + w.Header().Set("X-Reply", "apple") + w.WriteHeader(http.StatusCreated) + _, _ = w.Write([]byte("response body")) + }) + + transport := newAppleHTTPTestTransport(t, server, option.HTTPClientOptions{ + Version: 2, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: appleHTTPServerTLSOptions(server), + }, + }) + + request, err := http.NewRequest(http.MethodPost, server.URL("/roundtrip"), bytes.NewReader([]byte("request body"))) + if err != nil { + t.Fatal(err) + } + request.Header.Add("X-Test", "one") + request.Header.Add("X-Test", "two") + request.Host = "custom.example" + + response, err := transport.RoundTrip(request) + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + + responseBody := readResponseBody(t, response) + if response.StatusCode != http.StatusCreated { + t.Fatalf("unexpected status code: %d", response.StatusCode) + } + if response.Status != "201 Created" { + t.Fatalf("unexpected status: %q", response.Status) + } + if response.Header.Get("X-Reply") != "apple" { + t.Fatalf("unexpected response header: %q", response.Header.Get("X-Reply")) + } + if responseBody != "response body" { + t.Fatalf("unexpected response body: %q", responseBody) + } + if response.ContentLength != int64(len(responseBody)) { + t.Fatalf("unexpected content length: %d", response.ContentLength) + } + + observed := waitObservedRequest(t, requests) + if observed.method != http.MethodPost { + t.Fatalf("unexpected method: %q", observed.method) + } + if observed.body != "request body" { + t.Fatalf("unexpected request body: %q", observed.body) + } + if observed.host != "custom.example" { + t.Fatalf("unexpected host: %q", observed.host) + } + if observed.protoMajor != 2 { + t.Fatalf("expected HTTP/2 request, got HTTP/%d", observed.protoMajor) + } + var normalizedValues []string + for _, value := range observed.values { + for _, part := range strings.Split(value, ",") { + normalizedValues = append(normalizedValues, strings.TrimSpace(part)) + } + } + slices.Sort(normalizedValues) + if !slices.Equal(normalizedValues, []string{"one", "two"}) { + t.Fatalf("unexpected header values: %#v", observed.values) + } +} + +func TestAppleTransportPinnedPublicKey(t *testing.T) { + server := startAppleHTTPTestServer(t, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("pinned")) + }) + + goodTransport := newAppleHTTPTestTransport(t, server, option.HTTPClientOptions{ + Version: 2, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "localhost", + Insecure: true, + CertificatePublicKeySHA256: badoption.Listable[[]byte]{server.publicKeyHash}, + }, + }, + }) + + response, err := goodTransport.RoundTrip(newAppleHTTPRequest(t, http.MethodGet, server.URL("/good"), nil)) + if err != nil { + t.Fatalf("expected pinned request to succeed: %v", err) + } + response.Body.Close() + + badHash := append([]byte(nil), server.publicKeyHash...) + badHash[0] ^= 0xff + badTransport := newAppleHTTPTestTransport(t, server, option.HTTPClientOptions{ + Version: 2, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "localhost", + Insecure: true, + CertificatePublicKeySHA256: badoption.Listable[[]byte]{badHash}, + }, + }, + }) + + response, err = badTransport.RoundTrip(newAppleHTTPRequest(t, http.MethodGet, server.URL("/bad"), nil)) + if err == nil { + response.Body.Close() + t.Fatal("expected incorrect pinned public key to fail") + } +} + +func TestAppleTransportGuardrails(t *testing.T) { + testCases := []struct { + name string + options option.HTTPClientOptions + buildRequest func(t *testing.T) *http.Request + wantErrSubstr string + }{ + { + name: "websocket upgrade rejected", + options: option.HTTPClientOptions{ + Version: 2, + }, + buildRequest: func(t *testing.T) *http.Request { + t.Helper() + request := newAppleHTTPRequest(t, http.MethodGet, "https://localhost/socket", nil) + request.Header.Set("Connection", "Upgrade") + request.Header.Set("Upgrade", "websocket") + return request + }, + wantErrSubstr: "HTTP upgrade requests are unsupported in Apple HTTP engine", + }, + { + name: "missing url rejected", + options: option.HTTPClientOptions{ + Version: 2, + }, + buildRequest: func(t *testing.T) *http.Request { + t.Helper() + return &http.Request{Method: http.MethodGet} + }, + wantErrSubstr: "missing request URL", + }, + { + name: "unsupported scheme rejected", + options: option.HTTPClientOptions{ + Version: 2, + }, + buildRequest: func(t *testing.T) *http.Request { + t.Helper() + return newAppleHTTPRequest(t, http.MethodGet, "ftp://localhost/file", nil) + }, + wantErrSubstr: "unsupported URL scheme: ftp", + }, + { + name: "server name mismatch rejected", + options: option.HTTPClientOptions{ + Version: 2, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.com", + }, + }, + }, + buildRequest: func(t *testing.T) *http.Request { + t.Helper() + return newAppleHTTPRequest(t, http.MethodGet, "https://localhost/path", nil) + }, + wantErrSubstr: "tls.server_name is unsupported in Apple HTTP engine unless it matches request host", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + transport := newAppleHTTPTestTransport(t, nil, testCase.options) + response, err := transport.RoundTrip(testCase.buildRequest(t)) + if err == nil { + response.Body.Close() + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), testCase.wantErrSubstr) { + t.Fatalf("unexpected error: %v", err) + } + }) + } +} + +func TestAppleTransportCancellationRecovery(t *testing.T) { + server := startAppleHTTPTestServer(t, func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/block": + select { + case <-r.Context().Done(): + return + case <-time.After(appleHTTPTestTimeout): + http.Error(w, "request was not canceled", http.StatusGatewayTimeout) + } + default: + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("ok")) + } + }) + + transport := newAppleHTTPTestTransport(t, server, option.HTTPClientOptions{ + Version: 2, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: appleHTTPServerTLSOptions(server), + }, + }) + + for index := 0; index < appleHTTPRecoveryLoops; index++ { + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + request := newAppleHTTPRequestWithContext(t, ctx, http.MethodGet, server.URL("/block"), nil) + response, err := transport.RoundTrip(request) + cancel() + if err == nil { + response.Body.Close() + t.Fatalf("iteration %d: expected cancellation error", index) + } + if !errors.Is(err, context.DeadlineExceeded) && !errors.Is(err, context.Canceled) { + t.Fatalf("iteration %d: unexpected cancellation error: %v", index, err) + } + + response, err = transport.RoundTrip(newAppleHTTPRequest(t, http.MethodGet, server.URL("/ok"), nil)) + if err != nil { + t.Fatalf("iteration %d: follow-up request failed: %v", index, err) + } + if body := readResponseBody(t, response); body != "ok" { + response.Body.Close() + t.Fatalf("iteration %d: unexpected follow-up body: %q", index, body) + } + response.Body.Close() + } +} + +func TestAppleTransportLifecycle(t *testing.T) { + server := startAppleHTTPTestServer(t, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("ok")) + }) + + transport := newAppleHTTPTestTransport(t, server, option.HTTPClientOptions{ + Version: 2, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: appleHTTPServerTLSOptions(server), + }, + }) + + assertAppleHTTPSucceeds(t, transport, server.URL("/original")) + + transport.CloseIdleConnections() + assertAppleHTTPSucceeds(t, transport, server.URL("/reset")) + + innerTransport := transport.(*appleTransport) + if err := innerTransport.Close(); err != nil { + t.Fatal(err) + } + + response, err := innerTransport.RoundTrip(newAppleHTTPRequest(t, http.MethodGet, server.URL("/closed"), nil)) + if err == nil { + response.Body.Close() + t.Fatal("expected closed transport to fail") + } + if !errors.Is(err, net.ErrClosed) { + t.Fatalf("unexpected closed transport error: %v", err) + } +} + +func startAppleHTTPTestServer(t *testing.T, handler http.HandlerFunc) *appleHTTPTestServer { + t.Helper() + + serverCertificate, serverCertificatePEM := newAppleHTTPTestCertificate(t, "localhost") + server := httptest.NewUnstartedServer(handler) + server.EnableHTTP2 = true + server.TLS = &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: stdtls.VersionTLS12, + } + server.StartTLS() + t.Cleanup(server.Close) + + parsedURL, err := url.Parse(server.URL) + if err != nil { + t.Fatal(err) + } + baseURL := *parsedURL + baseURL.Host = net.JoinHostPort("localhost", parsedURL.Port()) + + return &appleHTTPTestServer{ + server: server, + baseURL: baseURL.String(), + dialHost: parsedURL.Hostname(), + certificate: serverCertificate, + certificatePEM: serverCertificatePEM, + publicKeyHash: certificatePublicKeySHA256(t, serverCertificate.Certificate[0]), + } +} + +func (s *appleHTTPTestServer) URL(path string) string { + if path == "" { + return s.baseURL + } + if strings.HasPrefix(path, "/") { + return s.baseURL + path + } + return s.baseURL + "/" + path +} + +func newAppleHTTPTestTransport(t *testing.T, server *appleHTTPTestServer, options option.HTTPClientOptions) innerTransport { + t.Helper() + + ctx := service.ContextWith[adapter.ConnectionManager]( + context.Background(), + route.NewConnectionManager(log.NewNOPFactory().NewLogger("connection")), + ) + dialer := &appleHTTPTestDialer{ + hostMap: make(map[string]string), + } + if server != nil { + dialer.hostMap["localhost"] = server.dialHost + } + + transport, err := newAppleTransport(ctx, log.NewNOPFactory().NewLogger("httpclient"), dialer, options) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + _ = transport.Close() + }) + return transport +} + +func (d *appleHTTPTestDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + host := destination.AddrString() + if destination.IsDomain() { + host = destination.Fqdn + if mappedHost, loaded := d.hostMap[host]; loaded { + host = mappedHost + } + } + return d.dialer.DialContext(ctx, network, net.JoinHostPort(host, strconv.Itoa(int(destination.Port)))) +} + +func (d *appleHTTPTestDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + host := destination.AddrString() + if destination.IsDomain() { + host = destination.Fqdn + if mappedHost, loaded := d.hostMap[host]; loaded { + host = mappedHost + } + } + if host == "" { + host = "127.0.0.1" + } + return d.listener.ListenPacket(ctx, N.NetworkUDP, net.JoinHostPort(host, strconv.Itoa(int(destination.Port)))) +} + +func newAppleHTTPTestCertificate(t *testing.T, serverName string) (stdtls.Certificate, string) { + t.Helper() + + privateKeyPEM, certificatePEM, err := boxTLS.GenerateCertificate(nil, nil, time.Now, serverName, time.Now().Add(time.Hour)) + if err != nil { + t.Fatal(err) + } + certificate, err := stdtls.X509KeyPair(certificatePEM, privateKeyPEM) + if err != nil { + t.Fatal(err) + } + return certificate, string(certificatePEM) +} + +func certificatePublicKeySHA256(t *testing.T, certificateDER []byte) []byte { + t.Helper() + + certificate, err := x509.ParseCertificate(certificateDER) + if err != nil { + t.Fatal(err) + } + publicKeyDER, err := x509.MarshalPKIXPublicKey(certificate.PublicKey) + if err != nil { + t.Fatal(err) + } + hashValue := sha256.Sum256(publicKeyDER) + return append([]byte(nil), hashValue[:]...) +} + +func appleHTTPServerTLSOptions(server *appleHTTPTestServer) *option.OutboundTLSOptions { + return &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "localhost", + Certificate: badoption.Listable[string]{server.certificatePEM}, + } +} + +func newAppleHTTPRequest(t *testing.T, method string, rawURL string, body []byte) *http.Request { + t.Helper() + return newAppleHTTPRequestWithContext(t, context.Background(), method, rawURL, body) +} + +func newAppleHTTPRequestWithContext(t *testing.T, ctx context.Context, method string, rawURL string, body []byte) *http.Request { + t.Helper() + request, err := http.NewRequestWithContext(ctx, method, rawURL, bytes.NewReader(body)) + if err != nil { + t.Fatal(err) + } + return request +} + +func waitObservedRequest(t *testing.T, requests <-chan appleHTTPObservedRequest) appleHTTPObservedRequest { + t.Helper() + + select { + case request := <-requests: + return request + case <-time.After(appleHTTPTestTimeout): + t.Fatal("timed out waiting for observed request") + return appleHTTPObservedRequest{} + } +} + +func readResponseBody(t *testing.T, response *http.Response) string { + t.Helper() + + body, err := io.ReadAll(response.Body) + if err != nil { + t.Fatal(err) + } + return string(body) +} + +func assertAppleHTTPSucceeds(t *testing.T, transport http.RoundTripper, rawURL string) { + t.Helper() + + response, err := transport.RoundTrip(newAppleHTTPRequest(t, http.MethodGet, rawURL, nil)) + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + if body := readResponseBody(t, response); body != "ok" { + t.Fatalf("unexpected response body: %q", body) + } +} diff --git a/common/httpclient/apple_transport_stub.go b/common/httpclient/apple_transport_stub.go new file mode 100644 index 0000000000..9735998f4e --- /dev/null +++ b/common/httpclient/apple_transport_stub.go @@ -0,0 +1,16 @@ +//go:build !darwin || !cgo + +package httpclient + +import ( + "context" + + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + N "github.com/sagernet/sing/common/network" +) + +func newAppleTransport(ctx context.Context, logger logger.ContextLogger, rawDialer N.Dialer, options option.HTTPClientOptions) (innerTransport, error) { + return nil, E.New("Apple HTTP engine is not available on non-Apple platforms") +} diff --git a/common/httpclient/client.go b/common/httpclient/client.go new file mode 100644 index 0000000000..c8eb0fef8e --- /dev/null +++ b/common/httpclient/client.go @@ -0,0 +1,130 @@ +package httpclient + +import ( + "context" + "time" + + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + N "github.com/sagernet/sing/common/network" +) + +func NewTransport(ctx context.Context, logger logger.ContextLogger, tag string, options option.HTTPClientOptions) (*ManagedTransport, error) { + rawDialer, err := dialer.NewWithOptions(dialer.Options{ + Context: ctx, + Options: options.DialerOptions, + RemoteIsDomain: true, + DirectResolver: options.DirectResolver, + ResolverOnDetour: options.ResolveOnDetour, + NewDialer: options.ResolveOnDetour, + DefaultOutbound: options.DefaultOutbound, + }) + if err != nil { + return nil, err + } + headers := options.Headers.Build() + host := headers.Get("Host") + headers.Del("Host") + + var cheapRebuild bool + switch options.Engine { + case C.TLSEngineApple: + inner, transportErr := newAppleTransport(ctx, logger, rawDialer, options) + if transportErr != nil { + return nil, transportErr + } + managedTransport := &ManagedTransport{ + dialer: rawDialer, + headers: headers, + host: host, + tag: tag, + factory: func() (innerTransport, error) { + return newAppleTransport(ctx, logger, rawDialer, options) + }, + } + managedTransport.epoch.Store(&transportEpoch{transport: inner}) + return managedTransport, nil + case C.TLSEngineDefault, "go": + cheapRebuild = true + default: + return nil, E.New("unknown HTTP engine: ", options.Engine) + } + tlsOptions := common.PtrValueOrDefault(options.TLS) + tlsOptions.Enabled = true + baseTLSConfig, err := tls.NewClientWithOptions(tls.ClientOptions{ + Context: ctx, + Logger: logger, + Options: tlsOptions, + AllowEmptyServerName: true, + }) + if err != nil { + return nil, err + } + inner, err := newTransport(rawDialer, baseTLSConfig, options) + if err != nil { + return nil, err + } + managedTransport := &ManagedTransport{ + cheapRebuild: cheapRebuild, + dialer: rawDialer, + headers: headers, + host: host, + tag: tag, + factory: func() (innerTransport, error) { + return newTransport(rawDialer, baseTLSConfig, options) + }, + } + managedTransport.epoch.Store(&transportEpoch{transport: inner}) + return managedTransport, nil +} + +func newTransport(rawDialer N.Dialer, baseTLSConfig tls.Config, options option.HTTPClientOptions) (innerTransport, error) { + version := options.Version + if version == 0 { + version = 2 + } + fallbackDelay := time.Duration(options.DialerOptions.FallbackDelay) + if fallbackDelay == 0 { + fallbackDelay = 300 * time.Millisecond + } + var transport innerTransport + var err error + switch version { + case 1: + transport = newHTTP1Transport(rawDialer, baseTLSConfig) + case 2: + if options.DisableVersionFallback { + transport, err = newHTTP2Transport(rawDialer, baseTLSConfig, options.HTTP2Options) + } else { + transport, err = newHTTP2FallbackTransport(rawDialer, baseTLSConfig, options.HTTP2Options) + } + case 3: + if baseTLSConfig != nil { + _, err = baseTLSConfig.STDConfig() + if err != nil { + return nil, err + } + } + if options.DisableVersionFallback { + transport, err = newHTTP3Transport(rawDialer, baseTLSConfig, options.HTTP3Options) + } else { + var h2Fallback innerTransport + h2Fallback, err = newHTTP2FallbackTransport(rawDialer, baseTLSConfig, options.HTTP2Options) + if err != nil { + return nil, err + } + transport, err = newHTTP3FallbackTransport(rawDialer, baseTLSConfig, h2Fallback, options.HTTP3Options, fallbackDelay) + } + default: + return nil, E.New("unknown HTTP version: ", version) + } + if err != nil { + return nil, err + } + return transport, nil +} diff --git a/common/httpclient/context.go b/common/httpclient/context.go new file mode 100644 index 0000000000..883a25e20c --- /dev/null +++ b/common/httpclient/context.go @@ -0,0 +1,14 @@ +package httpclient + +import "context" + +type transportKey struct{} + +func contextWithTransportTag(ctx context.Context, transportTag string) context.Context { + return context.WithValue(ctx, transportKey{}, transportTag) +} + +func transportTagFromContext(ctx context.Context) (string, bool) { + value, loaded := ctx.Value(transportKey{}).(string) + return value, loaded +} diff --git a/common/httpclient/helpers.go b/common/httpclient/helpers.go new file mode 100644 index 0000000000..cffc797198 --- /dev/null +++ b/common/httpclient/helpers.go @@ -0,0 +1,86 @@ +package httpclient + +import ( + "context" + stdTLS "crypto/tls" + "io" + "net" + "net/http" + "strings" + + "github.com/sagernet/sing-box/common/tls" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func dialTLS(ctx context.Context, rawDialer N.Dialer, baseTLSConfig tls.Config, destination M.Socksaddr, nextProtos []string, expectProto string) (net.Conn, error) { + if baseTLSConfig == nil { + return nil, E.New("TLS transport unavailable") + } + tlsConfig := baseTLSConfig.Clone() + if tlsConfig.ServerName() == "" && destination.IsValid() { + tlsConfig.SetServerName(destination.AddrString()) + } + tlsConfig.SetNextProtos(nextProtos) + conn, err := rawDialer.DialContext(ctx, N.NetworkTCP, destination) + if err != nil { + return nil, err + } + tlsConn, err := tls.ClientHandshake(ctx, conn, tlsConfig) + if err != nil { + conn.Close() + return nil, err + } + if expectProto != "" && tlsConn.ConnectionState().NegotiatedProtocol != expectProto { + tlsConn.Close() + return nil, errHTTP2Fallback + } + return tlsConn, nil +} + +func applyHeaders(request *http.Request, headers http.Header, host string) { + for header, values := range headers { + request.Header[header] = append([]string(nil), values...) + } + if host != "" { + request.Host = host + } +} + +func requestRequiresHTTP1(request *http.Request) bool { + return strings.Contains(strings.ToLower(request.Header.Get("Connection")), "upgrade") && + strings.EqualFold(request.Header.Get("Upgrade"), "websocket") +} + +func requestReplayable(request *http.Request) bool { + return request.Body == nil || request.Body == http.NoBody || request.GetBody != nil +} + +func cloneRequestForRetry(request *http.Request) *http.Request { + cloned := request.Clone(request.Context()) + if request.Body != nil && request.Body != http.NoBody && request.GetBody != nil { + cloned.Body = mustGetBody(request) + } + return cloned +} + +func mustGetBody(request *http.Request) io.ReadCloser { + body, err := request.GetBody() + if err != nil { + panic(err) + } + return body +} + +func buildSTDTLSConfig(baseTLSConfig tls.Config, destination M.Socksaddr, nextProtos []string) (*stdTLS.Config, error) { + if baseTLSConfig == nil { + return nil, nil + } + tlsConfig := baseTLSConfig.Clone() + if tlsConfig.ServerName() == "" && destination.IsValid() { + tlsConfig.SetServerName(destination.AddrString()) + } + tlsConfig.SetNextProtos(nextProtos) + return tlsConfig.STDConfig() +} diff --git a/common/httpclient/http1_transport.go b/common/httpclient/http1_transport.go new file mode 100644 index 0000000000..ad2ccedb8f --- /dev/null +++ b/common/httpclient/http1_transport.go @@ -0,0 +1,42 @@ +package httpclient + +import ( + "context" + "net" + "net/http" + + "github.com/sagernet/sing-box/common/tls" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +type http1Transport struct { + transport *http.Transport +} + +func newHTTP1Transport(rawDialer N.Dialer, baseTLSConfig tls.Config) *http1Transport { + transport := &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return rawDialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + }, + } + if baseTLSConfig != nil { + transport.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + return dialTLS(ctx, rawDialer, baseTLSConfig, M.ParseSocksaddr(addr), []string{"http/1.1"}, "") + } + } + return &http1Transport{transport: transport} +} + +func (t *http1Transport) RoundTrip(request *http.Request) (*http.Response, error) { + return t.transport.RoundTrip(request) +} + +func (t *http1Transport) CloseIdleConnections() { + t.transport.CloseIdleConnections() +} + +func (t *http1Transport) Close() error { + t.CloseIdleConnections() + return nil +} diff --git a/common/httpclient/http2_config.go b/common/httpclient/http2_config.go new file mode 100644 index 0000000000..9e1d871fdc --- /dev/null +++ b/common/httpclient/http2_config.go @@ -0,0 +1,42 @@ +package httpclient + +import ( + stdTLS "crypto/tls" + "net/http" + "time" + + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + + "golang.org/x/net/http2" +) + +func CloneHTTP2Transport(transport *http2.Transport) *http2.Transport { + return &http2.Transport{ + ReadIdleTimeout: transport.ReadIdleTimeout, + PingTimeout: transport.PingTimeout, + DialTLSContext: transport.DialTLSContext, + } +} + +func ConfigureHTTP2Transport(options option.HTTP2Options) (*http2.Transport, error) { + stdTransport := &http.Transport{ + TLSClientConfig: &stdTLS.Config{}, + HTTP2: &http.HTTP2Config{ + MaxReceiveBufferPerStream: int(options.StreamReceiveWindow.Value()), + MaxReceiveBufferPerConnection: int(options.ConnectionReceiveWindow.Value()), + MaxConcurrentStreams: options.MaxConcurrentStreams, + SendPingTimeout: time.Duration(options.KeepAlivePeriod), + PingTimeout: time.Duration(options.IdleTimeout), + }, + } + h2Transport, err := http2.ConfigureTransports(stdTransport) + if err != nil { + return nil, E.Cause(err, "configure HTTP/2 transport") + } + // ConfigureTransports binds ConnPool to the throwaway http.Transport; sever it so DialTLSContext is used directly. + h2Transport.ConnPool = nil + h2Transport.ReadIdleTimeout = time.Duration(options.KeepAlivePeriod) + h2Transport.PingTimeout = time.Duration(options.IdleTimeout) + return h2Transport, nil +} diff --git a/common/httpclient/http2_fallback_transport.go b/common/httpclient/http2_fallback_transport.go new file mode 100644 index 0000000000..5b16dff187 --- /dev/null +++ b/common/httpclient/http2_fallback_transport.go @@ -0,0 +1,84 @@ +package httpclient + +import ( + "context" + stdTLS "crypto/tls" + "errors" + "net" + "net/http" + "sync/atomic" + + "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + "golang.org/x/net/http2" +) + +var errHTTP2Fallback = E.New("fallback to HTTP/1.1") + +type http2FallbackTransport struct { + h2Transport *http2.Transport + h1Transport *http1Transport + h2Fallback *atomic.Bool +} + +func newHTTP2FallbackTransport(rawDialer N.Dialer, baseTLSConfig tls.Config, options option.HTTP2Options) (*http2FallbackTransport, error) { + h1 := newHTTP1Transport(rawDialer, baseTLSConfig) + var fallback atomic.Bool + h2Transport, err := ConfigureHTTP2Transport(options) + if err != nil { + return nil, err + } + h2Transport.DialTLSContext = func(ctx context.Context, network, addr string, _ *stdTLS.Config) (net.Conn, error) { + conn, dialErr := dialTLS(ctx, rawDialer, baseTLSConfig, M.ParseSocksaddr(addr), []string{http2.NextProtoTLS, "http/1.1"}, http2.NextProtoTLS) + if dialErr != nil { + if errors.Is(dialErr, errHTTP2Fallback) { + fallback.Store(true) + } + return nil, dialErr + } + return conn, nil + } + return &http2FallbackTransport{ + h2Transport: h2Transport, + h1Transport: h1, + h2Fallback: &fallback, + }, nil +} + +func (t *http2FallbackTransport) RoundTrip(request *http.Request) (*http.Response, error) { + return t.roundTrip(request, true) +} + +func (t *http2FallbackTransport) roundTrip(request *http.Request, allowHTTP1Fallback bool) (*http.Response, error) { + if request.URL.Scheme != "https" || requestRequiresHTTP1(request) { + return t.h1Transport.RoundTrip(request) + } + if t.h2Fallback.Load() { + if !allowHTTP1Fallback { + return nil, errHTTP2Fallback + } + return t.h1Transport.RoundTrip(request) + } + response, err := t.h2Transport.RoundTrip(request) + if err == nil { + return response, nil + } + if !errors.Is(err, errHTTP2Fallback) || !allowHTTP1Fallback { + return nil, err + } + return t.h1Transport.RoundTrip(cloneRequestForRetry(request)) +} + +func (t *http2FallbackTransport) CloseIdleConnections() { + t.h1Transport.CloseIdleConnections() + t.h2Transport.CloseIdleConnections() +} + +func (t *http2FallbackTransport) Close() error { + t.CloseIdleConnections() + return nil +} diff --git a/common/httpclient/http2_transport.go b/common/httpclient/http2_transport.go new file mode 100644 index 0000000000..78e7ae6824 --- /dev/null +++ b/common/httpclient/http2_transport.go @@ -0,0 +1,52 @@ +package httpclient + +import ( + "context" + stdTLS "crypto/tls" + "net" + "net/http" + + "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/option" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + "golang.org/x/net/http2" +) + +type http2Transport struct { + h2Transport *http2.Transport + h1Transport *http1Transport +} + +func newHTTP2Transport(rawDialer N.Dialer, baseTLSConfig tls.Config, options option.HTTP2Options) (*http2Transport, error) { + h1 := newHTTP1Transport(rawDialer, baseTLSConfig) + h2Transport, err := ConfigureHTTP2Transport(options) + if err != nil { + return nil, err + } + h2Transport.DialTLSContext = func(ctx context.Context, network, addr string, _ *stdTLS.Config) (net.Conn, error) { + return dialTLS(ctx, rawDialer, baseTLSConfig, M.ParseSocksaddr(addr), []string{http2.NextProtoTLS}, http2.NextProtoTLS) + } + return &http2Transport{ + h2Transport: h2Transport, + h1Transport: h1, + }, nil +} + +func (t *http2Transport) RoundTrip(request *http.Request) (*http.Response, error) { + if request.URL.Scheme != "https" || requestRequiresHTTP1(request) { + return t.h1Transport.RoundTrip(request) + } + return t.h2Transport.RoundTrip(request) +} + +func (t *http2Transport) CloseIdleConnections() { + t.h1Transport.CloseIdleConnections() + t.h2Transport.CloseIdleConnections() +} + +func (t *http2Transport) Close() error { + t.CloseIdleConnections() + return nil +} diff --git a/common/httpclient/http3_transport.go b/common/httpclient/http3_transport.go new file mode 100644 index 0000000000..0b8855d7cd --- /dev/null +++ b/common/httpclient/http3_transport.go @@ -0,0 +1,297 @@ +//go:build with_quic + +package httpclient + +import ( + "context" + stdTLS "crypto/tls" + "errors" + "net/http" + "sync" + "time" + + "github.com/sagernet/quic-go" + "github.com/sagernet/quic-go/http3" + "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +type http3Transport struct { + h3Transport *http3.Transport +} + +type http3FallbackTransport struct { + h3Transport *http3.Transport + h2Fallback innerTransport + fallbackDelay time.Duration + brokenAccess sync.Mutex + brokenUntil time.Time + brokenBackoff time.Duration +} + +func newHTTP3RoundTripper( + rawDialer N.Dialer, + baseTLSConfig tls.Config, + options option.QUICOptions, +) *http3.Transport { + var handshakeTimeout time.Duration + if baseTLSConfig != nil { + handshakeTimeout = baseTLSConfig.HandshakeTimeout() + } + quicConfig := &quic.Config{ + InitialStreamReceiveWindow: options.StreamReceiveWindow.Value(), + MaxStreamReceiveWindow: options.StreamReceiveWindow.Value(), + InitialConnectionReceiveWindow: options.ConnectionReceiveWindow.Value(), + MaxConnectionReceiveWindow: options.ConnectionReceiveWindow.Value(), + KeepAlivePeriod: time.Duration(options.KeepAlivePeriod), + MaxIdleTimeout: time.Duration(options.IdleTimeout), + DisablePathMTUDiscovery: options.DisablePathMTUDiscovery, + } + if options.InitialPacketSize > 0 { + quicConfig.InitialPacketSize = uint16(options.InitialPacketSize) + } + if options.MaxConcurrentStreams > 0 { + quicConfig.MaxIncomingStreams = int64(options.MaxConcurrentStreams) + } + if handshakeTimeout > 0 { + quicConfig.HandshakeIdleTimeout = handshakeTimeout + } + h3Transport := &http3.Transport{ + TLSClientConfig: &stdTLS.Config{}, + QUICConfig: quicConfig, + Dial: func(ctx context.Context, addr string, tlsConfig *stdTLS.Config, quicConfig *quic.Config) (*quic.Conn, error) { + if handshakeTimeout > 0 && quicConfig.HandshakeIdleTimeout == 0 { + quicConfig = quicConfig.Clone() + quicConfig.HandshakeIdleTimeout = handshakeTimeout + } + if baseTLSConfig != nil { + var err error + tlsConfig, err = buildSTDTLSConfig(baseTLSConfig, M.ParseSocksaddr(addr), []string{http3.NextProtoH3}) + if err != nil { + return nil, err + } + } else { + tlsConfig = tlsConfig.Clone() + tlsConfig.NextProtos = []string{http3.NextProtoH3} + } + conn, err := rawDialer.DialContext(ctx, N.NetworkUDP, M.ParseSocksaddr(addr)) + if err != nil { + return nil, err + } + quicConn, err := quic.DialEarly(ctx, bufio.NewUnbindPacketConn(conn), conn.RemoteAddr(), tlsConfig, quicConfig) + if err != nil { + conn.Close() + return nil, err + } + return quicConn, nil + }, + } + return h3Transport +} + +func newHTTP3Transport( + rawDialer N.Dialer, + baseTLSConfig tls.Config, + options option.QUICOptions, +) (innerTransport, error) { + return &http3Transport{ + h3Transport: newHTTP3RoundTripper(rawDialer, baseTLSConfig, options), + }, nil +} + +func newHTTP3FallbackTransport( + rawDialer N.Dialer, + baseTLSConfig tls.Config, + h2Fallback innerTransport, + options option.QUICOptions, + fallbackDelay time.Duration, +) (innerTransport, error) { + return &http3FallbackTransport{ + h3Transport: newHTTP3RoundTripper(rawDialer, baseTLSConfig, options), + h2Fallback: h2Fallback, + fallbackDelay: fallbackDelay, + }, nil +} + +func (t *http3Transport) RoundTrip(request *http.Request) (*http.Response, error) { + return t.h3Transport.RoundTrip(request) +} + +func (t *http3Transport) CloseIdleConnections() { + t.h3Transport.CloseIdleConnections() +} + +func (t *http3Transport) Close() error { + t.CloseIdleConnections() + return t.h3Transport.Close() +} + +func (t *http3FallbackTransport) RoundTrip(request *http.Request) (*http.Response, error) { + if request.URL.Scheme != "https" || requestRequiresHTTP1(request) { + return t.h2Fallback.RoundTrip(request) + } + return t.roundTripHTTP3(request) +} + +func (t *http3FallbackTransport) roundTripHTTP3(request *http.Request) (*http.Response, error) { + if t.h3Broken() { + return t.h2FallbackRoundTrip(request) + } + response, err := t.h3Transport.RoundTripOpt(request, http3.RoundTripOpt{OnlyCachedConn: true}) + if err == nil { + t.clearH3Broken() + return response, nil + } + if !errors.Is(err, http3.ErrNoCachedConn) { + t.markH3Broken() + return t.h2FallbackRoundTrip(cloneRequestForRetry(request)) + } + if !requestReplayable(request) { + response, err = t.h3Transport.RoundTrip(request) + if err == nil { + t.clearH3Broken() + return response, nil + } + t.markH3Broken() + return nil, err + } + return t.roundTripHTTP3Race(request) +} + +func (t *http3FallbackTransport) roundTripHTTP3Race(request *http.Request) (*http.Response, error) { + ctx, cancel := context.WithCancel(request.Context()) + defer cancel() + type result struct { + response *http.Response + err error + h3 bool + } + results := make(chan result, 2) + startRoundTrip := func(request *http.Request, useH3 bool) { + request = request.WithContext(ctx) + var ( + response *http.Response + err error + ) + if useH3 { + response, err = t.h3Transport.RoundTrip(request) + } else { + response, err = t.h2FallbackRoundTrip(request) + } + results <- result{response: response, err: err, h3: useH3} + } + goroutines := 1 + received := 0 + drainRemaining := func() { + cancel() + for range goroutines - received { + go func() { + loser := <-results + if loser.response != nil && loser.response.Body != nil { + loser.response.Body.Close() + } + }() + } + } + go startRoundTrip(cloneRequestForRetry(request), true) + timer := time.NewTimer(t.fallbackDelay) + defer timer.Stop() + var ( + h3Err error + fallbackErr error + ) + for { + select { + case <-timer.C: + if goroutines == 1 { + goroutines++ + go startRoundTrip(cloneRequestForRetry(request), false) + } + case raceResult := <-results: + received++ + if raceResult.err == nil { + if raceResult.h3 { + t.clearH3Broken() + } + drainRemaining() + return raceResult.response, nil + } + if raceResult.h3 { + t.markH3Broken() + h3Err = raceResult.err + if goroutines == 1 { + goroutines++ + if !timer.Stop() { + select { + case <-timer.C: + default: + } + } + go startRoundTrip(cloneRequestForRetry(request), false) + } + } else { + fallbackErr = raceResult.err + } + if received < goroutines { + continue + } + drainRemaining() + switch { + case h3Err != nil && fallbackErr != nil: + return nil, E.Errors(h3Err, fallbackErr) + case fallbackErr != nil: + return nil, fallbackErr + default: + return nil, h3Err + } + } + } +} + +func (t *http3FallbackTransport) h2FallbackRoundTrip(request *http.Request) (*http.Response, error) { + if fallback, isFallback := t.h2Fallback.(*http2FallbackTransport); isFallback { + return fallback.roundTrip(request, true) + } + return t.h2Fallback.RoundTrip(request) +} + +func (t *http3FallbackTransport) CloseIdleConnections() { + t.h3Transport.CloseIdleConnections() + t.h2Fallback.CloseIdleConnections() +} + +func (t *http3FallbackTransport) Close() error { + t.CloseIdleConnections() + return t.h3Transport.Close() +} + +func (t *http3FallbackTransport) h3Broken() bool { + t.brokenAccess.Lock() + defer t.brokenAccess.Unlock() + return !t.brokenUntil.IsZero() && time.Now().Before(t.brokenUntil) +} + +func (t *http3FallbackTransport) clearH3Broken() { + t.brokenAccess.Lock() + t.brokenUntil = time.Time{} + t.brokenBackoff = 0 + t.brokenAccess.Unlock() +} + +func (t *http3FallbackTransport) markH3Broken() { + t.brokenAccess.Lock() + defer t.brokenAccess.Unlock() + if t.brokenBackoff == 0 { + t.brokenBackoff = 5 * time.Minute + } else { + t.brokenBackoff *= 2 + if t.brokenBackoff > 48*time.Hour { + t.brokenBackoff = 48 * time.Hour + } + } + t.brokenUntil = time.Now().Add(t.brokenBackoff) +} diff --git a/common/httpclient/http3_transport_stub.go b/common/httpclient/http3_transport_stub.go new file mode 100644 index 0000000000..f86a9f3653 --- /dev/null +++ b/common/httpclient/http3_transport_stub.go @@ -0,0 +1,30 @@ +//go:build !with_quic + +package httpclient + +import ( + "time" + + "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" +) + +func newHTTP3FallbackTransport( + rawDialer N.Dialer, + baseTLSConfig tls.Config, + h2Fallback innerTransport, + options option.QUICOptions, + fallbackDelay time.Duration, +) (innerTransport, error) { + return nil, E.New("HTTP/3 requires building with the with_quic tag") +} + +func newHTTP3Transport( + rawDialer N.Dialer, + baseTLSConfig tls.Config, + options option.QUICOptions, +) (innerTransport, error) { + return nil, E.New("HTTP/3 requires building with the with_quic tag") +} diff --git a/common/httpclient/managed_transport.go b/common/httpclient/managed_transport.go new file mode 100644 index 0000000000..779eccda8f --- /dev/null +++ b/common/httpclient/managed_transport.go @@ -0,0 +1,209 @@ +package httpclient + +import ( + "io" + "net/http" + "sync" + "sync/atomic" + + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" +) + +type innerTransport interface { + http.RoundTripper + CloseIdleConnections() + Close() error +} + +var _ adapter.HTTPTransport = (*ManagedTransport)(nil) + +type ManagedTransport struct { + epoch atomic.Pointer[transportEpoch] + rebuildAccess sync.Mutex + factory func() (innerTransport, error) + cheapRebuild bool + + dialer N.Dialer + headers http.Header + host string + tag string +} + +type transportEpoch struct { + transport innerTransport + active atomic.Int64 + marked atomic.Bool + closeOnce sync.Once +} + +type managedResponseBody struct { + body io.ReadCloser + release func() + once sync.Once +} + +func (e *transportEpoch) tryClose() { + e.closeOnce.Do(func() { + e.transport.Close() + }) +} + +func (b *managedResponseBody) Read(p []byte) (int, error) { + return b.body.Read(p) +} + +func (b *managedResponseBody) Close() error { + err := b.body.Close() + b.once.Do(b.release) + return err +} + +func (t *ManagedTransport) getEpoch() (*transportEpoch, error) { + epoch := t.epoch.Load() + if epoch != nil { + return epoch, nil + } + t.rebuildAccess.Lock() + defer t.rebuildAccess.Unlock() + epoch = t.epoch.Load() + if epoch != nil { + return epoch, nil + } + inner, err := t.factory() + if err != nil { + return nil, err + } + epoch = &transportEpoch{transport: inner} + t.epoch.Store(epoch) + return epoch, nil +} + +func (t *ManagedTransport) acquireEpoch() (*transportEpoch, error) { + for { + epoch, err := t.getEpoch() + if err != nil { + return nil, err + } + epoch.active.Add(1) + if epoch == t.epoch.Load() { + return epoch, nil + } + t.releaseEpoch(epoch) + } +} + +func (t *ManagedTransport) releaseEpoch(epoch *transportEpoch) { + if epoch.active.Add(-1) == 0 && epoch.marked.Load() { + epoch.tryClose() + } +} + +func (t *ManagedTransport) retireEpoch(epoch *transportEpoch) { + if epoch == nil { + return + } + epoch.marked.Store(true) + if epoch.active.Load() == 0 { + epoch.tryClose() + } +} + +func (t *ManagedTransport) RoundTrip(request *http.Request) (*http.Response, error) { + epoch, err := t.acquireEpoch() + if err != nil { + return nil, E.Cause(err, "rebuild http transport") + } + if t.tag != "" { + if transportTag, loaded := transportTagFromContext(request.Context()); loaded && transportTag == t.tag { + t.releaseEpoch(epoch) + return nil, E.New("HTTP request loopback in transport[", t.tag, "]") + } + request = request.Clone(contextWithTransportTag(request.Context(), t.tag)) + } else if len(t.headers) > 0 || t.host != "" { + request = request.Clone(request.Context()) + } + applyHeaders(request, t.headers, t.host) + response, roundTripErr := epoch.transport.RoundTrip(request) + if roundTripErr != nil || response == nil || response.Body == nil { + t.releaseEpoch(epoch) + return response, roundTripErr + } + response.Body = &managedResponseBody{ + body: response.Body, + release: func() { t.releaseEpoch(epoch) }, + } + return response, roundTripErr +} + +func (t *ManagedTransport) CloseIdleConnections() { + oldEpoch := t.epoch.Swap(nil) + if oldEpoch == nil { + return + } + oldEpoch.transport.CloseIdleConnections() + t.retireEpoch(oldEpoch) +} + +func (t *ManagedTransport) Reset() { + oldEpoch := t.epoch.Swap(nil) + if t.cheapRebuild { + t.rebuildAccess.Lock() + if t.epoch.Load() == nil { + inner, err := t.factory() + if err == nil { + t.epoch.Store(&transportEpoch{transport: inner}) + } + } + t.rebuildAccess.Unlock() + } + t.retireEpoch(oldEpoch) +} + +func (t *ManagedTransport) close() error { + epoch := t.epoch.Swap(nil) + if epoch != nil { + return epoch.transport.Close() + } + return nil +} + +var _ adapter.HTTPTransport = (*sharedRef)(nil) + +type sharedRef struct { + managed *ManagedTransport + shared *sharedState + idle atomic.Bool +} + +type sharedState struct { + activeRefs atomic.Int32 +} + +func newSharedRef(managed *ManagedTransport, shared *sharedState) *sharedRef { + shared.activeRefs.Add(1) + return &sharedRef{ + managed: managed, + shared: shared, + } +} + +func (r *sharedRef) RoundTrip(request *http.Request) (*http.Response, error) { + if r.idle.CompareAndSwap(true, false) { + r.shared.activeRefs.Add(1) + } + return r.managed.RoundTrip(request) +} + +func (r *sharedRef) CloseIdleConnections() { + if r.idle.CompareAndSwap(false, true) { + if r.shared.activeRefs.Add(-1) == 0 { + r.managed.CloseIdleConnections() + } + } +} + +func (r *sharedRef) Reset() { + r.managed.Reset() +} diff --git a/common/httpclient/manager.go b/common/httpclient/manager.go new file mode 100644 index 0000000000..2b4f9d5be3 --- /dev/null +++ b/common/httpclient/manager.go @@ -0,0 +1,175 @@ +package httpclient + +import ( + "context" + "sync" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" +) + +var ( + _ adapter.HTTPClientManager = (*Manager)(nil) + _ adapter.LifecycleService = (*Manager)(nil) +) + +type Manager struct { + ctx context.Context + logger log.ContextLogger + access sync.Mutex + defines map[string]option.HTTPClient + sharedTransports map[string]*sharedManagedTransport + managedTransports []*ManagedTransport + defaultTag string + defaultTransport *sharedManagedTransport + defaultTransportFallback func() (*ManagedTransport, error) +} + +type sharedManagedTransport struct { + managed *ManagedTransport + shared *sharedState +} + +func NewManager(ctx context.Context, logger log.ContextLogger, clients []option.HTTPClient, defaultHTTPClient string) *Manager { + defines := make(map[string]option.HTTPClient, len(clients)) + for _, client := range clients { + defines[client.Tag] = client + } + defaultTag := defaultHTTPClient + if defaultTag == "" && len(clients) > 0 { + defaultTag = clients[0].Tag + } + return &Manager{ + ctx: ctx, + logger: logger, + defines: defines, + sharedTransports: make(map[string]*sharedManagedTransport), + defaultTag: defaultTag, + } +} + +func (m *Manager) Initialize(defaultTransportFallback func() (*ManagedTransport, error)) { + m.defaultTransportFallback = defaultTransportFallback +} + +func (m *Manager) Name() string { + return "http-client" +} + +func (m *Manager) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + if m.defaultTag != "" { + sharedTransport, err := m.resolveShared(m.defaultTag) + if err != nil { + return E.Cause(err, "resolve default http client") + } + m.defaultTransport = sharedTransport + } else if m.defaultTransportFallback != nil { + transport, err := m.defaultTransportFallback() + if err != nil { + return E.Cause(err, "create default http client") + } + m.trackTransport(transport) + m.defaultTransport = &sharedManagedTransport{ + managed: transport, + shared: &sharedState{}, + } + } + return nil +} + +func (m *Manager) DefaultTransport() adapter.HTTPTransport { + if m.defaultTransport == nil { + return nil + } + return newSharedRef(m.defaultTransport.managed, m.defaultTransport.shared) +} + +func (m *Manager) ResolveTransport(ctx context.Context, logger logger.ContextLogger, options option.HTTPClientOptions) (adapter.HTTPTransport, error) { + if options.Tag != "" { + if options.ResolveOnDetour { + define, loaded := m.defines[options.Tag] + if !loaded { + return nil, E.New("http_client not found: ", options.Tag) + } + resolvedOptions := define.Options() + resolvedOptions.ResolveOnDetour = true + transport, err := NewTransport(ctx, logger, options.Tag, resolvedOptions) + if err != nil { + return nil, err + } + m.trackTransport(transport) + return transport, nil + } + sharedTransport, err := m.resolveShared(options.Tag) + if err != nil { + return nil, err + } + return newSharedRef(sharedTransport.managed, sharedTransport.shared), nil + } + transport, err := NewTransport(ctx, logger, "", options) + if err != nil { + return nil, err + } + m.trackTransport(transport) + return transport, nil +} + +func (m *Manager) trackTransport(transport *ManagedTransport) { + m.access.Lock() + defer m.access.Unlock() + m.managedTransports = append(m.managedTransports, transport) +} + +func (m *Manager) resolveShared(tag string) (*sharedManagedTransport, error) { + m.access.Lock() + defer m.access.Unlock() + if sharedTransport, loaded := m.sharedTransports[tag]; loaded { + return sharedTransport, nil + } + define, loaded := m.defines[tag] + if !loaded { + return nil, E.New("http_client not found: ", tag) + } + transport, err := NewTransport(m.ctx, m.logger, tag, define.Options()) + if err != nil { + return nil, E.Cause(err, "create shared http_client[", tag, "]") + } + sharedTransport := &sharedManagedTransport{ + managed: transport, + shared: &sharedState{}, + } + m.sharedTransports[tag] = sharedTransport + m.managedTransports = append(m.managedTransports, transport) + return sharedTransport, nil +} + +func (m *Manager) ResetNetwork() { + m.access.Lock() + defer m.access.Unlock() + for _, transport := range m.managedTransports { + transport.Reset() + } +} + +func (m *Manager) Close() error { + m.access.Lock() + defer m.access.Unlock() + if m.managedTransports == nil { + return nil + } + var err error + for _, transport := range m.managedTransports { + err = E.Append(err, transport.close(), func(err error) error { + return E.Cause(err, "close http client") + }) + } + m.managedTransports = nil + m.sharedTransports = nil + return err +} diff --git a/common/proxybridge/bridge.go b/common/proxybridge/bridge.go new file mode 100644 index 0000000000..3380cae447 --- /dev/null +++ b/common/proxybridge/bridge.go @@ -0,0 +1,115 @@ +package proxybridge + +import ( + std_bufio "bufio" + "context" + "crypto/rand" + "encoding/hex" + "net" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/auth" + 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/protocol/socks" + "github.com/sagernet/sing/service" +) + +type Bridge struct { + ctx context.Context + logger logger.ContextLogger + tag string + dialer N.Dialer + connection adapter.ConnectionManager + tcpListener *net.TCPListener + username string + password string + authenticator *auth.Authenticator +} + +func New(ctx context.Context, logger logger.ContextLogger, tag string, dialer N.Dialer) (*Bridge, error) { + username := randomHex(16) + password := randomHex(16) + tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1)}) + if err != nil { + return nil, err + } + bridge := &Bridge{ + ctx: ctx, + logger: logger, + tag: tag, + dialer: dialer, + connection: service.FromContext[adapter.ConnectionManager](ctx), + tcpListener: tcpListener, + username: username, + password: password, + authenticator: auth.NewAuthenticator([]auth.User{{Username: username, Password: password}}), + } + go bridge.acceptLoop() + return bridge, nil +} + +func randomHex(size int) string { + raw := make([]byte, size) + rand.Read(raw) + return hex.EncodeToString(raw) +} + +func (b *Bridge) Port() uint16 { + return M.SocksaddrFromNet(b.tcpListener.Addr()).Port +} + +func (b *Bridge) Username() string { + return b.username +} + +func (b *Bridge) Password() string { + return b.password +} + +func (b *Bridge) Close() error { + return common.Close(b.tcpListener) +} + +func (b *Bridge) acceptLoop() { + for { + tcpConn, err := b.tcpListener.AcceptTCP() + if err != nil { + return + } + ctx := log.ContextWithNewID(b.ctx) + go func() { + hErr := socks.HandleConnectionEx(ctx, tcpConn, std_bufio.NewReader(tcpConn), b.authenticator, b, nil, 0, M.SocksaddrFromNet(tcpConn.RemoteAddr()), nil) + if hErr == nil { + return + } + if E.IsClosedOrCanceled(hErr) { + b.logger.DebugContext(ctx, E.Cause(hErr, b.tag, " connection closed")) + return + } + b.logger.ErrorContext(ctx, E.Cause(hErr, b.tag)) + }() + } +} + +func (b *Bridge) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + var metadata adapter.InboundContext + metadata.Source = source + metadata.Destination = destination + metadata.Network = N.NetworkTCP + b.logger.InfoContext(ctx, b.tag, " connection to ", metadata.Destination) + b.connection.NewConnection(ctx, b.dialer, conn, metadata, onClose) +} + +func (b *Bridge) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + var metadata adapter.InboundContext + metadata.Source = source + metadata.Destination = destination + metadata.Network = N.NetworkUDP + b.logger.InfoContext(ctx, b.tag, " packet connection to ", metadata.Destination) + b.connection.NewPacketConnection(ctx, b.dialer, conn, metadata, onClose) +} diff --git a/common/tls/apple_client.go b/common/tls/apple_client.go new file mode 100644 index 0000000000..4b84a31b24 --- /dev/null +++ b/common/tls/apple_client.go @@ -0,0 +1,218 @@ +//go:build darwin && cgo + +package tls + +import ( + "context" + "net" + "os" + "strings" + "time" + + "github.com/sagernet/sing-box/adapter" + boxConstant "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/common/ntp" + "github.com/sagernet/sing/service" +) + +type appleCertificateStore interface { + StoreKind() string + CurrentPEM() []string +} + +type appleClientConfig struct { + serverName string + nextProtos []string + handshakeTimeout time.Duration + minVersion uint16 + maxVersion uint16 + insecure bool + anchorPEM string + anchorOnly bool + certificatePublicKeySHA256 [][]byte + timeFunc func() time.Time +} + +func (c *appleClientConfig) ServerName() string { + return c.serverName +} + +func (c *appleClientConfig) SetServerName(serverName string) { + c.serverName = serverName +} + +func (c *appleClientConfig) NextProtos() []string { + return c.nextProtos +} + +func (c *appleClientConfig) SetNextProtos(nextProto []string) { + c.nextProtos = append(c.nextProtos[:0], nextProto...) +} + +func (c *appleClientConfig) HandshakeTimeout() time.Duration { + return c.handshakeTimeout +} + +func (c *appleClientConfig) SetHandshakeTimeout(timeout time.Duration) { + c.handshakeTimeout = timeout +} + +func (c *appleClientConfig) STDConfig() (*STDConfig, error) { + return nil, E.New("unsupported usage for Apple TLS engine") +} + +func (c *appleClientConfig) Client(conn net.Conn) (Conn, error) { + return nil, os.ErrInvalid +} + +func (c *appleClientConfig) Clone() Config { + return &appleClientConfig{ + serverName: c.serverName, + nextProtos: append([]string(nil), c.nextProtos...), + handshakeTimeout: c.handshakeTimeout, + minVersion: c.minVersion, + maxVersion: c.maxVersion, + insecure: c.insecure, + anchorPEM: c.anchorPEM, + anchorOnly: c.anchorOnly, + certificatePublicKeySHA256: append([][]byte(nil), c.certificatePublicKeySHA256...), + timeFunc: c.timeFunc, + } +} + +func newAppleClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) { + validated, err := ValidateAppleTLSOptions(ctx, options, "Apple TLS engine") + if err != nil { + return nil, err + } + + var serverName string + if options.ServerName != "" { + serverName = options.ServerName + } else if serverAddress != "" { + serverName = serverAddress + } + if serverName == "" && !options.Insecure && !allowEmptyServerName { + return nil, errMissingServerName + } + + var handshakeTimeout time.Duration + if options.HandshakeTimeout > 0 { + handshakeTimeout = options.HandshakeTimeout.Build() + } else { + handshakeTimeout = boxConstant.TCPTimeout + } + + return &appleClientConfig{ + serverName: serverName, + nextProtos: append([]string(nil), options.ALPN...), + handshakeTimeout: handshakeTimeout, + minVersion: validated.MinVersion, + maxVersion: validated.MaxVersion, + insecure: options.Insecure || len(options.CertificatePublicKeySHA256) > 0, + anchorPEM: validated.AnchorPEM, + anchorOnly: validated.AnchorOnly, + certificatePublicKeySHA256: append([][]byte(nil), options.CertificatePublicKeySHA256...), + timeFunc: ntp.TimeFuncFromContext(ctx), + }, nil +} + +type AppleTLSValidated struct { + MinVersion uint16 + MaxVersion uint16 + AnchorPEM string + AnchorOnly bool +} + +func ValidateAppleTLSOptions(ctx context.Context, options option.OutboundTLSOptions, engineName string) (AppleTLSValidated, error) { + if options.Reality != nil && options.Reality.Enabled { + return AppleTLSValidated{}, E.New("reality is unsupported in ", engineName) + } + if options.UTLS != nil && options.UTLS.Enabled { + return AppleTLSValidated{}, E.New("utls is unsupported in ", engineName) + } + if options.ECH != nil && options.ECH.Enabled { + return AppleTLSValidated{}, E.New("ech is unsupported in ", engineName) + } + if options.DisableSNI { + return AppleTLSValidated{}, E.New("disable_sni is unsupported in ", engineName) + } + if len(options.CipherSuites) > 0 { + return AppleTLSValidated{}, E.New("cipher_suites is unsupported in ", engineName) + } + if len(options.CurvePreferences) > 0 { + return AppleTLSValidated{}, E.New("curve_preferences is unsupported in ", engineName) + } + if len(options.ClientCertificate) > 0 || options.ClientCertificatePath != "" || len(options.ClientKey) > 0 || options.ClientKeyPath != "" { + return AppleTLSValidated{}, E.New("client certificate is unsupported in ", engineName) + } + if options.Fragment || options.RecordFragment { + return AppleTLSValidated{}, E.New("tls fragment is unsupported in ", engineName) + } + if options.KernelTx || options.KernelRx { + return AppleTLSValidated{}, E.New("ktls is unsupported in ", engineName) + } + if len(options.CertificatePublicKeySHA256) > 0 && (len(options.Certificate) > 0 || options.CertificatePath != "") { + return AppleTLSValidated{}, E.New("certificate_public_key_sha256 is conflict with certificate or certificate_path") + } + var minVersion uint16 + if options.MinVersion != "" { + var err error + minVersion, err = ParseTLSVersion(options.MinVersion) + if err != nil { + return AppleTLSValidated{}, E.Cause(err, "parse min_version") + } + } + var maxVersion uint16 + if options.MaxVersion != "" { + var err error + maxVersion, err = ParseTLSVersion(options.MaxVersion) + if err != nil { + return AppleTLSValidated{}, E.Cause(err, "parse max_version") + } + } + anchorPEM, anchorOnly, err := AppleAnchorPEM(ctx, options) + if err != nil { + return AppleTLSValidated{}, err + } + return AppleTLSValidated{ + MinVersion: minVersion, + MaxVersion: maxVersion, + AnchorPEM: anchorPEM, + AnchorOnly: anchorOnly, + }, nil +} + +func AppleAnchorPEM(ctx context.Context, options option.OutboundTLSOptions) (string, bool, error) { + if len(options.Certificate) > 0 { + return strings.Join(options.Certificate, "\n"), true, nil + } + if options.CertificatePath != "" { + content, err := os.ReadFile(options.CertificatePath) + if err != nil { + return "", false, E.Cause(err, "read certificate") + } + return string(content), true, nil + } + + certificateStore := service.FromContext[adapter.CertificateStore](ctx) + if certificateStore == nil { + return "", false, nil + } + store, ok := certificateStore.(appleCertificateStore) + if !ok { + return "", false, nil + } + + switch store.StoreKind() { + case boxConstant.CertificateStoreSystem, "": + return strings.Join(store.CurrentPEM(), "\n"), false, nil + case boxConstant.CertificateStoreMozilla, boxConstant.CertificateStoreChrome, boxConstant.CertificateStoreNone: + return strings.Join(store.CurrentPEM(), "\n"), true, nil + default: + return "", false, E.New("unsupported certificate store for Apple TLS engine: ", store.StoreKind()) + } +} diff --git a/common/tls/apple_client_platform.go b/common/tls/apple_client_platform.go new file mode 100644 index 0000000000..9e7d6e73a2 --- /dev/null +++ b/common/tls/apple_client_platform.go @@ -0,0 +1,517 @@ +//go:build darwin && cgo + +package tls + +/* +#cgo CFLAGS: -x objective-c -fobjc-arc +#cgo LDFLAGS: -framework Foundation -framework Network -framework Security + +#include +#include "apple_client_platform_darwin.h" +*/ +import "C" + +import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/binary" + "io" + "math" + "net" + "os" + "strings" + "sync" + "syscall" + "time" + "unsafe" + + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + + "golang.org/x/sys/unix" +) + +func (c *appleClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (Conn, error) { + rawSyscallConn, ok := common.Cast[syscall.Conn](conn) + if !ok { + return nil, E.New("apple TLS: requires fd-backed TCP connection") + } + syscallConn, err := rawSyscallConn.SyscallConn() + if err != nil { + return nil, E.Cause(err, "access raw connection") + } + + var dupFD int + controlErr := syscallConn.Control(func(fd uintptr) { + dupFD, err = unix.Dup(int(fd)) + }) + if controlErr != nil { + return nil, E.Cause(controlErr, "access raw connection") + } + if err != nil { + return nil, E.Cause(err, "duplicate raw connection") + } + + serverName := c.serverName + serverNamePtr := cStringOrNil(serverName) + defer cFree(serverNamePtr) + + alpn := strings.Join(c.nextProtos, "\n") + alpnPtr := cStringOrNil(alpn) + defer cFree(alpnPtr) + + anchorPEMPtr := cStringOrNil(c.anchorPEM) + defer cFree(anchorPEMPtr) + + var ( + hasVerifyTime bool + verifyTimeUnixMilli int64 + ) + if c.timeFunc != nil { + hasVerifyTime = true + verifyTimeUnixMilli = c.timeFunc().UnixMilli() + } + + var errorPtr *C.char + client := C.box_apple_tls_client_create( + C.int(dupFD), + serverNamePtr, + alpnPtr, + C.size_t(len(alpn)), + C.uint16_t(c.minVersion), + C.uint16_t(c.maxVersion), + C.bool(c.insecure), + anchorPEMPtr, + C.size_t(len(c.anchorPEM)), + C.bool(c.anchorOnly), + C.bool(hasVerifyTime), + C.int64_t(verifyTimeUnixMilli), + &errorPtr, + ) + if client == nil { + if errorPtr != nil { + defer C.free(unsafe.Pointer(errorPtr)) + return nil, E.New(C.GoString(errorPtr)) + } + return nil, E.New("apple TLS: create connection") + } + if err = waitAppleTLSClientReady(ctx, client); err != nil { + C.box_apple_tls_client_cancel(client) + C.box_apple_tls_client_free(client) + return nil, err + } + + var state C.box_apple_tls_state_t + stateOK := C.box_apple_tls_client_copy_state(client, &state, &errorPtr) + if !bool(stateOK) { + C.box_apple_tls_client_cancel(client) + C.box_apple_tls_client_free(client) + if errorPtr != nil { + defer C.free(unsafe.Pointer(errorPtr)) + return nil, E.New(C.GoString(errorPtr)) + } + return nil, E.New("apple TLS: read metadata") + } + defer C.box_apple_tls_state_free(&state) + + connectionState, rawCerts, err := parseAppleTLSState(&state) + if err != nil { + C.box_apple_tls_client_cancel(client) + C.box_apple_tls_client_free(client) + return nil, err + } + if len(c.certificatePublicKeySHA256) > 0 { + err = VerifyPublicKeySHA256(c.certificatePublicKeySHA256, rawCerts) + if err != nil { + C.box_apple_tls_client_cancel(client) + C.box_apple_tls_client_free(client) + return nil, err + } + } + + return &appleTLSConn{ + rawConn: conn, + client: client, + state: connectionState, + closed: make(chan struct{}), + }, nil +} + +const appleTLSHandshakePollInterval = 100 * time.Millisecond + +func waitAppleTLSClientReady(ctx context.Context, client *C.box_apple_tls_client_t) error { + for { + if err := ctx.Err(); err != nil { + C.box_apple_tls_client_cancel(client) + return err + } + + waitTimeout := appleTLSHandshakePollInterval + if deadline, loaded := ctx.Deadline(); loaded { + remaining := time.Until(deadline) + if remaining <= 0 { + C.box_apple_tls_client_cancel(client) + if err := ctx.Err(); err != nil { + return err + } + return context.DeadlineExceeded + } + if remaining < waitTimeout { + waitTimeout = remaining + } + } + + var errorPtr *C.char + waitResult := C.box_apple_tls_client_wait_ready(client, C.int(timeoutFromDuration(waitTimeout)), &errorPtr) + switch waitResult { + case 1: + return nil + case -2: + continue + case 0: + if errorPtr != nil { + defer C.free(unsafe.Pointer(errorPtr)) + return E.New(C.GoString(errorPtr)) + } + return E.New("apple TLS: handshake failed") + default: + return E.New("apple TLS: invalid handshake state") + } + } +} + +type appleTLSConn struct { + rawConn net.Conn + client *C.box_apple_tls_client_t + state tls.ConnectionState + + readAccess sync.Mutex + writeAccess sync.Mutex + stateAccess sync.RWMutex + closeOnce sync.Once + ioAccess sync.Mutex + ioGroup sync.WaitGroup + closed chan struct{} + readEOF bool + deadlineAccess sync.Mutex + readDeadline time.Time + writeDeadline time.Time + readTimedOut bool + writeTimedOut bool +} + +func (c *appleTLSConn) Read(p []byte) (int, error) { + c.readAccess.Lock() + defer c.readAccess.Unlock() + if c.readEOF { + return 0, io.EOF + } + if len(p) == 0 { + return 0, nil + } + + timeoutMs, err := c.prepareReadTimeout() + if err != nil { + return 0, err + } + + client, err := c.acquireClient() + if err != nil { + return 0, err + } + defer c.releaseClient() + + var eof C.bool + var errorPtr *C.char + n := C.box_apple_tls_client_read(client, unsafe.Pointer(&p[0]), C.size_t(len(p)), C.int(timeoutMs), &eof, &errorPtr) + switch { + case n == -2: + c.markReadTimedOut() + return 0, os.ErrDeadlineExceeded + case n >= 0: + if bool(eof) { + c.readEOF = true + if n == 0 { + return 0, io.EOF + } + } + return int(n), nil + default: + if errorPtr != nil { + defer C.free(unsafe.Pointer(errorPtr)) + if c.isClosed() { + return 0, net.ErrClosed + } + return 0, E.New(C.GoString(errorPtr)) + } + return 0, net.ErrClosed + } +} + +func (c *appleTLSConn) Write(p []byte) (int, error) { + c.writeAccess.Lock() + defer c.writeAccess.Unlock() + if len(p) == 0 { + return 0, nil + } + + timeoutMs, err := c.prepareWriteTimeout() + if err != nil { + return 0, err + } + + client, err := c.acquireClient() + if err != nil { + return 0, err + } + defer c.releaseClient() + + var errorPtr *C.char + n := C.box_apple_tls_client_write(client, unsafe.Pointer(&p[0]), C.size_t(len(p)), C.int(timeoutMs), &errorPtr) + switch { + case n == -2: + c.markWriteTimedOut() + return 0, os.ErrDeadlineExceeded + case n >= 0: + return int(n), nil + } + if errorPtr != nil { + defer C.free(unsafe.Pointer(errorPtr)) + if c.isClosed() { + return 0, net.ErrClosed + } + return 0, E.New(C.GoString(errorPtr)) + } + return 0, net.ErrClosed +} + +func (c *appleTLSConn) Close() error { + var closeErr error + c.closeOnce.Do(func() { + close(c.closed) + C.box_apple_tls_client_cancel(c.client) + closeErr = c.rawConn.Close() + c.ioAccess.Lock() + c.ioGroup.Wait() + C.box_apple_tls_client_free(c.client) + c.client = nil + c.ioAccess.Unlock() + }) + return closeErr +} + +func (c *appleTLSConn) LocalAddr() net.Addr { + return c.rawConn.LocalAddr() +} + +func (c *appleTLSConn) RemoteAddr() net.Addr { + return c.rawConn.RemoteAddr() +} + +// SetDeadline installs deadlines for subsequent Read and Write calls. +// +// Deadlines only apply to subsequent Read or Write calls; an in-flight call +// does not observe later updates to its deadline. Callers that need to cancel +// an in-flight I/O must Close the connection instead. +// +// Once an active Read or Write trips its deadline, the underlying +// nw_connection is cancelled and the conn is no longer usable — callers must +// Close after a deadline error. +func (c *appleTLSConn) SetDeadline(t time.Time) error { + c.deadlineAccess.Lock() + c.readDeadline = t + c.writeDeadline = t + c.readTimedOut = false + c.writeTimedOut = false + c.deadlineAccess.Unlock() + return nil +} + +func (c *appleTLSConn) SetReadDeadline(t time.Time) error { + c.deadlineAccess.Lock() + c.readDeadline = t + c.readTimedOut = false + c.deadlineAccess.Unlock() + return nil +} + +func (c *appleTLSConn) SetWriteDeadline(t time.Time) error { + c.deadlineAccess.Lock() + c.writeDeadline = t + c.writeTimedOut = false + c.deadlineAccess.Unlock() + return nil +} + +func (c *appleTLSConn) prepareReadTimeout() (int, error) { + c.deadlineAccess.Lock() + defer c.deadlineAccess.Unlock() + if c.readTimedOut { + return 0, os.ErrDeadlineExceeded + } + timeoutMs, expired := deadlineTimeoutMs(c.readDeadline) + if expired { + c.readTimedOut = true + return 0, os.ErrDeadlineExceeded + } + return timeoutMs, nil +} + +func (c *appleTLSConn) prepareWriteTimeout() (int, error) { + c.deadlineAccess.Lock() + defer c.deadlineAccess.Unlock() + if c.writeTimedOut { + return 0, os.ErrDeadlineExceeded + } + timeoutMs, expired := deadlineTimeoutMs(c.writeDeadline) + if expired { + c.writeTimedOut = true + return 0, os.ErrDeadlineExceeded + } + return timeoutMs, nil +} + +func (c *appleTLSConn) markReadTimedOut() { + c.deadlineAccess.Lock() + c.readTimedOut = true + c.deadlineAccess.Unlock() +} + +func (c *appleTLSConn) markWriteTimedOut() { + c.deadlineAccess.Lock() + c.writeTimedOut = true + c.deadlineAccess.Unlock() +} + +func deadlineTimeoutMs(deadline time.Time) (int, bool) { + if deadline.IsZero() { + return -1, false + } + remaining := time.Until(deadline) + if remaining <= 0 { + return 0, true + } + return timeoutFromDuration(remaining), false +} + +func (c *appleTLSConn) isClosed() bool { + select { + case <-c.closed: + return true + default: + return false + } +} + +func (c *appleTLSConn) acquireClient() (*C.box_apple_tls_client_t, error) { + c.ioAccess.Lock() + defer c.ioAccess.Unlock() + if c.isClosed() { + return nil, net.ErrClosed + } + client := c.client + if client == nil { + return nil, net.ErrClosed + } + c.ioGroup.Add(1) + return client, nil +} + +func (c *appleTLSConn) releaseClient() { + c.ioGroup.Done() +} + +func (c *appleTLSConn) NetConn() net.Conn { + return c.rawConn +} + +func (c *appleTLSConn) HandshakeContext(ctx context.Context) error { + return nil +} + +func (c *appleTLSConn) ConnectionState() ConnectionState { + c.stateAccess.RLock() + defer c.stateAccess.RUnlock() + return c.state +} + +func parseAppleTLSState(state *C.box_apple_tls_state_t) (tls.ConnectionState, [][]byte, error) { + rawCerts, peerCertificates, err := parseAppleCertChain(state.peer_cert_chain, state.peer_cert_chain_len) + if err != nil { + return tls.ConnectionState{}, nil, err + } + var negotiatedProtocol string + if state.alpn != nil { + negotiatedProtocol = C.GoString(state.alpn) + } + var serverName string + if state.server_name != nil { + serverName = C.GoString(state.server_name) + } + return tls.ConnectionState{ + Version: uint16(state.version), + HandshakeComplete: true, + CipherSuite: uint16(state.cipher_suite), + NegotiatedProtocol: negotiatedProtocol, + ServerName: serverName, + PeerCertificates: peerCertificates, + }, rawCerts, nil +} + +func parseAppleCertChain(chain *C.uint8_t, chainLen C.size_t) ([][]byte, []*x509.Certificate, error) { + if chain == nil || chainLen == 0 { + return nil, nil, nil + } + chainBytes := C.GoBytes(unsafe.Pointer(chain), C.int(chainLen)) + var ( + rawCerts [][]byte + peerCertificates []*x509.Certificate + ) + for len(chainBytes) >= 4 { + certificateLen := binary.BigEndian.Uint32(chainBytes[:4]) + chainBytes = chainBytes[4:] + if len(chainBytes) < int(certificateLen) { + return nil, nil, E.New("apple TLS: invalid certificate chain") + } + certificateData := append([]byte(nil), chainBytes[:certificateLen]...) + certificate, err := x509.ParseCertificate(certificateData) + if err != nil { + return nil, nil, E.Cause(err, "parse peer certificate") + } + rawCerts = append(rawCerts, certificateData) + peerCertificates = append(peerCertificates, certificate) + chainBytes = chainBytes[certificateLen:] + } + if len(chainBytes) != 0 { + return nil, nil, E.New("apple TLS: invalid certificate chain") + } + return rawCerts, peerCertificates, nil +} + +func timeoutFromDuration(timeout time.Duration) int { + if timeout <= 0 { + return 0 + } + timeoutMilliseconds := int64(timeout / time.Millisecond) + if timeout%time.Millisecond != 0 { + timeoutMilliseconds++ + } + if timeoutMilliseconds > math.MaxInt32 { + return math.MaxInt32 + } + return int(timeoutMilliseconds) +} + +func cStringOrNil(value string) *C.char { + if value == "" { + return nil + } + return C.CString(value) +} + +func cFree(pointer *C.char) { + if pointer != nil { + C.free(unsafe.Pointer(pointer)) + } +} diff --git a/common/tls/apple_client_platform_darwin.h b/common/tls/apple_client_platform_darwin.h new file mode 100644 index 0000000000..9d765835fc --- /dev/null +++ b/common/tls/apple_client_platform_darwin.h @@ -0,0 +1,39 @@ +#include +#include +#include +#include + +typedef struct box_apple_tls_client box_apple_tls_client_t; + +typedef struct box_apple_tls_state { + uint16_t version; + uint16_t cipher_suite; + char *alpn; + char *server_name; + uint8_t *peer_cert_chain; + size_t peer_cert_chain_len; +} box_apple_tls_state_t; + +box_apple_tls_client_t *box_apple_tls_client_create( + int connected_socket, + const char *server_name, + const char *alpn, + size_t alpn_len, + uint16_t min_version, + uint16_t max_version, + bool insecure, + const char *anchor_pem, + size_t anchor_pem_len, + bool anchor_only, + bool has_verify_time, + int64_t verify_time_unix_millis, + char **error_out +); + +int box_apple_tls_client_wait_ready(box_apple_tls_client_t *client, int timeout_msec, char **error_out); +void box_apple_tls_client_cancel(box_apple_tls_client_t *client); +void box_apple_tls_client_free(box_apple_tls_client_t *client); +ssize_t box_apple_tls_client_read(box_apple_tls_client_t *client, void *buffer, size_t buffer_len, int timeout_msec, bool *eof_out, char **error_out); +ssize_t box_apple_tls_client_write(box_apple_tls_client_t *client, const void *buffer, size_t buffer_len, int timeout_msec, char **error_out); +bool box_apple_tls_client_copy_state(box_apple_tls_client_t *client, box_apple_tls_state_t *state, char **error_out); +void box_apple_tls_state_free(box_apple_tls_state_t *state); diff --git a/common/tls/apple_client_platform_darwin.m b/common/tls/apple_client_platform_darwin.m new file mode 100644 index 0000000000..c4a6c19f67 --- /dev/null +++ b/common/tls/apple_client_platform_darwin.m @@ -0,0 +1,667 @@ +#import "apple_client_platform_darwin.h" + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +typedef nw_connection_t _Nullable (*box_nw_connection_create_with_connected_socket_and_parameters_f)(int connected_socket, nw_parameters_t parameters); +typedef const char * _Nullable (*box_sec_protocol_metadata_string_accessor_f)(sec_protocol_metadata_t metadata); + +typedef struct box_apple_tls_client { + void *connection; + void *queue; + void *ready_semaphore; + atomic_int ref_count; + atomic_bool ready; + atomic_bool ready_done; + char *ready_error; + box_apple_tls_state_t state; +} box_apple_tls_client_t; + +static nw_connection_t box_apple_tls_connection(box_apple_tls_client_t *client) { + if (client == NULL || client->connection == NULL) { + return nil; + } + return (__bridge nw_connection_t)client->connection; +} + +static dispatch_queue_t box_apple_tls_client_queue(box_apple_tls_client_t *client) { + if (client == NULL || client->queue == NULL) { + return nil; + } + return (__bridge dispatch_queue_t)client->queue; +} + +static dispatch_semaphore_t box_apple_tls_ready_semaphore(box_apple_tls_client_t *client) { + if (client == NULL || client->ready_semaphore == NULL) { + return nil; + } + return (__bridge dispatch_semaphore_t)client->ready_semaphore; +} + +static void box_apple_tls_state_reset(box_apple_tls_state_t *state) { + if (state == NULL) { + return; + } + free(state->alpn); + free(state->server_name); + free(state->peer_cert_chain); + memset(state, 0, sizeof(box_apple_tls_state_t)); +} + +static void box_apple_tls_client_destroy(box_apple_tls_client_t *client) { + free(client->ready_error); + box_apple_tls_state_reset(&client->state); + if (client->ready_semaphore != NULL) { + CFBridgingRelease(client->ready_semaphore); + } + if (client->connection != NULL) { + CFBridgingRelease(client->connection); + } + if (client->queue != NULL) { + CFBridgingRelease(client->queue); + } + free(client); +} + +static void box_apple_tls_client_release(box_apple_tls_client_t *client) { + if (client == NULL) { + return; + } + if (atomic_fetch_sub(&client->ref_count, 1) == 1) { + box_apple_tls_client_destroy(client); + } +} + +static void box_set_error_string(char **error_out, NSString *message) { + if (error_out == NULL || *error_out != NULL) { + return; + } + const char *utf8 = [message UTF8String]; + *error_out = strdup(utf8 != NULL ? utf8 : "unknown error"); +} + +static void box_set_error_message(char **error_out, const char *message) { + if (error_out == NULL || *error_out != NULL) { + return; + } + *error_out = strdup(message != NULL ? message : "unknown error"); +} + +static void box_set_error_from_nw_error(char **error_out, nw_error_t error) { + if (error == NULL) { + box_set_error_message(error_out, "unknown network error"); + return; + } + CFErrorRef cfError = nw_error_copy_cf_error(error); + if (cfError == NULL) { + box_set_error_message(error_out, "unknown network error"); + return; + } + NSString *description = [(__bridge NSError *)cfError description]; + box_set_error_string(error_out, description); + CFRelease(cfError); +} + +static char *box_apple_tls_metadata_copy_negotiated_protocol(sec_protocol_metadata_t metadata) { + static box_sec_protocol_metadata_string_accessor_f copy_fn; + static box_sec_protocol_metadata_string_accessor_f get_fn; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + copy_fn = (box_sec_protocol_metadata_string_accessor_f)dlsym(RTLD_DEFAULT, "sec_protocol_metadata_copy_negotiated_protocol"); + get_fn = (box_sec_protocol_metadata_string_accessor_f)dlsym(RTLD_DEFAULT, "sec_protocol_metadata_get_negotiated_protocol"); + }); + if (copy_fn != NULL) { + return (char *)copy_fn(metadata); + } + if (get_fn != NULL) { + const char *protocol = get_fn(metadata); + if (protocol != NULL) { + return strdup(protocol); + } + } + return NULL; +} + +static char *box_apple_tls_metadata_copy_server_name(sec_protocol_metadata_t metadata) { + static box_sec_protocol_metadata_string_accessor_f copy_fn; + static box_sec_protocol_metadata_string_accessor_f get_fn; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + copy_fn = (box_sec_protocol_metadata_string_accessor_f)dlsym(RTLD_DEFAULT, "sec_protocol_metadata_copy_server_name"); + get_fn = (box_sec_protocol_metadata_string_accessor_f)dlsym(RTLD_DEFAULT, "sec_protocol_metadata_get_server_name"); + }); + if (copy_fn != NULL) { + return (char *)copy_fn(metadata); + } + if (get_fn != NULL) { + const char *server_name = get_fn(metadata); + if (server_name != NULL) { + return strdup(server_name); + } + } + return NULL; +} + +static NSArray *box_split_lines(const char *content, size_t content_len) { + if (content == NULL || content_len == 0) { + return @[]; + } + NSString *string = [[NSString alloc] initWithBytes:content length:content_len encoding:NSUTF8StringEncoding]; + if (string == nil) { + return @[]; + } + NSMutableArray *lines = [NSMutableArray array]; + [string enumerateLinesUsingBlock:^(NSString *line, BOOL *stop) { + if (line.length > 0) { + [lines addObject:line]; + } + }]; + return lines; +} + +static NSArray *box_parse_certificates_from_pem(const char *pem, size_t pem_len) { + if (pem == NULL || pem_len == 0) { + return @[]; + } + NSString *content = [[NSString alloc] initWithBytes:pem length:pem_len encoding:NSUTF8StringEncoding]; + if (content == nil) { + return @[]; + } + NSString *beginMarker = @"-----BEGIN CERTIFICATE-----"; + NSString *endMarker = @"-----END CERTIFICATE-----"; + NSMutableArray *certificates = [NSMutableArray array]; + NSUInteger searchFrom = 0; + while (searchFrom < content.length) { + NSRange beginRange = [content rangeOfString:beginMarker options:0 range:NSMakeRange(searchFrom, content.length - searchFrom)]; + if (beginRange.location == NSNotFound) { + break; + } + NSUInteger bodyStart = beginRange.location + beginRange.length; + NSRange endRange = [content rangeOfString:endMarker options:0 range:NSMakeRange(bodyStart, content.length - bodyStart)]; + if (endRange.location == NSNotFound) { + break; + } + NSString *base64Section = [content substringWithRange:NSMakeRange(bodyStart, endRange.location - bodyStart)]; + NSArray *components = [base64Section componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + NSString *base64Content = [components componentsJoinedByString:@""]; + NSData *der = [[NSData alloc] initWithBase64EncodedString:base64Content options:0]; + if (der != nil) { + SecCertificateRef certificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)der); + if (certificate != NULL) { + [certificates addObject:(__bridge id)certificate]; + CFRelease(certificate); + } + } + searchFrom = endRange.location + endRange.length; + } + return certificates; +} + +static bool box_evaluate_trust(sec_trust_t trust, NSArray *anchors, bool anchor_only, NSDate *verify_date) { + bool result = false; + SecTrustRef trustRef = sec_trust_copy_ref(trust); + if (trustRef == NULL) { + return false; + } + if (verify_date != nil && SecTrustSetVerifyDate(trustRef, (__bridge CFDateRef)verify_date) != errSecSuccess) { + CFRelease(trustRef); + return false; + } + if (anchors.count > 0 || anchor_only) { + CFMutableArrayRef anchorArray = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks); + for (id certificate in anchors) { + CFArrayAppendValue(anchorArray, (__bridge const void *)certificate); + } + SecTrustSetAnchorCertificates(trustRef, anchorArray); + SecTrustSetAnchorCertificatesOnly(trustRef, anchor_only); + CFRelease(anchorArray); + } + CFErrorRef error = NULL; + result = SecTrustEvaluateWithError(trustRef, &error); + if (error != NULL) { + CFRelease(error); + } + CFRelease(trustRef); + return result; +} + +static nw_connection_t box_apple_tls_create_connection(int connected_socket, nw_parameters_t parameters) { + static box_nw_connection_create_with_connected_socket_and_parameters_f create_fn; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + char name[] = "sretemarap_dna_tekcos_detcennoc_htiw_etaerc_noitcennoc_wn"; + for (size_t i = 0, j = sizeof(name) - 2; i < j; i++, j--) { + char t = name[i]; + name[i] = name[j]; + name[j] = t; + } + create_fn = (box_nw_connection_create_with_connected_socket_and_parameters_f)dlsym(RTLD_DEFAULT, name); + }); + if (create_fn == NULL) { + return nil; + } + return create_fn(connected_socket, parameters); +} + +static bool box_apple_tls_state_copy(const box_apple_tls_state_t *source, box_apple_tls_state_t *destination) { + memset(destination, 0, sizeof(box_apple_tls_state_t)); + destination->version = source->version; + destination->cipher_suite = source->cipher_suite; + if (source->alpn != NULL) { + destination->alpn = strdup(source->alpn); + if (destination->alpn == NULL) { + goto oom; + } + } + if (source->server_name != NULL) { + destination->server_name = strdup(source->server_name); + if (destination->server_name == NULL) { + goto oom; + } + } + if (source->peer_cert_chain_len > 0) { + destination->peer_cert_chain = malloc(source->peer_cert_chain_len); + if (destination->peer_cert_chain == NULL) { + goto oom; + } + memcpy(destination->peer_cert_chain, source->peer_cert_chain, source->peer_cert_chain_len); + destination->peer_cert_chain_len = source->peer_cert_chain_len; + } + return true; + +oom: + box_apple_tls_state_reset(destination); + return false; +} + +static bool box_apple_tls_state_load(nw_connection_t connection, box_apple_tls_state_t *state, char **error_out) { + box_apple_tls_state_reset(state); + if (connection == nil) { + box_set_error_message(error_out, "apple TLS: invalid client"); + return false; + } + + nw_protocol_definition_t tls_definition = nw_protocol_copy_tls_definition(); + nw_protocol_metadata_t metadata = nw_connection_copy_protocol_metadata(connection, tls_definition); + if (metadata == NULL || !nw_protocol_metadata_is_tls(metadata)) { + box_set_error_message(error_out, "apple TLS: metadata unavailable"); + return false; + } + + sec_protocol_metadata_t sec_metadata = nw_tls_copy_sec_protocol_metadata(metadata); + if (sec_metadata == NULL) { + box_set_error_message(error_out, "apple TLS: metadata unavailable"); + return false; + } + + state->version = (uint16_t)sec_protocol_metadata_get_negotiated_tls_protocol_version(sec_metadata); + state->cipher_suite = (uint16_t)sec_protocol_metadata_get_negotiated_tls_ciphersuite(sec_metadata); + state->alpn = box_apple_tls_metadata_copy_negotiated_protocol(sec_metadata); + state->server_name = box_apple_tls_metadata_copy_server_name(sec_metadata); + + NSMutableData *chain_data = [NSMutableData data]; + sec_protocol_metadata_access_peer_certificate_chain(sec_metadata, ^(sec_certificate_t certificate) { + SecCertificateRef certificate_ref = sec_certificate_copy_ref(certificate); + if (certificate_ref == NULL) { + return; + } + CFDataRef certificate_data = SecCertificateCopyData(certificate_ref); + CFRelease(certificate_ref); + if (certificate_data == NULL) { + return; + } + uint32_t certificate_len = (uint32_t)CFDataGetLength(certificate_data); + uint32_t network_len = htonl(certificate_len); + [chain_data appendBytes:&network_len length:sizeof(network_len)]; + [chain_data appendBytes:CFDataGetBytePtr(certificate_data) length:certificate_len]; + CFRelease(certificate_data); + }); + if (chain_data.length > 0) { + state->peer_cert_chain = malloc(chain_data.length); + if (state->peer_cert_chain == NULL) { + box_set_error_message(error_out, "apple TLS: out of memory"); + box_apple_tls_state_reset(state); + return false; + } + memcpy(state->peer_cert_chain, chain_data.bytes, chain_data.length); + state->peer_cert_chain_len = chain_data.length; + } + return true; +} + +box_apple_tls_client_t *box_apple_tls_client_create( + int connected_socket, + const char *server_name, + const char *alpn, + size_t alpn_len, + uint16_t min_version, + uint16_t max_version, + bool insecure, + const char *anchor_pem, + size_t anchor_pem_len, + bool anchor_only, + bool has_verify_time, + int64_t verify_time_unix_millis, + char **error_out +) { + box_apple_tls_client_t *client = calloc(1, sizeof(box_apple_tls_client_t)); + if (client == NULL) { + close(connected_socket); + box_set_error_message(error_out, "apple TLS: out of memory"); + return NULL; + } + client->queue = (__bridge_retained void *)dispatch_queue_create("sing-box.apple-private-tls", DISPATCH_QUEUE_SERIAL); + client->ready_semaphore = (__bridge_retained void *)dispatch_semaphore_create(0); + atomic_init(&client->ref_count, 1); + atomic_init(&client->ready, false); + atomic_init(&client->ready_done, false); + + NSArray *alpnList = box_split_lines(alpn, alpn_len); + NSArray *anchors = box_parse_certificates_from_pem(anchor_pem, anchor_pem_len); + NSDate *verifyDate = nil; + if (has_verify_time) { + verifyDate = [NSDate dateWithTimeIntervalSince1970:(NSTimeInterval)verify_time_unix_millis / 1000.0]; + } + nw_parameters_t parameters = nw_parameters_create_secure_tcp(^(nw_protocol_options_t tls_options) { + sec_protocol_options_t sec_options = nw_tls_copy_sec_protocol_options(tls_options); + if (min_version != 0) { + sec_protocol_options_set_min_tls_protocol_version(sec_options, (tls_protocol_version_t)min_version); + } + if (max_version != 0) { + sec_protocol_options_set_max_tls_protocol_version(sec_options, (tls_protocol_version_t)max_version); + } + if (server_name != NULL && server_name[0] != '\0') { + sec_protocol_options_set_tls_server_name(sec_options, server_name); + } + for (NSString *protocol in alpnList) { + sec_protocol_options_add_tls_application_protocol(sec_options, protocol.UTF8String); + } + sec_protocol_options_set_peer_authentication_required(sec_options, !insecure); + if (insecure) { + sec_protocol_options_set_verify_block(sec_options, ^(sec_protocol_metadata_t metadata, sec_trust_t trust, sec_protocol_verify_complete_t complete) { + complete(true); + }, box_apple_tls_client_queue(client)); + } else if (verifyDate != nil || anchors.count > 0 || anchor_only) { + sec_protocol_options_set_verify_block(sec_options, ^(sec_protocol_metadata_t metadata, sec_trust_t trust, sec_protocol_verify_complete_t complete) { + complete(box_evaluate_trust(trust, anchors, anchor_only, verifyDate)); + }, box_apple_tls_client_queue(client)); + } + }, NW_PARAMETERS_DEFAULT_CONFIGURATION); + + nw_connection_t connection = box_apple_tls_create_connection(connected_socket, parameters); + if (connection == NULL) { + close(connected_socket); + if (client->ready_semaphore != NULL) { + CFBridgingRelease(client->ready_semaphore); + } + if (client->queue != NULL) { + CFBridgingRelease(client->queue); + } + free(client); + box_set_error_message(error_out, "apple TLS: failed to create connection"); + return NULL; + } + + client->connection = (__bridge_retained void *)connection; + atomic_fetch_add(&client->ref_count, 1); + + nw_connection_set_state_changed_handler(connection, ^(nw_connection_state_t state, nw_error_t error) { + switch (state) { + case nw_connection_state_ready: + if (!atomic_load(&client->ready_done)) { + atomic_store(&client->ready, box_apple_tls_state_load(connection, &client->state, &client->ready_error)); + atomic_store(&client->ready_done, true); + dispatch_semaphore_signal(box_apple_tls_ready_semaphore(client)); + } + break; + case nw_connection_state_failed: + if (!atomic_load(&client->ready_done)) { + box_set_error_from_nw_error(&client->ready_error, error); + atomic_store(&client->ready_done, true); + dispatch_semaphore_signal(box_apple_tls_ready_semaphore(client)); + } + break; + case nw_connection_state_cancelled: + if (!atomic_load(&client->ready_done)) { + box_set_error_from_nw_error(&client->ready_error, error); + atomic_store(&client->ready_done, true); + dispatch_semaphore_signal(box_apple_tls_ready_semaphore(client)); + } + box_apple_tls_client_release(client); + break; + default: + break; + } + }); + nw_connection_set_queue(connection, box_apple_tls_client_queue(client)); + nw_connection_start(connection); + return client; +} + +int box_apple_tls_client_wait_ready(box_apple_tls_client_t *client, int timeout_msec, char **error_out) { + dispatch_semaphore_t ready_semaphore = box_apple_tls_ready_semaphore(client); + if (ready_semaphore == nil) { + box_set_error_message(error_out, "apple TLS: invalid client"); + return 0; + } + if (!atomic_load(&client->ready_done)) { + dispatch_time_t timeout = DISPATCH_TIME_FOREVER; + if (timeout_msec >= 0) { + timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)timeout_msec * NSEC_PER_MSEC); + } + long wait_result = dispatch_semaphore_wait(ready_semaphore, timeout); + if (wait_result != 0) { + return -2; + } + } + if (atomic_load(&client->ready)) { + return 1; + } + if (client->ready_error != NULL) { + if (error_out != NULL) { + *error_out = client->ready_error; + client->ready_error = NULL; + } else { + free(client->ready_error); + client->ready_error = NULL; + } + } else { + box_set_error_message(error_out, "apple TLS: handshake failed"); + } + return 0; +} + +void box_apple_tls_client_cancel(box_apple_tls_client_t *client) { + if (client == NULL) { + return; + } + nw_connection_t connection = box_apple_tls_connection(client); + if (connection != nil) { + nw_connection_cancel(connection); + } +} + +void box_apple_tls_client_free(box_apple_tls_client_t *client) { + if (client == NULL) { + return; + } + nw_connection_t connection = box_apple_tls_connection(client); + if (connection != nil) { + nw_connection_cancel(connection); + } + box_apple_tls_client_release(client); +} + +ssize_t box_apple_tls_client_read(box_apple_tls_client_t *client, void *buffer, size_t buffer_len, int timeout_msec, bool *eof_out, char **error_out) { + nw_connection_t connection = box_apple_tls_connection(client); + if (connection == nil) { + box_set_error_message(error_out, "apple TLS: invalid client"); + return -1; + } + + dispatch_semaphore_t read_semaphore = dispatch_semaphore_create(0); + __block NSData *content_data = nil; + __block bool read_eof = false; + __block char *local_error = NULL; + + nw_connection_receive(connection, 1, (uint32_t)buffer_len, ^(dispatch_data_t content, nw_content_context_t context, bool is_complete, nw_error_t error) { + if (content != NULL) { + const void *mapped = NULL; + size_t mapped_len = 0; + dispatch_data_t mapped_data = dispatch_data_create_map(content, &mapped, &mapped_len); + if (mapped != NULL && mapped_len > 0) { + content_data = [NSData dataWithBytes:mapped length:mapped_len]; + } + (void)mapped_data; + } + if (error != NULL && content_data.length == 0) { + box_set_error_from_nw_error(&local_error, error); + } + if (is_complete && (context == NULL || nw_content_context_get_is_final(context))) { + read_eof = true; + } + dispatch_semaphore_signal(read_semaphore); + }); + + dispatch_time_t wait_deadline = DISPATCH_TIME_FOREVER; + if (timeout_msec >= 0) { + wait_deadline = dispatch_time(DISPATCH_TIME_NOW, (int64_t)timeout_msec * NSEC_PER_MSEC); + } + long wait_result = dispatch_semaphore_wait(read_semaphore, wait_deadline); + if (wait_result != 0) { + nw_connection_cancel(connection); + dispatch_semaphore_wait(read_semaphore, DISPATCH_TIME_FOREVER); + if (local_error != NULL) { + free(local_error); + local_error = NULL; + } + return -2; + } + if (local_error != NULL) { + if (error_out != NULL) { + *error_out = local_error; + } else { + free(local_error); + } + return -1; + } + if (eof_out != NULL) { + *eof_out = read_eof; + } + if (content_data == nil || content_data.length == 0) { + return 0; + } + memcpy(buffer, content_data.bytes, content_data.length); + return (ssize_t)content_data.length; +} + +ssize_t box_apple_tls_client_write(box_apple_tls_client_t *client, const void *buffer, size_t buffer_len, int timeout_msec, char **error_out) { + nw_connection_t connection = box_apple_tls_connection(client); + if (connection == nil) { + box_set_error_message(error_out, "apple TLS: invalid client"); + return -1; + } + if (buffer_len == 0) { + return 0; + } + + void *content_copy = malloc(buffer_len); + dispatch_queue_t queue = box_apple_tls_client_queue(client); + if (content_copy == NULL) { + free(content_copy); + box_set_error_message(error_out, "apple TLS: out of memory"); + return -1; + } + if (queue == nil) { + free(content_copy); + box_set_error_message(error_out, "apple TLS: invalid client"); + return -1; + } + memcpy(content_copy, buffer, buffer_len); + dispatch_data_t content = dispatch_data_create(content_copy, buffer_len, queue, ^{ + free(content_copy); + }); + + dispatch_semaphore_t write_semaphore = dispatch_semaphore_create(0); + __block char *local_error = NULL; + + nw_connection_send(connection, content, NW_CONNECTION_DEFAULT_STREAM_CONTEXT, false, ^(nw_error_t error) { + if (error != NULL) { + box_set_error_from_nw_error(&local_error, error); + } + dispatch_semaphore_signal(write_semaphore); + }); + + dispatch_time_t wait_deadline = DISPATCH_TIME_FOREVER; + if (timeout_msec >= 0) { + wait_deadline = dispatch_time(DISPATCH_TIME_NOW, (int64_t)timeout_msec * NSEC_PER_MSEC); + } + long wait_result = dispatch_semaphore_wait(write_semaphore, wait_deadline); + if (wait_result != 0) { + nw_connection_cancel(connection); + dispatch_semaphore_wait(write_semaphore, DISPATCH_TIME_FOREVER); + if (local_error != NULL) { + free(local_error); + local_error = NULL; + } + return -2; + } + if (local_error != NULL) { + if (error_out != NULL) { + *error_out = local_error; + } else { + free(local_error); + } + return -1; + } + return (ssize_t)buffer_len; +} + +bool box_apple_tls_client_copy_state(box_apple_tls_client_t *client, box_apple_tls_state_t *state, char **error_out) { + dispatch_queue_t queue = box_apple_tls_client_queue(client); + if (queue == nil || state == NULL) { + box_set_error_message(error_out, "apple TLS: invalid client"); + return false; + } + memset(state, 0, sizeof(box_apple_tls_state_t)); + __block bool copied = false; + __block char *local_error = NULL; + dispatch_sync(queue, ^{ + if (!atomic_load(&client->ready)) { + box_set_error_message(&local_error, "apple TLS: metadata unavailable"); + return; + } + if (!box_apple_tls_state_copy(&client->state, state)) { + box_set_error_message(&local_error, "apple TLS: out of memory"); + return; + } + copied = true; + }); + if (copied) { + return true; + } + if (local_error != NULL) { + if (error_out != NULL) { + *error_out = local_error; + } else { + free(local_error); + } + } + box_apple_tls_state_reset(state); + return false; +} + +void box_apple_tls_state_free(box_apple_tls_state_t *state) { + box_apple_tls_state_reset(state); +} diff --git a/common/tls/apple_client_platform_test.go b/common/tls/apple_client_platform_test.go new file mode 100644 index 0000000000..6c915f68ca --- /dev/null +++ b/common/tls/apple_client_platform_test.go @@ -0,0 +1,453 @@ +//go:build darwin && cgo + +package tls + +import ( + "context" + stdtls "crypto/tls" + "errors" + "net" + "os" + "testing" + "time" + + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/json/badoption" + "github.com/sagernet/sing/common/logger" +) + +const appleTLSTestTimeout = 5 * time.Second + +const ( + appleTLSSuccessHandshakeLoops = 20 + appleTLSFailureRecoveryLoops = 10 +) + +type appleTLSServerResult struct { + state stdtls.ConnectionState + err error +} + +func TestAppleClientHandshakeAppliesALPNAndVersion(t *testing.T) { + serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost") + for index := 0; index < appleTLSSuccessHandshakeLoops; index++ { + serverResult, serverAddress := startAppleTLSTestServer(t, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: stdtls.VersionTLS12, + MaxVersion: stdtls.VersionTLS12, + NextProtos: []string{"h2"}, + }) + + clientConn, err := newAppleTestClientConn(t, serverAddress, option.OutboundTLSOptions{ + Enabled: true, + Engine: "apple", + ServerName: "localhost", + MinVersion: "1.2", + MaxVersion: "1.2", + ALPN: badoption.Listable[string]{"h2"}, + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }) + if err != nil { + t.Fatalf("iteration %d: %v", index, err) + } + + clientState := clientConn.ConnectionState() + if clientState.Version != stdtls.VersionTLS12 { + _ = clientConn.Close() + t.Fatalf("iteration %d: unexpected negotiated version: %x", index, clientState.Version) + } + if clientState.NegotiatedProtocol != "h2" { + _ = clientConn.Close() + t.Fatalf("iteration %d: unexpected negotiated protocol: %q", index, clientState.NegotiatedProtocol) + } + _ = clientConn.Close() + + result := <-serverResult + if result.err != nil { + t.Fatalf("iteration %d: %v", index, result.err) + } + if result.state.Version != stdtls.VersionTLS12 { + t.Fatalf("iteration %d: server negotiated unexpected version: %x", index, result.state.Version) + } + if result.state.NegotiatedProtocol != "h2" { + t.Fatalf("iteration %d: server negotiated unexpected protocol: %q", index, result.state.NegotiatedProtocol) + } + } +} + +func TestAppleClientHandshakeRejectsVersionMismatch(t *testing.T) { + serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost") + serverResult, serverAddress := startAppleTLSTestServer(t, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: stdtls.VersionTLS13, + MaxVersion: stdtls.VersionTLS13, + }) + + clientConn, err := newAppleTestClientConn(t, serverAddress, option.OutboundTLSOptions{ + Enabled: true, + Engine: "apple", + ServerName: "localhost", + MaxVersion: "1.2", + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }) + if err == nil { + clientConn.Close() + t.Fatal("expected version mismatch handshake to fail") + } + + if result := <-serverResult; result.err == nil { + t.Fatal("expected server handshake to fail on version mismatch") + } +} + +func TestAppleClientHandshakeRejectsServerNameMismatch(t *testing.T) { + serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost") + serverResult, serverAddress := startAppleTLSTestServer(t, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + }) + + clientConn, err := newAppleTestClientConn(t, serverAddress, option.OutboundTLSOptions{ + Enabled: true, + Engine: "apple", + ServerName: "example.com", + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }) + if err == nil { + clientConn.Close() + t.Fatal("expected server name mismatch handshake to fail") + } + + if result := <-serverResult; result.err == nil { + t.Fatal("expected server handshake to fail on server name mismatch") + } +} + +func TestAppleClientHandshakeRecoversAfterFailure(t *testing.T) { + serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost") + testCases := []struct { + name string + serverConfig *stdtls.Config + clientOptions option.OutboundTLSOptions + }{ + { + name: "version mismatch", + serverConfig: &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: stdtls.VersionTLS13, + MaxVersion: stdtls.VersionTLS13, + }, + clientOptions: option.OutboundTLSOptions{ + Enabled: true, + Engine: "apple", + ServerName: "localhost", + MaxVersion: "1.2", + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }, + }, + { + name: "server name mismatch", + serverConfig: &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + }, + clientOptions: option.OutboundTLSOptions{ + Enabled: true, + Engine: "apple", + ServerName: "example.com", + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }, + }, + } + successClientOptions := option.OutboundTLSOptions{ + Enabled: true, + Engine: "apple", + ServerName: "localhost", + MinVersion: "1.2", + MaxVersion: "1.2", + ALPN: badoption.Listable[string]{"h2"}, + Certificate: badoption.Listable[string]{serverCertificatePEM}, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + for index := 0; index < appleTLSFailureRecoveryLoops; index++ { + failedResult, failedAddress := startAppleTLSTestServer(t, testCase.serverConfig) + failedConn, err := newAppleTestClientConn(t, failedAddress, testCase.clientOptions) + if err == nil { + _ = failedConn.Close() + t.Fatalf("iteration %d: expected handshake failure", index) + } + if result := <-failedResult; result.err == nil { + t.Fatalf("iteration %d: expected server handshake failure", index) + } + + successResult, successAddress := startAppleTLSTestServer(t, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: stdtls.VersionTLS12, + MaxVersion: stdtls.VersionTLS12, + NextProtos: []string{"h2"}, + }) + successConn, err := newAppleTestClientConn(t, successAddress, successClientOptions) + if err != nil { + t.Fatalf("iteration %d: follow-up handshake failed: %v", index, err) + } + clientState := successConn.ConnectionState() + if clientState.NegotiatedProtocol != "h2" { + _ = successConn.Close() + t.Fatalf("iteration %d: unexpected negotiated protocol after failure: %q", index, clientState.NegotiatedProtocol) + } + _ = successConn.Close() + + result := <-successResult + if result.err != nil { + t.Fatalf("iteration %d: follow-up server handshake failed: %v", index, result.err) + } + if result.state.NegotiatedProtocol != "h2" { + t.Fatalf("iteration %d: follow-up server negotiated unexpected protocol: %q", index, result.state.NegotiatedProtocol) + } + } + }) + } +} + +func TestAppleClientReadDeadline(t *testing.T) { + serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost") + serverDone, serverAddress := startAppleTLSSilentServer(t, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: stdtls.VersionTLS12, + MaxVersion: stdtls.VersionTLS12, + }) + + clientConn, err := newAppleTestClientConn(t, serverAddress, option.OutboundTLSOptions{ + Enabled: true, + Engine: "apple", + ServerName: "localhost", + MinVersion: "1.2", + MaxVersion: "1.2", + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }) + if err != nil { + t.Fatal(err) + } + defer clientConn.Close() + defer close(serverDone) + + err = clientConn.SetReadDeadline(time.Now().Add(200 * time.Millisecond)) + if err != nil { + t.Fatalf("SetReadDeadline: %v", err) + } + + readDone := make(chan error, 1) + buffer := make([]byte, 64) + go func() { + _, readErr := clientConn.Read(buffer) + readDone <- readErr + }() + + select { + case readErr := <-readDone: + if !errors.Is(readErr, os.ErrDeadlineExceeded) { + t.Fatalf("expected os.ErrDeadlineExceeded, got %v", readErr) + } + case <-time.After(2 * time.Second): + t.Fatal("Read did not return within 2s after deadline") + } + + _, err = clientConn.Read(buffer) + if !errors.Is(err, os.ErrDeadlineExceeded) { + t.Fatalf("sticky deadline: expected os.ErrDeadlineExceeded, got %v", err) + } +} + +func TestAppleClientSetDeadlineClearsPreExpiredSticky(t *testing.T) { + serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost") + serverDone, serverAddress := startAppleTLSSilentServer(t, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: stdtls.VersionTLS12, + MaxVersion: stdtls.VersionTLS12, + }) + + clientConn, err := newAppleTestClientConn(t, serverAddress, option.OutboundTLSOptions{ + Enabled: true, + Engine: "apple", + ServerName: "localhost", + MinVersion: "1.2", + MaxVersion: "1.2", + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }) + if err != nil { + t.Fatal(err) + } + defer clientConn.Close() + defer close(serverDone) + + err = clientConn.SetReadDeadline(time.Now().Add(-time.Second)) + if err != nil { + t.Fatalf("SetReadDeadline past: %v", err) + } + + // Pre-expired deadline trips sticky flag without cancelling nw_connection + // (prepareReadTimeout short-circuits before the C read is issued). + buffer := make([]byte, 64) + _, err = clientConn.Read(buffer) + if !errors.Is(err, os.ErrDeadlineExceeded) { + t.Fatalf("pre-expired: expected os.ErrDeadlineExceeded, got %v", err) + } + + err = clientConn.SetReadDeadline(time.Time{}) + if err != nil { + t.Fatalf("SetReadDeadline zero: %v", err) + } + + newDeadline := 300 * time.Millisecond + err = clientConn.SetReadDeadline(time.Now().Add(newDeadline)) + if err != nil { + t.Fatalf("SetReadDeadline future: %v", err) + } + + readStart := time.Now() + _, err = clientConn.Read(buffer) + readElapsed := time.Since(readStart) + if !errors.Is(err, os.ErrDeadlineExceeded) { + t.Fatalf("after clear: expected os.ErrDeadlineExceeded, got %v", err) + } + if readElapsed < newDeadline-50*time.Millisecond { + t.Fatalf("sticky flag was not cleared: Read returned after %v, expected ~%v", readElapsed, newDeadline) + } +} + +func startAppleTLSSilentServer(t *testing.T, tlsConfig *stdtls.Config) (chan<- struct{}, string) { + t.Helper() + + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + listener.Close() + }) + + if tcpListener, isTCP := listener.(*net.TCPListener); isTCP { + err = tcpListener.SetDeadline(time.Now().Add(appleTLSTestTimeout)) + if err != nil { + t.Fatal(err) + } + } + + done := make(chan struct{}) + go func() { + conn, acceptErr := listener.Accept() + if acceptErr != nil { + return + } + defer conn.Close() + handshakeErr := conn.SetDeadline(time.Now().Add(appleTLSTestTimeout)) + if handshakeErr != nil { + return + } + tlsConn := stdtls.Server(conn, tlsConfig) + defer tlsConn.Close() + handshakeErr = tlsConn.Handshake() + if handshakeErr != nil { + return + } + handshakeErr = conn.SetDeadline(time.Time{}) + if handshakeErr != nil { + return + } + <-done + }() + return done, listener.Addr().String() +} + +func newAppleTestCertificate(t *testing.T, serverName string) (stdtls.Certificate, string) { + t.Helper() + + privateKeyPEM, certificatePEM, err := GenerateCertificate(nil, nil, time.Now, serverName, time.Now().Add(time.Hour)) + if err != nil { + t.Fatal(err) + } + certificate, err := stdtls.X509KeyPair(certificatePEM, privateKeyPEM) + if err != nil { + t.Fatal(err) + } + return certificate, string(certificatePEM) +} + +func startAppleTLSTestServer(t *testing.T, tlsConfig *stdtls.Config) (<-chan appleTLSServerResult, string) { + t.Helper() + + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + listener.Close() + }) + + if tcpListener, isTCP := listener.(*net.TCPListener); isTCP { + err = tcpListener.SetDeadline(time.Now().Add(appleTLSTestTimeout)) + if err != nil { + t.Fatal(err) + } + } + + result := make(chan appleTLSServerResult, 1) + go func() { + defer close(result) + + conn, err := listener.Accept() + if err != nil { + result <- appleTLSServerResult{err: err} + return + } + defer conn.Close() + + err = conn.SetDeadline(time.Now().Add(appleTLSTestTimeout)) + if err != nil { + result <- appleTLSServerResult{err: err} + return + } + + tlsConn := stdtls.Server(conn, tlsConfig) + defer tlsConn.Close() + + err = tlsConn.Handshake() + if err != nil { + result <- appleTLSServerResult{err: err} + return + } + + result <- appleTLSServerResult{state: tlsConn.ConnectionState()} + }() + + return result, listener.Addr().String() +} + +func newAppleTestClientConn(t *testing.T, serverAddress string, options option.OutboundTLSOptions) (Conn, error) { + t.Helper() + + ctx, cancel := context.WithTimeout(context.Background(), appleTLSTestTimeout) + t.Cleanup(cancel) + + clientConfig, err := NewClientWithOptions(ClientOptions{ + Context: ctx, + Logger: logger.NOP(), + ServerAddress: "", + Options: options, + }) + if err != nil { + return nil, err + } + + conn, err := net.DialTimeout("tcp", serverAddress, appleTLSTestTimeout) + if err != nil { + return nil, err + } + + tlsConn, err := ClientHandshake(ctx, conn, clientConfig) + if err != nil { + conn.Close() + return nil, err + } + return tlsConn, nil +} diff --git a/common/tls/apple_client_stub.go b/common/tls/apple_client_stub.go new file mode 100644 index 0000000000..33b7df47c5 --- /dev/null +++ b/common/tls/apple_client_stub.go @@ -0,0 +1,15 @@ +//go:build !darwin || !cgo + +package tls + +import ( + "context" + + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" +) + +func newAppleClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) { + return nil, E.New("Apple TLS engine is not available on non-Apple platforms") +} diff --git a/common/tls/client.go b/common/tls/client.go index 839699547c..40560b9a59 100644 --- a/common/tls/client.go +++ b/common/tls/client.go @@ -10,12 +10,15 @@ import ( "github.com/sagernet/sing-box/common/badtls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" aTLS "github.com/sagernet/sing/common/tls" ) +var errMissingServerName = E.New("missing server_name or insecure=true") + func NewDialerFromOptions(ctx context.Context, logger logger.ContextLogger, dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) { if !options.Enabled { return dialer, nil @@ -42,11 +45,12 @@ func NewClient(ctx context.Context, logger logger.ContextLogger, serverAddress s } type ClientOptions struct { - Context context.Context - Logger logger.ContextLogger - ServerAddress string - Options option.OutboundTLSOptions - KTLSCompatible bool + Context context.Context + Logger logger.ContextLogger + ServerAddress string + Options option.OutboundTLSOptions + AllowEmptyServerName bool + KTLSCompatible bool } func NewClientWithOptions(options ClientOptions) (Config, error) { @@ -61,17 +65,22 @@ func NewClientWithOptions(options ClientOptions) (Config, error) { if options.Options.KernelRx { options.Logger.Warn("enabling kTLS RX will definitely reduce performance, please checkout https://sing-box.sagernet.org/configuration/shared/tls/#kernel_rx") } + switch options.Options.Engine { + case C.TLSEngineDefault, "go": + case C.TLSEngineApple: + return newAppleClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName) + default: + return nil, E.New("unknown tls engine: ", options.Options.Engine) + } if options.Options.Reality != nil && options.Options.Reality.Enabled { - return NewRealityClient(options.Context, options.Logger, options.ServerAddress, options.Options) + return newRealityClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName) } else if options.Options.UTLS != nil && options.Options.UTLS.Enabled { - return NewUTLSClient(options.Context, options.Logger, options.ServerAddress, options.Options) + return newUTLSClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName) } - return NewSTDClient(options.Context, options.Logger, options.ServerAddress, options.Options) + return newSTDClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName) } func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) { - ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout) - defer cancel() tlsConn, err := aTLS.ClientHandshake(ctx, conn, config) if err != nil { return nil, err diff --git a/common/tls/reality_client.go b/common/tls/reality_client.go index 9362d2f848..38f0965e24 100644 --- a/common/tls/reality_client.go +++ b/common/tls/reality_client.go @@ -52,11 +52,15 @@ type RealityClientConfig struct { } func NewRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) { + return newRealityClient(ctx, logger, serverAddress, options, false) +} + +func newRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) { if options.UTLS == nil || !options.UTLS.Enabled { return nil, E.New("uTLS is required by reality client") } - uClient, err := NewUTLSClient(ctx, logger, serverAddress, options) + uClient, err := newUTLSClient(ctx, logger, serverAddress, options, allowEmptyServerName) if err != nil { return nil, err } @@ -108,6 +112,14 @@ func (e *RealityClientConfig) SetNextProtos(nextProto []string) { e.uClient.SetNextProtos(nextProto) } +func (e *RealityClientConfig) HandshakeTimeout() time.Duration { + return e.uClient.HandshakeTimeout() +} + +func (e *RealityClientConfig) SetHandshakeTimeout(timeout time.Duration) { + e.uClient.SetHandshakeTimeout(timeout) +} + func (e *RealityClientConfig) STDConfig() (*STDConfig, error) { return nil, E.New("unsupported usage for reality") } diff --git a/common/tls/reality_server.go b/common/tls/reality_server.go index c2e70733a3..10d6061870 100644 --- a/common/tls/reality_server.go +++ b/common/tls/reality_server.go @@ -26,7 +26,8 @@ import ( var _ ServerConfigCompat = (*RealityServerConfig)(nil) type RealityServerConfig struct { - config *utls.RealityConfig + config *utls.RealityConfig + handshakeTimeout time.Duration } func NewRealityServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) { @@ -130,7 +131,16 @@ func NewRealityServer(ctx context.Context, logger log.ContextLogger, options opt if options.ECH != nil && options.ECH.Enabled { return nil, E.New("Reality is conflict with ECH") } - var config ServerConfig = &RealityServerConfig{&tlsConfig} + var handshakeTimeout time.Duration + if options.HandshakeTimeout > 0 { + handshakeTimeout = options.HandshakeTimeout.Build() + } else { + handshakeTimeout = C.TCPTimeout + } + var config ServerConfig = &RealityServerConfig{ + config: &tlsConfig, + handshakeTimeout: handshakeTimeout, + } if options.KernelTx || options.KernelRx { if !C.IsLinux { return nil, E.New("kTLS is only supported on Linux") @@ -161,6 +171,14 @@ func (c *RealityServerConfig) SetNextProtos(nextProto []string) { c.config.NextProtos = nextProto } +func (c *RealityServerConfig) HandshakeTimeout() time.Duration { + return c.handshakeTimeout +} + +func (c *RealityServerConfig) SetHandshakeTimeout(timeout time.Duration) { + c.handshakeTimeout = timeout +} + func (c *RealityServerConfig) STDConfig() (*tls.Config, error) { return nil, E.New("unsupported usage for reality") } @@ -191,7 +209,8 @@ func (c *RealityServerConfig) ServerHandshake(ctx context.Context, conn net.Conn func (c *RealityServerConfig) Clone() Config { return &RealityServerConfig{ - config: c.config.Clone(), + config: c.config.Clone(), + handshakeTimeout: c.handshakeTimeout, } } diff --git a/common/tls/server.go b/common/tls/server.go index 74b240fc75..8f4b3c38d0 100644 --- a/common/tls/server.go +++ b/common/tls/server.go @@ -46,8 +46,11 @@ func NewServerWithOptions(options ServerOptions) (ServerConfig, error) { } func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) { - ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout) - defer cancel() + if config.HandshakeTimeout() == 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, C.TCPTimeout) + defer cancel() + } tlsConn, err := aTLS.ServerHandshake(ctx, conn, config) if err != nil { return nil, err diff --git a/common/tls/std_client.go b/common/tls/std_client.go index 1611c83e7c..7da36defe5 100644 --- a/common/tls/std_client.go +++ b/common/tls/std_client.go @@ -24,16 +24,30 @@ import ( type STDClientConfig struct { ctx context.Context config *tls.Config + serverName string + disableSNI bool + verifyServerName bool + handshakeTimeout time.Duration fragment bool fragmentFallbackDelay time.Duration recordFragment bool } func (c *STDClientConfig) ServerName() string { - return c.config.ServerName + return c.serverName } func (c *STDClientConfig) SetServerName(serverName string) { + c.serverName = serverName + if c.disableSNI { + c.config.ServerName = "" + if c.verifyServerName { + c.config.VerifyConnection = verifyConnection(c.config.RootCAs, c.config.Time, serverName) + } else { + c.config.VerifyConnection = nil + } + return + } c.config.ServerName = serverName } @@ -45,6 +59,14 @@ func (c *STDClientConfig) SetNextProtos(nextProto []string) { c.config.NextProtos = nextProto } +func (c *STDClientConfig) HandshakeTimeout() time.Duration { + return c.handshakeTimeout +} + +func (c *STDClientConfig) SetHandshakeTimeout(timeout time.Duration) { + c.handshakeTimeout = timeout +} + func (c *STDClientConfig) STDConfig() (*STDConfig, error) { return c.config, nil } @@ -57,13 +79,19 @@ func (c *STDClientConfig) Client(conn net.Conn) (Conn, error) { } func (c *STDClientConfig) Clone() Config { - return &STDClientConfig{ + cloned := &STDClientConfig{ ctx: c.ctx, config: c.config.Clone(), + serverName: c.serverName, + disableSNI: c.disableSNI, + verifyServerName: c.verifyServerName, + handshakeTimeout: c.handshakeTimeout, fragment: c.fragment, fragmentFallbackDelay: c.fragmentFallbackDelay, recordFragment: c.recordFragment, } + cloned.SetServerName(cloned.serverName) + return cloned } func (c *STDClientConfig) ECHConfigList() []byte { @@ -75,41 +103,27 @@ func (c *STDClientConfig) SetECHConfigList(EncryptedClientHelloConfigList []byte } func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) { + return newSTDClient(ctx, logger, serverAddress, options, false) +} + +func newSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) { var serverName string if options.ServerName != "" { serverName = options.ServerName } else if serverAddress != "" { serverName = serverAddress } - if serverName == "" && !options.Insecure { - return nil, E.New("missing server_name or insecure=true") + if serverName == "" && !options.Insecure && !allowEmptyServerName { + return nil, errMissingServerName } var tlsConfig tls.Config tlsConfig.Time = ntp.TimeFuncFromContext(ctx) tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx) - if !options.DisableSNI { - tlsConfig.ServerName = serverName - } if options.Insecure { tlsConfig.InsecureSkipVerify = options.Insecure } else if options.DisableSNI { tlsConfig.InsecureSkipVerify = true - tlsConfig.VerifyConnection = func(state tls.ConnectionState) error { - verifyOptions := x509.VerifyOptions{ - Roots: tlsConfig.RootCAs, - DNSName: serverName, - Intermediates: x509.NewCertPool(), - } - for _, cert := range state.PeerCertificates[1:] { - verifyOptions.Intermediates.AddCert(cert) - } - if tlsConfig.Time != nil { - verifyOptions.CurrentTime = tlsConfig.Time() - } - _, err := state.PeerCertificates[0].Verify(verifyOptions) - return err - } } if len(options.CertificatePublicKeySHA256) > 0 { if len(options.Certificate) > 0 || options.CertificatePath != "" { @@ -117,7 +131,7 @@ func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres } tlsConfig.InsecureSkipVerify = true tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { - return verifyPublicKeySHA256(options.CertificatePublicKeySHA256, rawCerts, tlsConfig.Time) + return VerifyPublicKeySHA256(options.CertificatePublicKeySHA256, rawCerts) } } if len(options.ALPN) > 0 { @@ -198,7 +212,24 @@ func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres } else if len(clientCertificate) > 0 || len(clientKey) > 0 { return nil, E.New("client certificate and client key must be provided together") } - var config Config = &STDClientConfig{ctx, &tlsConfig, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment} + var handshakeTimeout time.Duration + if options.HandshakeTimeout > 0 { + handshakeTimeout = options.HandshakeTimeout.Build() + } else { + handshakeTimeout = C.TCPTimeout + } + var config Config = &STDClientConfig{ + ctx: ctx, + config: &tlsConfig, + serverName: serverName, + disableSNI: options.DisableSNI, + verifyServerName: options.DisableSNI && !options.Insecure, + handshakeTimeout: handshakeTimeout, + fragment: options.Fragment, + fragmentFallbackDelay: time.Duration(options.FragmentFallbackDelay), + recordFragment: options.RecordFragment, + } + config.SetServerName(serverName) if options.ECH != nil && options.ECH.Enabled { var err error config, err = parseECHClientConfig(ctx, config.(ECHCapableConfig), options) @@ -220,7 +251,28 @@ func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres return config, nil } -func verifyPublicKeySHA256(knownHashValues [][]byte, rawCerts [][]byte, timeFunc func() time.Time) error { +func verifyConnection(rootCAs *x509.CertPool, timeFunc func() time.Time, serverName string) func(state tls.ConnectionState) error { + return func(state tls.ConnectionState) error { + if serverName == "" { + return errMissingServerName + } + verifyOptions := x509.VerifyOptions{ + Roots: rootCAs, + DNSName: serverName, + Intermediates: x509.NewCertPool(), + } + for _, cert := range state.PeerCertificates[1:] { + verifyOptions.Intermediates.AddCert(cert) + } + if timeFunc != nil { + verifyOptions.CurrentTime = timeFunc() + } + _, err := state.PeerCertificates[0].Verify(verifyOptions) + return err + } +} + +func VerifyPublicKeySHA256(knownHashValues [][]byte, rawCerts [][]byte) error { leafCertificate, err := x509.ParseCertificate(rawCerts[0]) if err != nil { return E.Cause(err, "failed to parse leaf certificate") diff --git a/common/tls/std_server.go b/common/tls/std_server.go index 86584cd482..b673c367c7 100644 --- a/common/tls/std_server.go +++ b/common/tls/std_server.go @@ -92,6 +92,7 @@ func getACMENextProtos(provider adapter.CertificateProvider) []string { type STDServerConfig struct { access sync.RWMutex config *tls.Config + handshakeTimeout time.Duration logger log.Logger certificateProvider managedCertificateProvider acmeService adapter.SimpleLifecycle @@ -139,6 +140,18 @@ func (c *STDServerConfig) SetNextProtos(nextProto []string) { c.config = config } +func (c *STDServerConfig) HandshakeTimeout() time.Duration { + c.access.RLock() + defer c.access.RUnlock() + return c.handshakeTimeout +} + +func (c *STDServerConfig) SetHandshakeTimeout(timeout time.Duration) { + c.access.Lock() + defer c.access.Unlock() + c.handshakeTimeout = timeout +} + func (c *STDServerConfig) hasACMEALPN() bool { if c.acmeService != nil { return true @@ -165,7 +178,8 @@ func (c *STDServerConfig) Server(conn net.Conn) (Conn, error) { func (c *STDServerConfig) Clone() Config { return &STDServerConfig{ - config: c.config.Clone(), + config: c.config.Clone(), + handshakeTimeout: c.handshakeTimeout, } } @@ -458,7 +472,7 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option. tlsConfig.ClientAuth = tls.RequestClientCert } tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { - return verifyPublicKeySHA256(options.ClientCertificatePublicKeySHA256, rawCerts, tlsConfig.Time) + return VerifyPublicKeySHA256(options.ClientCertificatePublicKeySHA256, rawCerts) } } else { return nil, E.New("missing client_certificate, client_certificate_path or client_certificate_public_key_sha256 for client authentication") @@ -471,8 +485,15 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option. return nil, err } } + var handshakeTimeout time.Duration + if options.HandshakeTimeout > 0 { + handshakeTimeout = options.HandshakeTimeout.Build() + } else { + handshakeTimeout = C.TCPTimeout + } serverConfig := &STDServerConfig{ config: tlsConfig, + handshakeTimeout: handshakeTimeout, logger: logger, certificateProvider: certificateProvider, acmeService: acmeService, diff --git a/common/tls/utls_client.go b/common/tls/utls_client.go index 941192ba16..20261bfd4a 100644 --- a/common/tls/utls_client.go +++ b/common/tls/utls_client.go @@ -28,6 +28,10 @@ import ( type UTLSClientConfig struct { ctx context.Context config *utls.Config + serverName string + disableSNI bool + verifyServerName bool + handshakeTimeout time.Duration id utls.ClientHelloID fragment bool fragmentFallbackDelay time.Duration @@ -35,10 +39,20 @@ type UTLSClientConfig struct { } func (c *UTLSClientConfig) ServerName() string { - return c.config.ServerName + return c.serverName } func (c *UTLSClientConfig) SetServerName(serverName string) { + c.serverName = serverName + if c.disableSNI { + c.config.ServerName = "" + if c.verifyServerName { + c.config.InsecureServerNameToVerify = serverName + } else { + c.config.InsecureServerNameToVerify = "" + } + return + } c.config.ServerName = serverName } @@ -53,6 +67,14 @@ func (c *UTLSClientConfig) SetNextProtos(nextProto []string) { c.config.NextProtos = nextProto } +func (c *UTLSClientConfig) HandshakeTimeout() time.Duration { + return c.handshakeTimeout +} + +func (c *UTLSClientConfig) SetHandshakeTimeout(timeout time.Duration) { + c.handshakeTimeout = timeout +} + func (c *UTLSClientConfig) STDConfig() (*STDConfig, error) { return nil, E.New("unsupported usage for uTLS") } @@ -69,9 +91,20 @@ func (c *UTLSClientConfig) SetSessionIDGenerator(generator func(clientHello []by } func (c *UTLSClientConfig) Clone() Config { - return &UTLSClientConfig{ - c.ctx, c.config.Clone(), c.id, c.fragment, c.fragmentFallbackDelay, c.recordFragment, + cloned := &UTLSClientConfig{ + ctx: c.ctx, + config: c.config.Clone(), + serverName: c.serverName, + disableSNI: c.disableSNI, + verifyServerName: c.verifyServerName, + handshakeTimeout: c.handshakeTimeout, + id: c.id, + fragment: c.fragment, + fragmentFallbackDelay: c.fragmentFallbackDelay, + recordFragment: c.recordFragment, } + cloned.SetServerName(cloned.serverName) + return cloned } func (c *UTLSClientConfig) ECHConfigList() []byte { @@ -143,29 +176,29 @@ func (c *utlsALPNWrapper) HandshakeContext(ctx context.Context) error { } func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) { + return newUTLSClient(ctx, logger, serverAddress, options, false) +} + +func newUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) { var serverName string if options.ServerName != "" { serverName = options.ServerName } else if serverAddress != "" { serverName = serverAddress } - if serverName == "" && !options.Insecure { - return nil, E.New("missing server_name or insecure=true") + if serverName == "" && !options.Insecure && !allowEmptyServerName { + return nil, errMissingServerName } var tlsConfig utls.Config tlsConfig.Time = ntp.TimeFuncFromContext(ctx) tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx) - if !options.DisableSNI { - tlsConfig.ServerName = serverName - } if options.Insecure { tlsConfig.InsecureSkipVerify = options.Insecure } else if options.DisableSNI { if options.Reality != nil && options.Reality.Enabled { return nil, E.New("disable_sni is unsupported in reality") } - tlsConfig.InsecureServerNameToVerify = serverName } if len(options.CertificatePublicKeySHA256) > 0 { if len(options.Certificate) > 0 || options.CertificatePath != "" { @@ -173,7 +206,7 @@ func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddre } tlsConfig.InsecureSkipVerify = true tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { - return verifyPublicKeySHA256(options.CertificatePublicKeySHA256, rawCerts, tlsConfig.Time) + return VerifyPublicKeySHA256(options.CertificatePublicKeySHA256, rawCerts) } } if len(options.ALPN) > 0 { @@ -251,11 +284,29 @@ func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddre } else if len(clientCertificate) > 0 || len(clientKey) > 0 { return nil, E.New("client certificate and client key must be provided together") } + var handshakeTimeout time.Duration + if options.HandshakeTimeout > 0 { + handshakeTimeout = options.HandshakeTimeout.Build() + } else { + handshakeTimeout = C.TCPTimeout + } id, err := uTLSClientHelloID(options.UTLS.Fingerprint) if err != nil { return nil, err } - var config Config = &UTLSClientConfig{ctx, &tlsConfig, id, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment} + var config Config = &UTLSClientConfig{ + ctx: ctx, + config: &tlsConfig, + serverName: serverName, + disableSNI: options.DisableSNI, + verifyServerName: options.DisableSNI && !options.Insecure, + handshakeTimeout: handshakeTimeout, + id: id, + fragment: options.Fragment, + fragmentFallbackDelay: time.Duration(options.FragmentFallbackDelay), + recordFragment: options.RecordFragment, + } + config.SetServerName(serverName) if options.ECH != nil && options.ECH.Enabled { if options.Reality != nil && options.Reality.Enabled { return nil, E.New("Reality is conflict with ECH") diff --git a/common/tls/utls_stub.go b/common/tls/utls_stub.go index 3eddd28e85..67fac20983 100644 --- a/common/tls/utls_stub.go +++ b/common/tls/utls_stub.go @@ -12,10 +12,18 @@ import ( ) func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) { + return newUTLSClient(ctx, logger, serverAddress, options, false) +} + +func newUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) { return nil, E.New(`uTLS is not included in this build, rebuild with -tags with_utls`) } func NewRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) { + return newRealityClient(ctx, logger, serverAddress, options, false) +} + +func newRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) { return nil, E.New(`uTLS, which is required by reality is not included in this build, rebuild with -tags with_utls`) } diff --git a/constant/tls.go b/constant/tls.go index 2d4f64bc3a..c81740a492 100644 --- a/constant/tls.go +++ b/constant/tls.go @@ -1,3 +1,8 @@ package constant const ACMETLS1Protocol = "acme-tls/1" + +const ( + TLSEngineDefault = "" + TLSEngineApple = "apple" +) diff --git a/dns/transport/https.go b/dns/transport/https.go index b508e6eae5..1999728439 100644 --- a/dns/transport/https.go +++ b/dns/transport/https.go @@ -138,10 +138,7 @@ func (t *HTTPSTransport) Start(stage adapter.StartStage) error { } func (t *HTTPSTransport) Close() error { - t.transportAccess.Lock() - defer t.transportAccess.Unlock() - t.transport.CloseIdleConnections() - t.transport = t.transport.Clone() + t.Reset() return nil } diff --git a/docs/configuration/endpoint/tailscale.md b/docs/configuration/endpoint/tailscale.md index 6cf10e2ba9..9ecebab9ac 100644 --- a/docs/configuration/endpoint/tailscale.md +++ b/docs/configuration/endpoint/tailscale.md @@ -2,6 +2,11 @@ icon: material/new-box --- +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [control_http_client](#control_http_client) + :material-delete-clock: [Dial Fields](#dial-fields) + !!! quote "Changes in sing-box 1.13.0" :material-plus: [relay_server_port](#relay_server_port) @@ -22,6 +27,7 @@ icon: material/new-box "state_directory": "", "auth_key": "", "control_url": "", + "control_http_client": {}, // or "" "ephemeral": false, "hostname": "", "accept_routes": false, @@ -148,10 +154,18 @@ UDP NAT expiration time. `5m` will be used by default. +#### control_http_client + +!!! question "Since sing-box 1.14.0" + +HTTP Client for connecting to the Tailscale control plane. + +See [HTTP Client Fields](/configuration/shared/http-client/) for details. + ### Dial Fields -!!! note +!!! failure "Deprecated in sing-box 1.14.0" - Dial Fields in Tailscale endpoints only control how it connects to the control plane and have nothing to do with actual connections. + Dial Fields in Tailscale endpoints are deprecated in sing-box 1.14.0 and will be removed in sing-box 1.16.0, use `control_http_client` instead. See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/endpoint/tailscale.zh.md b/docs/configuration/endpoint/tailscale.zh.md index f881dd67f2..58b4708ae2 100644 --- a/docs/configuration/endpoint/tailscale.zh.md +++ b/docs/configuration/endpoint/tailscale.zh.md @@ -2,6 +2,11 @@ icon: material/new-box --- +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [control_http_client](#control_http_client) + :material-delete-clock: [拨号字段](#拨号字段) + !!! quote "sing-box 1.13.0 中的更改" :material-plus: [relay_server_port](#relay_server_port) @@ -22,6 +27,7 @@ icon: material/new-box "state_directory": "", "auth_key": "", "control_url": "", + "control_http_client": {}, // 或 "" "ephemeral": false, "hostname": "", "accept_routes": false, @@ -147,10 +153,18 @@ UDP NAT 过期时间。 默认使用 `5m`。 +#### control_http_client + +!!! question "自 sing-box 1.14.0 起" + +用于连接 Tailscale 控制平面的 HTTP 客户端。 + +参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。 + ### 拨号字段 -!!! note +!!! failure "已在 sing-box 1.14.0 废弃" - Tailscale 端点中的拨号字段仅控制它如何连接到控制平面,与实际连接无关。 + Tailscale 端点中的拨号字段已在 sing-box 1.14.0 废弃且将在 sing-box 1.16.0 中被移除,请使用 `control_http_client` 代替。 参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。 diff --git a/docs/configuration/inbound/hysteria.md b/docs/configuration/inbound/hysteria.md index 4725aafcea..bd30973f77 100644 --- a/docs/configuration/inbound/hysteria.md +++ b/docs/configuration/inbound/hysteria.md @@ -21,11 +21,16 @@ } ], + "tls": {}, + + ... // QUIC Fields + + // Deprecated + "recv_window_conn": 0, "recv_window_client": 0, "max_conn_client": 0, - "disable_mtu_discovery": false, - "tls": {} + "disable_mtu_discovery": false } ``` @@ -76,32 +81,38 @@ Authentication password, in base64. Authentication password. +#### tls + +==Required== + +TLS configuration, see [TLS](/configuration/shared/tls/#inbound). + +### QUIC Fields + +See [QUIC Fields](/configuration/shared/quic/) for details. + +### Deprecated Fields + #### recv_window_conn -The QUIC stream-level flow control window for receiving data. +!!! failure "Deprecated in sing-box 1.14.0" -`15728640 (15 MB/s)` will be used if empty. + Use QUIC fields `stream_receive_window` instead. #### recv_window_client -The QUIC connection-level flow control window for receiving data. +!!! failure "Deprecated in sing-box 1.14.0" -`67108864 (64 MB/s)` will be used if empty. + Use QUIC fields `connection_receive_window` instead. #### max_conn_client -The maximum number of QUIC concurrent bidirectional streams that a peer is allowed to open. +!!! failure "Deprecated in sing-box 1.14.0" -`1024` will be used if empty. + Use QUIC fields `max_concurrent_streams` instead. #### disable_mtu_discovery -Disables Path MTU Discovery (RFC 8899). Packets will then be at most 1252 (IPv4) / 1232 (IPv6) bytes in size. - -Force enabled on for systems other than Linux and Windows (according to upstream). - -#### tls - -==Required== +!!! failure "Deprecated in sing-box 1.14.0" -TLS configuration, see [TLS](/configuration/shared/tls/#inbound). \ No newline at end of file + Use QUIC fields `disable_path_mtu_discovery` instead. \ No newline at end of file diff --git a/docs/configuration/inbound/hysteria.zh.md b/docs/configuration/inbound/hysteria.zh.md index 561d7102e2..af1be8f6de 100644 --- a/docs/configuration/inbound/hysteria.zh.md +++ b/docs/configuration/inbound/hysteria.zh.md @@ -21,11 +21,16 @@ } ], + "tls": {}, + + ... // QUIC 字段 + + // 废弃的 + "recv_window_conn": 0, "recv_window_client": 0, "max_conn_client": 0, - "disable_mtu_discovery": false, - "tls": {} + "disable_mtu_discovery": false } ``` @@ -76,32 +81,38 @@ base64 编码的认证密码。 认证密码。 +#### tls + +==必填== + +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 + +### QUIC 字段 + +参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。 + +### 废弃字段 + #### recv_window_conn -用于接收数据的 QUIC 流级流控制窗口。 +!!! failure "已在 sing-box 1.14.0 废弃" -默认 `15728640 (15 MB/s)`。 + 请使用 QUIC 字段 `stream_receive_window` 代替。 #### recv_window_client -用于接收数据的 QUIC 连接级流控制窗口。 +!!! failure "已在 sing-box 1.14.0 废弃" -默认 `67108864 (64 MB/s)`。 + 请使用 QUIC 字段 `connection_receive_window` 代替。 #### max_conn_client -允许对等点打开的 QUIC 并发双向流的最大数量。 +!!! failure "已在 sing-box 1.14.0 废弃" -默认 `1024`。 + 请使用 QUIC 字段 `max_concurrent_streams` 代替。 #### disable_mtu_discovery -禁用路径 MTU 发现 (RFC 8899)。 数据包的大小最多为 1252 (IPv4) / 1232 (IPv6) 字节。 - -强制为 Linux 和 Windows 以外的系统启用(根据上游)。 - -#### tls - -==必填== +!!! failure "已在 sing-box 1.14.0 废弃" -TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 \ No newline at end of file + 请使用 QUIC 字段 `disable_path_mtu_discovery` 代替。 \ No newline at end of file diff --git a/docs/configuration/inbound/hysteria2.md b/docs/configuration/inbound/hysteria2.md index 8426be2459..62fbb209ef 100644 --- a/docs/configuration/inbound/hysteria2.md +++ b/docs/configuration/inbound/hysteria2.md @@ -34,6 +34,9 @@ icon: material/alert-decagram ], "ignore_client_bandwidth": false, "tls": {}, + + ... // QUIC Fields + "masquerade": "", // or {} "bbr_profile": "", "brutal_debug": false @@ -95,6 +98,10 @@ Deny clients to use the BBR CC. TLS configuration, see [TLS](/configuration/shared/tls/#inbound). +### QUIC Fields + +See [QUIC Fields](/configuration/shared/quic/) for details. + #### masquerade HTTP3 server behavior (URL string configuration) when authentication fails. diff --git a/docs/configuration/inbound/hysteria2.zh.md b/docs/configuration/inbound/hysteria2.zh.md index 0c5e918ed9..0c5fdb014f 100644 --- a/docs/configuration/inbound/hysteria2.zh.md +++ b/docs/configuration/inbound/hysteria2.zh.md @@ -34,6 +34,9 @@ icon: material/alert-decagram ], "ignore_client_bandwidth": false, "tls": {}, + + ... // QUIC 字段 + "masquerade": "", // 或 {} "bbr_profile": "", "brutal_debug": false @@ -92,6 +95,10 @@ Hysteria 用户 TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 +### QUIC 字段 + +参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。 + #### masquerade HTTP3 服务器认证失败时的行为 (URL 字符串配置)。 diff --git a/docs/configuration/inbound/tuic.md b/docs/configuration/inbound/tuic.md index 8a2d8c7e06..e7d1129b24 100644 --- a/docs/configuration/inbound/tuic.md +++ b/docs/configuration/inbound/tuic.md @@ -18,7 +18,9 @@ "auth_timeout": "3s", "zero_rtt_handshake": false, "heartbeat": "10s", - "tls": {} + "tls": {}, + + ... // QUIC Fields } ``` @@ -75,4 +77,8 @@ Interval for sending heartbeat packets for keeping the connection alive ==Required== -TLS configuration, see [TLS](/configuration/shared/tls/#inbound). \ No newline at end of file +TLS configuration, see [TLS](/configuration/shared/tls/#inbound). + +### QUIC Fields + +See [QUIC Fields](/configuration/shared/quic/) for details. \ No newline at end of file diff --git a/docs/configuration/inbound/tuic.zh.md b/docs/configuration/inbound/tuic.zh.md index ae531635e6..6a231d794e 100644 --- a/docs/configuration/inbound/tuic.zh.md +++ b/docs/configuration/inbound/tuic.zh.md @@ -18,7 +18,9 @@ "auth_timeout": "3s", "zero_rtt_handshake": false, "heartbeat": "10s", - "tls": {} + "tls": {}, + + ... // QUIC 字段 } ``` @@ -75,4 +77,8 @@ QUIC 拥塞控制算法 ==必填== -TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 \ No newline at end of file +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 + +### QUIC 字段 + +参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。 \ No newline at end of file diff --git a/docs/configuration/index.md b/docs/configuration/index.md index 81cb8f3863..311161c1cc 100644 --- a/docs/configuration/index.md +++ b/docs/configuration/index.md @@ -10,6 +10,7 @@ sing-box uses JSON for configuration files. "ntp": {}, "certificate": {}, "certificate_providers": [], + "http_clients": [], "endpoints": [], "inbounds": [], "outbounds": [], @@ -28,6 +29,7 @@ sing-box uses JSON for configuration files. | `ntp` | [NTP](./ntp/) | | `certificate` | [Certificate](./certificate/) | | `certificate_providers` | [Certificate Provider](./shared/certificate-provider/) | +| `http_clients` | [HTTP Client](./shared/http-client/) | | `endpoints` | [Endpoint](./endpoint/) | | `inbounds` | [Inbound](./inbound/) | | `outbounds` | [Outbound](./outbound/) | diff --git a/docs/configuration/index.zh.md b/docs/configuration/index.zh.md index 350db5d4c4..fbb44c79e1 100644 --- a/docs/configuration/index.zh.md +++ b/docs/configuration/index.zh.md @@ -10,6 +10,7 @@ sing-box 使用 JSON 作为配置文件格式。 "ntp": {}, "certificate": {}, "certificate_providers": [], + "http_clients": [], "endpoints": [], "inbounds": [], "outbounds": [], @@ -28,6 +29,7 @@ sing-box 使用 JSON 作为配置文件格式。 | `ntp` | [NTP](./ntp/) | | `certificate` | [证书](./certificate/) | | `certificate_providers` | [证书提供者](./shared/certificate-provider/) | +| `http_clients` | [HTTP 客户端](./shared/http-client/) | | `endpoints` | [端点](./endpoint/) | | `inbounds` | [入站](./inbound/) | | `outbounds` | [出站](./outbound/) | diff --git a/docs/configuration/outbound/hysteria.md b/docs/configuration/outbound/hysteria.md index b326e06dcd..4908799bdb 100644 --- a/docs/configuration/outbound/hysteria.md +++ b/docs/configuration/outbound/hysteria.md @@ -27,13 +27,18 @@ icon: material/new-box "obfs": "fuck me till the daylight", "auth": "", "auth_str": "password", - "recv_window_conn": 0, - "recv_window": 0, - "disable_mtu_discovery": false, - "network": "tcp", + "network": "", "tls": {}, - + + ... // QUIC Fields + ... // Dial Fields + + // Deprecated + + "recv_window_conn": 0, + "recv_window": 0, + "disable_mtu_discovery": false } ``` @@ -104,24 +109,6 @@ Authentication password, in base64. Authentication password. -#### recv_window_conn - -The QUIC stream-level flow control window for receiving data. - -`15728640 (15 MB/s)` will be used if empty. - -#### recv_window - -The QUIC connection-level flow control window for receiving data. - -`67108864 (64 MB/s)` will be used if empty. - -#### disable_mtu_discovery - -Disables Path MTU Discovery (RFC 8899). Packets will then be at most 1252 (IPv4) / 1232 (IPv6) bytes in size. - -Force enabled on for systems other than Linux and Windows (according to upstream). - #### network Enabled network @@ -136,6 +123,30 @@ Both is enabled by default. TLS configuration, see [TLS](/configuration/shared/tls/#outbound). +### QUIC Fields + +See [QUIC Fields](/configuration/shared/quic/) for details. + ### Dial Fields See [Dial Fields](/configuration/shared/dial/) for details. + +### Deprecated Fields + +#### recv_window_conn + +!!! failure "Deprecated in sing-box 1.14.0" + + Use QUIC fields `stream_receive_window` instead. + +#### recv_window + +!!! failure "Deprecated in sing-box 1.14.0" + + Use QUIC fields `connection_receive_window` instead. + +#### disable_mtu_discovery + +!!! failure "Deprecated in sing-box 1.14.0" + + Use QUIC fields `disable_path_mtu_discovery` instead. diff --git a/docs/configuration/outbound/hysteria.zh.md b/docs/configuration/outbound/hysteria.zh.md index ae1d359004..004d2d165a 100644 --- a/docs/configuration/outbound/hysteria.zh.md +++ b/docs/configuration/outbound/hysteria.zh.md @@ -27,13 +27,18 @@ icon: material/new-box "obfs": "fuck me till the daylight", "auth": "", "auth_str": "password", - "recv_window_conn": 0, - "recv_window": 0, - "disable_mtu_discovery": false, - "network": "tcp", + "network": "", "tls": {}, - + + ... // QUIC 字段 + ... // 拨号字段 + + // 废弃的 + + "recv_window_conn": 0, + "recv_window": 0, + "disable_mtu_discovery": false } ``` @@ -104,24 +109,6 @@ base64 编码的认证密码。 认证密码。 -#### recv_window_conn - -用于接收数据的 QUIC 流级流控制窗口。 - -默认 `15728640 (15 MB/s)`。 - -#### recv_window - -用于接收数据的 QUIC 连接级流控制窗口。 - -默认 `67108864 (64 MB/s)`。 - -#### disable_mtu_discovery - -禁用路径 MTU 发现 (RFC 8899)。 数据包的大小最多为 1252 (IPv4) / 1232 (IPv6) 字节。 - -强制为 Linux 和 Windows 以外的系统启用(根据上游)。 - #### network 启用的网络协议。 @@ -136,7 +123,30 @@ base64 编码的认证密码。 TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 +### QUIC 字段 + +参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。 ### 拨号字段 参阅 [拨号字段](/zh/configuration/shared/dial/)。 + +### 废弃字段 + +#### recv_window_conn + +!!! failure "已在 sing-box 1.14.0 废弃" + + 请使用 QUIC 字段 `stream_receive_window` 代替。 + +#### recv_window + +!!! failure "已在 sing-box 1.14.0 废弃" + + 请使用 QUIC 字段 `connection_receive_window` 代替。 + +#### disable_mtu_discovery + +!!! failure "已在 sing-box 1.14.0 废弃" + + 请使用 QUIC 字段 `disable_path_mtu_discovery` 代替。 diff --git a/docs/configuration/outbound/hysteria2.md b/docs/configuration/outbound/hysteria2.md index a71dd1e070..2d5a9bcb1b 100644 --- a/docs/configuration/outbound/hysteria2.md +++ b/docs/configuration/outbound/hysteria2.md @@ -31,6 +31,9 @@ "password": "goofy_ahh_password", "network": "tcp", "tls": {}, + + ... // QUIC Fields + "bbr_profile": "", "brutal_debug": false, @@ -124,6 +127,10 @@ Both is enabled by default. TLS configuration, see [TLS](/configuration/shared/tls/#outbound). +### QUIC Fields + +See [QUIC Fields](/configuration/shared/quic/) for details. + #### bbr_profile !!! question "Since sing-box 1.14.0" diff --git a/docs/configuration/outbound/hysteria2.zh.md b/docs/configuration/outbound/hysteria2.zh.md index 0fb17bbdc3..aa0e6e11f9 100644 --- a/docs/configuration/outbound/hysteria2.zh.md +++ b/docs/configuration/outbound/hysteria2.zh.md @@ -31,6 +31,9 @@ "password": "goofy_ahh_password", "network": "tcp", "tls": {}, + + ... // QUIC 字段 + "bbr_profile": "", "brutal_debug": false, @@ -122,6 +125,10 @@ QUIC 流量混淆器密码. TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 +### QUIC 字段 + +参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。 + #### bbr_profile !!! question "自 sing-box 1.14.0 起" diff --git a/docs/configuration/outbound/tuic.md b/docs/configuration/outbound/tuic.md index 4f4ef4850d..3701e73dd1 100644 --- a/docs/configuration/outbound/tuic.md +++ b/docs/configuration/outbound/tuic.md @@ -16,7 +16,9 @@ "heartbeat": "10s", "network": "tcp", "tls": {}, - + + ... // QUIC Fields + ... // Dial Fields } ``` @@ -91,6 +93,10 @@ Both is enabled by default. TLS configuration, see [TLS](/configuration/shared/tls/#outbound). +### QUIC Fields + +See [QUIC Fields](/configuration/shared/quic/) for details. + ### Dial Fields See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/outbound/tuic.zh.md b/docs/configuration/outbound/tuic.zh.md index 6d31d7bcb5..43df2cfc8a 100644 --- a/docs/configuration/outbound/tuic.zh.md +++ b/docs/configuration/outbound/tuic.zh.md @@ -16,7 +16,9 @@ "heartbeat": "10s", "network": "tcp", "tls": {}, - + + ... // QUIC 字段 + ... // 拨号字段 } ``` @@ -99,6 +101,10 @@ UDP 包中继模式 TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 +### QUIC 字段 + +参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。 + ### 拨号字段 参阅 [拨号字段](/zh/configuration/shared/dial/)。 diff --git a/docs/configuration/route/index.md b/docs/configuration/route/index.md index 6c59f85079..1255723f47 100644 --- a/docs/configuration/route/index.md +++ b/docs/configuration/route/index.md @@ -6,6 +6,7 @@ icon: material/alert-decagram !!! quote "Changes in sing-box 1.14.0" + :material-plus: [default_http_client](#default_http_client) :material-plus: [find_neighbor](#find_neighbor) :material-plus: [dhcp_lease_files](#dhcp_lease_files) @@ -43,6 +44,7 @@ icon: material/alert-decagram "find_process": false, "find_neighbor": false, "dhcp_lease_files": [], + "default_http_client": "", "default_domain_resolver": "", // or {} "default_network_strategy": "", "default_network_type": [], @@ -147,6 +149,14 @@ Custom DHCP lease file paths for hostname and MAC address resolution. Automatically detected from common DHCP servers (dnsmasq, odhcpd, ISC dhcpd, Kea) if empty. +#### default_http_client + +!!! question "Since sing-box 1.14.0" + +Tag of the default [HTTP Client](/configuration/shared/http-client/) used by remote rule-sets. + +If empty and `http_clients` is defined, the first HTTP client is used. + #### default_domain_resolver !!! question "Since sing-box 1.12.0" diff --git a/docs/configuration/route/index.zh.md b/docs/configuration/route/index.zh.md index 4977b084e2..58d718df02 100644 --- a/docs/configuration/route/index.zh.md +++ b/docs/configuration/route/index.zh.md @@ -6,6 +6,7 @@ icon: material/alert-decagram !!! quote "sing-box 1.14.0 中的更改" + :material-plus: [default_http_client](#default_http_client) :material-plus: [find_neighbor](#find_neighbor) :material-plus: [dhcp_lease_files](#dhcp_lease_files) @@ -45,6 +46,7 @@ icon: material/alert-decagram "find_process": false, "find_neighbor": false, "dhcp_lease_files": [], + "default_http_client": "", "default_network_strategy": "", "default_fallback_delay": "" } @@ -146,6 +148,14 @@ icon: material/alert-decagram 为空时自动从常见 DHCP 服务器(dnsmasq、odhcpd、ISC dhcpd、Kea)检测。 +#### default_http_client + +!!! question "自 sing-box 1.14.0 起" + +远程规则集使用的默认 [HTTP 客户端](/zh/configuration/shared/http-client/) 的标签。 + +如果为空且 `http_clients` 已定义,将使用第一个 HTTP 客户端。 + #### default_domain_resolver !!! question "自 sing-box 1.12.0 起" diff --git a/docs/configuration/rule-set/index.md b/docs/configuration/rule-set/index.md index 73ec7b859f..c6d4a17c08 100644 --- a/docs/configuration/rule-set/index.md +++ b/docs/configuration/rule-set/index.md @@ -1,3 +1,8 @@ +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [http_client](#http_client) + :material-delete-clock: [download_detour](#download_detour) + !!! quote "Changes in sing-box 1.10.0" :material-plus: `type: inline` @@ -43,8 +48,12 @@ "tag": "", "format": "source", // or binary "url": "", - "download_detour": "", // optional - "update_interval": "" // optional + "http_client": "", // or {} + "update_interval": "", + + // Deprecated + + "download_detour": "" } ``` @@ -102,14 +111,26 @@ File path of rule-set. Download URL of rule-set. -#### download_detour +#### http_client -Tag of the outbound to download rule-set. +!!! question "Since sing-box 1.14.0" + +HTTP Client for downloading rule-set. -Default outbound will be used if empty. +See [HTTP Client Fields](/configuration/shared/http-client/) for details. + +Default transport will be used if empty. #### update_interval Update interval of rule-set. `1d` will be used if empty. + +#### download_detour + +!!! failure "Deprecated in sing-box 1.14.0" + + `download_detour` is deprecated in sing-box 1.14.0 and will be removed in sing-box 1.16.0, use `http_client` instead. + +Tag of the outbound to download rule-set. diff --git a/docs/configuration/rule-set/index.zh.md b/docs/configuration/rule-set/index.zh.md index eac519539c..2cd6f93793 100644 --- a/docs/configuration/rule-set/index.zh.md +++ b/docs/configuration/rule-set/index.zh.md @@ -1,3 +1,8 @@ +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [http_client](#http_client) + :material-delete-clock: [download_detour](#download_detour) + !!! quote "sing-box 1.10.0 中的更改" :material-plus: `type: inline` @@ -43,8 +48,12 @@ "tag": "", "format": "source", // or binary "url": "", - "download_detour": "", // 可选 - "update_interval": "" // 可选 + "http_client": "", // 或 {} + "update_interval": "", + + // 废弃的 + + "download_detour": "" } ``` @@ -102,14 +111,26 @@ 规则集的下载 URL。 -#### download_detour +#### http_client -用于下载规则集的出站的标签。 +!!! question "自 sing-box 1.14.0 起" + +用于下载规则集的 HTTP 客户端。 -如果为空,将使用默认出站。 +参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。 + +如果为空,将使用默认传输。 #### update_interval 规则集的更新间隔。 默认使用 `1d`。 + +#### download_detour + +!!! failure "已在 sing-box 1.14.0 废弃" + + `download_detour` 已在 sing-box 1.14.0 废弃且将在 sing-box 1.16.0 中被移除,请使用 `http_client` 代替。 + +用于下载规则集的出站的标签。 diff --git a/docs/configuration/service/derp.md b/docs/configuration/service/derp.md index 3d7443a313..2925db993e 100644 --- a/docs/configuration/service/derp.md +++ b/docs/configuration/service/derp.md @@ -58,9 +58,9 @@ Object format: ```json { - "url": "https://my-headscale.com/verify", - - ... // Dial Fields + "url": "", + + ... // HTTP Client Fields } ``` diff --git a/docs/configuration/service/derp.zh.md b/docs/configuration/service/derp.zh.md index b22ff41345..01e2ac39fc 100644 --- a/docs/configuration/service/derp.zh.md +++ b/docs/configuration/service/derp.zh.md @@ -58,9 +58,9 @@ Derper 配置文件路径。 ```json { - "url": "https://my-headscale.com/verify", + "url": "", - ... // 拨号字段 + ... // HTTP 客户端字段 } ``` diff --git a/docs/configuration/shared/certificate-provider/acme.md b/docs/configuration/shared/certificate-provider/acme.md index 440ed1568d..5f167c2e0b 100644 --- a/docs/configuration/shared/certificate-provider/acme.md +++ b/docs/configuration/shared/certificate-provider/acme.md @@ -6,7 +6,7 @@ icon: material/new-box :material-plus: [account_key](#account_key) :material-plus: [key_type](#key_type) - :material-plus: [detour](#detour) + :material-plus: [http_client](#http_client) # ACME @@ -37,7 +37,7 @@ icon: material/new-box }, "dns01_challenge": {}, "key_type": "", - "detour": "" + "http_client": "" // or {} } ``` @@ -141,10 +141,10 @@ The private key type to generate for new certificates. | `rsa2048` | RSA | | `rsa4096` | RSA | -#### detour +#### http_client !!! question "Since sing-box 1.14.0" -The tag of the upstream outbound. +HTTP Client for all provider HTTP requests. -All provider HTTP requests will use this outbound. +See [HTTP Client Fields](/configuration/shared/http-client/) for details. diff --git a/docs/configuration/shared/certificate-provider/acme.zh.md b/docs/configuration/shared/certificate-provider/acme.zh.md index d95930a550..2c895f5fe7 100644 --- a/docs/configuration/shared/certificate-provider/acme.zh.md +++ b/docs/configuration/shared/certificate-provider/acme.zh.md @@ -6,7 +6,7 @@ icon: material/new-box :material-plus: [account_key](#account_key) :material-plus: [key_type](#key_type) - :material-plus: [detour](#detour) + :material-plus: [http_client](#http_client) # ACME @@ -37,7 +37,7 @@ icon: material/new-box }, "dns01_challenge": {}, "key_type": "", - "detour": "" + "http_client": "" // 或 {} } ``` @@ -136,10 +136,12 @@ ACME DNS01 质询字段。如果配置,将禁用其他质询方法。 | `rsa2048` | RSA | | `rsa4096` | RSA | -#### detour +#### http_client !!! question "自 sing-box 1.14.0 起" -上游出站的标签。 +用于所有提供者 HTTP 请求的 HTTP 客户端。 + +参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。 所有提供者 HTTP 请求将使用此出站。 diff --git a/docs/configuration/shared/certificate-provider/cloudflare-origin-ca.md b/docs/configuration/shared/certificate-provider/cloudflare-origin-ca.md index cfd2da4fe1..506def982d 100644 --- a/docs/configuration/shared/certificate-provider/cloudflare-origin-ca.md +++ b/docs/configuration/shared/certificate-provider/cloudflare-origin-ca.md @@ -19,7 +19,7 @@ icon: material/new-box "origin_ca_key": "", "request_type": "", "requested_validity": 0, - "detour": "" + "http_client": "" // or {} } ``` @@ -75,8 +75,8 @@ Available values: `7`, `30`, `90`, `365`, `730`, `1095`, `5475`. `5475` days (15 years) is used if empty. -#### detour +#### http_client -The tag of the upstream outbound. +HTTP Client for all provider HTTP requests. -All provider HTTP requests will use this outbound. +See [HTTP Client Fields](/configuration/shared/http-client/) for details. diff --git a/docs/configuration/shared/certificate-provider/cloudflare-origin-ca.zh.md b/docs/configuration/shared/certificate-provider/cloudflare-origin-ca.zh.md index 85036268df..d526be56a9 100644 --- a/docs/configuration/shared/certificate-provider/cloudflare-origin-ca.zh.md +++ b/docs/configuration/shared/certificate-provider/cloudflare-origin-ca.zh.md @@ -19,7 +19,7 @@ icon: material/new-box "origin_ca_key": "", "request_type": "", "requested_validity": 0, - "detour": "" + "http_client": "" // 或 {} } ``` @@ -75,8 +75,8 @@ Cloudflare Origin CA Key。 如果为空,使用 `5475` 天(15 年)。 -#### detour +#### http_client -上游出站的标签。 +用于所有提供者 HTTP 请求的 HTTP 客户端。 -所有提供者 HTTP 请求将使用此出站。 +参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。 diff --git a/docs/configuration/shared/http-client.md b/docs/configuration/shared/http-client.md new file mode 100644 index 0000000000..a0aa9d2308 --- /dev/null +++ b/docs/configuration/shared/http-client.md @@ -0,0 +1,114 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.14.0" + +### Structure + +A string or an object. + +When string, the tag of a shared [HTTP Client](/configuration/shared/http-client/) defined in top-level `http_clients`. + +When object: + +```json +{ + "engine": "", + "version": 0, + "disable_version_fallback": false, + "headers": {}, + + ... // HTTP2 Fields + + "tls": {}, + + ... // Dial Fields +} +``` + +### Fields + +#### engine + +HTTP engine to use. + +Values: + +* `go` (default) +* `apple` + +`apple` uses NSURLSession, only available on Apple platforms. + +!!! warning "" + + Experimental only: due to the high memory overhead of both CGO and Network.framework, + do not use in hot paths on iOS and tvOS. + +Supported fields: + +* `headers` +* `tls.server_name` (must match request host) +* `tls.insecure` +* `tls.min_version` / `tls.max_version` +* `tls.certificate` / `tls.certificate_path` +* `tls.certificate_public_key_sha256` +* Dial Fields + +Unsupported fields: + +* `version` +* `disable_version_fallback` +* HTTP2 Fields +* QUIC Fields +* `tls.engine` +* `tls.alpn` +* `tls.disable_sni` +* `tls.cipher_suites` +* `tls.curve_preferences` +* `tls.client_certificate` / `tls.client_certificate_path` / `tls.client_key` / `tls.client_key_path` +* `tls.fragment` / `tls.record_fragment` +* `tls.kernel_tx` / `tls.kernel_rx` +* `tls.ech` +* `tls.utls` +* `tls.reality` + +#### version + +HTTP version. + +Available values: `1`, `2`, `3`. + +`2` is used by default. + +When `3`, [HTTP2 Fields](#http2-fields) are replaced by [QUIC Fields](#quic-fields). + +#### disable_version_fallback + +Disable automatic fallback to lower HTTP version. + +#### headers + +Custom HTTP headers. + +`Host` header is used as request host. + +### HTTP2 Fields + +When `version` is `2` (default). + +See [HTTP2 Fields](/configuration/shared/http2/) for details. + +### QUIC Fields + +When `version` is `3`. + +See [QUIC Fields](/configuration/shared/quic/) for details. + +### TLS Fields + +See [TLS](/configuration/shared/tls/#outbound) for details. + +### Dial Fields + +See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/shared/http-client.zh.md b/docs/configuration/shared/http-client.zh.md new file mode 100644 index 0000000000..5c05968ad4 --- /dev/null +++ b/docs/configuration/shared/http-client.zh.md @@ -0,0 +1,114 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.14.0 起" + +### 结构 + +字符串或对象。 + +当为字符串时,为顶层 `http_clients` 中定义的共享 [HTTP 客户端](/zh/configuration/shared/http-client/) 的标签。 + +当为对象时: + +```json +{ + "engine": "", + "version": 0, + "disable_version_fallback": false, + "headers": {}, + + ... // HTTP2 字段 + + "tls": {}, + + ... // 拨号字段 +} +``` + +### 字段 + +#### engine + +要使用的 HTTP 引擎。 + +可用值: + +* `go`(默认) +* `apple` + +`apple` 使用 NSURLSession,仅在 Apple 平台可用。 + +!!! warning "" + + 仅供实验用途:由于 CGO 和 Network.framework 占用的内存都很多, + 不应在 iOS 和 tvOS 的热路径中使用。 + +支持的字段: + +* `headers` +* `tls.server_name`(必须与请求主机匹配) +* `tls.insecure` +* `tls.min_version` / `tls.max_version` +* `tls.certificate` / `tls.certificate_path` +* `tls.certificate_public_key_sha256` +* 拨号字段 + +不支持的字段: + +* `version` +* `disable_version_fallback` +* HTTP2 字段 +* QUIC 字段 +* `tls.engine` +* `tls.alpn` +* `tls.disable_sni` +* `tls.cipher_suites` +* `tls.curve_preferences` +* `tls.client_certificate` / `tls.client_certificate_path` / `tls.client_key` / `tls.client_key_path` +* `tls.fragment` / `tls.record_fragment` +* `tls.kernel_tx` / `tls.kernel_rx` +* `tls.ech` +* `tls.utls` +* `tls.reality` + +#### version + +HTTP 版本。 + +可用值:`1`、`2`、`3`。 + +默认使用 `2`。 + +当为 `3` 时,[HTTP2 字段](#http2-字段) 替换为 [QUIC 字段](#quic-字段)。 + +#### disable_version_fallback + +禁用自动回退到更低的 HTTP 版本。 + +#### headers + +自定义 HTTP 标头。 + +`Host` 标头用作请求主机。 + +### HTTP2 字段 + +当 `version` 为 `2`(默认)时。 + +参阅 [HTTP2 字段](/zh/configuration/shared/http2/) 了解详情。 + +### QUIC 字段 + +当 `version` 为 `3` 时。 + +参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。 + +### TLS 字段 + +参阅 [TLS](/zh/configuration/shared/tls/#出站) 了解详情。 + +### 拨号字段 + +参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。 diff --git a/docs/configuration/shared/http2.md b/docs/configuration/shared/http2.md new file mode 100644 index 0000000000..e0e0afb473 --- /dev/null +++ b/docs/configuration/shared/http2.md @@ -0,0 +1,43 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.14.0" + +### Structure + +```json +{ + "idle_timeout": "", + "keep_alive_period": "", + "stream_receive_window": "", + "connection_receive_window": "", + "max_concurrent_streams": 0 +} +``` + +### Fields + +#### idle_timeout + +Idle connection timeout, in golang's Duration format. + +#### keep_alive_period + +Keep alive period, in golang's Duration format. + +#### stream_receive_window + +HTTP2 stream-level flow-control receive window size. + +Accepts memory size format, e.g. `"64 MB"`. + +#### connection_receive_window + +HTTP2 connection-level flow-control receive window size. + +Accepts memory size format, e.g. `"64 MB"`. + +#### max_concurrent_streams + +Maximum concurrent streams per connection. diff --git a/docs/configuration/shared/http2.zh.md b/docs/configuration/shared/http2.zh.md new file mode 100644 index 0000000000..344e865a55 --- /dev/null +++ b/docs/configuration/shared/http2.zh.md @@ -0,0 +1,43 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.14.0 起" + +### 结构 + +```json +{ + "idle_timeout": "", + "keep_alive_period": "", + "stream_receive_window": "", + "connection_receive_window": "", + "max_concurrent_streams": 0 +} +``` + +### 字段 + +#### idle_timeout + +空闲连接超时,采用 golang 的 Duration 格式。 + +#### keep_alive_period + +Keep alive 周期,采用 golang 的 Duration 格式。 + +#### stream_receive_window + +HTTP2 流级别流控接收窗口大小。 + +接受内存大小格式,例如 `"64 MB"`。 + +#### connection_receive_window + +HTTP2 连接级别流控接收窗口大小。 + +接受内存大小格式,例如 `"64 MB"`。 + +#### max_concurrent_streams + +每个连接的最大并发流数。 diff --git a/docs/configuration/shared/quic.md b/docs/configuration/shared/quic.md new file mode 100644 index 0000000000..485a8ff8d8 --- /dev/null +++ b/docs/configuration/shared/quic.md @@ -0,0 +1,30 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.14.0" + +### Structure + +```json +{ + "initial_packet_size": 0, + "disable_path_mtu_discovery": false, + + ... // HTTP2 Fields +} +``` + +### Fields + +#### initial_packet_size + +Initial QUIC packet size. + +#### disable_path_mtu_discovery + +Disable QUIC path MTU discovery. + +### HTTP2 Fields + +See [HTTP2 Fields](/configuration/shared/http2/) for details. diff --git a/docs/configuration/shared/quic.zh.md b/docs/configuration/shared/quic.zh.md new file mode 100644 index 0000000000..1e840e8f58 --- /dev/null +++ b/docs/configuration/shared/quic.zh.md @@ -0,0 +1,30 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.14.0 起" + +### 结构 + +```json +{ + "initial_packet_size": 0, + "disable_path_mtu_discovery": false, + + ... // HTTP2 字段 +} +``` + +### 字段 + +#### initial_packet_size + +初始 QUIC 数据包大小。 + +#### disable_path_mtu_discovery + +禁用 QUIC 路径 MTU 发现。 + +### HTTP2 字段 + +参阅 [HTTP2 字段](/zh/configuration/shared/http2/) 了解详情。 diff --git a/docs/configuration/shared/tls.md b/docs/configuration/shared/tls.md index 518b2f9176..2e57b30faf 100644 --- a/docs/configuration/shared/tls.md +++ b/docs/configuration/shared/tls.md @@ -5,6 +5,7 @@ icon: material/new-box !!! quote "Changes in sing-box 1.14.0" :material-plus: [certificate_provider](#certificate_provider) + :material-plus: [handshake_timeout](#handshake_timeout) :material-delete-clock: [acme](#acme-fields) !!! quote "Changes in sing-box 1.13.0" @@ -54,6 +55,7 @@ icon: material/new-box "key_path": "", "kernel_tx": false, "kernel_rx": false, + "handshake_timeout": "", "certificate_provider": "", // Deprecated @@ -106,6 +108,7 @@ icon: material/new-box ```json { "enabled": true, + "engine": "", "disable_sni": false, "server_name": "", "insecure": false, @@ -124,6 +127,9 @@ icon: material/new-box "fragment": false, "fragment_fallback_delay": "", "record_fragment": false, + "kernel_tx": false, + "kernel_rx": false, + "handshake_timeout": "", "ech": { "enabled": false, "config": [], @@ -183,6 +189,49 @@ Cipher suite values: Enable TLS. +#### engine + +==Client only== + +TLS engine to use. + +Values: + +* `go` (default) +* `apple` + +`apple` uses Network.framework, only available on Apple platforms and only supports **direct** TCP TLS client connections. + +!!! warning "" + + Experimental only: due to the high memory overhead of both CGO and Network.framework, + do not use in hot paths on iOS and tvOS. + If you want to circumvent TLS fingerprint-based proxy censorship, + use [NaiveProxy](/configuration/outbound/naive/) instead. + +Supported fields: + +* `server_name` +* `insecure` +* `alpn` +* `min_version` +* `max_version` +* `certificate` / `certificate_path` +* `certificate_public_key_sha256` +* `handshake_timeout` + +Unsupported fields: + +* `disable_sni` +* `cipher_suites` +* `curve_preferences` +* `client_certificate` / `client_certificate_path` / `client_key` / `client_key_path` +* `fragment` / `record_fragment` +* `kernel_tx` / `kernel_rx` +* `ech` +* `utls` +* `reality` + #### disable_sni ==Client only== @@ -417,6 +466,14 @@ Enable kernel TLS transmit support. Enable kernel TLS receive support. +#### handshake_timeout + +!!! question "Since sing-box 1.14.0" + +TLS handshake timeout, in golang's Duration format. + +`15s` is used by default. + #### certificate_provider !!! question "Since sing-box 1.14.0" diff --git a/docs/configuration/shared/tls.zh.md b/docs/configuration/shared/tls.zh.md index 56b90d33f1..ebe5a32709 100644 --- a/docs/configuration/shared/tls.zh.md +++ b/docs/configuration/shared/tls.zh.md @@ -5,6 +5,7 @@ icon: material/new-box !!! quote "sing-box 1.14.0 中的更改" :material-plus: [certificate_provider](#certificate_provider) + :material-plus: [handshake_timeout](#handshake_timeout) :material-delete-clock: [acme](#acme-字段) !!! quote "sing-box 1.13.0 中的更改" @@ -54,6 +55,7 @@ icon: material/new-box "key_path": "", "kernel_tx": false, "kernel_rx": false, + "handshake_timeout": "", "certificate_provider": "", // 废弃的 @@ -106,6 +108,7 @@ icon: material/new-box ```json { "enabled": true, + "engine": "", "disable_sni": false, "server_name": "", "insecure": false, @@ -124,6 +127,9 @@ icon: material/new-box "fragment": false, "fragment_fallback_delay": "", "record_fragment": false, + "kernel_tx": false, + "kernel_rx": false, + "handshake_timeout": "", "ech": { "enabled": false, "config": [], @@ -183,6 +189,48 @@ TLS 版本值: 启用 TLS +#### engine + +==仅客户端== + +要使用的 TLS 引擎。 + +可用值: + +* `go`(默认) +* `apple` + +`apple` 使用 Network.framework,仅在 Apple 平台可用,且仅支持 **直接** TCP TLS 客户端连接。 + +!!! warning "" + + 仅供实验用途:由于 CGO 和 Network.framework 占用的内存都很多, + 不应在 iOS 和 tvOS 的热路径中使用。 + 如果您想规避基于 TLS 指纹的代理审查,应使用 [NaiveProxy](/zh/configuration/outbound/naive/)。 + +支持的字段: + +* `server_name` +* `insecure` +* `alpn` +* `min_version` +* `max_version` +* `certificate` / `certificate_path` +* `certificate_public_key_sha256` +* `handshake_timeout` + +不支持的字段: + +* `disable_sni` +* `cipher_suites` +* `curve_preferences` +* `client_certificate` / `client_certificate_path` / `client_key` / `client_key_path` +* `fragment` / `record_fragment` +* `kernel_tx` / `kernel_rx` +* `ech` +* `utls` +* `reality` + #### disable_sni ==仅客户端== @@ -416,6 +464,14 @@ echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/ 启用内核 TLS 接收支持。 +#### handshake_timeout + +!!! question "自 sing-box 1.14.0 起" + +TLS 握手超时,采用 golang 的 Duration 格式。 + +默认使用 `15s`。 + #### certificate_provider !!! question "自 sing-box 1.14.0 起" diff --git a/docs/deprecated.md b/docs/deprecated.md index 1eeab10d33..8a4b05e98b 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -6,6 +6,27 @@ icon: material/delete-alert ## 1.14.0 +#### Legacy `download_detour` remote rule-set option + +Legacy `download_detour` remote rule-set option is deprecated, +use `http_client` instead. + +Old field will be removed in sing-box 1.16.0. + +#### Implicit default HTTP client + +Implicit default HTTP client using the default outbound for remote rule-sets is deprecated. +Configure `http_clients` and `route.default_http_client` explicitly. + +Old behavior will be removed in sing-box 1.16.0. + +#### Legacy dialer options in Tailscale endpoint + +Legacy dialer options in Tailscale endpoints are deprecated, +use `control_http_client` instead. + +Old fields will be removed in sing-box 1.16.0. + #### Inline ACME options in TLS Inline ACME options (`tls.acme`) are deprecated diff --git a/docs/deprecated.zh.md b/docs/deprecated.zh.md index 5dabd69fb4..8888807d29 100644 --- a/docs/deprecated.zh.md +++ b/docs/deprecated.zh.md @@ -6,6 +6,27 @@ icon: material/delete-alert ## 1.14.0 +#### 旧版远程规则集 `download_detour` 选项 + +旧版远程规则集 `download_detour` 选项已废弃, +请使用 `http_client` 代替。 + +旧字段将在 sing-box 1.16.0 中被移除。 + +#### 隐式默认 HTTP 客户端 + +使用默认出站为远程规则集隐式创建默认 HTTP 客户端的行为已废弃。 +请显式配置 `http_clients` 和 `route.default_http_client`。 + +旧行为将在 sing-box 1.16.0 中被移除。 + +#### Tailscale 端点中的旧版拨号选项 + +Tailscale 端点中的旧版拨号选项已废弃, +请使用 `control_http_client` 代替。 + +旧字段将在 sing-box 1.16.0 中被移除。 + #### TLS 中的内联 ACME 选项 TLS 中的内联 ACME 选项(`tls.acme`)已废弃, diff --git a/experimental/deprecated/constants.go b/experimental/deprecated/constants.go index 108eba575b..106514caae 100644 --- a/experimental/deprecated/constants.go +++ b/experimental/deprecated/constants.go @@ -93,6 +93,22 @@ var OptionInlineACME = Note{ MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-inline-acme-to-certificate-provider", } +var OptionLegacyRuleSetDownloadDetour = Note{ + Name: "legacy-rule-set-download-detour", + Description: "legacy `download_detour` remote rule-set option", + DeprecatedVersion: "1.14.0", + ScheduledVersion: "1.16.0", + EnvName: "LEGACY_RULE_SET_DOWNLOAD_DETOUR", +} + +var OptionLegacyTailscaleEndpointDialer = Note{ + Name: "legacy-tailscale-endpoint-dialer", + Description: "legacy dialer options in Tailscale endpoint", + DeprecatedVersion: "1.14.0", + ScheduledVersion: "1.16.0", + EnvName: "LEGACY_TAILSCALE_ENDPOINT_DIALER", +} + var OptionRuleSetIPCIDRAcceptEmpty = Note{ Name: "dns-rule-rule-set-ip-cidr-accept-empty", Description: "Legacy `rule_set_ip_cidr_accept_empty` DNS rule item", @@ -138,14 +154,25 @@ var OptionStoreRDRC = Note{ MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-store-rdrc", } +var OptionImplicitDefaultHTTPClient = Note{ + Name: "implicit-default-http-client", + Description: "implicit default HTTP client using default outbound for remote rule-sets", + DeprecatedVersion: "1.14.0", + ScheduledVersion: "1.16.0", + EnvName: "IMPLICIT_DEFAULT_HTTP_CLIENT", +} + var Options = []Note{ OptionOutboundDNSRuleItem, OptionMissingDomainResolver, OptionLegacyDomainStrategyOptions, OptionInlineACME, + OptionLegacyRuleSetDownloadDetour, + OptionLegacyTailscaleEndpointDialer, OptionRuleSetIPCIDRAcceptEmpty, OptionLegacyDNSAddressFilter, OptionLegacyDNSRuleStrategy, OptionIndependentDNSCache, OptionStoreRDRC, + OptionImplicitDefaultHTTPClient, } diff --git a/go.mod b/go.mod index f652867a0d..bfdf00193c 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( github.com/sagernet/sing v0.8.10-0.20260421111925-3e730f2301b4 github.com/sagernet/sing-cloudflared v0.1.0 github.com/sagernet/sing-mux v0.3.4 - github.com/sagernet/sing-quic v0.6.2-0.20260330152607-bf674c163212 + github.com/sagernet/sing-quic v0.6.2-0.20260412143638-8f65b6be7cd6 github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 diff --git a/go.sum b/go.sum index 89ed708e01..328595092f 100644 --- a/go.sum +++ b/go.sum @@ -248,8 +248,8 @@ github.com/sagernet/sing-cloudflared v0.1.0 h1:to+2fcCx8zu4X/DirRw9Ihc+FrEZ7oEyI github.com/sagernet/sing-cloudflared v0.1.0/go.mod h1:bH2NKX+NpDTY1Zkxfboxw6MXB/ZywaNLmrDJYgKMJ2Y= github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s= github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= -github.com/sagernet/sing-quic v0.6.2-0.20260330152607-bf674c163212 h1:7mFOUqy+DyOj7qKGd1X54UMXbnbJiiMileK/tn17xYc= -github.com/sagernet/sing-quic v0.6.2-0.20260330152607-bf674c163212/go.mod h1:K5bWvITOm4vE10fwLfrWpw27bCoVJ+tfQ79tOWg+Ko8= +github.com/sagernet/sing-quic v0.6.2-0.20260412143638-8f65b6be7cd6 h1:j3ISQRDyY5rs27NzUS/le+DHR0iOO0K0x+mWDLzu4Ok= +github.com/sagernet/sing-quic v0.6.2-0.20260412143638-8f65b6be7cd6/go.mod h1:r5Adw0EMUyhGBCjPI2JEupDtC040DrrvreXtua7Ifdc= github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE= github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI= github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo= diff --git a/mkdocs.yml b/mkdocs.yml index 5387be9d51..8a583f15ba 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -122,6 +122,9 @@ nav: - Listen Fields: configuration/shared/listen.md - Dial Fields: configuration/shared/dial.md - TLS: configuration/shared/tls.md + - HTTP Client: configuration/shared/http-client.md + - HTTP2 Fields: configuration/shared/http2.md + - QUIC Fields: configuration/shared/quic.md - Certificate Provider: - configuration/shared/certificate-provider/index.md - ACME: configuration/shared/certificate-provider/acme.md diff --git a/option/acme.go b/option/acme.go index ea9349b724..79260b5dff 100644 --- a/option/acme.go +++ b/option/acme.go @@ -24,7 +24,7 @@ type ACMECertificateProviderOptions struct { ExternalAccount *ACMEExternalAccountOptions `json:"external_account,omitempty"` DNS01Challenge *ACMEProviderDNS01ChallengeOptions `json:"dns01_challenge,omitempty"` KeyType ACMEKeyType `json:"key_type,omitempty"` - Detour string `json:"detour,omitempty"` + HTTPClient *HTTPClientOptions `json:"http_client,omitempty"` } type _ACMEProviderDNS01ChallengeOptions struct { diff --git a/option/http.go b/option/http.go new file mode 100644 index 0000000000..fc7e16df96 --- /dev/null +++ b/option/http.go @@ -0,0 +1,126 @@ +package option + +import ( + "reflect" + + "github.com/sagernet/sing/common/byteformats" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/json/badoption" +) + +type HTTP2Options struct { + IdleTimeout badoption.Duration `json:"idle_timeout,omitempty"` + KeepAlivePeriod badoption.Duration `json:"keep_alive_period,omitempty"` + StreamReceiveWindow byteformats.MemoryBytes `json:"stream_receive_window,omitempty"` + ConnectionReceiveWindow byteformats.MemoryBytes `json:"connection_receive_window,omitempty"` + MaxConcurrentStreams int `json:"max_concurrent_streams,omitempty"` +} + +type QUICOptions struct { + HTTP2Options + InitialPacketSize int `json:"initial_packet_size,omitempty"` + DisablePathMTUDiscovery bool `json:"disable_path_mtu_discovery,omitempty"` +} + +type _HTTPClientOptions struct { + Tag string `json:"tag,omitempty"` + Engine string `json:"engine,omitempty"` + Version int `json:"version,omitempty"` + DisableVersionFallback bool `json:"disable_version_fallback,omitempty"` + Headers badoption.HTTPHeader `json:"headers,omitempty"` + HTTP2Options HTTP2Options `json:"-"` + HTTP3Options QUICOptions `json:"-"` + DefaultOutbound bool `json:"-"` + ResolveOnDetour bool `json:"-"` + DirectResolver bool `json:"-"` + OutboundTLSOptionsContainer + DialerOptions +} + +type ( + HTTPClient _HTTPClientOptions + HTTPClientOptions _HTTPClientOptions +) + +func (h HTTPClient) Options() HTTPClientOptions { + options := HTTPClientOptions(h) + options.Tag = "" + return options +} + +func (o HTTPClientOptions) IsEmpty() bool { + if o.Tag != "" { + return false + } + o.DefaultOutbound = false + o.ResolveOnDetour = false + o.DirectResolver = false + return reflect.ValueOf(_HTTPClientOptions(o)).IsZero() +} + +func (o HTTPClientOptions) MarshalJSON() ([]byte, error) { + if o.Tag != "" { + return json.Marshal(o.Tag) + } + return badjson.MarshallObjects(_HTTPClientOptions(o), httpClientVariant(_HTTPClientOptions(o))) +} + +func (o *HTTPClientOptions) UnmarshalJSON(content []byte) error { + if len(content) > 0 && content[0] == '"' { + *o = HTTPClientOptions{} + return json.Unmarshal(content, &o.Tag) + } + var options _HTTPClientOptions + err := json.Unmarshal(content, &options) + if err != nil { + return err + } + err = unmarshalHTTPClientVersionOptions(content, &options, &options) + if err != nil { + return err + } + options.Tag = "" + *o = HTTPClientOptions(options) + return nil +} + +func (h HTTPClient) MarshalJSON() ([]byte, error) { + return badjson.MarshallObjects(_HTTPClientOptions(h), httpClientVariant(_HTTPClientOptions(h))) +} + +func (h *HTTPClient) UnmarshalJSON(content []byte) error { + err := json.Unmarshal(content, (*_HTTPClientOptions)(h)) + if err != nil { + return err + } + return unmarshalHTTPClientVersionOptions(content, (*_HTTPClientOptions)(h), (*_HTTPClientOptions)(h)) +} + +func unmarshalHTTPClientVersionOptions(content []byte, baseStruct any, options *_HTTPClientOptions) error { + switch options.Version { + case 1: + return json.UnmarshalDisallowUnknownFields(content, baseStruct) + case 0, 2: + options.Version = 2 + return badjson.UnmarshallExcluded(content, baseStruct, &options.HTTP2Options) + case 3: + return badjson.UnmarshallExcluded(content, baseStruct, &options.HTTP3Options) + default: + return E.New("unknown HTTP version: ", options.Version) + } +} + +func httpClientVariant(options _HTTPClientOptions) any { + switch options.Version { + case 1: + return nil + case 0, 2: + return options.HTTP2Options + case 3: + return options.HTTP3Options + default: + return nil + } +} diff --git a/option/hysteria.go b/option/hysteria.go index 186759010e..f5ab87cec1 100644 --- a/option/hysteria.go +++ b/option/hysteria.go @@ -7,17 +7,22 @@ import ( type HysteriaInboundOptions struct { ListenOptions - Up *byteformats.NetworkBytesCompat `json:"up,omitempty"` - UpMbps int `json:"up_mbps,omitempty"` - Down *byteformats.NetworkBytesCompat `json:"down,omitempty"` - DownMbps int `json:"down_mbps,omitempty"` - Obfs string `json:"obfs,omitempty"` - Users []HysteriaUser `json:"users,omitempty"` - ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"` - ReceiveWindowClient uint64 `json:"recv_window_client,omitempty"` - MaxConnClient int `json:"max_conn_client,omitempty"` - DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"` + Up *byteformats.NetworkBytesCompat `json:"up,omitempty"` + UpMbps int `json:"up_mbps,omitempty"` + Down *byteformats.NetworkBytesCompat `json:"down,omitempty"` + DownMbps int `json:"down_mbps,omitempty"` + Obfs string `json:"obfs,omitempty"` + Users []HysteriaUser `json:"users,omitempty"` + // Deprecated: use QUIC fields instead + ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"` + // Deprecated: use QUIC fields instead + ReceiveWindowClient uint64 `json:"recv_window_client,omitempty"` + // Deprecated: use QUIC fields instead + MaxConnClient int `json:"max_conn_client,omitempty"` + // Deprecated: use QUIC fields instead + DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"` InboundTLSOptionsContainer + QUICOptions } type HysteriaUser struct { @@ -29,18 +34,22 @@ type HysteriaUser struct { type HysteriaOutboundOptions struct { DialerOptions ServerOptions - ServerPorts badoption.Listable[string] `json:"server_ports,omitempty"` - HopInterval badoption.Duration `json:"hop_interval,omitempty"` - Up *byteformats.NetworkBytesCompat `json:"up,omitempty"` - UpMbps int `json:"up_mbps,omitempty"` - Down *byteformats.NetworkBytesCompat `json:"down,omitempty"` - DownMbps int `json:"down_mbps,omitempty"` - Obfs string `json:"obfs,omitempty"` - Auth []byte `json:"auth,omitempty"` - AuthString string `json:"auth_str,omitempty"` - ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"` - ReceiveWindow uint64 `json:"recv_window,omitempty"` - DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"` - Network NetworkList `json:"network,omitempty"` + ServerPorts badoption.Listable[string] `json:"server_ports,omitempty"` + HopInterval badoption.Duration `json:"hop_interval,omitempty"` + Up *byteformats.NetworkBytesCompat `json:"up,omitempty"` + UpMbps int `json:"up_mbps,omitempty"` + Down *byteformats.NetworkBytesCompat `json:"down,omitempty"` + DownMbps int `json:"down_mbps,omitempty"` + Obfs string `json:"obfs,omitempty"` + Auth []byte `json:"auth,omitempty"` + AuthString string `json:"auth_str,omitempty"` + // Deprecated: use QUIC fields instead + ReceiveWindowConn uint64 `json:"recv_window_conn,omitempty"` + // Deprecated: use QUIC fields instead + ReceiveWindow uint64 `json:"recv_window,omitempty"` + // Deprecated: use QUIC fields instead + DisableMTUDiscovery bool `json:"disable_mtu_discovery,omitempty"` + Network NetworkList `json:"network,omitempty"` OutboundTLSOptionsContainer + QUICOptions } diff --git a/option/hysteria2.go b/option/hysteria2.go index e31c8de345..e1a54e4b8a 100644 --- a/option/hysteria2.go +++ b/option/hysteria2.go @@ -18,6 +18,7 @@ type Hysteria2InboundOptions struct { Users []Hysteria2User `json:"users,omitempty"` IgnoreClientBandwidth bool `json:"ignore_client_bandwidth,omitempty"` InboundTLSOptionsContainer + QUICOptions Masquerade *Hysteria2Masquerade `json:"masquerade,omitempty"` BBRProfile string `json:"bbr_profile,omitempty"` BrutalDebug bool `json:"brutal_debug,omitempty"` @@ -122,6 +123,7 @@ type Hysteria2OutboundOptions struct { Password string `json:"password,omitempty"` Network NetworkList `json:"network,omitempty"` OutboundTLSOptionsContainer + QUICOptions BBRProfile string `json:"bbr_profile,omitempty"` BrutalDebug bool `json:"brutal_debug,omitempty"` } diff --git a/option/options.go b/option/options.go index a08dcbc0f1..1b4685bac8 100644 --- a/option/options.go +++ b/option/options.go @@ -17,6 +17,7 @@ type _Options struct { NTP *NTPOptions `json:"ntp,omitempty"` Certificate *CertificateOptions `json:"certificate,omitempty"` CertificateProviders []CertificateProvider `json:"certificate_providers,omitempty"` + HTTPClients []HTTPClient `json:"http_clients,omitempty"` Endpoints []Endpoint `json:"endpoints,omitempty"` Inbounds []Inbound `json:"inbounds,omitempty"` Outbounds []Outbound `json:"outbounds,omitempty"` @@ -61,6 +62,10 @@ func checkOptions(options *Options) error { if err != nil { return err } + err = checkHTTPClients(options.HTTPClients) + if err != nil { + return err + } return nil } @@ -79,6 +84,20 @@ func checkCertificateProviders(providers []CertificateProvider) error { return nil } +func checkHTTPClients(clients []HTTPClient) error { + seen := make(map[string]bool) + for _, client := range clients { + if client.Tag == "" { + return E.New("missing http client tag") + } + if seen[client.Tag] { + return E.New("duplicate http client tag: ", client.Tag) + } + seen[client.Tag] = true + } + return nil +} + func checkInbounds(inbounds []Inbound) error { seen := make(map[string]bool) for i, inbound := range inbounds { diff --git a/option/origin_ca.go b/option/origin_ca.go index ee8b370414..5a9f956af7 100644 --- a/option/origin_ca.go +++ b/option/origin_ca.go @@ -15,7 +15,7 @@ type CloudflareOriginCACertificateProviderOptions struct { OriginCAKey string `json:"origin_ca_key,omitempty"` RequestType CloudflareOriginCARequestType `json:"request_type,omitempty"` RequestedValidity CloudflareOriginCARequestValidity `json:"requested_validity,omitempty"` - Detour string `json:"detour,omitempty"` + HTTPClient *HTTPClientOptions `json:"http_client,omitempty"` } type CloudflareOriginCARequestType string diff --git a/option/route.go b/option/route.go index 0c3e576d13..893be8ece2 100644 --- a/option/route.go +++ b/option/route.go @@ -20,6 +20,7 @@ type RouteOptions struct { DefaultNetworkType badoption.Listable[InterfaceType] `json:"default_network_type,omitempty"` DefaultFallbackNetworkType badoption.Listable[InterfaceType] `json:"default_fallback_network_type,omitempty"` DefaultFallbackDelay badoption.Duration `json:"default_fallback_delay,omitempty"` + DefaultHTTPClient string `json:"default_http_client,omitempty"` } type GeoIPOptions struct { diff --git a/option/rule_set.go b/option/rule_set.go index 2ca2529af8..024d101f2f 100644 --- a/option/rule_set.go +++ b/option/rule_set.go @@ -122,8 +122,10 @@ type LocalRuleSet struct { type RemoteRuleSet struct { URL string `json:"url"` - DownloadDetour string `json:"download_detour,omitempty"` + HTTPClient *HTTPClientOptions `json:"http_client,omitempty"` UpdateInterval badoption.Duration `json:"update_interval,omitempty"` + // Deprecated: use http_client instead + DownloadDetour string `json:"download_detour,omitempty"` } type _HeadlessRule struct { diff --git a/option/tailscale.go b/option/tailscale.go index a4f82ce0de..f763c905d9 100644 --- a/option/tailscale.go +++ b/option/tailscale.go @@ -3,18 +3,20 @@ package option import ( "net/netip" "net/url" - "reflect" "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" "github.com/sagernet/sing/common/json/badoption" M "github.com/sagernet/sing/common/metadata" ) type TailscaleEndpointOptions struct { + // Deprecated: use control_http_client instead DialerOptions StateDirectory string `json:"state_directory,omitempty"` AuthKey string `json:"auth_key,omitempty"` ControlURL string `json:"control_url,omitempty"` + ControlHTTPClient *HTTPClientOptions `json:"control_http_client,omitempty"` Ephemeral bool `json:"ephemeral,omitempty"` Hostname string `json:"hostname,omitempty"` AcceptRoutes bool `json:"accept_routes,omitempty"` @@ -53,9 +55,13 @@ type DERPServiceOptions struct { STUN *DERPSTUNListenOptions `json:"stun,omitempty"` } -type _DERPVerifyClientURLOptions struct { +type _DERPVerifyClientURLBase struct { URL string `json:"url,omitempty"` - DialerOptions +} + +type _DERPVerifyClientURLOptions struct { + _DERPVerifyClientURLBase + HTTPClientOptions } type DERPVerifyClientURLOptions _DERPVerifyClientURLOptions @@ -69,21 +75,32 @@ func (d DERPVerifyClientURLOptions) ServerIsDomain() bool { } func (d DERPVerifyClientURLOptions) MarshalJSON() ([]byte, error) { - if reflect.DeepEqual(d, _DERPVerifyClientURLOptions{}) { + if d.URL != "" && d.HTTPClientOptions.IsEmpty() { return json.Marshal(d.URL) - } else { - return json.Marshal(_DERPVerifyClientURLOptions(d)) } + return badjson.MarshallObjects(d._DERPVerifyClientURLBase, HTTPClient(d.HTTPClientOptions)) } func (d *DERPVerifyClientURLOptions) UnmarshalJSON(bytes []byte) error { var stringValue string err := json.Unmarshal(bytes, &stringValue) if err == nil { - d.URL = stringValue + *d = DERPVerifyClientURLOptions{ + _DERPVerifyClientURLBase: _DERPVerifyClientURLBase{URL: stringValue}, + } return nil } - return json.Unmarshal(bytes, (*_DERPVerifyClientURLOptions)(d)) + err = json.Unmarshal(bytes, &d._DERPVerifyClientURLBase) + if err != nil { + return err + } + var client HTTPClient + err = badjson.UnmarshallExcluded(bytes, &d._DERPVerifyClientURLBase, &client) + if err != nil { + return err + } + d.HTTPClientOptions = HTTPClientOptions(client) + return nil } type DERPMeshOptions struct { diff --git a/option/tls.go b/option/tls.go index dbbb7620ed..644c9f12ff 100644 --- a/option/tls.go +++ b/option/tls.go @@ -28,6 +28,7 @@ type InboundTLSOptions struct { KeyPath string `json:"key_path,omitempty"` KernelTx bool `json:"kernel_tx,omitempty"` KernelRx bool `json:"kernel_rx,omitempty"` + HandshakeTimeout badoption.Duration `json:"handshake_timeout,omitempty"` CertificateProvider *CertificateProviderOptions `json:"certificate_provider,omitempty"` // Deprecated: use certificate_provider @@ -100,6 +101,7 @@ func (o *InboundTLSOptionsContainer) ReplaceInboundTLSOptions(options *InboundTL type OutboundTLSOptions struct { Enabled bool `json:"enabled,omitempty"` + Engine string `json:"engine,omitempty"` DisableSNI bool `json:"disable_sni,omitempty"` ServerName string `json:"server_name,omitempty"` Insecure bool `json:"insecure,omitempty"` @@ -120,6 +122,7 @@ type OutboundTLSOptions struct { RecordFragment bool `json:"record_fragment,omitempty"` KernelTx bool `json:"kernel_tx,omitempty"` KernelRx bool `json:"kernel_rx,omitempty"` + HandshakeTimeout badoption.Duration `json:"handshake_timeout,omitempty"` ECH *OutboundECHOptions `json:"ech,omitempty"` UTLS *OutboundUTLSOptions `json:"utls,omitempty"` Reality *OutboundRealityOptions `json:"reality,omitempty"` diff --git a/option/tuic.go b/option/tuic.go index a9b739ec69..51cc0a5b96 100644 --- a/option/tuic.go +++ b/option/tuic.go @@ -10,6 +10,7 @@ type TUICInboundOptions struct { ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"` Heartbeat badoption.Duration `json:"heartbeat,omitempty"` InboundTLSOptionsContainer + QUICOptions } type TUICUser struct { @@ -30,4 +31,5 @@ type TUICOutboundOptions struct { Heartbeat badoption.Duration `json:"heartbeat,omitempty"` Network NetworkList `json:"network,omitempty"` OutboundTLSOptionsContainer + QUICOptions } diff --git a/protocol/hysteria/inbound.go b/protocol/hysteria/inbound.go index 98d7cb8106..6fd4fe9716 100644 --- a/protocol/hysteria/inbound.go +++ b/protocol/hysteria/inbound.go @@ -77,15 +77,9 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo ReceiveBPS: receiveBps, XPlusPassword: options.Obfs, TLSConfig: tlsConfig, + QUICOptions: buildInboundQUICOptions(options), UDPTimeout: udpTimeout, Handler: inbound, - - // Legacy options - - ConnReceiveWindow: options.ReceiveWindowConn, - StreamReceiveWindow: options.ReceiveWindowClient, - MaxIncomingStreams: int64(options.MaxConnClient), - DisableMTUDiscovery: options.DisableMTUDiscovery, }) if err != nil { return nil, err diff --git a/protocol/hysteria/outbound.go b/protocol/hysteria/outbound.go index bcadd878ab..bd6c5e4a37 100644 --- a/protocol/hysteria/outbound.go +++ b/protocol/hysteria/outbound.go @@ -70,21 +70,19 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL receiveBps = uint64(options.DownMbps) * hysteria.MbpsToBps } client, err := hysteria.NewClient(hysteria.ClientOptions{ - Context: ctx, - Dialer: outboundDialer, - Logger: logger, - ServerAddress: options.ServerOptions.Build(), - ServerPorts: options.ServerPorts, - HopInterval: time.Duration(options.HopInterval), - SendBPS: sendBps, - ReceiveBPS: receiveBps, - XPlusPassword: options.Obfs, - Password: password, - TLSConfig: tlsConfig, - UDPDisabled: !common.Contains(networkList, N.NetworkUDP), - ConnReceiveWindow: options.ReceiveWindowConn, - StreamReceiveWindow: options.ReceiveWindow, - DisableMTUDiscovery: options.DisableMTUDiscovery, + Context: ctx, + Dialer: outboundDialer, + Logger: logger, + ServerAddress: options.ServerOptions.Build(), + ServerPorts: options.ServerPorts, + HopInterval: time.Duration(options.HopInterval), + SendBPS: sendBps, + ReceiveBPS: receiveBps, + XPlusPassword: options.Obfs, + Password: password, + TLSConfig: tlsConfig, + QUICOptions: buildOutboundQUICOptions(options), + UDPDisabled: !common.Contains(networkList, N.NetworkUDP), }) if err != nil { return nil, err diff --git a/protocol/hysteria/quic.go b/protocol/hysteria/quic.go new file mode 100644 index 0000000000..5d6e039c3f --- /dev/null +++ b/protocol/hysteria/quic.go @@ -0,0 +1,49 @@ +package hysteria + +import ( + "github.com/sagernet/sing-box/option" + qtls "github.com/sagernet/sing-quic" +) + +func buildBaseQUICOptions(options option.QUICOptions) qtls.QUICOptions { + return qtls.QUICOptions{ + IdleTimeout: options.IdleTimeout.Build(), + KeepAlivePeriod: options.KeepAlivePeriod.Build(), + StreamReceiveWindow: options.StreamReceiveWindow.Value(), + ConnectionReceiveWindow: options.ConnectionReceiveWindow.Value(), + MaxConcurrentStreams: options.MaxConcurrentStreams, + InitialPacketSize: options.InitialPacketSize, + DisablePathMTUDiscovery: options.DisablePathMTUDiscovery, + } +} + +func buildInboundQUICOptions(options option.HysteriaInboundOptions) qtls.QUICOptions { + quicOptions := buildBaseQUICOptions(options.QUICOptions) + if quicOptions.ConnectionReceiveWindow == 0 { + quicOptions.ConnectionReceiveWindow = options.ReceiveWindowConn //nolint:staticcheck + } + if quicOptions.StreamReceiveWindow == 0 { + quicOptions.StreamReceiveWindow = options.ReceiveWindowClient //nolint:staticcheck + } + if quicOptions.MaxConcurrentStreams == 0 { + quicOptions.MaxConcurrentStreams = options.MaxConnClient //nolint:staticcheck + } + if !quicOptions.DisablePathMTUDiscovery { + quicOptions.DisablePathMTUDiscovery = options.DisableMTUDiscovery //nolint:staticcheck + } + return quicOptions +} + +func buildOutboundQUICOptions(options option.HysteriaOutboundOptions) qtls.QUICOptions { + quicOptions := buildBaseQUICOptions(options.QUICOptions) + if quicOptions.ConnectionReceiveWindow == 0 { + quicOptions.ConnectionReceiveWindow = options.ReceiveWindowConn //nolint:staticcheck + } + if quicOptions.StreamReceiveWindow == 0 { + quicOptions.StreamReceiveWindow = options.ReceiveWindow //nolint:staticcheck + } + if !quicOptions.DisablePathMTUDiscovery { + quicOptions.DisablePathMTUDiscovery = options.DisableMTUDiscovery //nolint:staticcheck + } + return quicOptions +} diff --git a/protocol/hysteria2/inbound.go b/protocol/hysteria2/inbound.go index 5fe8848d9a..a94c26dd79 100644 --- a/protocol/hysteria2/inbound.go +++ b/protocol/hysteria2/inbound.go @@ -15,6 +15,7 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" + qtls "github.com/sagernet/sing-quic" "github.com/sagernet/sing-quic/hysteria" "github.com/sagernet/sing-quic/hysteria2" "github.com/sagernet/sing/common" @@ -114,13 +115,22 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo udpTimeout = C.UDPTimeout } service, err := hysteria2.NewService[int](hysteria2.ServiceOptions{ - Context: ctx, - Logger: logger, - BrutalDebug: options.BrutalDebug, - SendBPS: uint64(options.UpMbps * hysteria.MbpsToBps), - ReceiveBPS: uint64(options.DownMbps * hysteria.MbpsToBps), - SalamanderPassword: salamanderPassword, - TLSConfig: tlsConfig, + Context: ctx, + Logger: logger, + BrutalDebug: options.BrutalDebug, + SendBPS: uint64(options.UpMbps * hysteria.MbpsToBps), + ReceiveBPS: uint64(options.DownMbps * hysteria.MbpsToBps), + SalamanderPassword: salamanderPassword, + TLSConfig: tlsConfig, + QUICOptions: qtls.QUICOptions{ + IdleTimeout: options.IdleTimeout.Build(), + KeepAlivePeriod: options.KeepAlivePeriod.Build(), + StreamReceiveWindow: options.StreamReceiveWindow.Value(), + ConnectionReceiveWindow: options.ConnectionReceiveWindow.Value(), + MaxConcurrentStreams: options.MaxConcurrentStreams, + InitialPacketSize: options.InitialPacketSize, + DisablePathMTUDiscovery: options.DisablePathMTUDiscovery, + }, IgnoreClientBandwidth: options.IgnoreClientBandwidth, UDPTimeout: udpTimeout, Handler: inbound, diff --git a/protocol/hysteria2/outbound.go b/protocol/hysteria2/outbound.go index 4a0c9f2430..fe23109a23 100644 --- a/protocol/hysteria2/outbound.go +++ b/protocol/hysteria2/outbound.go @@ -14,6 +14,7 @@ import ( "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/protocol/tuic" + qtls "github.com/sagernet/sing-quic" "github.com/sagernet/sing-quic/hysteria" "github.com/sagernet/sing-quic/hysteria2" "github.com/sagernet/sing/common" @@ -79,8 +80,17 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL SalamanderPassword: salamanderPassword, Password: options.Password, TLSConfig: tlsConfig, - UDPDisabled: !common.Contains(networkList, N.NetworkUDP), - BBRProfile: options.BBRProfile, + QUICOptions: qtls.QUICOptions{ + IdleTimeout: options.IdleTimeout.Build(), + KeepAlivePeriod: options.KeepAlivePeriod.Build(), + StreamReceiveWindow: options.StreamReceiveWindow.Value(), + ConnectionReceiveWindow: options.ConnectionReceiveWindow.Value(), + MaxConcurrentStreams: options.MaxConcurrentStreams, + InitialPacketSize: options.InitialPacketSize, + DisablePathMTUDiscovery: options.DisablePathMTUDiscovery, + }, + UDPDisabled: !common.Contains(networkList, N.NetworkUDP), + BBRProfile: options.BBRProfile, }) if err != nil { return nil, err diff --git a/protocol/tailscale/certificate_provider.go b/protocol/tailscale/certificate_provider.go index 5ac18a3073..8170943b12 100644 --- a/protocol/tailscale/certificate_provider.go +++ b/protocol/tailscale/certificate_provider.go @@ -39,9 +39,6 @@ func NewCertificateProvider(ctx context.Context, _ log.ContextLogger, tag string return nil, E.New("missing tailscale endpoint tag") } endpointManager := service.FromContext[adapter.EndpointManager](ctx) - if endpointManager == nil { - return nil, E.New("missing endpoint manager in context") - } rawEndpoint, loaded := endpointManager.Get(options.Endpoint) if !loaded { return nil, E.New("endpoint not found: ", options.Endpoint) diff --git a/protocol/tailscale/endpoint.go b/protocol/tailscale/endpoint.go index 30db4b6ab5..3717a9c865 100644 --- a/protocol/tailscale/endpoint.go +++ b/protocol/tailscale/endpoint.go @@ -4,7 +4,6 @@ package tailscale import ( "context" - "crypto/tls" "fmt" "net" "net/http" @@ -28,6 +27,7 @@ import ( "github.com/sagernet/sing-box/adapter/endpoint" "github.com/sagernet/sing-box/common/dialer" C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/experimental/deprecated" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/route/rule" @@ -41,7 +41,6 @@ import ( "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" - "github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/filemanager" _ "github.com/sagernet/tailscale/feature/relayserver" @@ -196,6 +195,19 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL // controlplane.tailscale.com remoteIsDomain = true } + hasLegacyDialer := !reflect.DeepEqual(options.DialerOptions, option.DialerOptions{}) + hasControlHTTPClient := options.ControlHTTPClient != nil && !options.ControlHTTPClient.IsEmpty() + if hasLegacyDialer && hasControlHTTPClient { + return nil, E.New("control_http_client is conflict with deprecated dialer options") + } + controlHTTPClientOptions := common.PtrValueOrDefault(options.ControlHTTPClient) + if hasLegacyDialer { + deprecated.Report(ctx, deprecated.OptionLegacyTailscaleEndpointDialer) + controlHTTPClientOptions.DialerOptions = options.DialerOptions + } + if remoteIsDomain { + controlHTTPClientOptions.ResolveOnDetour = true + } outboundDialer, err := dialer.NewWithOptions(dialer.Options{ Context: ctx, Options: options.DialerOptions, @@ -207,6 +219,12 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL return nil, err } dnsRouter := service.FromContext[adapter.DNSRouter](ctx) + httpClientManager := service.FromContext[adapter.HTTPClientManager](ctx) + controlTransport, err := httpClientManager.ResolveTransport(ctx, logger, controlHTTPClientOptions) + if err != nil { + return nil, E.Cause(err, "create control HTTP client") + } + controlHTTPClient := &http.Client{Transport: controlTransport} server := &tsnet.Server{ Dir: stateDirectory, Hostname: hostname, @@ -224,19 +242,8 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL LookupHook: func(ctx context.Context, host string) ([]netip.Addr, error) { return dnsRouter.Lookup(ctx, host, outboundDialer.(dialer.ResolveDialer).QueryOptions()) }, - DNS: &dnsConfigurtor{}, - HTTPClient: &http.Client{ - Transport: &http.Transport{ - ForceAttemptHTTP2: true, - DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { - return outboundDialer.DialContext(ctx, network, M.ParseSocksaddr(address)) - }, - TLSClientConfig: &tls.Config{ - RootCAs: adapter.RootPoolFromContext(ctx), - Time: ntp.TimeFuncFromContext(ctx), - }, - }, - }, + DNS: &dnsConfigurtor{}, + HTTPClient: controlHTTPClient, } return &Endpoint{ Adapter: endpoint.NewAdapter(C.TypeTailscale, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, nil), diff --git a/protocol/tor/outbound.go b/protocol/tor/outbound.go index 9a0e2d6506..6f0c3fd6d9 100644 --- a/protocol/tor/outbound.go +++ b/protocol/tor/outbound.go @@ -10,6 +10,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/proxybridge" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" @@ -34,7 +35,7 @@ type Outbound struct { outbound.Adapter ctx context.Context logger logger.ContextLogger - proxy *ProxyListener + proxy *proxybridge.Bridge startConf *tor.StartConf options map[string]string events chan control.Event @@ -79,11 +80,15 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL if err != nil { return nil, err } + proxy, err := proxybridge.New(ctx, logger, "proxy", outboundDialer) + if err != nil { + return nil, err + } return &Outbound{ Adapter: outbound.NewAdapterWithDialerOptions(C.TypeTor, tag, []string{N.NetworkTCP}, options.DialerOptions), ctx: ctx, logger: logger, - proxy: NewProxyListener(ctx, logger, outboundDialer), + proxy: proxy, startConf: &startConf, options: options.Options, }, nil @@ -117,10 +122,6 @@ func (t *Outbound) start() error { return err } go t.recvLoop() - err = t.proxy.Start() - if err != nil { - return err - } proxyPort := "127.0.0.1:" + F.ToString(t.proxy.Port()) proxyUsername := t.proxy.Username() proxyPassword := t.proxy.Password() diff --git a/protocol/tor/proxy.go b/protocol/tor/proxy.go deleted file mode 100644 index 378e74fc8c..0000000000 --- a/protocol/tor/proxy.go +++ /dev/null @@ -1,121 +0,0 @@ -package tor - -import ( - std_bufio "bufio" - "context" - "crypto/rand" - "encoding/hex" - "net" - - "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/auth" - E "github.com/sagernet/sing/common/exceptions" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" - "github.com/sagernet/sing/protocol/socks" - "github.com/sagernet/sing/service" -) - -type ProxyListener struct { - ctx context.Context - logger log.ContextLogger - dialer N.Dialer - connection adapter.ConnectionManager - tcpListener *net.TCPListener - username string - password string - authenticator *auth.Authenticator -} - -func NewProxyListener(ctx context.Context, logger log.ContextLogger, dialer N.Dialer) *ProxyListener { - var usernameB [64]byte - var passwordB [64]byte - rand.Read(usernameB[:]) - rand.Read(passwordB[:]) - username := hex.EncodeToString(usernameB[:]) - password := hex.EncodeToString(passwordB[:]) - return &ProxyListener{ - ctx: ctx, - logger: logger, - dialer: dialer, - connection: service.FromContext[adapter.ConnectionManager](ctx), - authenticator: auth.NewAuthenticator([]auth.User{{Username: username, Password: password}}), - username: username, - password: password, - } -} - -func (l *ProxyListener) Start() error { - tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{ - IP: net.IPv4(127, 0, 0, 1), - }) - if err != nil { - return err - } - l.tcpListener = tcpListener - go l.acceptLoop() - return nil -} - -func (l *ProxyListener) Port() uint16 { - if l.tcpListener == nil { - panic("start listener first") - } - return M.SocksaddrFromNet(l.tcpListener.Addr()).Port -} - -func (l *ProxyListener) Username() string { - return l.username -} - -func (l *ProxyListener) Password() string { - return l.password -} - -func (l *ProxyListener) Close() error { - return common.Close(l.tcpListener) -} - -func (l *ProxyListener) acceptLoop() { - for { - tcpConn, err := l.tcpListener.AcceptTCP() - if err != nil { - return - } - ctx := log.ContextWithNewID(l.ctx) - go func() { - hErr := l.accept(ctx, tcpConn) - if hErr != nil { - if E.IsClosedOrCanceled(hErr) { - l.logger.DebugContext(ctx, E.Cause(hErr, "proxy connection closed")) - return - } - l.logger.ErrorContext(ctx, E.Cause(hErr, "proxy")) - } - }() - } -} - -func (l *ProxyListener) accept(ctx context.Context, conn *net.TCPConn) error { - return socks.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), l.authenticator, l, nil, 0, M.SocksaddrFromNet(conn.RemoteAddr()), nil) -} - -func (l *ProxyListener) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { - var metadata adapter.InboundContext - metadata.Source = source - metadata.Destination = destination - metadata.Network = N.NetworkTCP - l.logger.InfoContext(ctx, "proxy connection to ", metadata.Destination) - l.connection.NewConnection(ctx, l.dialer, conn, metadata, onClose) -} - -func (l *ProxyListener) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { - var metadata adapter.InboundContext - metadata.Source = source - metadata.Destination = destination - metadata.Network = N.NetworkUDP - l.logger.InfoContext(ctx, "proxy packet connection to ", metadata.Destination) - l.connection.NewPacketConnection(ctx, l.dialer, conn, metadata, onClose) -} diff --git a/protocol/tuic/inbound.go b/protocol/tuic/inbound.go index 600c7f93a2..531d426361 100644 --- a/protocol/tuic/inbound.go +++ b/protocol/tuic/inbound.go @@ -13,6 +13,7 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" + qtls "github.com/sagernet/sing-quic" "github.com/sagernet/sing-quic/tuic" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/auth" @@ -64,9 +65,18 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo udpTimeout = C.UDPTimeout } service, err := tuic.NewService[int](tuic.ServiceOptions{ - Context: ctx, - Logger: logger, - TLSConfig: tlsConfig, + Context: ctx, + Logger: logger, + TLSConfig: tlsConfig, + QUICOptions: qtls.QUICOptions{ + IdleTimeout: options.IdleTimeout.Build(), + KeepAlivePeriod: options.KeepAlivePeriod.Build(), + StreamReceiveWindow: options.StreamReceiveWindow.Value(), + ConnectionReceiveWindow: options.ConnectionReceiveWindow.Value(), + MaxConcurrentStreams: options.MaxConcurrentStreams, + InitialPacketSize: options.InitialPacketSize, + DisablePathMTUDiscovery: options.DisablePathMTUDiscovery, + }, CongestionControl: options.CongestionControl, AuthTimeout: time.Duration(options.AuthTimeout), ZeroRTTHandshake: options.ZeroRTTHandshake, diff --git a/protocol/tuic/outbound.go b/protocol/tuic/outbound.go index 94d3cb774c..694c845152 100644 --- a/protocol/tuic/outbound.go +++ b/protocol/tuic/outbound.go @@ -13,6 +13,7 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" + qtls "github.com/sagernet/sing-quic" "github.com/sagernet/sing-quic/tuic" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/bufio" @@ -65,10 +66,19 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL return nil, err } client, err := tuic.NewClient(tuic.ClientOptions{ - Context: ctx, - Dialer: outboundDialer, - ServerAddress: options.ServerOptions.Build(), - TLSConfig: tlsConfig, + Context: ctx, + Dialer: outboundDialer, + ServerAddress: options.ServerOptions.Build(), + TLSConfig: tlsConfig, + QUICOptions: qtls.QUICOptions{ + IdleTimeout: options.IdleTimeout.Build(), + KeepAlivePeriod: options.KeepAlivePeriod.Build(), + StreamReceiveWindow: options.StreamReceiveWindow.Value(), + ConnectionReceiveWindow: options.ConnectionReceiveWindow.Value(), + MaxConcurrentStreams: options.MaxConcurrentStreams, + InitialPacketSize: options.InitialPacketSize, + DisablePathMTUDiscovery: options.DisablePathMTUDiscovery, + }, UUID: userUUID, Password: options.Password, CongestionControl: options.CongestionControl, diff --git a/route/router.go b/route/router.go index 03546b2a7e..72f549c382 100644 --- a/route/router.go +++ b/route/router.go @@ -33,6 +33,7 @@ type Router struct { dnsTransport adapter.DNSTransportManager connection adapter.ConnectionManager network adapter.NetworkManager + httpClientManager adapter.HTTPClientManager rules []adapter.Rule needFindProcess bool needFindNeighbor bool @@ -58,6 +59,7 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.Route dnsTransport: service.FromContext[adapter.DNSTransportManager](ctx), connection: service.FromContext[adapter.ConnectionManager](ctx), network: service.FromContext[adapter.NetworkManager](ctx), + httpClientManager: service.FromContext[adapter.HTTPClientManager](ctx), rules: make([]adapter.Rule, 0, len(options.Rules)), ruleSetMap: make(map[string]adapter.RuleSet), needFindProcess: hasRule(options.Rules, isProcessRule) || hasDNSRule(dnsOptions.Rules, isProcessDNSRule) || options.FindProcess, @@ -98,15 +100,15 @@ func (r *Router) Start(stage adapter.StartStage) error { monitor := taskmonitor.New(r.logger, C.StartTimeout) switch stage { case adapter.StartStateStart: - var cacheContext *adapter.HTTPStartContext + var startContext *adapter.HTTPStartContext if len(r.ruleSets) > 0 { monitor.Start("initialize rule-set") - cacheContext = adapter.NewHTTPStartContext(r.ctx) + startContext = adapter.NewHTTPStartContext() var ruleSetStartGroup task.Group for i, ruleSet := range r.ruleSets { ruleSetInPlace := ruleSet ruleSetStartGroup.Append0(func(ctx context.Context) error { - err := ruleSetInPlace.StartContext(ctx, cacheContext) + err := ruleSetInPlace.StartContext(ctx, startContext) if err != nil { return E.Cause(err, "initialize rule-set[", i, "]") } @@ -121,8 +123,8 @@ func (r *Router) Start(stage adapter.StartStage) error { return err } } - if cacheContext != nil { - cacheContext.Close() + if startContext != nil { + startContext.Close() } r.network.Initialize(r.ruleSets) needFindProcess := r.needFindProcess @@ -280,5 +282,6 @@ func (r *Router) NeighborResolver() adapter.NeighborResolver { func (r *Router) ResetNetwork() { r.network.ResetNetwork() + r.httpClientManager.ResetNetwork() r.dns.ResetNetwork() } diff --git a/route/rule/rule_set.go b/route/rule/rule_set.go index 7c82b6022e..3c2d9eda2a 100644 --- a/route/rule/rule_set.go +++ b/route/rule/rule_set.go @@ -19,7 +19,7 @@ func NewRuleSet(ctx context.Context, logger logger.ContextLogger, options option case C.RuleSetTypeInline, C.RuleSetTypeLocal, "": return NewLocalRuleSet(ctx, logger, options) case C.RuleSetTypeRemote: - return NewRemoteRuleSet(ctx, logger, options), nil + return NewRemoteRuleSet(ctx, logger, options) default: return nil, E.New("unknown rule-set type: ", options.Type) } diff --git a/route/rule/rule_set_remote.go b/route/rule/rule_set_remote.go index 53d353b3c1..24066d75af 100644 --- a/route/rule/rule_set_remote.go +++ b/route/rule/rule_set_remote.go @@ -3,9 +3,7 @@ package rule import ( "bytes" "context" - "crypto/tls" "io" - "net" "net/http" "runtime" "strings" @@ -16,15 +14,13 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/srs" C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/experimental/deprecated" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/logger" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" - "github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/pause" @@ -41,7 +37,7 @@ type RemoteRuleSet struct { outbound adapter.OutboundManager options option.RuleSet updateInterval time.Duration - dialer N.Dialer + httpClient *http.Client access sync.RWMutex rules []adapter.HeadlessRule metadata adapter.RuleSetMetadata @@ -54,7 +50,7 @@ type RemoteRuleSet struct { refs atomic.Int32 } -func NewRemoteRuleSet(ctx context.Context, logger logger.ContextLogger, options option.RuleSet) *RemoteRuleSet { +func NewRemoteRuleSet(ctx context.Context, logger logger.ContextLogger, options option.RuleSet) (*RemoteRuleSet, error) { ctx, cancel := context.WithCancel(ctx) var updateInterval time.Duration if options.RemoteOptions.UpdateInterval > 0 { @@ -70,7 +66,7 @@ func NewRemoteRuleSet(ctx context.Context, logger logger.ContextLogger, options options: options, updateInterval: updateInterval, pauseManager: service.FromContext[pause.Manager](ctx), - } + }, nil } func (s *RemoteRuleSet) Name() string { @@ -83,20 +79,15 @@ func (s *RemoteRuleSet) String() string { func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext *adapter.HTTPStartContext) error { s.cacheFile = service.FromContext[adapter.CacheFile](s.ctx) - var dialer N.Dialer - if s.options.RemoteOptions.DownloadDetour != "" { - outbound, loaded := s.outbound.Outbound(s.options.RemoteOptions.DownloadDetour) - if !loaded { - return E.New("download detour not found: ", s.options.RemoteOptions.DownloadDetour) - } - dialer = outbound - } else { - dialer = s.outbound.Default() + transport, err := s.resolveTransport() + if err != nil { + return E.Cause(err, "create rule-set http client") } - s.dialer = dialer + startContext.Register(transport) + s.httpClient = &http.Client{Transport: transport} if s.cacheFile != nil { if savedSet := s.cacheFile.LoadRuleSet(s.options.Tag); savedSet != nil { - err := s.loadBytes(savedSet.Content) + err = s.loadBytes(savedSet.Content) if err != nil { return E.Cause(err, "restore cached rule-set") } @@ -105,7 +96,7 @@ func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext *adapter. } } if s.lastUpdated.IsZero() { - err := s.fetch(ctx, startContext) + err = s.fetch(ctx, true) if err != nil { return E.Cause(err, "initial rule-set: ", s.options.Tag) } @@ -207,12 +198,7 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error { func (s *RemoteRuleSet) loopUpdate() { if time.Since(s.lastUpdated) > s.updateInterval { - err := s.fetch(s.ctx, nil) - if err != nil { - s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err) - } else if s.refs.Load() == 0 { - s.rules = nil - } + s.updateOnce() } for { runtime.GC() @@ -226,7 +212,7 @@ func (s *RemoteRuleSet) loopUpdate() { } func (s *RemoteRuleSet) updateOnce() { - err := s.fetch(s.ctx, nil) + err := s.fetch(s.ctx, false) if err != nil { s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err) } else if s.refs.Load() == 0 { @@ -234,26 +220,8 @@ func (s *RemoteRuleSet) updateOnce() { } } -func (s *RemoteRuleSet) fetch(ctx context.Context, startContext *adapter.HTTPStartContext) error { +func (s *RemoteRuleSet) fetch(ctx context.Context, isStart bool) error { s.logger.Debug("updating rule-set ", s.options.Tag, " from URL: ", s.options.RemoteOptions.URL) - var httpClient *http.Client - if startContext != nil { - httpClient = startContext.HTTPClient(s.options.RemoteOptions.DownloadDetour, s.dialer) - } else { - httpClient = &http.Client{ - Transport: &http.Transport{ - ForceAttemptHTTP2: true, - TLSHandshakeTimeout: C.TCPTimeout, - DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - return s.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) - }, - TLSClientConfig: &tls.Config{ - Time: ntp.TimeFuncFromContext(s.ctx), - RootCAs: adapter.RootPoolFromContext(s.ctx), - }, - }, - } - } request, err := http.NewRequest("GET", s.options.RemoteOptions.URL, nil) if err != nil { return err @@ -261,10 +229,14 @@ func (s *RemoteRuleSet) fetch(ctx context.Context, startContext *adapter.HTTPSta if s.lastEtag != "" { request.Header.Set("If-None-Match", s.lastEtag) } - response, err := httpClient.Do(request.WithContext(ctx)) + if !isStart { + defer s.httpClient.CloseIdleConnections() + } + response, err := s.httpClient.Do(request.WithContext(ctx)) if err != nil { return err } + defer response.Body.Close() switch response.StatusCode { case http.StatusOK: case http.StatusNotModified: @@ -287,15 +259,12 @@ func (s *RemoteRuleSet) fetch(ctx context.Context, startContext *adapter.HTTPSta } content, err := io.ReadAll(response.Body) if err != nil { - response.Body.Close() return err } err = s.loadBytes(content) if err != nil { - response.Body.Close() return err } - response.Body.Close() eTagHeader := response.Header.Get("Etag") if eTagHeader != "" { s.lastEtag = eTagHeader @@ -315,6 +284,29 @@ func (s *RemoteRuleSet) fetch(ctx context.Context, startContext *adapter.HTTPSta return nil } +func (s *RemoteRuleSet) resolveTransport() (adapter.HTTPTransport, error) { + httpClientManager := service.FromContext[adapter.HTTPClientManager](s.ctx) + if s.options.RemoteOptions.HTTPClient != nil && !s.options.RemoteOptions.HTTPClient.IsEmpty() { + if s.options.RemoteOptions.DownloadDetour != "" { //nolint:staticcheck + return nil, E.New("http_client is conflict with deprecated download_detour field") + } + return httpClientManager.ResolveTransport(s.ctx, s.logger, *s.options.RemoteOptions.HTTPClient) + } + if s.options.RemoteOptions.DownloadDetour != "" { //nolint:staticcheck + deprecated.Report(s.ctx, deprecated.OptionLegacyRuleSetDownloadDetour) + var httpClientOptions option.HTTPClientOptions + httpClientOptions.DialerOptions = option.DialerOptions{ + Detour: s.options.RemoteOptions.DownloadDetour, //nolint:staticcheck + } + return httpClientManager.ResolveTransport(s.ctx, s.logger, httpClientOptions) + } + defaultTransport := httpClientManager.DefaultTransport() + if defaultTransport == nil { + return nil, E.New("default http client transport is not initialized") + } + return defaultTransport, nil +} + func (s *RemoteRuleSet) Close() error { s.rules = nil s.cancel() diff --git a/service/acme/service.go b/service/acme/service.go index 8286a19717..b29be131f2 100644 --- a/service/acme/service.go +++ b/service/acme/service.go @@ -7,7 +7,6 @@ import ( "context" "crypto/tls" "encoding/json" - "net" "net/http" "net/url" "reflect" @@ -17,14 +16,13 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/certificate" - "github.com/sagernet/sing-box/common/dialer" boxtls "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" - M "github.com/sagernet/sing/common/metadata" - "github.com/sagernet/sing/common/ntp" + "github.com/sagernet/sing/service" "github.com/caddyserver/certmagic" "github.com/caddyserver/zerossl" @@ -125,7 +123,7 @@ func NewCertificateProvider(ctx context.Context, logger log.ContextLogger, tag s AltTLSALPNPort: int(options.AlternativeTLSPort), Logger: zapLogger, } - acmeHTTPClient, err := newACMEHTTPClient(ctx, options.Detour) + acmeHTTPClient, err := newACMEHTTPClient(ctx, logger, options) if err != nil { return nil, err } @@ -310,33 +308,16 @@ func createZeroSSLExternalAccountBinding(ctx context.Context, acmeIssuer *certma }, account, nil } -func newACMEHTTPClient(ctx context.Context, detour string) (*http.Client, error) { - outboundDialer, err := dialer.NewWithOptions(dialer.Options{ - Context: ctx, - Options: option.DialerOptions{ - Detour: detour, - }, - RemoteIsDomain: true, - }) +func newACMEHTTPClient(ctx context.Context, logger log.ContextLogger, options option.ACMECertificateProviderOptions) (*http.Client, error) { + httpClientOptions := common.PtrValueOrDefault(options.HTTPClient) + httpClientManager := service.FromContext[adapter.HTTPClientManager](ctx) + transport, err := httpClientManager.ResolveTransport(ctx, logger, httpClientOptions) if err != nil { - return nil, E.Cause(err, "create ACME provider dialer") + return nil, E.Cause(err, "create ACME provider http client") } return &http.Client{ - Transport: &http.Transport{ - DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - return outboundDialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) - }, - TLSClientConfig: &tls.Config{ - RootCAs: adapter.RootPoolFromContext(ctx), - Time: ntp.TimeFuncFromContext(ctx), - }, - // from certmagic defaults (acmeissuer.go) - TLSHandshakeTimeout: 30 * time.Second, - ResponseHeaderTimeout: 30 * time.Second, - ExpectContinueTimeout: 2 * time.Second, - ForceAttemptHTTP2: true, - }, - Timeout: certmagic.HTTPTimeout, + Transport: transport, + Timeout: certmagic.HTTPTimeout, }, nil } diff --git a/service/derp/service.go b/service/derp/service.go index 02dac60bfa..ee91e3a166 100644 --- a/service/derp/service.go +++ b/service/derp/service.go @@ -5,7 +5,6 @@ package derp import ( "bufio" "context" - stdTLS "crypto/tls" "encoding/json" "fmt" "io" @@ -34,7 +33,6 @@ import ( "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" - "github.com/sagernet/sing/common/ntp" aTLS "github.com/sagernet/sing/common/tls" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/filemanager" @@ -151,29 +149,14 @@ func (d *Service) Start(stage adapter.StartStage) error { if len(d.verifyClientURL) > 0 { var httpClients []*http.Client var urls []string - for index, options := range d.verifyClientURL { - verifyDialer, createErr := dialer.NewWithOptions(dialer.Options{ - Context: d.ctx, - Options: options.DialerOptions, - RemoteIsDomain: options.ServerIsDomain(), - NewDialer: true, - }) + httpClientManager := service.FromContext[adapter.HTTPClientManager](d.ctx) + for index, verifyOptions := range d.verifyClientURL { + transport, createErr := httpClientManager.ResolveTransport(d.ctx, d.logger, verifyOptions.HTTPClientOptions) if createErr != nil { return E.Cause(createErr, "verify_client_url[", index, "]") } - httpClients = append(httpClients, &http.Client{ - Transport: &http.Transport{ - ForceAttemptHTTP2: true, - TLSClientConfig: &stdTLS.Config{ - RootCAs: adapter.RootPoolFromContext(d.ctx), - Time: ntp.TimeFuncFromContext(d.ctx), - }, - DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - return verifyDialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) - }, - }, - }) - urls = append(urls, options.URL) + httpClients = append(httpClients, &http.Client{Transport: transport}) + urls = append(urls, verifyOptions.URL) } server.SetVerifyClientHTTPClient(httpClients) server.SetVerifyClientURL(urls) @@ -310,7 +293,7 @@ func (d *Service) startMeshWithHost(derpServer *derpserver.Server, server *optio } var stdConfig *tls.STDConfig if server.TLS != nil && server.TLS.Enabled { - tlsConfig, err := tls.NewClient(d.ctx, d.logger, hostname, common.PtrValueOrDefault(server.TLS)) + tlsConfig, err := tls.NewClient(d.ctx, d.logger, hostname, *server.TLS) if err != nil { return err } @@ -352,10 +335,11 @@ func (d *Service) startMeshWithHost(derpServer *derpserver.Server, server *optio } func (d *Service) Close() error { - return common.Close( + err := common.Close( common.PtrOrNil(d.listener), d.tlsConfig, ) + return err } var homePage = ` diff --git a/service/origin_ca/service.go b/service/origin_ca/service.go index 85588c37d5..d0a4442121 100644 --- a/service/origin_ca/service.go +++ b/service/origin_ca/service.go @@ -26,13 +26,13 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/certificate" - "github.com/sagernet/sing-box/common/dialer" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" - M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/ntp" + "github.com/sagernet/sing/service" "github.com/caddyserver/certmagic" ) @@ -102,16 +102,10 @@ func NewCertificateProvider(ctx context.Context, logger log.ContextLogger, tag s requestedValidity = defaultRequestedValidity } ctx, cancel := context.WithCancel(ctx) - serviceDialer, err := dialer.NewWithOptions(dialer.Options{ - Context: ctx, - Options: option.DialerOptions{ - Detour: options.Detour, - }, - RemoteIsDomain: true, - }) + httpClient, err := originCAHTTPClient(ctx, logger, options) if err != nil { cancel() - return nil, E.Cause(err, "create Cloudflare Origin CA dialer") + return nil, err } var storage certmagic.Storage if options.DataDirectory != "" { @@ -131,21 +125,12 @@ func NewCertificateProvider(ctx context.Context, logger log.ContextLogger, tag s certmagic.StorageKeys.Safe(storageNamesKey), }, "/") return &Service{ - Adapter: certificate.NewAdapter(C.TypeCloudflareOriginCA, tag), - logger: logger, - ctx: ctx, - cancel: cancel, - timeFunc: timeFunc, - httpClient: &http.Client{Transport: &http.Transport{ - DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - return serviceDialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) - }, - TLSClientConfig: &tls.Config{ - RootCAs: adapter.RootPoolFromContext(ctx), - Time: timeFunc, - }, - ForceAttemptHTTP2: true, - }}, + Adapter: certificate.NewAdapter(C.TypeCloudflareOriginCA, tag), + logger: logger, + ctx: ctx, + cancel: cancel, + timeFunc: timeFunc, + httpClient: httpClient, storage: storage, storageIssuerKey: storageIssuerKey, storageNamesKey: storageNamesKey, @@ -158,6 +143,16 @@ func NewCertificateProvider(ctx context.Context, logger log.ContextLogger, tag s }, nil } +func originCAHTTPClient(ctx context.Context, logger log.ContextLogger, options option.CloudflareOriginCACertificateProviderOptions) (*http.Client, error) { + httpClientOptions := common.PtrValueOrDefault(options.HTTPClient) + httpClientManager := service.FromContext[adapter.HTTPClientManager](ctx) + transport, err := httpClientManager.ResolveTransport(ctx, logger, httpClientOptions) + if err != nil { + return nil, E.Cause(err, "create Cloudflare Origin CA http client") + } + return &http.Client{Transport: transport}, nil +} + func (s *Service) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil @@ -189,9 +184,6 @@ func (s *Service) Close() error { if done := s.done; done != nil { <-done } - if transport, loaded := s.httpClient.Transport.(*http.Transport); loaded { - transport.CloseIdleConnections() - } return nil } @@ -374,6 +366,7 @@ func (s *Service) requestCertificate(ctx context.Context) ([]byte, []byte, *tls. } else { request.Header.Set("X-Auth-User-Service-Key", s.originCAKey) } + defer s.httpClient.CloseIdleConnections() response, err := s.httpClient.Do(request) if err != nil { return nil, nil, nil, nil, E.Cause(err, "request certificate from Cloudflare") From 6ef38042f3d638cf826c2f031ada02964df8ad22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 15 Apr 2026 17:58:54 +0800 Subject: [PATCH 33/93] Standardize hosts path --- dns/transport/hosts/hosts.go | 6 +++++- dns/transport/hosts/hosts_file.go | 10 ++++++++++ dns/transport/hosts/hosts_test.go | 21 +++++++++++++++++---- dns/transport/hosts/hosts_unix.go | 4 +++- dns/transport/hosts/hosts_windows.go | 11 +++++------ dns/transport/local/local.go | 10 ++++++++-- dns/transport/local/local_darwin.go | 7 ++++++- dns/transport/local/local_darwin_cgo.go | 2 +- 8 files changed, 55 insertions(+), 16 deletions(-) diff --git a/dns/transport/hosts/hosts.go b/dns/transport/hosts/hosts.go index f0e70a9a3c..aeb8781799 100644 --- a/dns/transport/hosts/hosts.go +++ b/dns/transport/hosts/hosts.go @@ -33,7 +33,11 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt predefined = make(map[string][]netip.Addr) ) if len(options.Path) == 0 { - files = append(files, NewFile(DefaultPath)) + defaultFile, err := NewDefault() + if err != nil { + return nil, err + } + files = append(files, defaultFile) } else { for _, path := range options.Path { files = append(files, NewFile(filemanager.BasePath(ctx, os.ExpandEnv(path)))) diff --git a/dns/transport/hosts/hosts_file.go b/dns/transport/hosts/hosts_file.go index ec384882a8..af507f012a 100644 --- a/dns/transport/hosts/hosts_file.go +++ b/dns/transport/hosts/hosts_file.go @@ -10,6 +10,8 @@ import ( "sync" "time" + E "github.com/sagernet/sing/common/exceptions" + "github.com/miekg/dns" ) @@ -30,6 +32,14 @@ func NewFile(path string) *File { } } +func NewDefault() (*File, error) { + defaultPathResolved, err := defaultPath() + if err != nil { + return nil, E.Cause(err, "resolve default hosts path") + } + return NewFile(defaultPathResolved), nil +} + func (f *File) Lookup(name string) []netip.Addr { f.access.Lock() defer f.access.Unlock() diff --git a/dns/transport/hosts/hosts_test.go b/dns/transport/hosts/hosts_test.go index 3ae160b789..61d20e0c63 100644 --- a/dns/transport/hosts/hosts_test.go +++ b/dns/transport/hosts/hosts_test.go @@ -1,16 +1,29 @@ -package hosts_test +package hosts import ( "net/netip" + "os" + "runtime" "testing" - "github.com/sagernet/sing-box/dns/transport/hosts" + E "github.com/sagernet/sing/common/exceptions" "github.com/stretchr/testify/require" ) func TestHosts(t *testing.T) { t.Parallel() - require.Equal(t, []netip.Addr{netip.AddrFrom4([4]byte{127, 0, 0, 1}), netip.IPv6Loopback()}, hosts.NewFile("testdata/hosts").Lookup("localhost")) - require.NotEmpty(t, hosts.NewFile(hosts.DefaultPath).Lookup("localhost")) + require.Equal(t, []netip.Addr{netip.AddrFrom4([4]byte{127, 0, 0, 1}), netip.IPv6Loopback()}, NewFile("testdata/hosts").Lookup("localhost")) + if runtime.GOOS != "windows" { + defaultPathResolved, err := defaultPath() + if err != nil { + t.Fatal(E.Cause(err, "resolve default hosts path")) + } + content, readErr := os.ReadFile(defaultPathResolved) + require.NoError(t, readErr) + hFile := NewFile(defaultPathResolved) + if len(hFile.Lookup("localhost")) == 0 { + t.Fatal("failed to resolve localhost: ", defaultPathResolved, ": \n", string(content)) + } + } } diff --git a/dns/transport/hosts/hosts_unix.go b/dns/transport/hosts/hosts_unix.go index 4caed8b406..9c44853194 100644 --- a/dns/transport/hosts/hosts_unix.go +++ b/dns/transport/hosts/hosts_unix.go @@ -2,4 +2,6 @@ package hosts -var DefaultPath = "/etc/hosts" +func defaultPath() (string, error) { + return "/etc/hosts", nil +} diff --git a/dns/transport/hosts/hosts_windows.go b/dns/transport/hosts/hosts_windows.go index 3144e50d56..a17e6d2b87 100644 --- a/dns/transport/hosts/hosts_windows.go +++ b/dns/transport/hosts/hosts_windows.go @@ -2,16 +2,15 @@ package hosts import ( "path/filepath" + "sync" "golang.org/x/sys/windows" ) -var DefaultPath string - -func init() { +var defaultPath = sync.OnceValues(func() (string, error) { systemDirectory, err := windows.GetSystemDirectory() if err != nil { - systemDirectory = "C:\\Windows\\System32" + return "", err } - DefaultPath = filepath.Join(systemDirectory, "Drivers/etc/hosts") -} + return filepath.Join(systemDirectory, "Drivers", "etc", "hosts"), nil +}) diff --git a/dns/transport/local/local.go b/dns/transport/local/local.go index a3909acc81..55933510ad 100644 --- a/dns/transport/local/local.go +++ b/dns/transport/local/local.go @@ -39,11 +39,11 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt if err != nil { return nil, err } + return &Transport{ TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options), ctx: ctx, logger: logger, - hosts: hosts.NewFile(hosts.DefaultPath), dialer: transportDialer, preferGo: options.PreferGo, }, nil @@ -52,6 +52,12 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt func (t *Transport) Start(stage adapter.StartStage) error { switch stage { case adapter.StartStateInitialize: + defaultHosts, err := hosts.NewDefault() + if err != nil { + t.logger.Warn(err) + } else { + t.hosts = defaultHosts + } if !t.preferGo { if isSystemdResolvedManaged() { resolvedResolver, err := NewResolvedResolver(t.ctx, t.logger) @@ -84,7 +90,7 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, return t.resolved.Exchange(ctx, message) } question := message.Question[0] - if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA { + if t.hosts != nil && (question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA) { addresses := t.hosts.Lookup(dns.FqdnToDomain(question.Name)) if len(addresses) > 0 { return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil diff --git a/dns/transport/local/local_darwin.go b/dns/transport/local/local_darwin.go index eb33d64fa7..75fdfd9abb 100644 --- a/dns/transport/local/local_darwin.go +++ b/dns/transport/local/local_darwin.go @@ -51,7 +51,6 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options), ctx: ctx, logger: logger, - hosts: hosts.NewFile(hosts.DefaultPath), dialer: transportDialer, }, nil } @@ -60,6 +59,12 @@ func (t *Transport) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } + defaultHosts, err := hosts.NewDefault() + if err != nil { + t.logger.Warn(err) + } else { + t.hosts = defaultHosts + } inboundManager := service.FromContext[adapter.InboundManager](t.ctx) for _, inbound := range inboundManager.Inbounds() { if inbound.Type() == C.TypeTun { diff --git a/dns/transport/local/local_darwin_cgo.go b/dns/transport/local/local_darwin_cgo.go index 318c38f387..11adf76fb4 100644 --- a/dns/transport/local/local_darwin_cgo.go +++ b/dns/transport/local/local_darwin_cgo.go @@ -197,7 +197,7 @@ func darwinResolverHErrno(name string, hErrno int) error { func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { question := message.Question[0] - if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA { + if t.hosts != nil && (question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA) { addresses := t.hosts.Lookup(dns.FqdnToDomain(question.Name)) if len(addresses) > 0 { return dns.FixedResponse(message.Id, question, addresses, boxC.DefaultDNSTTL), nil From 08ce083b9b59e479c38ff93f8aabd5dbef046152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 15 Apr 2026 17:59:18 +0800 Subject: [PATCH 34/93] Add TLS spoof support --- .github/workflows/test.yml | 55 + .golangci.yml | 1 - Makefile | 2 +- common/tls/apple_client.go | 3 + common/tls/client.go | 32 + common/tls/reality_client.go | 3 + common/tls/std_client.go | 15 + common/tls/utls_client.go | 15 + common/tlsfragment/index.go | 9 +- common/tlsspoof/client_hello.go | 86 ++ common/tlsspoof/client_hello_test.go | 79 ++ common/tlsspoof/conn_test.go | 126 ++ common/tlsspoof/endpoints.go | 29 + common/tlsspoof/integration_darwin_test.go | 5 + common/tlsspoof/integration_linux_test.go | 5 + common/tlsspoof/integration_test.go | 112 ++ common/tlsspoof/integration_unix_test.go | 100 ++ common/tlsspoof/integration_windows_test.go | 139 ++ common/tlsspoof/packet.go | 100 ++ common/tlsspoof/packet_test.go | 77 ++ common/tlsspoof/raw_darwin.go | 161 +++ common/tlsspoof/raw_linux.go | 127 ++ common/tlsspoof/raw_stub.go | 15 + common/tlsspoof/raw_unix.go | 26 + common/tlsspoof/raw_windows.go | 218 ++++ common/tlsspoof/raw_windows_test.go | 112 ++ common/tlsspoof/spoof.go | 100 ++ common/windivert/address_test.go | 53 + common/windivert/assets/LICENSE.txt | 1191 ++++++++++++++++++ common/windivert/assets/WinDivert32.sys | Bin 0 -> 79792 bytes common/windivert/assets/WinDivert64.sys | Bin 0 -> 94144 bytes common/windivert/assets_386.go | 14 + common/windivert/assets_amd64.go | 14 + common/windivert/assets_unsupported.go | 7 + common/windivert/driver_windows.go | 212 ++++ common/windivert/filter.go | 182 +++ common/windivert/filter_test.go | 140 ++ common/windivert/handle_windows.go | 320 +++++ common/windivert/handle_windows_test.go | 106 ++ common/windivert/integration_windows_test.go | 88 ++ common/windivert/windivert.go | 71 ++ docs/configuration/route/rule_action.zh.md | 5 +- docs/configuration/shared/tls.md | 39 + docs/configuration/shared/tls.zh.md | 37 + option/tls.go | 2 + 45 files changed, 4227 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 common/tlsspoof/client_hello.go create mode 100644 common/tlsspoof/client_hello_test.go create mode 100644 common/tlsspoof/conn_test.go create mode 100644 common/tlsspoof/endpoints.go create mode 100644 common/tlsspoof/integration_darwin_test.go create mode 100644 common/tlsspoof/integration_linux_test.go create mode 100644 common/tlsspoof/integration_test.go create mode 100644 common/tlsspoof/integration_unix_test.go create mode 100644 common/tlsspoof/integration_windows_test.go create mode 100644 common/tlsspoof/packet.go create mode 100644 common/tlsspoof/packet_test.go create mode 100644 common/tlsspoof/raw_darwin.go create mode 100644 common/tlsspoof/raw_linux.go create mode 100644 common/tlsspoof/raw_stub.go create mode 100644 common/tlsspoof/raw_unix.go create mode 100644 common/tlsspoof/raw_windows.go create mode 100644 common/tlsspoof/raw_windows_test.go create mode 100644 common/tlsspoof/spoof.go create mode 100644 common/windivert/address_test.go create mode 100644 common/windivert/assets/LICENSE.txt create mode 100644 common/windivert/assets/WinDivert32.sys create mode 100644 common/windivert/assets/WinDivert64.sys create mode 100644 common/windivert/assets_386.go create mode 100644 common/windivert/assets_amd64.go create mode 100644 common/windivert/assets_unsupported.go create mode 100644 common/windivert/driver_windows.go create mode 100644 common/windivert/filter.go create mode 100644 common/windivert/filter_test.go create mode 100644 common/windivert/handle_windows.go create mode 100644 common/windivert/handle_windows_test.go create mode 100644 common/windivert/integration_windows_test.go create mode 100644 common/windivert/windivert.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000000..cc9ee0ad80 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,55 @@ +name: Test + +on: + push: + branches: + - stable + - testing + - unstable + paths-ignore: + - '**.md' + - '.github/**' + - '!.github/workflows/test.yml' + pull_request: + branches: + - stable + - testing + - unstable + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}-${{ inputs.build }} + cancel-in-progress: true + +jobs: + test: + name: Test + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - windows-latest + - macos-latest + go: + - ~1.24 + - ~1.25 + runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go }} + - name: Set build tags and ldflags + shell: bash + run: | + echo "BUILD_TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS)" >> "$GITHUB_ENV" + echo "LDFLAGS_SHARED=$(cat release/LDFLAGS)" >> "$GITHUB_ENV" + - name: Test (unix) + if: matrix.os != 'windows-latest' + run: go test -v -exec sudo -tags "$BUILD_TAGS" -ldflags "$LDFLAGS_SHARED" ./... + - name: Test (windows) + if: matrix.os == 'windows-latest' + shell: bash + run: go test -v -tags "$BUILD_TAGS" -ldflags "$LDFLAGS_SHARED" ./... diff --git a/.golangci.yml b/.golangci.yml index d6905dc10d..53553d7140 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -19,7 +19,6 @@ linters: enable: - govet - ineffassign - - paralleltest - staticcheck settings: staticcheck: diff --git a/Makefile b/Makefile index 1a1138cc7a..6ec7bc9b09 100644 --- a/Makefile +++ b/Makefile @@ -52,7 +52,7 @@ lint: GOOS=android golangci-lint run ./... GOOS=windows golangci-lint run ./... GOOS=darwin golangci-lint run ./... - GOOS=freebsd golangci-lint run ./... + # GOOS=freebsd golangci-lint run ./... lint_install: go install -v github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest diff --git a/common/tls/apple_client.go b/common/tls/apple_client.go index 4b84a31b24..01043fd3d2 100644 --- a/common/tls/apple_client.go +++ b/common/tls/apple_client.go @@ -155,6 +155,9 @@ func ValidateAppleTLSOptions(ctx context.Context, options option.OutboundTLSOpti if options.KernelTx || options.KernelRx { return AppleTLSValidated{}, E.New("ktls is unsupported in ", engineName) } + if options.Spoof != "" || options.SpoofMethod != "" { + return AppleTLSValidated{}, E.New("spoof is unsupported in ", engineName) + } if len(options.CertificatePublicKeySHA256) > 0 && (len(options.Certificate) > 0 || options.CertificatePath != "") { return AppleTLSValidated{}, E.New("certificate_public_key_sha256 is conflict with certificate or certificate_path") } diff --git a/common/tls/client.go b/common/tls/client.go index 40560b9a59..00020ee2c9 100644 --- a/common/tls/client.go +++ b/common/tls/client.go @@ -8,6 +8,7 @@ import ( "os" "github.com/sagernet/sing-box/common/badtls" + "github.com/sagernet/sing-box/common/tlsspoof" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" @@ -19,6 +20,37 @@ import ( var errMissingServerName = E.New("missing server_name or insecure=true") +func parseTLSSpoofOptions(serverName string, options option.OutboundTLSOptions) (string, tlsspoof.Method, error) { + if options.Spoof == "" { + if options.SpoofMethod != "" { + return "", 0, E.New("`spoof_method` requires `spoof`") + } + return "", 0, nil + } + if !tlsspoof.PlatformSupported { + return "", 0, E.New("`spoof` is not supported on this platform") + } + if options.DisableSNI || serverName == "" { + return "", 0, E.New("`spoof` requires TLS ClientHello with SNI") + } + method, err := tlsspoof.ParseMethod(options.SpoofMethod) + if err != nil { + return "", 0, err + } + return options.Spoof, method, nil +} + +func applyTLSSpoof(conn net.Conn, spoof string, method tlsspoof.Method) (net.Conn, error) { + if spoof == "" { + return conn, nil + } + spoofer, err := tlsspoof.NewSpoofer(conn, method) + if err != nil { + return nil, err + } + return tlsspoof.NewConn(conn, spoofer, spoof), nil +} + func NewDialerFromOptions(ctx context.Context, logger logger.ContextLogger, dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) { if !options.Enabled { return dialer, nil diff --git a/common/tls/reality_client.go b/common/tls/reality_client.go index 38f0965e24..bb57e76d3c 100644 --- a/common/tls/reality_client.go +++ b/common/tls/reality_client.go @@ -59,6 +59,9 @@ func newRealityClient(ctx context.Context, logger logger.ContextLogger, serverAd if options.UTLS == nil || !options.UTLS.Enabled { return nil, E.New("uTLS is required by reality client") } + if options.Spoof != "" || options.SpoofMethod != "" { + return nil, E.New("spoof is unsupported in reality") + } uClient, err := newUTLSClient(ctx, logger, serverAddress, options, allowEmptyServerName) if err != nil { diff --git a/common/tls/std_client.go b/common/tls/std_client.go index 7da36defe5..f38981c687 100644 --- a/common/tls/std_client.go +++ b/common/tls/std_client.go @@ -14,6 +14,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/tlsfragment" + "github.com/sagernet/sing-box/common/tlsspoof" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" @@ -31,6 +32,8 @@ type STDClientConfig struct { fragment bool fragmentFallbackDelay time.Duration recordFragment bool + spoof string + spoofMethod tlsspoof.Method } func (c *STDClientConfig) ServerName() string { @@ -75,6 +78,10 @@ func (c *STDClientConfig) Client(conn net.Conn) (Conn, error) { if c.recordFragment { conn = tf.NewConn(conn, c.ctx, c.fragment, c.recordFragment, c.fragmentFallbackDelay) } + conn, err := applyTLSSpoof(conn, c.spoof, c.spoofMethod) + if err != nil { + return nil, err + } return tls.Client(conn, c.config), nil } @@ -89,6 +96,8 @@ func (c *STDClientConfig) Clone() Config { fragment: c.fragment, fragmentFallbackDelay: c.fragmentFallbackDelay, recordFragment: c.recordFragment, + spoof: c.spoof, + spoofMethod: c.spoofMethod, } cloned.SetServerName(cloned.serverName) return cloned @@ -218,6 +227,10 @@ func newSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres } else { handshakeTimeout = C.TCPTimeout } + spoof, spoofMethod, err := parseTLSSpoofOptions(serverName, options) + if err != nil { + return nil, err + } var config Config = &STDClientConfig{ ctx: ctx, config: &tlsConfig, @@ -228,6 +241,8 @@ func newSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres fragment: options.Fragment, fragmentFallbackDelay: time.Duration(options.FragmentFallbackDelay), recordFragment: options.RecordFragment, + spoof: spoof, + spoofMethod: spoofMethod, } config.SetServerName(serverName) if options.ECH != nil && options.ECH.Enabled { diff --git a/common/tls/utls_client.go b/common/tls/utls_client.go index 20261bfd4a..a8b91973c2 100644 --- a/common/tls/utls_client.go +++ b/common/tls/utls_client.go @@ -14,6 +14,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/tlsfragment" + "github.com/sagernet/sing-box/common/tlsspoof" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" @@ -36,6 +37,8 @@ type UTLSClientConfig struct { fragment bool fragmentFallbackDelay time.Duration recordFragment bool + spoof string + spoofMethod tlsspoof.Method } func (c *UTLSClientConfig) ServerName() string { @@ -83,6 +86,10 @@ func (c *UTLSClientConfig) Client(conn net.Conn) (Conn, error) { if c.recordFragment { conn = tf.NewConn(conn, c.ctx, c.fragment, c.recordFragment, c.fragmentFallbackDelay) } + conn, err := applyTLSSpoof(conn, c.spoof, c.spoofMethod) + if err != nil { + return nil, err + } return &utlsALPNWrapper{utlsConnWrapper{utls.UClient(conn, c.config.Clone(), c.id)}, c.config.NextProtos}, nil } @@ -102,6 +109,8 @@ func (c *UTLSClientConfig) Clone() Config { fragment: c.fragment, fragmentFallbackDelay: c.fragmentFallbackDelay, recordFragment: c.recordFragment, + spoof: c.spoof, + spoofMethod: c.spoofMethod, } cloned.SetServerName(cloned.serverName) return cloned @@ -290,6 +299,10 @@ func newUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddre } else { handshakeTimeout = C.TCPTimeout } + spoof, spoofMethod, err := parseTLSSpoofOptions(serverName, options) + if err != nil { + return nil, err + } id, err := uTLSClientHelloID(options.UTLS.Fingerprint) if err != nil { return nil, err @@ -305,6 +318,8 @@ func newUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddre fragment: options.Fragment, fragmentFallbackDelay: time.Duration(options.FragmentFallbackDelay), recordFragment: options.RecordFragment, + spoof: spoof, + spoofMethod: spoofMethod, } config.SetServerName(serverName) if options.ECH != nil && options.ECH.Enabled { diff --git a/common/tlsfragment/index.go b/common/tlsfragment/index.go index 0d58c445c8..83e4bcbc11 100644 --- a/common/tlsfragment/index.go +++ b/common/tlsfragment/index.go @@ -23,9 +23,10 @@ const ( ) type MyServerName struct { - Index int - Length int - ServerName string + Index int + Length int + ServerName string + ExtensionsListLengthIndex int } func IndexTLSServerName(payload []byte) *MyServerName { @@ -41,6 +42,7 @@ func IndexTLSServerName(payload []byte) *MyServerName { return nil } serverName.Index += recordLayerHeaderLen + serverName.ExtensionsListLengthIndex += recordLayerHeaderLen return serverName } @@ -82,6 +84,7 @@ func indexTLSServerNameFromHandshake(handshake []byte) *MyServerName { return nil } serverName.Index += currentIndex + serverName.ExtensionsListLengthIndex = currentIndex return serverName } diff --git a/common/tlsspoof/client_hello.go b/common/tlsspoof/client_hello.go new file mode 100644 index 0000000000..0ca7c5a9f2 --- /dev/null +++ b/common/tlsspoof/client_hello.go @@ -0,0 +1,86 @@ +package tlsspoof + +import ( + "encoding/binary" + + tf "github.com/sagernet/sing-box/common/tlsfragment" + E "github.com/sagernet/sing/common/exceptions" +) + +const ( + recordLengthOffset = 3 + handshakeLengthOffset = 6 +) + +// server_name extension layout (RFC 6066 §3). Offsets are relative to the +// SNI host name (index returned by the parser): +// +// ... uint16 extension_type = 0x0000 (host_name - 9) +// ... uint16 extension_data_length (host_name - 7) +// ... uint16 server_name_list_length (host_name - 5) +// ... uint8 name_type = host_name (host_name - 3) +// ... uint16 host_name_length (host_name - 2) +// sni host_name (host_name) +const ( + extensionDataLengthOffsetFromSNI = -7 + listLengthOffsetFromSNI = -5 + hostNameLengthOffsetFromSNI = -2 +) + +func rewriteSNI(record []byte, fakeSNI string) ([]byte, error) { + if len(fakeSNI) > 0xFFFF { + return nil, E.New("fake SNI too long: ", len(fakeSNI), " bytes") + } + serverName := tf.IndexTLSServerName(record) + if serverName == nil { + return nil, E.New("not a ClientHello with SNI") + } + + delta := len(fakeSNI) - serverName.Length + out := make([]byte, len(record)+delta) + copy(out, record[:serverName.Index]) + copy(out[serverName.Index:], fakeSNI) + copy(out[serverName.Index+len(fakeSNI):], record[serverName.Index+serverName.Length:]) + + err := patchUint16(out, recordLengthOffset, delta) + if err != nil { + return nil, E.Cause(err, "patch record length") + } + err = patchUint24(out, handshakeLengthOffset, delta) + if err != nil { + return nil, E.Cause(err, "patch handshake length") + } + for _, off := range []int{ + serverName.ExtensionsListLengthIndex, + serverName.Index + extensionDataLengthOffsetFromSNI, + serverName.Index + listLengthOffsetFromSNI, + serverName.Index + hostNameLengthOffsetFromSNI, + } { + err = patchUint16(out, off, delta) + if err != nil { + return nil, E.Cause(err, "patch length at offset ", off) + } + } + return out, nil +} + +func patchUint16(data []byte, offset, delta int) error { + patched := int(binary.BigEndian.Uint16(data[offset:])) + delta + if patched < 0 || patched > 0xFFFF { + return E.New("uint16 out of range: ", patched) + } + binary.BigEndian.PutUint16(data[offset:], uint16(patched)) + return nil +} + +func patchUint24(data []byte, offset, delta int) error { + original := int(data[offset])<<16 | int(data[offset+1])<<8 | int(data[offset+2]) + patched := original + delta + if patched < 0 || patched > 0xFFFFFF { + return E.New("uint24 out of range: ", patched) + } + data[offset] = byte(patched >> 16) + data[offset+1] = byte(patched >> 8) + data[offset+2] = byte(patched) + return nil +} diff --git a/common/tlsspoof/client_hello_test.go b/common/tlsspoof/client_hello_test.go new file mode 100644 index 0000000000..746d0482ad --- /dev/null +++ b/common/tlsspoof/client_hello_test.go @@ -0,0 +1,79 @@ +package tlsspoof + +import ( + "encoding/binary" + "encoding/hex" + "testing" + + tf "github.com/sagernet/sing-box/common/tlsfragment" + + "github.com/stretchr/testify/require" +) + +// realClientHello is a captured Chrome ClientHello for github.com, +// reused from common/tlsfragment/index_test.go. +const realClientHello = "16030105f8010005f403036e35de7389a679c54029cf452611f2211c70d9ac3897271de589ab6155f8e4ab20637d225f1ef969ad87ed78bfb9d171300bcb1703b6f314ccefb964f79b7d0961002a0a0a130213031301c02cc02bcca9c030c02fcca8c00ac009c014c013009d009c0035002fc008c012000a01000581baba00000000000f000d00000a6769746875622e636f6d00170000ff01000100000a000e000c3a3a11ec001d001700180019000b000201000010000e000c02683208687474702f312e31000500050100000000000d00160014040308040401050308050805050108060601020100120000003304ef04ed3a3a00010011ec04c0aeb2250c092a3463161cccb29d9183331a424964248579507ed23a180b0ceab2a5f5d9ce41547e497a89055471ea572867ba3a1fc3c9e45025274a20f60c6b60e62476b6afed0403af59ab83660ef4112ae20386a602010d0a5d454c0ed34c84ed4423e750213e6a2baab1bf9c4367a6007ab40a33d95220c2dcaa44f257024a5626b545db0510f4311b1a60714154909c6a61fdfca011fb2626d657aeb6070bf078508babe3b584555013e34acc56198ed4663742b3155a664a9901794c4586820a7dc162c01827291f3792e1237f801a8d1ef096013c181c4a58d2f6859ba75022d18cc4418bd4f351d5c18f83a58857d05af860c4b9ac018a5b63f17184e591532c6bc2cf2215d4a282c8a8a4f6f7aee110422c8bc9ebd3b1d609c568523aaae555db320e6c269473d87af38c256cbb9febc20aea6380c32a8916f7a373c8b1e37554e3260bf6621f6b804ee80b3c516b1d01985bf4c603b6daa9a5991de6a7a29f3a7122b8afb843a7660110fce62b43c615f5bcc2db688ba012649c0952b0a2c031e732d2b454c6b2968683cb8d244be2c9a7fa163222979eaf92722b92b862d81a3d94450c2b60c318421ebb4307c42d1f0473592a5c30e42039cc68cda9721e61aa63f49def17c15221680ed444896340133bbee67556f56b9f9d78a4df715f926a12add0cc9c862e46ea8b7316ae468282c18601b2771c9c9322f982228cf93effaacd3f80cbd12bce5fc36f56e2a3caf91e578a5fae00c9b23a8ed1a66764f4433c3628a70b8f0a6196adc60a4cb4226f07ba4c6b363fe9065563bfc1347452946386bab488686e837ab979c64f9047417fca635fe1bb4f074f256cc8af837c7b455e280426547755af90a61640169ef180aea3a77e662bb6dac1b6c3696027129b1a5edf495314e9c7f4b6110e16378ec893fa24642330a40aba1a85326101acb97c620fd8d71389e69eaed7bdb01bbe1fd428d66191150c7b2cd1ad4257391676a82ba8ce07fb2667c3b289f159003a7c7bc31d361b7b7f49a802961739d950dfcc0fa1c7abce5abdd2245101da391151490862028110465950b9e9c03d08a90998ab83267838d2e74a0593bc81f74cdf734519a05b351c0e5488c68dd810e6e9142ccc1e2f4a7f464297eb340e27acc6b9d64e12e38cce8492b3d939140b5a9e149a75597f10a23874c84323a07cdd657274378f887c85c4259b9c04cd33ba58ed630ef2a744f8e19dd34843dff331d2a6be7e2332c599289cd248a611c73d7481cd4a9bd43449a3836f14b2af18a1739e17999e4c67e85cc5bcecabb14185e5bcaff3c96098f03dc5aba819f29587758f49f940585354a2a780830528d68ccd166920dadcaa25cab5fc1907272a826aba3f08bc6b88757776812ecb6c7cec69a223ec0a13a7b62a2349a0f63ed7a27a3b15ba21d71fe6864ec6e089ae17cadd433fa3138f7ee24353c11365818f8fc34f43a05542d18efaac24bfccc1f748a0cc1a67ad379468b76fd34973dba785f5c91d618333cd810fe0700d1bbc8422029782628070a624c52c5309a4a64d625b11f8033ab28df34a1add297517fcc06b92b6817b3c5144438cf260867c57bde68c8c4b82e6a135ef676a52fbae5708002a404e6189a60e2836de565ad1b29e3819e5ed49f6810bcb28e1bd6de57306f94b79d9dae1cc4624d2a068499beef81cd5fe4b76dcbfff2a2008001d002001976128c6d5a934533f28b9914d2480aab2a8c1ab03d212529ce8b27640a716002d00020101002b000706caca03040303001b00030200015a5a000100" + +func decodeClientHello(t *testing.T) []byte { + t.Helper() + payload, err := hex.DecodeString(realClientHello) + require.NoError(t, err) + return payload +} + +func assertConsistent(t *testing.T, payload []byte, expectedSNI string) { + t.Helper() + serverName := tf.IndexTLSServerName(payload) + require.NotNil(t, serverName, "parser should find SNI in rewritten payload") + require.Equal(t, expectedSNI, serverName.ServerName) + require.Equal(t, expectedSNI, string(payload[serverName.Index:serverName.Index+serverName.Length])) + // Record length must equal len(payload) - 5. + recordLen := binary.BigEndian.Uint16(payload[3:5]) + require.Equal(t, len(payload)-5, int(recordLen), "record length must equal payload - 5") + // Handshake length must equal len(payload) - 5 - 4. + handshakeLen := int(payload[6])<<16 | int(payload[7])<<8 | int(payload[8]) + require.Equal(t, len(payload)-5-4, handshakeLen, "handshake length must equal payload - 9") +} + +func TestRewriteSNI_ShorterReplacement(t *testing.T) { + t.Parallel() + payload := decodeClientHello(t) + out, err := rewriteSNI(payload, "a.io") + require.NoError(t, err) + require.Len(t, out, len(payload)-6) // original "github.com" is 10 bytes, "a.io" is 4 bytes. + assertConsistent(t, out, "a.io") +} + +func TestRewriteSNI_SameLengthReplacement(t *testing.T) { + t.Parallel() + payload := decodeClientHello(t) + out, err := rewriteSNI(payload, "example.co") + require.NoError(t, err) + require.Len(t, out, len(payload)) + assertConsistent(t, out, "example.co") +} + +func TestRewriteSNI_LongerReplacement(t *testing.T) { + t.Parallel() + payload := decodeClientHello(t) + out, err := rewriteSNI(payload, "letsencrypt.org") + require.NoError(t, err) + require.Len(t, out, len(payload)+5) // "letsencrypt.org" is 15, original 10, delta 5. + assertConsistent(t, out, "letsencrypt.org") +} + +func TestRewriteSNI_NoSNIReturnsError(t *testing.T) { + t.Parallel() + // Truncated payload — not a valid ClientHello. + _, err := rewriteSNI([]byte{0x16, 0x03, 0x01, 0x00, 0x01, 0x01}, "x.com") + require.Error(t, err) +} + +func TestRewriteSNI_DoesNotMutateInput(t *testing.T) { + t.Parallel() + payload := decodeClientHello(t) + original := append([]byte(nil), payload...) + _, err := rewriteSNI(payload, "letsencrypt.org") + require.NoError(t, err) + require.Equal(t, original, payload, "input payload must not be mutated") +} diff --git a/common/tlsspoof/conn_test.go b/common/tlsspoof/conn_test.go new file mode 100644 index 0000000000..981f1a49c3 --- /dev/null +++ b/common/tlsspoof/conn_test.go @@ -0,0 +1,126 @@ +package tlsspoof + +import ( + "encoding/hex" + "io" + "net" + "testing" + + tf "github.com/sagernet/sing-box/common/tlsfragment" + + "github.com/stretchr/testify/require" +) + +type fakeSpoofer struct { + injected [][]byte + err error +} + +func (f *fakeSpoofer) Inject(payload []byte) error { + if f.err != nil { + return f.err + } + f.injected = append(f.injected, append([]byte(nil), payload...)) + return nil +} + +func (f *fakeSpoofer) Close() error { + return nil +} + +func readAll(t *testing.T, conn net.Conn) []byte { + t.Helper() + data, err := io.ReadAll(conn) + require.NoError(t, err) + return data +} + +func TestConn_Write_InjectsThenForwards(t *testing.T) { + t.Parallel() + payload, err := hex.DecodeString(realClientHello) + require.NoError(t, err) + + client, server := net.Pipe() + spoofer := &fakeSpoofer{} + wrapped := NewConn(client, spoofer, "letsencrypt.org") + + serverRead := make(chan []byte, 1) + go func() { + serverRead <- readAll(t, server) + }() + + n, err := wrapped.Write(payload) + require.NoError(t, err) + require.Equal(t, len(payload), n) + require.NoError(t, wrapped.Close()) + + forwarded := <-serverRead + require.Equal(t, payload, forwarded, "underlying conn must receive the real ClientHello unchanged") + require.Len(t, spoofer.injected, 1) + + injected := spoofer.injected[0] + serverName := tf.IndexTLSServerName(injected) + require.NotNil(t, serverName, "injected payload must parse as ClientHello") + require.Equal(t, "letsencrypt.org", serverName.ServerName) +} + +func TestConn_Write_SecondWriteDoesNotInject(t *testing.T) { + t.Parallel() + payload, err := hex.DecodeString(realClientHello) + require.NoError(t, err) + + client, server := net.Pipe() + spoofer := &fakeSpoofer{} + wrapped := NewConn(client, spoofer, "letsencrypt.org") + + serverRead := make(chan []byte, 1) + go func() { + serverRead <- readAll(t, server) + }() + + _, err = wrapped.Write(payload) + require.NoError(t, err) + _, err = wrapped.Write([]byte("second")) + require.NoError(t, err) + require.NoError(t, wrapped.Close()) + + forwarded := <-serverRead + require.Equal(t, append(append([]byte(nil), payload...), []byte("second")...), forwarded) + require.Len(t, spoofer.injected, 1) +} + +func TestConn_Write_NonClientHelloReturnsError(t *testing.T) { + t.Parallel() + client, server := net.Pipe() + defer client.Close() + defer server.Close() + + spoofer := &fakeSpoofer{} + wrapped := NewConn(client, spoofer, "letsencrypt.org") + + _, err := wrapped.Write([]byte("not a ClientHello")) + require.Error(t, err) + require.Empty(t, spoofer.injected) +} + +func TestParseMethod(t *testing.T) { + t.Parallel() + cases := map[string]struct { + want Method + ok bool + }{ + "": {MethodWrongSequence, true}, + "wrong-sequence": {MethodWrongSequence, true}, + "wrong-checksum": {MethodWrongChecksum, true}, + "nonsense": {0, false}, + } + for input, expected := range cases { + m, err := ParseMethod(input) + if !expected.ok { + require.Error(t, err, "input=%q", input) + continue + } + require.NoError(t, err, "input=%q", input) + require.Equal(t, expected.want, m, "input=%q", input) + } +} diff --git a/common/tlsspoof/endpoints.go b/common/tlsspoof/endpoints.go new file mode 100644 index 0000000000..6be458c850 --- /dev/null +++ b/common/tlsspoof/endpoints.go @@ -0,0 +1,29 @@ +package tlsspoof + +import ( + "net" + "net/netip" + + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" +) + +// The returned addresses are v4-unmapped and share the same family. +func tcpEndpoints(conn net.Conn) (*net.TCPConn, netip.AddrPort, netip.AddrPort, error) { + tcpConn, isTCP := common.Cast[*net.TCPConn](conn) + if !isTCP { + return nil, netip.AddrPort{}, netip.AddrPort{}, E.New("tls_spoof: underlying conn is not *net.TCPConn") + } + local := M.AddrPortFromNet(tcpConn.LocalAddr()) + remote := M.AddrPortFromNet(tcpConn.RemoteAddr()) + if !local.IsValid() || !remote.IsValid() { + return nil, netip.AddrPort{}, netip.AddrPort{}, E.New("tls_spoof: invalid conn address") + } + local = netip.AddrPortFrom(local.Addr().Unmap(), local.Port()) + remote = netip.AddrPortFrom(remote.Addr().Unmap(), remote.Port()) + if local.Addr().Is4() != remote.Addr().Is4() { + return nil, netip.AddrPort{}, netip.AddrPort{}, E.New("tls_spoof: local/remote address family mismatch") + } + return tcpConn, local, remote, nil +} diff --git a/common/tlsspoof/integration_darwin_test.go b/common/tlsspoof/integration_darwin_test.go new file mode 100644 index 0000000000..60a933e5f9 --- /dev/null +++ b/common/tlsspoof/integration_darwin_test.go @@ -0,0 +1,5 @@ +//go:build darwin + +package tlsspoof + +const loopbackInterface = "lo0" diff --git a/common/tlsspoof/integration_linux_test.go b/common/tlsspoof/integration_linux_test.go new file mode 100644 index 0000000000..3294c272e5 --- /dev/null +++ b/common/tlsspoof/integration_linux_test.go @@ -0,0 +1,5 @@ +//go:build linux + +package tlsspoof + +const loopbackInterface = "lo" diff --git a/common/tlsspoof/integration_test.go b/common/tlsspoof/integration_test.go new file mode 100644 index 0000000000..e365929089 --- /dev/null +++ b/common/tlsspoof/integration_test.go @@ -0,0 +1,112 @@ +//go:build linux || darwin + +package tlsspoof + +import ( + "bufio" + "context" + "fmt" + "io" + "net" + "os" + "os/exec" + "strings" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func requireRoot(t *testing.T) { + t.Helper() + if os.Geteuid() != 0 { + t.Fatal("integration test requires root") + } +} + +func tcpdumpObserver(t *testing.T, iface string, port uint16, needle string, do func(), wait time.Duration) bool { + t.Helper() + ctx, cancel := context.WithTimeout(context.Background(), wait) + defer cancel() + cmd := exec.CommandContext(ctx, "tcpdump", "-i", iface, "-n", "-A", "-l", + "-s", "4096", fmt.Sprintf("tcp and port %d", port)) + cmd.Cancel = func() error { + return cmd.Process.Signal(os.Interrupt) + } + stdout, err := cmd.StdoutPipe() + require.NoError(t, err) + stderr, err := cmd.StderrPipe() + require.NoError(t, err) + require.NoError(t, cmd.Start()) + t.Cleanup(func() { + _ = cmd.Process.Signal(os.Interrupt) + _ = cmd.Wait() + }) + + ready := make(chan struct{}) + go func() { + scanner := bufio.NewScanner(stderr) + for scanner.Scan() { + if strings.Contains(scanner.Text(), "listening on") { + close(ready) + io.Copy(io.Discard, stderr) + return + } + } + }() + + select { + case <-ready: + case <-time.After(2 * time.Second): + t.Fatal("tcpdump did not attach within 2s") + } + + var found atomic.Bool + readerDone := make(chan struct{}) + go func() { + defer close(readerDone) + scanner := bufio.NewScanner(stdout) + scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) + for scanner.Scan() { + if strings.Contains(scanner.Text(), needle) { + found.Store(true) + } + } + }() + + do() + + time.Sleep(200 * time.Millisecond) + _ = cmd.Process.Signal(os.Interrupt) + <-readerDone + return found.Load() +} + +func dialLocalEchoServer(t *testing.T) (client net.Conn, serverPort uint16) { + t.Helper() + listener, err := net.Listen("tcp4", "127.0.0.1:0") + require.NoError(t, err) + + accepted := make(chan net.Conn, 1) + go func() { + c, err := listener.Accept() + if err == nil { + accepted <- c + } + close(accepted) + }() + addr := listener.Addr().(*net.TCPAddr) + client, err = net.Dial("tcp4", addr.String()) + require.NoError(t, err) + server := <-accepted + require.NotNil(t, server) + + go io.Copy(io.Discard, server) + t.Cleanup(func() { + client.Close() + server.Close() + listener.Close() + }) + return client, uint16(addr.Port) +} diff --git a/common/tlsspoof/integration_unix_test.go b/common/tlsspoof/integration_unix_test.go new file mode 100644 index 0000000000..c734ed891a --- /dev/null +++ b/common/tlsspoof/integration_unix_test.go @@ -0,0 +1,100 @@ +//go:build linux || darwin + +package tlsspoof + +import ( + "encoding/hex" + "io" + "net" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestIntegrationSpoofer_WrongChecksum(t *testing.T) { + requireRoot(t) + client, serverPort := dialLocalEchoServer(t) + spoofer, err := NewSpoofer(client, MethodWrongChecksum) + require.NoError(t, err) + defer spoofer.Close() + + payload, err := hex.DecodeString(realClientHello) + require.NoError(t, err) + fake, err := rewriteSNI(payload, "letsencrypt.org") + require.NoError(t, err) + + captured := tcpdumpObserver(t, loopbackInterface, serverPort, "letsencrypt.org", func() { + require.NoError(t, spoofer.Inject(fake)) + }, 3*time.Second) + require.True(t, captured, "injected fake ClientHello must be observable on loopback") +} + +func TestIntegrationSpoofer_WrongSequence(t *testing.T) { + requireRoot(t) + client, serverPort := dialLocalEchoServer(t) + spoofer, err := NewSpoofer(client, MethodWrongSequence) + require.NoError(t, err) + defer spoofer.Close() + + payload, err := hex.DecodeString(realClientHello) + require.NoError(t, err) + fake, err := rewriteSNI(payload, "letsencrypt.org") + require.NoError(t, err) + + captured := tcpdumpObserver(t, loopbackInterface, serverPort, "letsencrypt.org", func() { + require.NoError(t, spoofer.Inject(fake)) + }, 3*time.Second) + require.True(t, captured, "injected fake ClientHello must be observable on loopback") +} + +// Loopback bypasses TCP checksum validation, so wrong-sequence is used instead. +func TestIntegrationConn_InjectsThenForwardsRealCH(t *testing.T) { + requireRoot(t) + + listener, err := net.Listen("tcp4", "127.0.0.1:0") + require.NoError(t, err) + + serverReceived := make(chan []byte, 1) + go func() { + conn, err := listener.Accept() + if err != nil { + return + } + defer conn.Close() + _ = conn.SetReadDeadline(time.Now().Add(2 * time.Second)) + got, _ := io.ReadAll(conn) + serverReceived <- got + }() + + addr := listener.Addr().(*net.TCPAddr) + serverPort := uint16(addr.Port) + client, err := net.Dial("tcp4", addr.String()) + require.NoError(t, err) + t.Cleanup(func() { + client.Close() + listener.Close() + }) + + spoofer, err := NewSpoofer(client, MethodWrongSequence) + require.NoError(t, err) + wrapped := NewConn(client, spoofer, "letsencrypt.org") + + payload, err := hex.DecodeString(realClientHello) + require.NoError(t, err) + + captured := tcpdumpObserver(t, loopbackInterface, serverPort, "letsencrypt.org", func() { + n, err := wrapped.Write(payload) + require.NoError(t, err) + require.Equal(t, len(payload), n) + }, 3*time.Second) + require.True(t, captured, "fake ClientHello with letsencrypt.org SNI must be on the wire") + + _ = wrapped.Close() + select { + case got := <-serverReceived: + require.Equal(t, payload, got, "server must receive real ClientHello unchanged (wrong-sequence fake must be dropped)") + case <-time.After(2 * time.Second): + t.Fatal("echo server did not receive real ClientHello") + } +} diff --git a/common/tlsspoof/integration_windows_test.go b/common/tlsspoof/integration_windows_test.go new file mode 100644 index 0000000000..d3f823841e --- /dev/null +++ b/common/tlsspoof/integration_windows_test.go @@ -0,0 +1,139 @@ +//go:build windows && (amd64 || 386) + +package tlsspoof + +import ( + "encoding/hex" + "io" + "net" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func newSpoofer(t *testing.T, conn net.Conn, method Method) Spoofer { + t.Helper() + spoofer, err := NewSpoofer(conn, method) + require.NoError(t, err) + return spoofer +} + +// Basic lifecycle: opening a spoofer against a live TCP conn installs +// the driver, spawns run(), then shuts down cleanly without ever +// injecting. Exercises the close path that cancels an in-flight Recv. +func TestIntegrationSpooferOpenClose(t *testing.T) { + listener, err := net.Listen("tcp4", "127.0.0.1:0") + require.NoError(t, err) + t.Cleanup(func() { listener.Close() }) + + accepted := make(chan net.Conn, 1) + go func() { + c, _ := listener.Accept() + accepted <- c + }() + client, err := net.Dial("tcp4", listener.Addr().String()) + require.NoError(t, err) + t.Cleanup(func() { client.Close() }) + server := <-accepted + t.Cleanup(func() { + if server != nil { + server.Close() + } + }) + + spoofer := newSpoofer(t, client, MethodWrongSequence) + require.NoError(t, spoofer.Close()) +} + +// End-to-end: Conn.Write injects a fake ClientHello with a rewritten +// SNI, then forwards the real ClientHello. With wrong-sequence, the +// fake lands before the connection's send-next sequence — the peer TCP +// stack treats it as already-received and only surfaces the real bytes +// to the echo server. +func TestIntegrationConnInjectsThenForwardsRealCH(t *testing.T) { + listener, err := net.Listen("tcp4", "127.0.0.1:0") + require.NoError(t, err) + t.Cleanup(func() { listener.Close() }) + + serverReceived := make(chan []byte, 1) + go func() { + conn, acceptErr := listener.Accept() + if acceptErr != nil { + return + } + defer conn.Close() + _ = conn.SetReadDeadline(time.Now().Add(5 * time.Second)) + got, _ := io.ReadAll(conn) + serverReceived <- got + }() + + client, err := net.Dial("tcp4", listener.Addr().String()) + require.NoError(t, err) + t.Cleanup(func() { client.Close() }) + + spoofer := newSpoofer(t, client, MethodWrongSequence) + wrapped := NewConn(client, spoofer, "letsencrypt.org") + + payload, err := hex.DecodeString(realClientHello) + require.NoError(t, err) + + n, err := wrapped.Write(payload) + require.NoError(t, err) + require.Equal(t, len(payload), n) + _ = wrapped.Close() + + select { + case got := <-serverReceived: + require.Equal(t, payload, got, + "server must receive real ClientHello unchanged (wrong-sequence fake must be dropped)") + case <-time.After(5 * time.Second): + t.Fatal("echo server did not receive real ClientHello within 5s") + } +} + +// Inject before any kernel payload: stages the fake, then Write flushes +// the real CH. Same terminal expectation as the Conn variant but via the +// Spoofer primitive directly. +func TestIntegrationSpooferInjectThenWrite(t *testing.T) { + listener, err := net.Listen("tcp4", "127.0.0.1:0") + require.NoError(t, err) + t.Cleanup(func() { listener.Close() }) + + serverReceived := make(chan []byte, 1) + go func() { + conn, acceptErr := listener.Accept() + if acceptErr != nil { + return + } + defer conn.Close() + _ = conn.SetReadDeadline(time.Now().Add(5 * time.Second)) + got, _ := io.ReadAll(conn) + serverReceived <- got + }() + + client, err := net.Dial("tcp4", listener.Addr().String()) + require.NoError(t, err) + t.Cleanup(func() { client.Close() }) + + spoofer := newSpoofer(t, client, MethodWrongSequence) + t.Cleanup(func() { spoofer.Close() }) + + payload, err := hex.DecodeString(realClientHello) + require.NoError(t, err) + fake, err := rewriteSNI(payload, "letsencrypt.org") + require.NoError(t, err) + require.NoError(t, spoofer.Inject(fake)) + + n, err := client.Write(payload) + require.NoError(t, err) + require.Equal(t, len(payload), n) + _ = client.Close() + + select { + case got := <-serverReceived: + require.Equal(t, payload, got) + case <-time.After(5 * time.Second): + t.Fatal("echo server did not receive real ClientHello within 5s") + } +} diff --git a/common/tlsspoof/packet.go b/common/tlsspoof/packet.go new file mode 100644 index 0000000000..d84fc4b12c --- /dev/null +++ b/common/tlsspoof/packet.go @@ -0,0 +1,100 @@ +package tlsspoof + +import ( + "net/netip" + + "github.com/sagernet/sing-tun/gtcpip/checksum" + "github.com/sagernet/sing-tun/gtcpip/header" + E "github.com/sagernet/sing/common/exceptions" +) + +const ( + defaultTTL uint8 = 64 + defaultWindowSize uint16 = 0xFFFF + tcpHeaderLen = header.TCPMinimumSize +) + +func buildTCPSegment( + src netip.AddrPort, + dst netip.AddrPort, + seqNum uint32, + ackNum uint32, + payload []byte, + corruptChecksum bool, +) []byte { + if src.Addr().Is4() != dst.Addr().Is4() { + panic("tlsspoof: mixed IPv4/IPv6 address family") + } + var ( + frame []byte + ipHeaderLen int + ) + if src.Addr().Is4() { + ipHeaderLen = header.IPv4MinimumSize + frame = make([]byte, ipHeaderLen+tcpHeaderLen+len(payload)) + ip := header.IPv4(frame[:ipHeaderLen]) + ip.Encode(&header.IPv4Fields{ + TotalLength: uint16(len(frame)), + ID: 0, + TTL: defaultTTL, + Protocol: uint8(header.TCPProtocolNumber), + SrcAddr: src.Addr(), + DstAddr: dst.Addr(), + }) + ip.SetChecksum(^ip.CalculateChecksum()) + } else { + ipHeaderLen = header.IPv6MinimumSize + frame = make([]byte, ipHeaderLen+tcpHeaderLen+len(payload)) + ip := header.IPv6(frame[:ipHeaderLen]) + ip.Encode(&header.IPv6Fields{ + PayloadLength: uint16(tcpHeaderLen + len(payload)), + TransportProtocol: header.TCPProtocolNumber, + HopLimit: defaultTTL, + SrcAddr: src.Addr(), + DstAddr: dst.Addr(), + }) + } + encodeTCP(frame, ipHeaderLen, src, dst, seqNum, ackNum, payload, corruptChecksum) + return frame +} + +func encodeTCP(frame []byte, ipHeaderLen int, src, dst netip.AddrPort, seqNum, ackNum uint32, payload []byte, corruptChecksum bool) { + tcp := header.TCP(frame[ipHeaderLen:]) + copy(frame[ipHeaderLen+tcpHeaderLen:], payload) + tcp.Encode(&header.TCPFields{ + SrcPort: src.Port(), + DstPort: dst.Port(), + SeqNum: seqNum, + AckNum: ackNum, + DataOffset: tcpHeaderLen, + Flags: header.TCPFlagAck | header.TCPFlagPsh, + WindowSize: defaultWindowSize, + }) + applyTCPChecksum(tcp, src.Addr(), dst.Addr(), payload, corruptChecksum) +} + +func buildSpoofFrame(method Method, src, dst netip.AddrPort, sendNext, receiveNext uint32, payload []byte) ([]byte, error) { + var sequence uint32 + corrupt := false + switch method { + case MethodWrongSequence: + sequence = sendNext - uint32(len(payload)) + case MethodWrongChecksum: + sequence = sendNext + corrupt = true + default: + return nil, E.New("tls_spoof: unknown method ", method) + } + return buildTCPSegment(src, dst, sequence, receiveNext, payload, corrupt), nil +} + +func applyTCPChecksum(tcp header.TCP, srcAddr, dstAddr netip.Addr, payload []byte, corrupt bool) { + tcpLen := tcpHeaderLen + len(payload) + pseudo := header.PseudoHeaderChecksum(header.TCPProtocolNumber, srcAddr.AsSlice(), dstAddr.AsSlice(), uint16(tcpLen)) + payloadChecksum := checksum.Checksum(payload, 0) + tcpChecksum := ^tcp.CalculateChecksum(checksum.Combine(pseudo, payloadChecksum)) + if corrupt { + tcpChecksum ^= 0xFFFF + } + tcp.SetChecksum(tcpChecksum) +} diff --git a/common/tlsspoof/packet_test.go b/common/tlsspoof/packet_test.go new file mode 100644 index 0000000000..992a96840e --- /dev/null +++ b/common/tlsspoof/packet_test.go @@ -0,0 +1,77 @@ +package tlsspoof + +import ( + "net/netip" + "testing" + + "github.com/sagernet/sing-tun/gtcpip" + "github.com/sagernet/sing-tun/gtcpip/checksum" + "github.com/sagernet/sing-tun/gtcpip/header" + + "github.com/stretchr/testify/require" +) + +func TestBuildTCPSegment_IPv4_ValidChecksum(t *testing.T) { + t.Parallel() + src := netip.MustParseAddrPort("10.0.0.1:54321") + dst := netip.MustParseAddrPort("1.2.3.4:443") + payload := []byte("fake-client-hello") + frame := buildTCPSegment(src, dst, 100_000, 200_000, payload, false) + + ip := header.IPv4(frame[:header.IPv4MinimumSize]) + require.True(t, ip.IsChecksumValid()) + + tcp := header.TCP(frame[header.IPv4MinimumSize:]) + payloadChecksum := checksum.Checksum(payload, 0) + require.True(t, tcp.IsChecksumValid( + tcpip.AddrFrom4(src.Addr().As4()), + tcpip.AddrFrom4(dst.Addr().As4()), + payloadChecksum, + uint16(len(payload)), + )) +} + +func TestBuildTCPSegment_IPv4_CorruptChecksum(t *testing.T) { + t.Parallel() + src := netip.MustParseAddrPort("10.0.0.1:54321") + dst := netip.MustParseAddrPort("1.2.3.4:443") + payload := []byte("fake-client-hello") + frame := buildTCPSegment(src, dst, 100_000, 200_000, payload, true) + + tcp := header.TCP(frame[header.IPv4MinimumSize:]) + payloadChecksum := checksum.Checksum(payload, 0) + require.False(t, tcp.IsChecksumValid( + tcpip.AddrFrom4(src.Addr().As4()), + tcpip.AddrFrom4(dst.Addr().As4()), + payloadChecksum, + uint16(len(payload)), + )) + // IP checksum must still be valid so the router forwards the packet. + require.True(t, header.IPv4(frame[:header.IPv4MinimumSize]).IsChecksumValid()) +} + +func TestBuildTCPSegment_IPv6_ValidChecksum(t *testing.T) { + t.Parallel() + src := netip.MustParseAddrPort("[fe80::1]:54321") + dst := netip.MustParseAddrPort("[2606:4700::1]:443") + payload := []byte("fake-client-hello") + frame := buildTCPSegment(src, dst, 0xDEADBEEF, 0x12345678, payload, false) + + tcp := header.TCP(frame[header.IPv6MinimumSize:]) + payloadChecksum := checksum.Checksum(payload, 0) + require.True(t, tcp.IsChecksumValid( + tcpip.AddrFrom16(src.Addr().As16()), + tcpip.AddrFrom16(dst.Addr().As16()), + payloadChecksum, + uint16(len(payload)), + )) +} + +func TestBuildTCPSegment_MixedFamilyPanics(t *testing.T) { + t.Parallel() + src := netip.MustParseAddrPort("10.0.0.1:54321") + dst := netip.MustParseAddrPort("[2606:4700::1]:443") + require.Panics(t, func() { + buildTCPSegment(src, dst, 0, 0, nil, false) + }) +} diff --git a/common/tlsspoof/raw_darwin.go b/common/tlsspoof/raw_darwin.go new file mode 100644 index 0000000000..170561a872 --- /dev/null +++ b/common/tlsspoof/raw_darwin.go @@ -0,0 +1,161 @@ +package tlsspoof + +import ( + "encoding/binary" + "net" + "net/netip" + "strconv" + "strings" + "sync" + "syscall" + + E "github.com/sagernet/sing/common/exceptions" + + "golang.org/x/sys/unix" +) + +const PlatformSupported = true + +// Offsets into xinpcb_n within each net.inet.tcp.pcblist_n record, identical +// to the values used by common/process/searcher_darwin_shared.go. +const ( + darwinXinpgenSize = 24 + darwinXsocketOffset = 104 + darwinXinpcbForeignPort = 16 + darwinXinpcbLocalPort = 18 + darwinXinpcbVFlag = 44 + darwinXinpcbForeignAddr = 48 + darwinXinpcbLocalAddr = 64 + darwinXinpcbIPv4Offset = 12 + + darwinTCPExtraSize = 208 + + darwinXtcpcbSndNxtOffset = 56 + darwinXtcpcbRcvNxtOffset = 80 +) + +var darwinStructSize = sync.OnceValue(func() int { + value, _ := syscall.Sysctl("kern.osrelease") + major, _, _ := strings.Cut(value, ".") + n, _ := strconv.ParseInt(major, 10, 64) + if n >= 22 { + return 408 + } + return 384 +}) + +type darwinSpoofer struct { + method Method + src netip.AddrPort + dst netip.AddrPort + rawFD int + rawSockAddr unix.Sockaddr + sendNext uint32 + receiveNext uint32 +} + +func newRawSpoofer(conn net.Conn, method Method) (Spoofer, error) { + _, src, dst, err := tcpEndpoints(conn) + if err != nil { + return nil, err + } + fd, sockaddr, err := openDarwinRawSocket(dst) + if err != nil { + return nil, err + } + sendNext, receiveNext, err := readDarwinTCPSequence(src, dst) + if err != nil { + unix.Close(fd) + return nil, err + } + return &darwinSpoofer{ + method: method, + src: src, + dst: dst, + rawFD: fd, + rawSockAddr: sockaddr, + sendNext: sendNext, + receiveNext: receiveNext, + }, nil +} + +// readDarwinTCPSequence scans net.inet.tcp.pcblist_n for the PCB that matches +// src -> dst and returns (snd_nxt, rcv_nxt). These live in xtcpcb_n at the end +// of each record; see darwin-xnu bsd/netinet/in_pcblist.c:get_pcblist_n. +func readDarwinTCPSequence(src, dst netip.AddrPort) (uint32, uint32, error) { + buffer, err := unix.SysctlRaw("net.inet.tcp.pcblist_n") + if err != nil { + return 0, 0, E.Cause(err, "sysctl net.inet.tcp.pcblist_n") + } + structSize := darwinStructSize() + itemSize := structSize + darwinTCPExtraSize + for i := darwinXinpgenSize; i+itemSize <= len(buffer); i += itemSize { + inpcb := buffer[i : i+darwinXsocketOffset] + xtcpcb := buffer[i+structSize : i+itemSize] + localPort := binary.BigEndian.Uint16(inpcb[darwinXinpcbLocalPort : darwinXinpcbLocalPort+2]) + remotePort := binary.BigEndian.Uint16(inpcb[darwinXinpcbForeignPort : darwinXinpcbForeignPort+2]) + if localPort != src.Port() || remotePort != dst.Port() { + continue + } + versionFlag := inpcb[darwinXinpcbVFlag] + var localAddr, remoteAddr netip.Addr + switch { + case versionFlag&0x1 != 0: + localAddr = netip.AddrFrom4([4]byte(inpcb[darwinXinpcbLocalAddr+darwinXinpcbIPv4Offset : darwinXinpcbLocalAddr+darwinXinpcbIPv4Offset+4])) + remoteAddr = netip.AddrFrom4([4]byte(inpcb[darwinXinpcbForeignAddr+darwinXinpcbIPv4Offset : darwinXinpcbForeignAddr+darwinXinpcbIPv4Offset+4])) + case versionFlag&0x2 != 0: + localAddr = netip.AddrFrom16([16]byte(inpcb[darwinXinpcbLocalAddr : darwinXinpcbLocalAddr+16])) + remoteAddr = netip.AddrFrom16([16]byte(inpcb[darwinXinpcbForeignAddr : darwinXinpcbForeignAddr+16])) + default: + continue + } + if localAddr.Unmap() != src.Addr() || remoteAddr.Unmap() != dst.Addr() { + continue + } + sendNext := binary.NativeEndian.Uint32(xtcpcb[darwinXtcpcbSndNxtOffset : darwinXtcpcbSndNxtOffset+4]) + receiveNext := binary.NativeEndian.Uint32(xtcpcb[darwinXtcpcbRcvNxtOffset : darwinXtcpcbRcvNxtOffset+4]) + return sendNext, receiveNext, nil + } + return 0, 0, E.New("tls_spoof: connection ", src, "->", dst, " not found in pcblist_n") +} + +func openDarwinRawSocket(dst netip.AddrPort) (int, unix.Sockaddr, error) { + if !dst.Addr().Is4() { + // macOS does not expose IPV6_HDRINCL; raw AF_INET6 injection would + // require either BPF link-layer writes or kernel-side IPv6 header + // synthesis, neither of which is implemented here. + return -1, nil, E.New("tls_spoof: IPv6 not supported on darwin") + } + return openIPv4RawSocket(dst) +} + +func (s *darwinSpoofer) Inject(payload []byte) error { + frame, err := buildSpoofFrame(s.method, s.src, s.dst, s.sendNext, s.receiveNext, payload) + if err != nil { + return err + } + // Darwin inherits the historical BSD quirk: with IP_HDRINCL the kernel + // expects ip_len and ip_off in host byte order, not network byte order. + // Apple's rip_output swaps them back before transmission. This does not + // apply to IPv6. + if s.src.Addr().Is4() { + totalLen := binary.BigEndian.Uint16(frame[2:4]) + binary.NativeEndian.PutUint16(frame[2:4], totalLen) + fragOff := binary.BigEndian.Uint16(frame[6:8]) + binary.NativeEndian.PutUint16(frame[6:8], fragOff) + } + err = unix.Sendto(s.rawFD, frame, 0, s.rawSockAddr) + if err != nil { + return E.Cause(err, "sendto raw socket") + } + return nil +} + +func (s *darwinSpoofer) Close() error { + if s.rawFD < 0 { + return nil + } + err := unix.Close(s.rawFD) + s.rawFD = -1 + return err +} diff --git a/common/tlsspoof/raw_linux.go b/common/tlsspoof/raw_linux.go new file mode 100644 index 0000000000..cb694aba96 --- /dev/null +++ b/common/tlsspoof/raw_linux.go @@ -0,0 +1,127 @@ +package tlsspoof + +import ( + "net" + "net/netip" + + "github.com/sagernet/sing/common/control" + E "github.com/sagernet/sing/common/exceptions" + + "golang.org/x/sys/unix" +) + +const PlatformSupported = true + +const ( + // Values of enum { TCP_NO_QUEUE, TCP_RECV_QUEUE, TCP_SEND_QUEUE } from + // include/net/tcp.h; not exported by golang.org/x/sys/unix. + tcpRecvQueue = 1 + tcpSendQueue = 2 +) + +type linuxSpoofer struct { + method Method + src netip.AddrPort + dst netip.AddrPort + rawFD int + rawSockAddr unix.Sockaddr + sendNext uint32 + receiveNext uint32 +} + +func newRawSpoofer(conn net.Conn, method Method) (Spoofer, error) { + tcpConn, src, dst, err := tcpEndpoints(conn) + if err != nil { + return nil, err + } + fd, sockaddr, err := openLinuxRawSocket(dst) + if err != nil { + return nil, err + } + spoofer := &linuxSpoofer{ + method: method, + src: src, + dst: dst, + rawFD: fd, + rawSockAddr: sockaddr, + } + err = spoofer.loadSequenceNumbers(tcpConn) + if err != nil { + unix.Close(fd) + return nil, err + } + return spoofer, nil +} + +func openLinuxRawSocket(dst netip.AddrPort) (int, unix.Sockaddr, error) { + if dst.Addr().Is4() { + return openIPv4RawSocket(dst) + } + fd, err := unix.Socket(unix.AF_INET6, unix.SOCK_RAW, unix.IPPROTO_TCP) + if err != nil { + return -1, nil, E.Cause(err, "open AF_INET6 SOCK_RAW") + } + err = unix.SetsockoptInt(fd, unix.IPPROTO_IPV6, unix.IPV6_HDRINCL, 1) + if err != nil { + unix.Close(fd) + return -1, nil, E.Cause(err, "set IPV6_HDRINCL") + } + sockaddr := &unix.SockaddrInet6{Port: int(dst.Port())} + sockaddr.Addr = dst.Addr().As16() + return fd, sockaddr, nil +} + +// loadSequenceNumbers puts the socket briefly into TCP_REPAIR mode to read +// snd_nxt and rcv_nxt from the kernel. TCP_REPAIR requires CAP_NET_ADMIN; +// callers must run as root or grant both CAP_NET_RAW and CAP_NET_ADMIN. +func (s *linuxSpoofer) loadSequenceNumbers(tcpConn *net.TCPConn) error { + return control.Conn(tcpConn, func(raw uintptr) error { + fd := int(raw) + err := unix.SetsockoptInt(fd, unix.IPPROTO_TCP, unix.TCP_REPAIR, unix.TCP_REPAIR_ON) + if err != nil { + return E.Cause(err, "enter TCP_REPAIR (need CAP_NET_ADMIN)") + } + defer unix.SetsockoptInt(fd, unix.IPPROTO_TCP, unix.TCP_REPAIR, unix.TCP_REPAIR_OFF) + + err = unix.SetsockoptInt(fd, unix.IPPROTO_TCP, unix.TCP_REPAIR_QUEUE, tcpSendQueue) + if err != nil { + return E.Cause(err, "select TCP_SEND_QUEUE") + } + sendSequence, err := unix.GetsockoptInt(fd, unix.IPPROTO_TCP, unix.TCP_QUEUE_SEQ) + if err != nil { + return E.Cause(err, "read send queue sequence") + } + err = unix.SetsockoptInt(fd, unix.IPPROTO_TCP, unix.TCP_REPAIR_QUEUE, tcpRecvQueue) + if err != nil { + return E.Cause(err, "select TCP_RECV_QUEUE") + } + receiveSequence, err := unix.GetsockoptInt(fd, unix.IPPROTO_TCP, unix.TCP_QUEUE_SEQ) + if err != nil { + return E.Cause(err, "read recv queue sequence") + } + s.sendNext = uint32(sendSequence) + s.receiveNext = uint32(receiveSequence) + return nil + }) +} + +func (s *linuxSpoofer) Inject(payload []byte) error { + frame, err := buildSpoofFrame(s.method, s.src, s.dst, s.sendNext, s.receiveNext, payload) + if err != nil { + return err + } + err = unix.Sendto(s.rawFD, frame, 0, s.rawSockAddr) + if err != nil { + return E.Cause(err, "sendto raw socket") + } + return nil +} + +func (s *linuxSpoofer) Close() error { + if s.rawFD < 0 { + return nil + } + err := unix.Close(s.rawFD) + s.rawFD = -1 + return err +} diff --git a/common/tlsspoof/raw_stub.go b/common/tlsspoof/raw_stub.go new file mode 100644 index 0000000000..a2da87d6b3 --- /dev/null +++ b/common/tlsspoof/raw_stub.go @@ -0,0 +1,15 @@ +//go:build !linux && !darwin && !(windows && (amd64 || 386)) + +package tlsspoof + +import ( + "net" + + E "github.com/sagernet/sing/common/exceptions" +) + +const PlatformSupported = false + +func newRawSpoofer(conn net.Conn, method Method) (Spoofer, error) { + return nil, E.New("tls_spoof: unsupported platform") +} diff --git a/common/tlsspoof/raw_unix.go b/common/tlsspoof/raw_unix.go new file mode 100644 index 0000000000..7ab1d44a27 --- /dev/null +++ b/common/tlsspoof/raw_unix.go @@ -0,0 +1,26 @@ +//go:build linux || darwin + +package tlsspoof + +import ( + "net/netip" + + E "github.com/sagernet/sing/common/exceptions" + + "golang.org/x/sys/unix" +) + +func openIPv4RawSocket(dst netip.AddrPort) (int, unix.Sockaddr, error) { + fd, err := unix.Socket(unix.AF_INET, unix.SOCK_RAW, unix.IPPROTO_TCP) + if err != nil { + return -1, nil, E.Cause(err, "open AF_INET SOCK_RAW") + } + err = unix.SetsockoptInt(fd, unix.IPPROTO_IP, unix.IP_HDRINCL, 1) + if err != nil { + unix.Close(fd) + return -1, nil, E.Cause(err, "set IP_HDRINCL") + } + sockaddr := &unix.SockaddrInet4{Port: int(dst.Port())} + sockaddr.Addr = dst.Addr().As4() + return fd, sockaddr, nil +} diff --git a/common/tlsspoof/raw_windows.go b/common/tlsspoof/raw_windows.go new file mode 100644 index 0000000000..b6961169f1 --- /dev/null +++ b/common/tlsspoof/raw_windows.go @@ -0,0 +1,218 @@ +//go:build windows && (amd64 || 386) + +package tlsspoof + +import ( + "errors" + "net" + "net/netip" + "sync" + "sync/atomic" + "time" + + "github.com/sagernet/sing-box/common/windivert" + "github.com/sagernet/sing-tun/gtcpip/header" + E "github.com/sagernet/sing/common/exceptions" + + "golang.org/x/sys/windows" +) + +const PlatformSupported = true + +// closeGracePeriod caps how long Close() waits for the divert goroutine to +// observe the kernel-emitted real ClientHello and perform the reorder +// (fake → real). In practice this completes in microseconds; the cap +// bounds the pathological case where the kernel buffers the packet. +const closeGracePeriod = 2 * time.Second + +type windowsSpoofer struct { + method Method + src, dst netip.AddrPort + divertH *windivert.Handle + injectH *windivert.Handle + + fakeReady chan []byte // buffered(1): staged by Inject + done chan struct{} // closed by run() on exit + closeOnce sync.Once + runErr atomic.Pointer[error] +} + +func newRawSpoofer(conn net.Conn, method Method) (Spoofer, error) { + _, src, dst, err := tcpEndpoints(conn) + if err != nil { + return nil, err + } + + filter, err := windivert.OutboundTCP(src, dst) + if err != nil { + return nil, err + } + divertH, err := windivert.Open(filter, windivert.LayerNetwork, 0, 0) + if err != nil { + return nil, E.Cause(err, "tls_spoof: open WinDivert") + } + injectH, err := windivert.Open(nil, windivert.LayerNetwork, 0, windivert.FlagSendOnly) + if err != nil { + divertH.Close() + return nil, E.Cause(err, "tls_spoof: open WinDivert") + } + s := &windowsSpoofer{ + method: method, + src: src, + dst: dst, + divertH: divertH, + injectH: injectH, + fakeReady: make(chan []byte, 1), + done: make(chan struct{}), + } + go s.run() + return s, nil +} + +func (s *windowsSpoofer) Inject(payload []byte) error { + select { + case s.fakeReady <- payload: + return nil + case <-s.done: + if p := s.runErr.Load(); p != nil { + return *p + } + return E.New("tls_spoof: spoofer closed before Inject") + } +} + +func (s *windowsSpoofer) Close() error { + s.closeOnce.Do(func() { + // Give run() a grace window to finish handling the real packet. + select { + case <-s.done: + case <-time.After(closeGracePeriod): + // Force Recv() to return by closing the divert handle. + s.divertH.Close() + <-s.done + } + s.injectH.Close() + }) + if p := s.runErr.Load(); p != nil { + return *p + } + return nil +} + +func (s *windowsSpoofer) recordErr(err error) { s.runErr.Store(&err) } + +func (s *windowsSpoofer) run() { + defer close(s.done) + defer s.divertH.Close() + + buf := make([]byte, windivert.MTUMax) + for { + n, addr, err := s.divertH.Recv(buf) + if err != nil { + if errors.Is(err, windows.ERROR_OPERATION_ABORTED) || + errors.Is(err, windows.ERROR_NO_DATA) { + return + } + s.recordErr(E.Cause(err, "windivert recv")) + return + } + pkt := buf[:n] + seq, ack, payloadLen, ok := parseTCPFields(pkt, addr.IPv6()) + if !ok { + // Malformed / not TCP — shouldn't match our filter, but be safe. + _, _ = s.divertH.Send(pkt, &addr) + continue + } + if payloadLen == 0 { + // Handshake ACK, keepalive, FIN — pass through unchanged. + _, err := s.divertH.Send(pkt, &addr) + if err != nil { + s.recordErr(E.Cause(err, "windivert re-inject empty")) + return + } + continue + } + + // Non-empty outbound TCP payload = the real ClientHello. + var fake []byte + select { + case fake = <-s.fakeReady: + default: + // Inject() not yet called — pass through and keep observing. + _, err := s.divertH.Send(pkt, &addr) + if err != nil { + s.recordErr(E.Cause(err, "windivert re-inject early data")) + return + } + continue + } + + frame, err := buildSpoofFrame(s.method, s.src, s.dst, seq, ack, fake) + if err != nil { + s.recordErr(err) + return + } + fakeAddr := addr // inherit Outbound, IfIdx + // buildSpoofFrame emits ready-to-wire bytes. The driver recomputes + // checksums on Send when TCPChecksum/IPChecksum are 0 — which would + // overwrite the intentionally corrupt checksum in WrongChecksum mode. + // Force both to 1 to keep our bytes intact. + fakeAddr.SetIPChecksum(true) + fakeAddr.SetTCPChecksum(true) + _, err = s.injectH.Send(frame, &fakeAddr) + if err != nil { + s.recordErr(E.Cause(err, "windivert inject fake")) + return + } + _, err = s.divertH.Send(pkt, &addr) + if err != nil { + s.recordErr(E.Cause(err, "windivert re-inject real")) + return + } + return // single-shot reorder complete + } +} + +func parseTCPFields(pkt []byte, isV6 bool) (seq, ack uint32, payloadLen int, ok bool) { + if isV6 { + if len(pkt) < header.IPv6MinimumSize+header.TCPMinimumSize { + return 0, 0, 0, false + } + ip := header.IPv6(pkt) + if ip.TransportProtocol() != header.TCPProtocolNumber { + return 0, 0, 0, false + } + tcp := header.TCP(pkt[header.IPv6MinimumSize:]) + tcpHdr := int(tcp.DataOffset()) + if tcpHdr < header.TCPMinimumSize || header.IPv6MinimumSize+tcpHdr > len(pkt) { + return 0, 0, 0, false + } + return tcp.SequenceNumber(), tcp.AckNumber(), + len(pkt) - header.IPv6MinimumSize - tcpHdr, true + } + if len(pkt) < header.IPv4MinimumSize+header.TCPMinimumSize { + return 0, 0, 0, false + } + ip := header.IPv4(pkt) + if ip.Protocol() != uint8(header.TCPProtocolNumber) { + return 0, 0, 0, false + } + ihl := int(ip.HeaderLength()) + // ihl+TCPMinimumSize guards the TCP-header field reads below; without + // this, an IPv4 packet with options (ihl>20) against a 40-byte buffer + // reads past the TCP slice when calling DataOffset. + if ihl < header.IPv4MinimumSize || ihl+header.TCPMinimumSize > len(pkt) { + return 0, 0, 0, false + } + tcp := header.TCP(pkt[ihl:]) + tcpHdr := int(tcp.DataOffset()) + if tcpHdr < header.TCPMinimumSize || ihl+tcpHdr > len(pkt) { + return 0, 0, 0, false + } + total := int(ip.TotalLength()) + if total == 0 || total > len(pkt) { + total = len(pkt) + } + return tcp.SequenceNumber(), tcp.AckNumber(), + total - ihl - tcpHdr, true +} diff --git a/common/tlsspoof/raw_windows_test.go b/common/tlsspoof/raw_windows_test.go new file mode 100644 index 0000000000..58566b8759 --- /dev/null +++ b/common/tlsspoof/raw_windows_test.go @@ -0,0 +1,112 @@ +//go:build windows && (amd64 || 386) + +package tlsspoof + +import ( + "net/netip" + "testing" + + "github.com/sagernet/sing-tun/gtcpip/header" + + "github.com/stretchr/testify/require" +) + +func TestParseTCPFieldsIPv4Valid(t *testing.T) { + t.Parallel() + src := netip.MustParseAddrPort("10.0.0.1:54321") + dst := netip.MustParseAddrPort("1.2.3.4:443") + payload := []byte("hello") + frame := buildTCPSegment(src, dst, 1000, 2000, payload, false) + + seq, ack, payloadLen, ok := parseTCPFields(frame, false) + require.True(t, ok) + require.Equal(t, uint32(1000), seq) + require.Equal(t, uint32(2000), ack) + require.Equal(t, len(payload), payloadLen) +} + +func TestParseTCPFieldsIPv4NoPayload(t *testing.T) { + t.Parallel() + src := netip.MustParseAddrPort("10.0.0.1:54321") + dst := netip.MustParseAddrPort("1.2.3.4:443") + frame := buildTCPSegment(src, dst, 42, 100, nil, false) + + seq, ack, payloadLen, ok := parseTCPFields(frame, false) + require.True(t, ok) + require.Equal(t, uint32(42), seq) + require.Equal(t, uint32(100), ack) + require.Equal(t, 0, payloadLen) +} + +func TestParseTCPFieldsIPv6Valid(t *testing.T) { + t.Parallel() + src := netip.MustParseAddrPort("[fe80::1]:54321") + dst := netip.MustParseAddrPort("[2606:4700::1]:443") + payload := []byte("hello-v6") + frame := buildTCPSegment(src, dst, 0xDEADBEEF, 0x12345678, payload, false) + + seq, ack, payloadLen, ok := parseTCPFields(frame, true) + require.True(t, ok) + require.Equal(t, uint32(0xDEADBEEF), seq) + require.Equal(t, uint32(0x12345678), ack) + require.Equal(t, len(payload), payloadLen) +} + +func TestParseTCPFieldsIPv4TooShort(t *testing.T) { + t.Parallel() + _, _, _, ok := parseTCPFields(make([]byte, header.IPv4MinimumSize+header.TCPMinimumSize-1), false) + require.False(t, ok) +} + +func TestParseTCPFieldsIPv6TooShort(t *testing.T) { + t.Parallel() + _, _, _, ok := parseTCPFields(make([]byte, header.IPv6MinimumSize+header.TCPMinimumSize-1), true) + require.False(t, ok) +} + +// buildTCPSegment only produces TCP; a UDP packet hitting parseTCPFields +// (for example from a mis-specified filter) must be rejected. +func TestParseTCPFieldsIPv4WrongProtocol(t *testing.T) { + t.Parallel() + frame := make([]byte, header.IPv4MinimumSize+header.TCPMinimumSize) + ip := header.IPv4(frame[:header.IPv4MinimumSize]) + ip.Encode(&header.IPv4Fields{ + TotalLength: uint16(len(frame)), + TTL: 64, + Protocol: 17, // UDP + SrcAddr: netip.MustParseAddr("10.0.0.1"), + DstAddr: netip.MustParseAddr("10.0.0.2"), + }) + _, _, _, ok := parseTCPFields(frame, false) + require.False(t, ok) +} + +func TestParseTCPFieldsIPv6WrongProtocol(t *testing.T) { + t.Parallel() + frame := make([]byte, header.IPv6MinimumSize+header.TCPMinimumSize) + ip := header.IPv6(frame[:header.IPv6MinimumSize]) + ip.Encode(&header.IPv6Fields{ + PayloadLength: header.TCPMinimumSize, + TransportProtocol: 17, // UDP + HopLimit: 64, + SrcAddr: netip.MustParseAddr("fe80::1"), + DstAddr: netip.MustParseAddr("fe80::2"), + }) + _, _, _, ok := parseTCPFields(frame, true) + require.False(t, ok) +} + +// ihl > 20 must not read past the TCP slice. Build an IPv4 packet with +// options header but truncate so ihl*4 + TCPMinimumSize exceeds len. +func TestParseTCPFieldsIPv4OptionsOverflow(t *testing.T) { + t.Parallel() + // Start with a valid IPv4+TCP frame, then lie about the header length. + src := netip.MustParseAddrPort("10.0.0.1:1") + dst := netip.MustParseAddrPort("10.0.0.2:2") + frame := buildTCPSegment(src, dst, 0, 0, []byte("x"), false) + ip := header.IPv4(frame[:header.IPv4MinimumSize]) + // ihl=15 → 60 bytes of IP header claimed, but buffer only has 20. + ip.SetHeaderLength(60) + _, _, _, ok := parseTCPFields(frame, false) + require.False(t, ok) +} diff --git a/common/tlsspoof/spoof.go b/common/tlsspoof/spoof.go new file mode 100644 index 0000000000..2a27ec3280 --- /dev/null +++ b/common/tlsspoof/spoof.go @@ -0,0 +1,100 @@ +package tlsspoof + +import ( + "net" + + E "github.com/sagernet/sing/common/exceptions" +) + +type Method int + +const ( + MethodWrongSequence Method = iota + MethodWrongChecksum +) + +const ( + MethodNameWrongSequence = "wrong-sequence" + MethodNameWrongChecksum = "wrong-checksum" +) + +func ParseMethod(s string) (Method, error) { + switch s { + case "", MethodNameWrongSequence: + return MethodWrongSequence, nil + case MethodNameWrongChecksum: + return MethodWrongChecksum, nil + default: + return 0, E.New("tls_spoof: unknown method: ", s) + } +} + +func (m Method) String() string { + switch m { + case MethodWrongSequence: + return MethodNameWrongSequence + case MethodWrongChecksum: + return MethodNameWrongChecksum + default: + return "unknown" + } +} + +type Spoofer interface { + Inject(payload []byte) error + Close() error +} + +func NewSpoofer(conn net.Conn, method Method) (Spoofer, error) { + return newRawSpoofer(conn, method) +} + +type Conn struct { + net.Conn + spoofer Spoofer + fakeSNI string + injected bool +} + +func NewConn(conn net.Conn, spoofer Spoofer, fakeSNI string) *Conn { + return &Conn{ + Conn: conn, + spoofer: spoofer, + fakeSNI: fakeSNI, + } +} + +func (c *Conn) Write(b []byte) (int, error) { + if c.injected { + return c.Conn.Write(b) + } + defer c.spoofer.Close() + fake, err := rewriteSNI(b, c.fakeSNI) + if err != nil { + return 0, E.Cause(err, "tls_spoof: rewrite SNI") + } + err = c.spoofer.Inject(fake) + if err != nil { + return 0, E.Cause(err, "tls_spoof: inject") + } + c.injected = true + return c.Conn.Write(b) +} + +func (c *Conn) Close() error { + return E.Append(c.Conn.Close(), c.spoofer.Close(), func(e error) error { + return E.Cause(e, "close spoofer") + }) +} + +func (c *Conn) ReaderReplaceable() bool { + return true +} + +func (c *Conn) WriterReplaceable() bool { + return c.injected +} + +func (c *Conn) Upstream() any { + return c.Conn +} diff --git a/common/windivert/address_test.go b/common/windivert/address_test.go new file mode 100644 index 0000000000..bfc995589d --- /dev/null +++ b/common/windivert/address_test.go @@ -0,0 +1,53 @@ +package windivert + +import ( + "testing" + "unsafe" + + "github.com/stretchr/testify/require" +) + +func TestAddressSize(t *testing.T) { + t.Parallel() + require.Equal(t, uintptr(80), unsafe.Sizeof(Address{})) +} + +func TestAddressIPv6(t *testing.T) { + t.Parallel() + var addr Address + require.False(t, addr.IPv6()) + addr.bits = 1 << addrBitIPv6 + require.True(t, addr.IPv6()) +} + +func TestAddressSetIPChecksum(t *testing.T) { + t.Parallel() + var addr Address + addr.SetIPChecksum(true) + require.Equal(t, uint32(1< + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + +============================================================================== + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. + +============================================================================== + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. + diff --git a/common/windivert/assets/WinDivert32.sys b/common/windivert/assets/WinDivert32.sys new file mode 100644 index 0000000000000000000000000000000000000000..d06738cbb78351cc57754fd484b77fac0df52cea GIT binary patch literal 79792 zcmeFa4R};VmOp$u-ANkKa9ao%B}yw%QBVU7NDPb}k`6&==n#_N@DWsGVun==-6SZ% zgqwz3ik@L+aR1Eej=1WK>$o#GqYwnK8;}kdH870Cfz|M_dfU!uP=*A|(C_cmz5S6u z6rFwFclUYzfx5T8?x|C!s!p9cb*kF&!;OMo5Cj8UI4lT_c+;PaKfn3Wf#iY1-xw&o z*6-aL8g(Cwdx z-7#Q5{|pWEFP@tJ#nH24cSYRaHjR7p&j^3CVcV`F{QcZ63Liad-SrvXf7@hz^CSMA z@aAE>Z7)xF^8>u?FTcBs-nN%Bd3g5250(?m-ZgOA1!0CRNqBS6tq(@h+JppMif*7F z{0m}MtFhNzg|``QD}`;UKS2=sBSbDq(BX-{MR*b;e0`i?&4@92vng-Mw@ zp_)84youI_;}!H)KH3#j`;6zJyh*N;PH)k z5MDori!UERiy)NWQMvej*ZqR9(TWJb6vn~*GhE!CO%Mw1P_qdWvyjjM2igb+;o|;m zg3vT==CnB!^}A#|PXY{(Z2{a@cVQGkU@c4v0jgc9X(5HmbJbRSy*@D*%smc+Ra#RR$5xRM2=7L~%9!l-qy|+aH?r9DQC}Jz8)LVN=He`NZd` z7J;ebs1BiP&)EzKuGHrY89Bo97D`AYZywUDzJ>G37DOtfe0aT1SP&deV2KtbyXOL> zQuf|k^tSr4-(NmS`oU=TSe9=oQ3)uIpN}LE38nTcfB9MXTSAG--D(vEO8ZA=cUHa= zN^Hac^p!0n8meTb&vp>l5_V>?6K~fY+5XDgSbl~EIRbNQ1ZKNe5C|SGam5E5){b&~ z$qqHrE4yX+EhVL+{1vGM*2DL8o_VX9(mL{4GEpSlB2Vp>0;x0IUz9D}+l)U{P--`# z5iRGYrs-Vsq#Co}DrR=0^*~8!*llLZj1@wKAg#9O#i#t!M$F8R9bMJ~(&~{Ew)zk= zT3Vf{mmS^WQ@(-``QxNEowGt$q4V0ioXnoeY$KgYeg(Q#8T+pVd(s6eHTI{Lk9;M} zZ9!hyo_a1Hh&;{_aRHH{QtL5RZIqtOM2UN+k0={=Zm-3icy6!GC6>(#Bqdn@TLsPR zCZP7DMUUQR1~88CEDhr)N9vu309yV~|7jy;jh0U7il}ZCI%n9OiV7&tJ}d}j^E6;8 zj{mRGM~J7-%_#VPE`5XueV#1ugFctGUm0(|`*=qxVsnks69sAqnm*&4-{uOcRzACqW?mF@eO^cwdxv0R&MZw8fQM`LNg zew^yhoW_8?m3-3UW<9$%)nyLY`P3M&w@`GbzwBs16v=PW~3tZpJh|3r7k_2y-H&O2c;7_>7u@n^MF8S>oA`UOu%d3izx++a`1dO&eXGKzp zvI~AzVw4{L|BMPKx+fJrbjDGkdu>lD@cQTV>Q5$Oj-{-6B(X-Z{&h4d%i-NBhj*(S ztrG?8`>44C_9pf9-MbTdhSf*Jk?n1={j_XXWP7_SDb`caAt=L09iqNOYpb1t_Xr#B z5qYRCIz`E}5!5FNX(p)9J4+R*My<8=-GxeWtkRdSZxNpj=8h!cLaj5Vz01^DZDb6b z_*Fh`P2rcXFSVbM&Fx)Z5;I~$vsbI#NX((+A8NH8bt#SYK%pWT zt)|oJ!dtCgx=dwICtja|9^+3Pe72FS#zcKlwl|9PDG)?SHi2m31T$HriY7l?d~UD7 zbBPUL^0B)vG9|2wObJtIG;4E#HWB>)I|GzpoO5AG zN29$$htZDiQegm3Z&ZG}60TF*3Y4Ndq*(r*e`OERxRa znQbwb_gXJU)3@s)1+gid5l-R6ToWPYng}tc5HU42p}aLSG7*$2e}tdSGD<5k5ft{A z4vRaam6#3-drXJLU$UH$QkRZR8{LZ>LJx90gT;Q79K)knMg`u>kCzC4@4+AHB9*C& zR3^sfIcL{82k(LZ0|bShvq1spD>M4wEAA>#U+XmIpNzC+Wc+Z3_>REYOXcOnMR!D8WR>4Izq>mMt-k&Cvq{ziG{3VcnY`uDF)_5y>j*lztihbp`XmTx~v>e>vXOFADvC`+Okum=BWpeDL)0I0kq<2})7f zhjP{|RQ4^(n&sEi|r z7{xL=KsY^Uy=e9v6YGm1R+N-hKp|$>YJ=K_U0}IJZ*dzImfiwGc!-f=vB)ei$&Pl0 zMPom14a{T%4o5s6Y)QdNrk7U|7R z2qaWqA4q(u!^TzVfyAxkgU&mU_=@VQ^?EXq4M3q;PHCq_ zfsMbk2~JK*5b$*(MiFq>dSeb(xQY#%u;-yty82@%I$!4i!b9?I$UzYe$nNpE`|rlq ziZA#uvQ`XIfdPhsEGl4q_;t#OxFd-3axy#BIGzCs?MNUE@5-xt#y289XaZoWTN;3* z@w%16Cwr>-a}}P?nNWS%Qwy)xvlvg*k#Yw->3D9Of;z^-4R|bw?#QtT0Z#^ez8{cp zG<*ht4|qnxhfV_@!Lo4QvS^$unr|t5=6sN4NOz>N<+YQDOj>=7pd(=(0V8`<%<02; z{3EE>BOa(z_JD^*^nnn`P>%TnzFbfAiHUQgTK!{G|SsWd@aAzUat z4q&Byrf)+h6$M7oQScoR1rY>dzvr)bLYewHA+2H*@SN5%1w3ZN(qF)H7IF9v)a;7- zy5Y0Ai0Tp2sP$E;9-(k}h;2lqBc`A_UOoBz}K1eg(zXM&SG|8o!X@ z>m%|19F5OMJlep%rB-JaWcv%U{S~i$3pnO}`-{4O{Uu^=${1vSeltGy_{93>y2D}m zVr#9=gfAMs_N~Ysu)oH$UVt3){4-kC706oNK!wsjw7KGl7W^*y4F>plN>-*sqkn^F z<(6rY>TgD{eAR2nB1PlsDefhTdo&uijN*<^+`MSqe2NoZN8HqCTmi+6rZ{UfZXCtU zrMThII19yP5WJLVoQdL`6xT^}HE*^q^rG^5`u0>b?li@1rMUN_aV->goZ@yy<91P; zaU0@(6ODU`;>J`S4(l}e?;7mwYchKuCbJ#T)dHanzBag*ylSv4m$3pHWuPVty z_Is6oV+&GA>=0OunSFKWb5}}S8tvjFi`(*shxIM&ptyaHKWNrBL0VAEWqSX@&X#%V zv>h)m0c(gB189T6JydGKmSn2HcN_XOqK60+gh**U;PH{>P2A*{YSM&KugO8o$Cbe< zO|0644Y?buZe*24Y%Re*v}!1;G_Yy|{Qax?vq4PA-m{{A=Z9s?kE*5$!+d2UR9_So z?&0!VkM)gl{Lk|4dJI|z>_&~>eCPTpvb~Af%|)d^I+qm~Sz6thGgqCt4~5lz_2RMD zgHYzzb-w?#?{$cE%x11TGjC5xmh9L!{?7~3e``GeRZ-oU7uMYKbLcZ2zR(k_IMs7l ztUnI!$EFzBy=mh)v}^0m=;ld-*y9nX_6XD|rtvDoo02hpw`WWS)WD6@>a_BaYg*+6M?B|T6)g4bT8tFy zP-c3&o;))R@|?GiBf5b?B}RG1+ighOz|%}fJB=8H`dXL1>N+;lY&~o>sW4JG1SB6v zwzkj@q^|_)YBLH#aiKBHo#8p>9?0BjcqKD;I*Lz6IhPcV0`dKE(nME{ClzFkDKx4VtC4#x7HmDc{)Gc@#@OEe#KhZe*P7`KShVO zx5|zt4sq~b#@sW{|3jt30R*5)`WX2v-<`gL?7M3$#Nn}mR*GS8x%>j*+m}^ZO#s^ z>55@blnJDq*}-XZLZwX?(dG?`;bi4S4ZdJKTv`}*x3JElu=@y58x(f8`cDi>Im&8U z!tQ4O=ddtnN;$*c_Xkt0KJkT*P5x7+lmM$alJX8)(5gqu;1+-I8l)s!eg0F)l-5jX zO_8+WNYhz;`eQ)p9sQZXEwf9M=KiqdaVl@CuiffvJ6^0q+Ha8#3()W$l)e-W1=$|! znO-zNJ$MWV053@O&fp8|sQAjE>-?WxXZ_H6VaKaKDjCWGZ=FY<==>DxxxKu#>5X17 z_)E4QL%?tSU||{Q452Ij>r#{qrfnfkZuLQ_YCSwpIU0>s*U!%Q#f#DF*b23m8Pqd5 zmH8N_5Wuf0Q{nxq_GK!qZW=LuQ$5FAW7vLd(o0Q(`<5kgZhvu`UTPElZ3#}k&{J-` z5DrtkC+teDn`IOPAY*?`+}`r<7(Y7qfKEFUlNaA|8>~K9~ z??D_S6%U!|ERk~(yi<(M!jclD0VV&Gr()4qBEUwQ3E zoZd=fNyXh^_b!MnYa#Em1^Zx=Aa3^+Igz|Xo}S=TRs$kXO$at~ESi8tiS&t)!=pI| zl$a}S&oTKG1FGm9w}t?hR3rgvkurt@ZR!cs>{M=5ftpeOYF>X>k31^XGz+eWrBBR& z?Y-50fPM+Nf<^glA;8! zrPk+^MIlhG9j%DKr}k$KBkEa!IZnEeTKRyuO-I#J46fmllHi^#T^L#ESf7(TNw*^Z zVpBs-vNkIkh4nc`M2@-G^u|S$*pP!sI;a1V>^+rc@MN?~|7cEIkC4?DlOp(hHxekI z<3b2+2azFro>pIN>Pt~yTs0{dcSDs>;yEDJH=}DZw@JE~Dz5Rtzy7Ksaobcnln$?H&pxK77oY8T&yrCEk& z4qj4Cm#_n!9sQS($UY>lp$drG^AlKWz}|%q1b@gVFX$GxfhTtRPZ@6_=`*EOdZ=3$ zGJ8#4`Z|>CHN8t70zfm)`lL~Z^k;xL>31H11QIj=RP9Nz_Clh#=06CJ?Kuh3DUm9r z7*Vg3F8`^7^*R0UP4?H~n)S$%{gt?84bS>m68iwd_vCQOd(xQF;{QBR`a~xex61Br zcM1?inF55iWDRFXA=zD=Ks9GWdQgeAc*muT`V)Aj=a~Bmahp8_s`{*|$HZ;5!bDDv zc927ME7ZGg5Vy}XG5<{+04jAkE3fhg4blnGe$1lJ7|PM6)MLj$FncENQOg-x=%jT2daX4D}FMYNuI2B5QQOzRyf4FyTV4s?Eq zZR6}36FAy1UuUb{m(s+h8g2sN1pkRN1kfp_8tYCVLeK2&@>J}vo|dPY88)n^rZQ|; zPfcU?C16ccDx&TtX>XcCuNjI~5)-2)rEoS^tRv zqxPh{gW_~3?y8g~88$}hNTdO7f_V_E#Z+(9^T2qUa1GpjaHa`10K4KpL8oR-hqE5Y7e1-~Xu~S8J0^dTxbW%z!wD@ckF{kuE;wLJ3xl@*JPI%EBgiVX+bH3j4Y`kJt3TL$f?Z| z+#grKH1bo#uU}vmx9xS7$ab@G6yf?~^IStP&zee0lu~>s1=6H{GV9zOWdO-$8nX{o zTELb~ENS8XJYyKOHh~9JnzUm0dWj|NHzWusDzbXw_=($#O<<^O&+QkVgzb()QgkkB z346?t{g|;bHIT#6TqqYq=WARLoDN)=ZoeUEMmWb1jU7+1g@)`x z_H-g)u1dn&Sf|%iJK{?=_)nj+Ch3#mogWT6Af0GizhPk0HuO8UvB5MnoNK_81;B#| zu#p2U2)#;>L%%Ish=m@lRDqAW**DXWJu|MA`VsAPWuP6IR_SjFL(l3gQ6_^8Fuh+% zk=lkMcn5;w_S_`0K`c=$l$dLTZz#{kKazZ>rLOT+Q^4-3vpKsf$E(!)glQ*MR02yz z_D`Xy@;+&hLOS;sD6TA&%5qKUD7mIYnyZoHZR9{f3Gm*ndJXX239nj_jaLKcU2v1& zro(+JQ{WkN#10Z7e|jCK2cPJRS0t&^FnS8-M}KOoU(*5NHUhK&Hl|I{)SmWKE@-1Y z`AWcguJhCzk;RM|ayrCyGnNA#5W0QrWATN~;fF$4#C$S5<)htPQ2zuA6ID{B_a|t- zofJ>&7!y7^V-4^EI!_^=y3V2ZDZPL2(ZmO_YARb;cen~4^j^HH<56m#-$Q=IQrYS& zg0+$L7)G07JqI%#n)>Z$AvrXrp?WmzupfEaYd@_jlS`el8_Uq+*b%uY2UE8)Q1rZr zK}j3^B;s-9V^Jy8$f-yN$8UI=hHOoXSzPlm0FvnPiUl6ozE!rrhE*@t#m`xw3d3UE zYd;PkuLSIz&XEEG_FVu0#jXk42?TK(g3Nw)vV3|F1d3OXCe{x?-0eSEJV-z%S4?94 z-w}Iz`;#z=CQ$&3y`4_Hq1MAwV42JYL$+27iMiO?D1<=;$D7ceq@his2#=)Aj5Z`F zP=DeZmWHN#NAho#TA**Q8dzzpJHKd_AXFJ>q1;G{n;;c}h4LygeNzci5&<1@-u^S7 zjY%0|<2*?*x=Lur#22apjbXe%$QhOTL?qeQe z1IXwb9UY*I^kG-2axDxyKrL+t4f3aVmVu2uk- zTKK^`0?B*G(}b4da-W-oDZ~h@EY&+THM`YS=5;9H!`><|hBPM_K{{+4Q{k|DcnvtC zN;ghMU+}qx@!G1JQU}4as2z>KmNuo>uOFR5SPR>eJ$3P_flnr8S1nqht{6fI5RxDiyTP;F z#v*0V988wiu<2=-<&wffnzq`6_|3Nn$yI`*QmjL*Q{6p8Oto5~=VsukR?ag5OxZgt zgX(z@IW+wY)z&?Q*VLWOYv~?`8oKX{)ovncHz}-bPsY`2Ft67DUN2GGGrZ#JS&ROW z>ba+@HSqwiFsoXIR$$lB_G$P}+NgkBfo6AM`0@w}SkMRJTVJJ=D^=?P)Wa|@Qvb(! zfhreuITadx+z%3fy}BPzJ9!;v-ARF@ zKf$XKmi zGA91ed|%i80!oXY5=`2-#wIfRQwg{~*yKK54X0f5n!I4Eihnt+B3TDXY=57}iwz`g z0VsL3QC7C>nonM9;pJuU=Scn>&7Yb4IgURk^5-P} zwDD&ie-`lP40`4py!olJ%^(Zj{ER?;I>8U*Tj-_5j^^*Nz6Sd{Smvr)RV;?RYqube z|CB+$7agkwot|}h!eRsM^?27V!n0Sxorqd*f9E0rnq;cnMSGI-F>Yv2a@-HqhU5|B z0N0zp3|oRo1&-JpTm&DX-`g3mZ?xc}fc+U@iq{r;;gAxB996B_Rgvkn zHxs&^`+%cl_@J}xoX2B`BTJ|Z5%^p`~O0&sqZ!1tI@H=rl0 z%U6{_T#i4)W>ex1k-4ev9%2gM53T_!XW-{~448e{cIy9XWI1IjRfZIzm~})GpazB4 zF)H>tGSYxVV&wBiN%&8gKeHydMzMiej~U!|vwTBVrrXMN6dIjXU~u2UGs;PY@-jpC zPgxV(e`d4H3+KfqAPohL*Hew2qrF|V%)3zLbF`(a21inmc5>C=H58-`Ts7FAg0yd| z2EQ&vkhW{pU^@lX4OptpZz!m4vQlsZ1!*6Z)EOT16>8=$5J_TU8N%gx2W*k8R)SnU zCl(G-cyTOzjKZ#1_#Fz@$HF@)yeSs`Ernl*g`cMIYq2n+@V;312?`&Hg?~)p)3NY8 z3ZIRIr&HK)Fp7URg;QhUQ54RIg@;jiTr8YI;k;P*>k@?L#KIv8FOG$eQP>p=zeC}1 zF?@cHaAfBbUPxMzjZit3vJ_r^yiL`YregMkskU_9Ag&|c7|!rIr`xbv=mPh!MYx9r z!w`m6)MzGTf~trOAgdhdfccy)Dbbb4oTkta7%QohA4b+ITWtbsA)V4;cmKwJA))&) znw!mqjbm!4pVenQ41ieHltTa6fYX42bY8dmX2Qc$ixBDNS}UPn#GZ$7wlcPuvk)wR z+@??kRHfM;A@QT4*)+{cP#Kf97>A^&pioNoN6R6Wc8@?nsu>MfIIHJm7ukMSivJ6P zUrF$Pt`GIY^4hQH{hu2;TRM+IwC(&r`V=PYCg==-;4zf7z+?^cr4K8%sq-#s#>E38 z!))L%VyTk0G(z@C4Tb%zhqH=DLNkQr zsJLklw9V4VGhV~r=nL#XLzROgUq1ceEc-aDUZEx7pxREPw5YL}h?Uy>k>F?q^#`Q3 zI_cBDr?jNJ&+?5>(p?PuPYg-1leaGHKM8#uN+aFili`#@a*1(&ehTS#7o)LBU8Un% zCIhrgq0}8W1gU%0bspiOk{$ADfL$Tz{aGE$q{ZVRPhK9Eu#Oz0k5260-IlTy5u0FZA!+MM#+G(|h zVH<+2a(O|!T-+=dHxYCCAQi+XrM<01Kf%AZ;RWzmf+AZdQc-(^V}W_>8rWWxIVWZjWCiI8|-1F z0N4aaU{Fctu?;X3u+-64fm5Yp2~*IbgZ9I}9t_a^caH=(pjmrGicR=+>n-X?ku&=*sZpwi+67Ey5@V%`d+M0Si9XJFgF9@h2@FzgVlHQWzgnBnOH z!8KH{IQk8~lWH1)j(Y`G$dyajy$OxsE)+YWDN-@D-i;V1Lg)_d(}W^rDNU6v_-L}Y zZHmr%ptdG#g@zjY2?i?g_kPJ|^qMlW9c@*^)(>K52w7(lI)pMBUxJKceJL$E(9Z7T z6!d(~5w(X&uP?4Ut)jg}MSBBY)UfX%HiAB}mru~!(dyXjJ~r%_bjqmz2>mViTjU~b zzF7aOUir6a`I~y=eMB7P^)6F67eYhfmF+E|f&6_Wl*He!cn!m0Wd&Sexw47d zVeF)BBW8bwc2Vq`VRA)@&`X8-X+4H<%dCBBbXVP8(iZa3F-~5vWhXusyapQ^=l2@? zb)S$56LP6^u~=MpDIhv!ujbGo zdNqYotj9cuLx~h0l>p7NEPrYZqj3@NT0r z=UN)P#e-p)0R%Tw&IGSL^D;SaLryBHRorgRL|NEnqn*Z<;Dhv$!E1l%G9Ufz8v5uZ zKtIHHD%4F2+OP1pnd?r_o(12q0ARjn5gd9sT*L0;!I`uW>Wvx_crAXyi@6ML0ciX# z!7cLD+<4?v%=l&sHc*&fMGjb|fQf6!tpqd#Xa>)~+#P(F!o}E;=?X67-bV5k!3%jt zT(=Z;VfGgg$IQXWltB$R4Z%BkcI+2-1v9vJGmV5UF3*^bQM9`!Q-Nk zc-)IT4#opr;x;pQZE#augi{yv#Dd#x3Gw~@$MGrHdykEX!nhP?lK-7*_=saIqo zMcxt@X-i@)7*Vu`O&i$3lvvs}r!m+~#y55qQD?v4Ut_`THrT4fO8=9ZVm+tvnYW1B zYPJYq9Ar&LgYl$U-7*ZKEVZ)(I+c8gy(ez7iNIjPw?Q&r38cY*G=)RzP^k>)IzFpG z8aE*EMm15~=G2MX3lfk-=&9TV!xGpBm%?HfJKXgBc3Tfx|5raEiwfD96#O7Qm>djH zu%<(!1kSOrGl2a`tm++V)x~X7Q~aM8VMjH~l+tlYtw1*95H+5_X*7#=UC#s9r!in% za`9;`56l(LGe?KGEoU&BmYUKP4-&=+LFoLoqE(Vp_p@CMVx;xLIIVAthiMq6_lfcF z9vEA$5aSVSTE-P(dio2Q3G{n;el! z*hqe3-!)H(t9e3P$xf3h5(fW@t!Y?wa6Bn;0I`$SUXdn>+}kTMg(82~E3!XD{<2qO zKZ>mH6$u8V3qBebX&V?%rsDV%l1U;XIhY-vGAKUf21+SzLtTPHc_tD@#O*!E|97%V z!v-Jc06qBdsrZy0yx~B6N>Y4@zo!(x#ug++fRUb(;2H=M1JUz3N8t!0XL}1PNN39| z%$drn(_j&d(!mOXF8bQhj^*&Zq)DZfguuBh1mG-t1~ejtCVXCKW_#F|>>!CcCoz=@ zFygNJgv&gK04ZgE#8M+@A%Haa;z@sqR@1E6@K8RPP{FoS(YxXrgTzr|ku*5q7F+kT}N$ z4^SRZQwzR9L5G5Fn+K^CE9*pYyDnrPJ3E+;#RDsWisD1pAzrkK)n;9hr(|H0kxPD6cJ6Y+r{Hxi^O1z$iWBEcmI zj02-OM!yy=$+U!jZM*bq2md-&RgZKwTGcT;jaT)(Gm(z>U@ZMAO7Gpa*K_*tw%z8O z82l^vgjQ7weuN;_aah)N1rJjaK2LDnMmAGd^RDqRFY^_o<9vMZ_xx)D#+>Hhb5U3n z`UOH!;gR_Rq}CL}a}*=0W)%0DB0KXuLbP(03=gskd! zYT#yujVHpz6oQfBE9gdPp!8d4ClS}Gp?f8C{)M^E(!%=y;kv-5Su-K2AGE(_=|DKN z5G|Fw1H}lTBIus!pbJ0bvQLPdak-u4re=36H*|g&8dG&A%QdqFr?Xpm_=c+CfXZ^o zD3sE#suE!$g|jXcfSu%sCRXfY393bv)Ric%`w0qyzN_s)QZS;^Z}Nq1=ANf~q3f7G zG?WK^j`Po=M{NdlL|0M@0%SZ2YQ?+em6JV6|ll%+WKUh6(Qb z9H-h2z@i!}%7rL{D_AoHUQNViABc+t5iZFHdy&XD8;?)73FL;%vI*D0-2~U*un7*h z58;G}fz`La1)Z8F`bpID?caV|4#65N-+F6L9|r?$>ay!@Uo8O|eaQ5Uv*PdAK*>n&FPYc}r}< zJ*8+zxcP7^;C>DF8r)vE|AZSb*CyNyXMt6_5 z;Wog%2zRWPetR@=(rB&3%RUyKuunJ8}-VF>ot2_blEFZm;Hoc>fu0 z@*>~^Hw&%^Za&;YaB=$&&vqyMX;7ui;65n|@T2=vj@arY?-V z96^*S;U{6O0&g`3=_BVO#CK+$;qfnZ*s$~4wH4T4&wz8T$w=1hrRdus2*QHUdN`zq z{jE2Bw*QnKR`kqZWgV&#lxr5xCkVn#0yO56g^?(FT@YbxUx?dh=o6TKGMUC!CQlCI zwPMhcY|Qj)=^$cuK$PB6bF%q*FqpkPa+rk__t1m@;ahX`^GFIINWZ8@aQ|DgV2NONF0=T#C7WgO6 z6cSLOG(^Y_1t+z{ZEM?q1hQp#w&Df3ns_-s763m4Lm>z9p9A|Ugh%F-5bztL=o4S(7@-{hS1h%ez zQdd9GLwnUI0V{DK=)nVi5wFlHc%Y3CLd(fB9G(jDAS!e}P{Ae)Wu>}>3VDJkgbHvQ zk*a`Ec*Zg6?L@VlgY!%{`E5s9_{k9T>K*VP+^)VT8X*+H71pRCHa+|yvwjcmoq|di z+g*0k>_Xhr()~7XyM}|nfpop6q2R?vQju~>P59}nNI;`vUdg~N8&0>n(+S+F1e4$% zgclBtrr?!|g~wXTy5KA`T?-K!+p8(>p`Xg8YnfrJ6?W!m1S{!8Wy7^O;`Y3;I3kmG zEhNaiu{cwZcddcJSrLHPtwYOpjiETZi3vzFvb?b-yiItU@kV=%O~pG^+~H>jw^`4v4?#NrHg`2PP-O64_|% z*FS~w@&Bd)cw^7=ILaZ{2KO199%aSx7pXl1hs`q1<y>vZ>uf4_PQlJ2wzAqUTNu;Os7w$i4(S=}f9c0`0y?_1x3Nq(wZb18dRT zbgV^lEvl{M1T$qMlWlAO$o7oLCVy%MZ0;N|9OlM(?rz#I3cD>~cRKb=umcJPk;*SI zDLP3V&1Hl2nT3unpr65>CcT8R^Ser5)y8uYgbD%nkr=(pO5l`b3j)*^-yDh z7VIYU60#|Ei)s!hk2mcIzJkA4m18Hszda}y78vQz5jBadEhWmo02Q@{ z%YimT)ZA2QZz`E^X0TjCR+{@dGBtA?m#>#I`FHYiL%2n=UbG0BEl&(03NBIXQJ@t7 z${Uj!A_JxhWJw$SI@+|+m`KOe-q?Q!LC~=3%`y0%jFQO0Qq6#LIxoUz_BL}ZQFFp{ zF_M*kz~QAbtUb+iZUG0Bs}tBE{g>HiR@vAB%c#BC9Z%jx7L1;At5f*qWU1n^;;;q! zQmOJg%tWj;E~yAfoG;K#$t2SDzA;(Fzl6?_gS$=gr!be_;2MFZUKp^clb8((M=)rc zfxqa%52N60ei&disT|%nT@Q_AOtVB6I zmQdBM0nn(+&nwimljUE$J*V0Gl!BuXbiodp^2?sH2*FYVy-ohtBk;>_`snFt##6Hz z<_y6#!t13k)!|ykZ85ZyvU|s1Y{4|v40GcC{u2iMJ0yRzurg?+#SwqwL{~eUFUSiv%QeriBr@s9N6mJ@{DqF{=bOr@%CL_0 zdb$9f49~vI1T+s-QQDU#^JwPb73D?9o^Ha(L$#DW_zej7sp_)FK>j4z!;j6N6i)_4 zpdimk?g5QPlPAXA;Oi@W^kH!6ti?8ad!`!k=gZ zzW+L(VsQsiCcV*Co^jlRvLf`cLOW_L- znSTRVySE7+6nY)ag-#_6lz^Pqz<2Fmr@jT_!x3s2 zD-o05H1b=NMB^9D3Na)*8vlz(Jf}30Mr%pctBRD4JJrvtm-Q;O?jD-&)KXoysHJ-C z?M|fvUo7m=(f@J>VDOI4JTyuI_3JF?j~HSR3+*ym`tsxBzl-KAQXU5+4)SV3Vm~r0 z#BuU@%KB&?8noaib?vo)6+d2$SQS)c!c^94e}=l$v-F9>Yz>pHvzAJh+RQLtO$oU5 za!oD8Zc<|{G{GTm6ZA5;QK3n2CG#y=940u7vwKt!kK9e!5qXZj-9UkxASCnY5npeh z*akHgEv&(8pjcRAQC_e(Ew-LwH>$DbXzWHvbQbKnzaP<*Bv=o?l*cso7&n@_24z9~ zp0wPCdeuU?G5}gZnASDZRX2asWz}qQ4MGeQ{(Ecp>H>M5X1ebN`39i}#l_N%H^?m3 zmjeOMIp*Fh)-QrL_8r}S6C@p(R={Y4|ESZlj_;9`uD}W2sAe4m`;tHEBwspkqHAzu z`!m`%)S1O??|Abqz)a5bJ`>3EHq)&*!B0UMlv0sZvw_!zuEn9cG{j>}ciUvu)W=8B zH8rewW6%21-887L1vR<2mk4ki&|=l1t8XA};j445N2$O#qR`bly-KCqb9$9Zm*Mm* zlCHl21$&fA*Ym_mr3-dYs(bU;aQ!jY6fd{WdY^#^PCS}%$$+wo?ie5&?Q1Ru?*6*6 zjpP76G=4D(eV7lU@>fBq5~VT&5rJs>@@P8p^8-}s5u|z1_>EURhV?hx3veI9?X9#4 z7(ZA{#|95q*r#uy7Q>X#f$0Zx&vRtID%+1dH<7nZk(mm=iwSF_ zOnk2RM*%&$0|K`~9KDU>P8O}ayv$g6B&^`nTg7M^r=653kH*b3}`(E8EPC%B30W_ z)}o%%&`d~c(nu|Eq%KUScVRg6Ed;D?N8B3G?5OW^@o*}qZ{`D zz~J590aN;J)y|zS@lXsM=2N-x8th%cFb&7~{tgBKIspy`oW_sa(ZM#o%cL6Mo|TKc zLZivp*K0r96JS~t;27Er1DIkQjzp8CsThmv3IHA&5Ach+FX&(&ETZ6-zQEzW%@~Pw zU(o43(UVPV>Vi%5JOWSM#EjC#4~w1$$X{((kvC(8=qaWg)kfS#FdaVVpQQ^HEW_!W zyD8!-w$dmUr{NGUo5pW&v4PgJ+Iz&;SUjUdogYL%c;0w||9m~RX1Gp6vwCafpjeYlfQW%!Av`rMw zc(eQo8(Kb%wD~>KPN`{u9DI+)(ovJe zpuUY%uCw@JAl7xSQeNCE4Y5wSN%TPt;m55PqK6Lj^u&D&popGV5#;zIXdH;BcC3c!ib-}sEiQU+ zPZhzh)0SdQ;TC33b1H9O(@G`u-+>{bP`J!j=&DNbp*H!C)kwkqy|`@;ns_eHh9+Lb zJ?*lzn0t=PQZ0FMtI^ivQCIg;1NYZcRiq8@FJZiw0MbT;=uqNmgS>MU{A4&THNr!G zva|_LlHl!a9OW=BJx<$*ejn-|G3dql|H%}?K4#-L;VtdevG-s$D;+|o!o?cyv<@+V zd!~+O2i$+o{=j`G<)ZsG9K4Na4^~0Mj2^vdyb)IkmTbYPA(&q(BY*~TA1P7#r%~&| z1XBsSPnRf9pF<0E1P^g$L5FU|`YrHFU*NWrp8;X%3-G<&kX(Gh|3Ey;!ni>@M1~Hv zsOY&Cmm^AF=v=qZ!$yz6(eAW+0Db!yz70qlksBWBYXOP0C=^h;LXmO0MHqy0EP8bO!NwIX!VTXnDgOx;Cd z(^U&l=Vsgq3rNsj9CJCK6)RF!-iQ^_wRCbm*Ps_NaV_ZbOxzH0`xuwKL@7Zc#Dykl z|FtE`Gz2ha_(6Na51dLC!qUD>Am<~Hk<;I8H-OL(+4x0+L_GX)`DY@tj|>P&79`s< zpyH*qJZ#WBwD(Fk0pj!xKQ#(X_>LJSm(Goau8zw~hrvQg;qi1vOc8)ER*+C&MkYCEN+NeG|1|+vMDAFo8A!Whxshmx z-$F8R?HINKSOYP+@*yI+u%nGNkd_nt*m0Z{RI3sej=P@5ea!L#@N-pjsm*lS{)%)_ z=UR^6-Elo4T}9hKg2{UwOLa9ZYK^V6+;nbL8n?w zt6H$I4&?(76dl2#YVT}7i(`SdMStP9C_^=c_Q-%N-(B@fHQR7x3muhe(gnWI16bE@ z)Rb|Q@+Mv5f}$35sOP(3lMG9BC{WXIUzr&?RT@5+p^B7~&D5uH%il2d6WmU8IAp+6 z{be^5=KX+4(qOBG5wC(+{~-86v>HQDjd>AFv9}Bx8T`T#KeHq|(#85NkO;dNM5)pe zI-J}E;aIhp2TEYtVED7S$VpdHqp%Doa5XApFj32P>?RP^21JYdHP``F-?f+x!&rbV z{&CL!sg6()_o`+0JK->H#HK=Xdl!l+qi3;nEKDIW1qCJ%boamU7Ex*OVlP@qHAgU2 z%mqio@Bjg%f^?mMA?2I zXy^}Mpz16VA-XzL`P1s_F?`kWn@akHNGFA|^@0~Yq&4m&x;pC(M-ySQl8%UUv%*w1 zb1p8PVKe7vLqO_60?rm8M`ge4Vxzc@?&QJL1>rgSJipyI$ozx;^=g$z= z5IR-;vS${EYkv(dTRqx4b9B}$gL=gszpFs>yiX~~Ja!+vS=C7136FMfLA#WXTNz4l zE5p>(Tl1aLLfp!rDw}5YRy9L3Bc*hdLKi?8P9bd?{sAXX{RuvIwQEptF_H4KQ$Mp>78C<&^+2mQ(x@$)wZb2{YkK*L%8SGwzJbk3%-p!e1YLPPP(@QZ(Wi=C7fs^^!A>1i=9jwp&pC!N~ zC#df{+vGbE~8Q$JF-rULfSKSEq&x1W$)M9ju* z@jj8kw~zw47G!|55}Jt(3CTAV^%U!~k*Tj*4krM1GXMY#7dSw9lbnls=Y92}QyG3O zNd`DLHW9`_xN(rV^Q5*j{K7kn{}Vk7!i&?bzJ?;R{v1sGaF(>82&Wxa5+grg4eTNl>4Ex9&kR@VqGHPt;wtdQ#TvNmc#oR!^jR zD^`qRt24YaGr)h`mFjr^G3Q3^#o$ElGR}qY9azvLDD`<(u!gB>R-2q6=NjZR`91^7 zcSQ$D%gwlBB#I>&b36*szf?JjjryLwUgH!j?2>E(xjX7rSB`f)N8s|C3HJ(c#Z}M2 z>l)&9U<;fIs)}HQ7GDdY;cO~c+t~(GQ;9R*=bcKNdA7P0;=WhA|EEV~ zqO}+uyZom7L$o_E2NS0?7qINvCazsPuQng`*sP7rN0TsWOX$!cOxxWfu-)1Bb}w}q zT%OCfEo;uQso({u1LyElQDt!WmJK%SvuGESVVN<6?hzoZX9qZB!!5|fU{F7V-z0F* zd@hIIB#>hY4Tg+~f4D}cP`gQhZsImU1n$2Rd+5GkJLE0)7_JgX!_}TRn+cH}TlXeh zu%s?d5A|4?V)dw9#i>~_F(X-^;sk&35pSUpt2k(cVcFya`lJv|iqJb!Bi#z9@eFPo ztFqu$F!VyY2VQE!ecgHbK=zpxIH27z0(jv{f-~w}KWkE)f~)5>i>nfN&1N~3Aoe4+8_HhLV{rJ zFZV|tvc3@vu)dLbViP4A1#43*(X1wxnyoE#)Z4j0M3aSP$ZCxC$*W z#Q=n`Z6wt4@Agm81HDMa4OJu2jHl6zP>f3G4;A2;Bg~v2SEEfj!SzWF}6v5 zCx~c+42&=oPIOS;aXi~`L|jKw>tyOAqLd0R6e!kn-r{}QIMVD~-axu{I=|zKWE?go zYvyI7$s``jrO9RS(j+Uv*aKSv!$@5<7#sUw7%i3kM}Y>Z!|cXT3hWW=#!`G->Faa`q*Hwc)at)>s*jFgd3Ii_czV1zuOU3$M5`bBTfZf*yfZk$o9_q$ee>R2h zq*pVARJ_@S1-7B znHnm5v`G1&lLR1*zCBuqw@)6w!qi*bT8djZYG7c>o2f&qLB1JkJnYS=HDK4Z5aGY3 zrz$#l*K)#QYYzLH-am5U4}Aj%r~<}DpK6OHP%ce zrTCpJr}ETGB**($%`S)x*rJ8pz!yk8o28vA#j$n*640|i?F5zJS)~iNO)Y`WCgE72gD|7ISnt`hV|icTqS%q zGzE8D8ob*npR1e=pzct&(m*fV5QB_t0QHx;EH!33VggCzr`d3fJf0GnyPJTx`W4N_ zuVP;K8Y{`K`?(8wn`*6`My(8~iK|gV6W9RiVkn1R?uXa_>VI`vJXAV6kVGC7;FQOd z%Ht_bG2pmfgz9|~-SZPHTFd4U75TgZWI;&2%=w1-gnA|n{%T0_!5qxynqh(wf5WGy zkAiw$wfy_6KUXtx#XR~qt>jn9#7@x9e?ss7Y)HyMT)~9}%a_lD!<}SiYs3N> z;gi2oS=!%FX_fC40rA(iqJ8K(LVlMjB03P^wHxWTE-*Eq>x|z_^fjYSn7A6Y;%ode z4O^IAdtT?xVnbX=C4mxl<4J7DGK8>t?)AI zi#q)z&$>qPOXMhk?-TibbI~uU^dhJ7Z$pqdvnGMd?mQc)1TC{$MA z3+U-bl6H6@{HPT*OrJ!DxD;hcoFApdsbA?gCheAUwltk9N!wF#aK+j*Z4VL`PS<5| zjiYA-#;LS}FXu!LT|LX{YC*3e_Dh-b4U4bvp#CaAa;jt^9>)`uuzJ zj^J+O`t=hR@oV2)!7dl1@sXBJTuhzYRZ%WIDtt!xXWZ{aX_CUOQe0^5U2S0R%lNq) zZOS!eC2{QwY&3}0zaleKuVYsj20KN{Pm_tfI9s_grAYY+0^I(~%WnhKwEW)Pi|`%x z665YC@)qxkDx%0x+_5VwN%Tx4`0L1^5;ql)e7KG*F3Ey+T>(W-poUyGgU5BTWX!zy zO$ID`u*QQRLkm2d?1-!gM$o=qk#ZYw!rVB4Y!HhSGeVG69r%fs>JbnSXutM|6fjHn zX55H`%e)clGKp*EqYTdhs?!@u_`=;9G8ZZT4lgR2RHVE}ku@#eUs*s4t(tZFx1hg8 zw)l&8vG*ec#3_1HmeIY6C83@yn<_@uvmM-e_AKULbjtu1=rob?mc zq_1(wpNwd68&32Z=(6`)all#hj6|IDH7?X!3Xgg-p0=Y*xA~os_Kyq=LNspoLo|4U zz@xK264zwo0~|o9KZjVZ)Gz6AZUxo=V*PQXp+93!cM)>4t4V>s=O&uvzt2<;i;Lu^ zFx7*BV5Yws!LEPiEFQN9whh{Xwk3V5%Ko7w&}MwO2EX4V))yivI_Y=gr=NOE`ojo4 z5)K;fK)%|V6rFoA9na>|JXC|2@M-#IHK#j2EGoAwr3t?qJWreRLz4e$%=uW<%vMeU zKoQXe2;0r9zdes6Q=mYsr);Al1cg0>7J&2n*PB~qnVv3jrSUSQK(4x z0(|gVU;*Z<(C#NKv-s=}`<{2fsf*7_0Vl4)(|>@UCK>e+ERt}Uh_v5nLnosSg=G^= z3fxDs-7VtUy|k`(cSIOa8#*+25ZACQ)b=7w0v`&hp)B~F2|tL&J1S;Qtnx59;{sfa zFj562gIkdxf?e<)zsnr=@mnJ>uHwSY*#kzR%5ME0OkmVySr{Ck5LTcd()iIx3Xi7@ zsBw8GlEXvVP(X9|rsH^pZ))dHg+EX8rBk`2v47@Mk@L*7D~n{;cLt7k@6}&!zlX&Yz39gLuyu zO5uA1aStQzA)!)mfn@Vx&T~&6X~n`!VTLeE$iX)lD3|&A(m0E-43Hn?r7q^B2;T@} ztZ;`g7SR9i-~R*()LV7Jui+kmqra1QUxce1rxRwwO@JE;_j5STq@E8IAx4Z zXocGaM}OO%wek0*{afv?WkQkQ79K#)zF%0$dwCARF8rWzF8cl>`1cUToJ*J{I585& zaai=|3!X(-&fzVM!n44qLbL!s=<~gR3kBrHCJW8+Lueu?S1z;9NO&1>0@HX1ETxiH$a4BU~$7ZJSM?Tg;cG z+c<^kXx_*jh=*%~%X|ZQ;o9IjKE^#7$Xk0|EbqjfNQdiyv+cq?6L1}HEyzo^pKrN7 zmbc(d6O?%5GEP`u<6MT^T;Woo<{n#eZt?A1wvAhc6 zC;E{Nr@$5Lfgdij%_h)|>X|ph@^&EJwikH8b-+3I!4D@OFWt6Ydt)qb7vi07qf9tq zKk$Lu3TH!Jx@q1y1oat#`8N!aEP!-4ADp=f_`&(;jl8cR@0OvlybUO)6>j1?xQ_*{ z4KA}8@Q^nTc{dJ<;n0BiHaOe6h==Qdv%Lp+$U6sl&2f1*BVK_kcpvqL>wvSh;5*Q4 zG4gI59D}RsA@7!}V{ls$pZOu` z1=j)R{0rd2`H=Sr@@`CvUJ7=7hMy}oA#ebhiieee1x*$ zTHwscdlq?}*T(XiU&Xg@jc~#rP)E2eaEp-FfcCTui{+)?zNR0&rXRMZ-?XORrlwz? zrr)xrADE`!sHR_+rk|jupO(J(>PNM%1-mOG< z;jW2UIa6};?DuN|_$V#vgga_QNH`IzQn zn(Zqw=SAl6NJIAp_j2;VL%kr=932n)ACVKT6zjh6?JX~SA8fC*{jU_;zTx$jAHFNL zebM3j^@r~_?ynr@*a+(z@1Anwl~DM*@qzDyZQt$JlOJ3uwtd6<{{7**@`s4bI|KY- zIhTVUiQ@`=wa?}FL2SLSG!E9~xIo|a?rXhpdF=Xz^G)l8Z^Evx`u1$!Z>AaX(!SvI zZr3ZNSzquj9p8Q9_RZtto7R7qr&w$h_g&vBZ_jU=CwuerZ=>9|jf2ar`!4Ne5l+|_ z{l9B_MrhU-yuRCWHeb=Nz#8>I{Cfy9di?6XFL>WPPDuiI5c&aHug0KWZp8KlzpwWF zW^DWJFW)q7uN>RH;a`3ne;c-a!|S{KuMFG1>Drfmr*B-p|M>nkTqC`j^jxQ|H zAX@;5B!S+r{}H}$rC9fkZ*TtpeXzaK{&1z(_6@H$U-+)r_C<$F=Z7o7H8P8R6TX+u z|5uLlcjpgRigjOfxOATQKG^n6kDl|ym15gByzk#1RN6lH=p(L&s$FHvF{~>edaNuy zym{|H`|q#++cZzxjXxc5Yt{{QKmYeNvi_vwcelS#c;C>QllCX?+?t(zF-Kypcl~Fh zJ177DVed`gq5Qtb@frJC_6S)bWP4_xv5kF8_9dchgULPyDLZLXDU!9cp`ui_79paA zl8{oODD9F&QUCjlM2p_<_h~qdnG~%)FMV#!U}ojYDf*o8~X3UE_Uukp7CaI@Y4%tX@&J$r>lU zBbboOU3O6~TMd(1g|JTrcl*6=Ei>dl&SJ8sD7(GNd4pf2sb3iHr5)x4mmf1dP(oi^ zwbSVR(%ZQPmAZ7TX1o3PL{B_Zc3P3a-i8ylo6n9sFh4ly9vRVYbiq})=j4iw{!)7T z)UCa^jB1T6xa)h!u`l18+BlqCwqmzbtfy+2oQg<$d_v7xA>C~1;YSN~JT`eB!pU!c z)nay}D=Fdb+7nMpkBykEiB~kZaDR_eg{X$^0{IK|?7b)S65o_={3>`87bTC};L@zL zvdV;|RBI!eYWL7a;)uVQ-qw8*9ouchWB#P)f1Q73APVBi*xi8RES5lb(h-s1o`&$B zqmAd6<6q|;ymvE;1J=6muIX$W(%!1ByYUjuIv;O_3yq%8^zskd3%kDq*(|@J)_xfPg3eV4nf36Sysh|GOsGXm$ zbI1Sh@$^r6`>)6U&pe%<&Y#z5e@9mTHGk|^&dxW(W+h>My8pR8__N&p?4SQD)c#3s z=gPz1=ka{~|7&^pGjHd^`>Q@MX7~b;U^PYiulWZ)KeX|yRrnKMf0Tp2NA3Lh{*GPv z6Sed6b?*58T`K=Xrw{O|X7WCyfq(V0&-EuN=g0f6jQ^kK)!*?Ce^2YI7Wfmr|8)HS zeS7dHYX4K?Kcivi!}r(o|4&r@iJw2)130D>##kTF7;#%Ya2Ez-&lkvCASes^m*co_ zWN9vDIEwo#?%zdi{zU(e^6<~7pC9k9df{KAe!lViasBX5ss0n)KkA8pjq3UF|Ee$k zKJEWhZ~Rl*{}ui5�fDFaN&0OjG}#mY093SN>I=&rkPX=@)ne0>Ce<3;wQg-e3KWzfbe5=9r)Ee=ZMy)^}b&;%D=Wr5V{xe~-hn)XvY3e=ZNxJf5%r|GGR( zQ@aBA4&ET=7~Zk=1bu~97g1mVY>3bR8Q+Rvmsk`0n0))z1li)wAm{E^xIU->M-lM| zB0?ABYzB6j2)J_#o9{3Tb!hvb!E*yG{Xru8nejf!Wk{o(-Ph$>mKXP1_FI-yrX=NW z@Ewusy|&@uA|ccAnq!}@xuC`StqUEv)?T+PwA=DQbJ_9K)&RQ;4kpJ|9QZi#dYqcK zQPebZW3!lsS(Q|h3O>0#Rlz%={LteK#x=1$t>WD-ocmPD53?Mg8CS>NeBPKO9~Xaf zaJ1#Mpyf98b3*nPSU28u#na1L7VFm5S7&2OV}+j`Xzi~U4b#u8b5=9mm}1Rk5`QYw zLC_+5kKO*@!6@I_+rd{`Wn#?=E-cb_-Qv3Hw$qM=B36t=npNDzH%}KNo^6*2Ir1n- z_teXEpFTcYFr`VNnIAx1G)!(dZ~x$IWx4-}vxmKLsiwOZy|+vhQt&!?z;#LTHk{sJ z)Lw3T+5N4hZWp%PjZo0f3w^|;bN5m#9D}G4Q1XJmnZWN#@SD~Tew$&)Y#A@OE<=L! zW>D4#%CZ=x|Jon;0Mg8d1I~f{4}NUz4DBAd_hV6&F{WjzvX|rdvsnPy2m=k*=~F)7 z$d4*o4g9!LgTm+@0RgTn?HmnUsTd-Dwgv{R7UJ#soh&{Cf)6l94c^fJKt2HkqIk!* zd%3_MxL)-E1mZIIX4H`byB=jAmm4&!M)mffd&uG7Ivl6rBXL0&$-01vp2hrm4z zt~1Sdco3%VA1$cyod4khSasUH3O9IMR z;JQDSF~-;R_hsJjbMA9-(xXBs)F3Jyp!EAK+zSx`;8%&D9nUa$iwA)KF9Ov9UuKqO z_VBG1xQ7vc>pvQX6#$sfEDQs~$^mQ+pRh0tK2tKn`u*8y#iWZw6o4`{S{Ths4no28 z3IME|`8$l!1`Pjy`GNZ*3i7+_z_=m4$u$g<127)&E%Xg$`BeA9X|Y;EX#i6MdJC3G zPVieMi9o>rUrV2~9=QAQWBu*fAEEzu_rHk))8{K~7%w0{2M;nh!O5aoA1=HQ#W-kr zm~k8v{13j2i;r!G;WL=vpga73kGOw3I|cX09-OTzDVv@TGyS^^V6Yx(1aoBOQwYv> z@d4QQ9GEURH)aT619M=901N`K-+lh?%-S?Rr`T=j0cK#I+LuDJ4|WKmdIft^?C3OV zkT0SKE;FIf9VxUhYH$#+cSh^=0s?})Jm?hb;NSo!D&5cC!xvEt;Knq{4=`TVm4W)< z)Bx|5-T}tJG|S*1YY$(FH~bia1j<%=gjfW7`2z?5huMWtyr@1=1|D916a*rM$t-xL z9U~eom}U{|YZOGMMIqwnlmNttc5n~33}5$yYe1|)IE_XLqFd8~y(mB}moS?JJJG0g z%8!T;tZY`EKDHDe3aIQwvGQC;@uGuQH^Wdv0Q(7Uj5J#B5Ki?rpiw~I5r`Z%>oCyP zw?@o>lAoMz-Ifv>P6?w2(`MS=$+Z^PS8&W0k;b?R3=cpIa0OBVy+VNNHw0gWQ9#R2 zm{(Br!+j0>C|>?X>k)_`dT^LOEhs>ZvL3W&i&#O?^9l{8(kNy@c61Lff17YhIK|s8 zgc=015xgY=)Uc%lP&~rstFao?08i1PtSK~~U|OID=(j;|I2d{w!c@;f%{w3f;RKJk zL2yXakAi@ZL|A%L!)C_Bl0w%H_W>+ojAk*T3?79a;o`r4#)29~XFON*>vP}Bt|8&} zY$J-P2={CfIIP<@@hso7$NwPX<8AMhGuqZ;6J@VAaW7Lks)CQ zU|fR3>9!PKKoNxo0b4p3?hr)#R-zxZf)WKGN*JZa0l|@aVPU~uR3Hcj(+M6BqV#*2 zEhR8GVzvNrkx^n61j`qgL#7@<-T{x(hUzR3*(y~JF|50xQCi7_<*h}JYEm^qLisv>+ML;Sr0 zg6L{tQNWMnKsb5(d{em&L9kbgK(Nh$=`$egKS2y&7Z`X`KR-JScQQ1#FtnYP{@+={ z|0IkF!41$!1GkLDAQ^t2^*nID-@rLpegOh}NCXT|16QyQY&fTdA#k4t zeBzl8Z0X?tF$DgFX~KeM<-z*{q`_Yd_^bHs?E`RzHXMsL175o}D8qtPHZRaMGoLhO zEVy(8>+-eWuMyD7cA%~m$RAn@zAeG2WGhD18kV_ zoN46?;E{~JdVj|Pm%+~l0$SYwp4EVMD!7Nwvcp_~aic&_aiCQf#z=Sq_y({tq=0)O z!0!zxcLq2d0M7yehb%y>1Pz|I1a0X9m=~ke4Q2}%ca}N4a0EDL3_i{9VCF3oT8IUp z9y|`yC(eTaKLh4)_*WjO0X$LQ7k*QQ1bEg0;Pz!a1)uZ}0c9}fd>MTO>+)Y<(uiZA zr?cY*^Be*_M**D*U!lPd95;gLvigP#<_}EQ%-F(r@LtvL;$yS)nInV&4r_oT81OC} z@P|I9XTN*)_u~(m0dffQ^G94ib9IKN@O%Yta{x5bzEeCa>0s>$aDf#8D23%Y4D`zb zP&O-{oGfKs@{MO7KjfLffOJJ3V}923D9O}2ecd72jxKrp&sZpGzuY6 zY$zTS9}0`oLfN34P+q8=sB}~=>IA9^bq&>tdVqR|a>5niiwOIOz9c6N1Bj_%!?>{4xA_d_Dd-o{b5GB>#|mx)-$bP zEeUOmw!ZcO?IP`R?KU&Mwf>L7iruHl6!AFLb`>u#suxNb(kP z4*4({p^HFBBSD-Ba)8{RATU-fkRoGDR-yt>VW>D%GAb37g(^hVgHgGO>Ozg85NHmx z2wD}LgDys2Kwm|FLbGE$up6*lU<4N9q;Xof&A1F)F77n$8m$>=TURCF3T1D%D=Ll>Zr z0M3@6%h2WMYV<{P9l8;4xdq*Z?m%~=AE5it1Lz_2EA%jW6g`ffL?bXP7!C{%h7Tiz z5yePgq%m?BMT`mt!eB8(j21>0V~8=uSYWI%_Lx-|SByKx8{>xwz=UAvnDv-wOgttT zvjvliNyB7dvM_m=0?ZLiF{T7lhAGEXV=jXK<+KrV71M%g!*pP}F%K|(m;uZX<`rfb zGm5dsI$}5DcH+8m1%xs}IiVViSskH~a21SO8=(Wtq6dUN!T@22U`R9tV`WXWCvG67 z6Q3|<6DP@ww272QIz%cX$!R!g_-gFZIHqw?qe-J(V?aYd6VlYu)YdZ8TBmhHt6ED> zdzW^jPLB>RS%#cM-bcO#&zles`vG5zAzqXeiiomAg`nbr7C8$f^(By0L9`;8gf>BY zp(D^+(YfeT=o<7aN1QMeOavwtlY-fa$p^h|#)x4}K#yFpUf6Zm)7Tm;6K(-+92bng zfWL?Tgcl$v5sU~Pgg79ZF9lSzwH%R=jb)}$7%wy?IMHc4Ag+g^K< z_A%}A+6~%6+VVQ;IwTzn9Uq+vo%1?3bn3}X%rS4zEfuBv^w{dqLnN$`CO`f8sG>8SxUamDo*`A}Nz>NvpxAMUnQAnm{i*Nn)BC zHO;kFYx#mPD$}aass|dWL#tcsfmWZ^fYy-KE1;c5wFI?gwNcuJU~XA!J88RVQ?vuL zBemnSleM>N@7CV0eHZj}q0V9*X`N7@ua4?m1U+RV3y_zPW63+n>EvuM8=t}BorXZ< z0evM6VIT`A2RaY+GUnh2GyzRP%qVsgFNzqKps}1l2F^hoIHR!gQ`Kb zF)Ygy)N^26#!)P2UbFyO3@wFLL7Ss(&`xMKASq$!STG}Vz%0A~=3F;g8l!;0U`QAv zFoOaxp^ zp}kBS($>@t(hk?&sJ#!^g<|baZDF0II_^3l!19#poYkqng$*ag~$lm02|aD0rVt6o6PD9!M0DhOm$+Ht>FAhiol7W^(C>Zu1 z`jml~8iI)l0VUcMuyHO=F-`f%i)3diNVMAxKs%U_NYp}zmyOds#S}?lW?@3IA|O{b zE@d_(3o?+Ly63b3z(UZOn?L!$v4uFtbl-xC~k<8joE>P zO_0e%A0-Zn!8aT{0w#FX`rw2KB#)*etQq z$Z+Q+ooJ5w%ZRa4Or^ zuzOPy>rBgvcTad6h>*4}-rej|@p8QV4MiVq)=)MeFf4CC~SduGiRcYvDcp$5}il)DJ8`_P#xkZT3#}Q3-ve zcvgkUl|iN36KSH&o_Uv&nwM?V*(Tpy_b~q{&W}$ze^+;JdqZ*_@0ua?YfL~ik%trN zpoAJo5{$Iu0u~V#p#pxcr8=4K*k3(9rSvxOXj~g~q?{o(k_(Z@uPiJ;4B4QCaCJfW zo)uySU(iMvAjLw5Xb`agUw}RrSk+Qkc~QvBk0oO0 z=^M9KObgw+c36vAEwe;Ed0KY#P3=Xxych)%fDpvFNWA854HlCxW zRI0}Kx3s@EeyHBmx2~Z9%d8TU`OxL^f+DW$de-eHc+}s!YTODSIB;(6wbKn)^>(uI z1SN=NOdR+UF2YUw@4a(2g=8qq0k59RguTjTKG;VnEf5yn_31p*%_5a!e1a++gB;6F z$nJ9=Pg_IpIFw^oGjhzY?@-X?y8)*UFWg`I((jq$$jOl>%0vy$gbM#ncQ2{FCNHi! z4jSU)fU{lJ7o?0A+TAj=|0SNkfmlxfel%9Hp_ zB_TTKmXoIgkJSq?~twLB|IQk`(Ue@G4tBU$OdZwKx7qthx5F&=VeK!_I1P^StfBI1OIH5}xc6 zcbk$D&`xBIc>0;_)1HB%ehNH`EvetI{=pvW5%1>Xsw^iO*1hOcFDiNvba&|MeVG%w zwddaO?r*xcKl^P{uhcP~h_edP2fNNs#Xm!~zkQjgv^QkO*9*e8zT94jKcr@bPNN(9 zo8SZ^Sus{#YSpU!wUi#42C&*>(u7j&@wvF#lRpd6@mSS^v~w77Y_adgP>4DrN8C7YjDUR#hLq1z?0 zNNLhFPgMDZ(TBG+oxWCzYgpf;S!{ULG}<=#sZ|GUEGUiN#`2=-IJ{n$$zP;-!mrA@2<{b2Lp)#Hc{%0Xlbx^Djt)-i$3&z6uow#%woq~@ z3y{9}-|NU(6^eejC+90J(^OqiS3g7=zuR@url&ut(3D?Qs35FDHBk@$34I=_}B# zEPc#GTx=ozxCi;M)!1s;p=h5v8+_$fCi~{eRi6V8Z@y#|s_i|&E6wg;ZFIXur# z9ntW#x%f(Mp;E`H_~ZejjV|3#BAWzIxh&sQF6J&r@orr^vByU*+##JS4f=^?(0@y- z0hbJDQw$pYU7KR?;P$&q=Ja>9sT!m*txe_sq)mapXyhnu3))CT9-7<7X?JTE>~3uX z?p6Zg!tF0hs`OW{|BzO6QYGo#4w*Yp%BGEv?zG+$Sy8g-$_3=D$Ek|^G?oaJgexZm zHt3pP;TuW-p|7=8>|>xWaX9(Gx-e~B&Wxer@s}iSht5i#?MoEKdv#>%Yu{4dguS6W zRD5_rDkc{lSC#Fs==d44VlBRP8VxdMvWpGwY9f1YyXGUyQoCJ z7e}R5Utju$`}M`PB?bmTWp=5T!h0(ArFLYtunCc9JyEH=*;=BYSCCDJ^uV=;}W)>9v0qC%kyti zIk94U#h-=(cCQG-ZVLvTMjpbfJCw@`CJB3cL%FZp^4NFEAo&e^6(?)mO9XECIbo zPg$|m_-f;!rsX7oSEk3vLcgdp0azC?JVFkL4S0mWenP+1Xp5&cnjpmgU8AwHvBDaS z8Co%|&d% zeaQAHq7o#`sS^@P}YF0e(R>!vsV5Fah#&P3mHm95Xs?wsA1 z$;Q<`#&uOMXSZ8IH$F1!dc5Ag;_=;NuLH+h?d?~@zxc4o=T1iG)cz?G)RX6nQzY+P zl5QuTwq1KBy(V=pXBKVOUgT(&!BO+iLd%Zl<7;VUlMiZ=d0)N87O&Z>Oce2A4Zk;r zQWN%noN;D(I~`Y%m2>#ZxUzcXq7@1OE>F)ztht}^@Pv2Qx)({JTf|X`AGVy$kgtVs?94Wu=a|Q9ZI$*+&VI)ZDf4i8-F%&olQ%X_g#(Mkgdn_ za8=xb;syb24P0(tzDnw%1bf5c4U$>;nQ_jm3hgOhZS=XvOq*fK{^JJ_MXgn`vDhai_FB~scPq_R<>P{1#Mf$W0Z z(*S7$Dm@Cd1d@QET!O;(eiUg3J0n|rTL(LP>2LWjs9DXG5mRnZ}Xi& zr9CweIP-LmK#()$3*r@j>BnCcN1nQ;61yck2AM^cPOHg_m$6SjcdJ0h?(JEZx`?jD zEJtseBvrg@Y%#;$cyB~GH&Ns*vW)X{C)zej?V8tR!Q9FeGLm(VPq561&QQLn&7;Q4 zTN_=j_1twcTS;?V_~6-qUjocZOuMqBVdyytqX5&6&#;LgYTetbS zoRY7_Irh~U=Otds$geGzE_08qUipES+|%}AQ|dOy(A%mR7FScX?g$;qt6BE-j-7O~ zJ2iI|m;ET~vnf=`#MMoUnyZz>jwq=cKY7@*d+h--US|R1t883_JC%9qzJ+^B@r47t zLLH)wP!>H==9Z^pg-n>hlBwg@IaY0sJR7V!&~MIOu(C%O2+&#m05lMhd^iIQ0lbeA=cb*C9ShJ1TUj zqtrm+h}m{Mk`VI}rBC8VWltV$See+i9S54SJDAiaT%P*0Kq(IQ*-IkApFNfh*4i0R#EXmA3}s4IFqg z$OJMfFeuPVA5%+)1jrPzxz_Fn3_mF^hP?~4PdJKk6X~z8OV}O$h|C|J)-^C#9!nUe0I>w{Lh+*$Hs z*zhSjSmNa(%Z!Hg6lu+cddH=nAJohLa=N;JvK-sjTRfrlv>v)pSKy^G=_-OKZflHt z!Jof`t*I~&?N(0YR5oKjKJ^N^|E@^=R@Ky}w!_Ky6!w2c`b3^{7zD-JQGoL72N?t(a-dg93h*EM^$j&tki`)f>3W&wLQ2JBt$w7ug; zq>i4mBIf#$X^K3Ysx;>wYwHv*vhCe0_dX zrUETzm@=NF^P4i*ho#V@mty|XqNSY)B}esloZGgjrD5OYv<%v77mZ+~L0p)N;sLqn z0-P?}r;Jlv2a_rV2UZBPty#BC8XLOtf_y1?HP;a-^?(>P51Y1BXM#?l+q)%h@s-_D z?oV0;)@_k$`XYKG-N11}^G)V1gtwxj?CBL^?ZcVZ45dhJd1aZeUCegG(gqCt7hjjPFdIsVDE6ubH6{S|rcm#o+8-Sc+gl~*SosRl2$w+Xo?=AT$ehSh5Wn819Va)~VN?P{d zJG83JCy&yVFP7~LoIGZlr88p8OFdl`G|Jke$J!)XXFV`p`lZCn!({hG;|gQDfGeZ@ zDGKSyq=4`~NuCYP+tqH2daLN0jM}f-L>O$Q7PZT7&#Y}K?W;LW_$c*Af8+6&63<_1 zyF46kPwO=Y+~ynIt4rsZ>{@Rb>hmIy3b><d>QQ8n=WB-}U`y(b(!B9@r`|mU2uGf)Q+$l!rt8b0bq6kU3mSn#BOpgK%s-j6JY3 zC?pe-nFE6Twr?RUCZq^sjT8p90_Lob)D5idz6Y|HAj4@ps10dCqyj<#ZjPx6_}%&p z*g)6|oN)pfrYZoU&ol~bpC9BiukD*l$G6$|JB0MvUVu>8KUwL8-GA*zyK(uP?m^Ur za^}OTLL23tm9G|#DSkdxdV3FVv^KtgdhdM>biah!MR$@T$1c>m_n{$5K496<4!`u9 z?;^L7;)+>V?_(Q;UL4@CKk>COHZwr^PR7gffIg#@DrY`wYPwX^f4;jlJTQw!8~ezv z`2NnBwwGd;gjLqRZ8a)D42-ixTyVOQb}7<6jlQ4TYGvbb!Cm`KA#ut#a{1RkADP(7 zqGkCcQNZ(Mxgk%u-sOeZ>Mi+KW#0*|>k(BCT3_18T0WG_k%(PcUshkD%I*G|KIuf=v*I2rIWUEG z=TKXnce{pu{bJ_NZLf1=s>!Dc&)C>^T3op#GCHB1*k|e20!heeI=H zF{jt*DFiWZJ52A}#+evY`(&)@;(l%=QNM(PdE_7w=M_SnoH6T)nYzq>qwZhY%$#~J8N-e+_K8zo-AV1+VP~nz>&sg+FkMx4;Yikb_Yg{D1j6Kly(V+vCDyt$@U0grr;dC>FX;sDk zZ4;@uh>xjxuUBSEAn_WL!IJ&p=vF zO;*}JWr@G1zRT0o&sE^#l*CZN_JOnDGz4Y#8 zFw?WM2@kdoQCcvjf%}eo9W~AvS^rWobqV(l&p!U!a_31x2}{y$=`(pv>hpQ6Jr^$2 z#_epJ^MR!%pY`gnv z_5))XJGbW^wz?ZSkaOBX(o}qb(AXLs1QDkoFnDKk|HVSDR~E^7%bhU}Jf`n+Fkxy^ zES3w-hB&))OGYHPb6`rY4>_8rtu)I*#SAzeKVB2HLDl`>KC=%K@0RQk?b_z+`z9yy zb&QZ~hwTb2+4FB6o}(x}Icv2OdA0ittCW6Pk!6Og!Hmu90yeY#2b=lZ_~xw5d_Wey zCtjQr7_y9ySiJgSME#cd;va1$d;Cu}^S6OdvuT3~LYqGtOg7}-HJJaoIH`f2kt2xN z|HtE`vv~ie^)!W>blZ7bNE>93+x#i*~GqX2J zS3QrIsy!XN;d*VexJ6(1J}ZjJ(8{GR%`B4yLq|~!l1{v(y>(eh?njt3`!_D%tke~L zyj$YYo%paVOn0IL1??Wn-tSiuu(9;n*?MSo)Rs*KPqHrD?(e1wO61w#UWHeMZX%5B zm*0ff;eFDflsJMY-EwB)rq#L&3TctzUdjAOt@xYq4mK9)HUrE9#O$@GM732=qMAJu z$XfFMubDX}zTX+xIXPMd79b~TCTEJ18O1XvL5l@apDkiR$+AeNpYJ7G32mLij4c=4 zEAAWR-2Qb{rohY1H_sc9tjGIu<_a3v<6=on->dKydY{YArBw`lY0Sv1a_)Ct z)iySMV1*31Sy_hvi|JT}zLBtvb!hIEg076i<7`LL1m*EA>m_`B9;zw!eDGZ)a<5dO zXPt$r<&v#hTSb(%xv@-{_BWmK3yo0p7twnD&N2z5c|6(J>9oK)8*RK)0q^-SQx~1< z0&%+{*Q{Vp!A#VS@Mzk1`szEQdS!xhUd9q{Nmkk_y>g9S5Y=15u2y>Rk!@39)bV}B z?`saqXz%P0V&N4Duq2((Uu*c5UN?Lt?K3m?LD|4dqwl0Nzufq=_oGB;aG@7OryP1eF#<2QfoyyDg>CUk`7^i>W4A=AQxiOL}$MbC6rlGsp*%p?H9 z!J?nA=YKf1!Sws!%ghvEVnq~0fjq!DQ-u5b4LcHSCVVepMJ;56Nl0KT0E@$-u`Xbh z^}Un@g=T@WKWMn7KD&zA^{GC*)70t9a8t*r4b2(nhF7KZqoj4BUG6);v8$4Ou?HVN4$-l9clrC$`nqYSmx0cyAbObD2G*cANE*uU!?p zmnCfFDacsqASB@$wE?=)vNztEj~_EcZ%8aLRfpa~)g}*7{_NeAUN? zMaVmRC%OARue#sy@Vzr4MuKbU=DH|)?d~3{_ zLcWsT4400G5tEml2B{yWM9r%L<)nqxk4X)-bJOeY9p7f!yGfO=1G`m1ak1Vr>m+_d zE4R)l(UPM z-In4V7#!q1XD4qZ)r%G!7VJX@$!xTcVA}L*iwNQ2#T2uk`fNFPHIjF5WSF$QAC=}U zZ4F*C5CxtI3<(Ybuaf}#WzuK}g@yO@K)4eRPN5MYfCbLn#eiF|tH-#75_bPr2nGS3 zX@b$e5^M@i?Sqs16b7}Rf1cC;u=^;@*yIoM18+9?tAyS{L$5*8+odnM9tAEI&YUtj z?QnJ|r^B`7<(G@zMn#F8=~fm@?PqgOn(udheQFn>X|f69Da9W)(#ZZ`0A<5pjeVK# zzLJAdBoed_vTs4%x>Z`Kp~#`%K79AJZjB_4#re#uUTpWJ&~pL3ftxlMEH0EcyWz|x z^JK@`vP^xut#8Q}*Hw6^<=&SWl4}+)yip_7CR2XaUM?=~2FoR_t5yz+yHPUao`^h7 zEzKp%I<~at9PqxdcTgsBzfGjr^X;Q!h3#BBde$u6 z?u1hl6$`SLS`BSZj&rYW3Dxvy;?hm)h`aqUcHs+)&36bA+Y)A$I4yuvR~gwmAfF5l z#?7Rtb1}2B%`E7_HF73k1`^F?*E@QVQj5&C9TnItyJh3F1XWE>wRP$YdyNH;!!?-Yo3@} z2q7%QsdEEX0e#Z1D`p_hcma*SxsmsBE*E>=sqxUpB=a2gt82pZpI4U>4U--u zK0ohfE4!g9x{jMGa>1DADQ&ujLyAG^)>~!5&Wkt;b3H?kRk~G)W<9%+?cGEOY@}~Z zLKX_5vdU8`M5pfEJx9~NHEFnn$q)0fSxq6!ec82*yORr8w)5To;%9QwIF4x%c>`5| z`&s;8<+-P%45pSCz45j?_Py+W2bAyM@IIdRIuKkBd8)F1#w6 zDAH*v_=s8I*$VA*?`@Xr);aFf7fy`J-LOos_XVx!)yk3H;WZA5+`@>7+bz7g_7V(H-e|Uef zKl5gj{9E=1xPkEGW)@VM)dP(D>e(%7%4{MDY8ZjGT*S#;*&GZ{n#qucctb!Y1rr zx}Z1Ky|25_SLlJ~IvRQPQ27V(ubp_G&>1 zOJ7dH$%5i#6qoaHr5DVuZ)Nk9y)v{%Tx=IpsMP9ux*S`{&<4e0eo064FRIJMCcKMN z@ZJ^T>(@a0Ouaieyg)@3Ga%k9eph~_`eejsRP;#Jft^z8vfWSAzC$m(K7}#OcXnM> z`dC|Oy&^=vinAPfn}Q$M?wc=Ws^qi+--N-Bxv9Y zUObwY_ZOknfKJ2lryYcN{tKsKT`VjM>Kx$ojQADlCy(pVA z#e8YV2b=LaHYY;w-L~Aj^J(|$_s4J{n>jU89fmu@aG^q9{1-S1pY_Mq(pldWXa&|#2nN5TRZ0s+c{|#H)!?uybzmj8)}IJ zi=-H^f&}5Q87l-f)MoNH=Cy-{AYWVEmJ$*iMx_VSqSXB8fp7%m8(Bh3EIdoNz$;q9 z!OL3>5PA?dOu$0M$)4Y&rNd{yX7b)U#~g3$8am|a>33T@BW?BFn>iqP$Pum}#bO1m zgjN)o6_}(Lea|xq_|;A`jC2Ss*gM>7+EM~_0h9&EVp$7o2UQ5KipDT3vCB-Cfgy&^ z)tS%S14FFyCqu06HDiV6!A<{3J2Gr)W<`T0YGkATK`G~lbQOa4>5B#F6Ynu5&nMhQ zB)5(_?SDe%JM3&N%Bm@(q*qzAV0FTwmYQV!`o>GQlP@>c@T8httl5(C*@YwXq>B2Z z+H{}8%F@@9w^5B0$YD#->Fbt0|2lO~^!3fJHU4^GJDl>y%LC1_Sf3uo9UG1fN~!ZN z=J1t2RhOOGQ^dAXH(hl5G3Te*Lgxe)B^Z`CHJsdeGGpTn&7BW7l!vZ9uiVhwwrS}K zfie#@)_i3w`iDLM$?^ zbNRxH0RoLzc%*$kEKs*j@4o&yZqV^HXLpg^>E`9s^$&*EK3gGOdCIGz|FmnoW!{Ek zk}E$d8n2gO+q)9|&|QC^a&hB`F}@cU##CQ1xb~c~v(bh<26r2udp*SOifC56UTi2n zgkJ2^I%w)@&gHIbyob#_GwZ=U%w3ttxGjMtCbav9jDt>i`zg0uW_`xB#JwU52J#Q~ zM82+Xh?QDrE;L?VB-0d{;v3T1Eq2&VsvtKf`ik31+DC>fym_!;R>zC zR3vvsi@l+$9H7f%5;=N^Ez!+Z0UG|73H*9<)XXpXqKYSoX)8ZWHtEwm3JX3QS1biC|1OX*cAS+vqDVstukk# zI3N~TkkART%(%Xt4&@YMK>_`P{8R8#;``Eo3CWFG06fo`r2-RD_@9SH9p^mGgEEDT z3q<2ZJo(b5RyVmz+N(s}_Kf%C%(S)%up2+PJYEr=#BgH3uZoNkq#7Qk9{Fu0J9oM= zLas>g%AtdbQ8AZdjlbT#=O~^rWh|Ch))aEP^!SMr$28Z6Ex_xPHumBOeRW2$@{#m` zExN-tXpi)dH$x7lIr^&{$bgKO?5g&>v_6zDcJac^e3_T-Pg5rk?`7vYYTfQim80G4 zF0f^jdW(OOJ1m*G|LcuCc1<`*`46lW)Ma0hw!wDLwi%aCn;W2cw8*bo%Z{Yb->+wWQDM|d%9i{OLB!^E{6|vcHN)$Hwx+ov5 zkvJ+*^7de&2#9}(OnhfJGcyR2h&%_d!5_jTB4EEuq#bPD{u(ApMB1=%&j<@2IK7;R z)MZITY5_Aw1ak8Yb+R)TGZJX#pU*BUhg2XMX03~>+^gHa=aXamF&&x9N;NN78f&wpxpI}a zhtQXGpO|xF4u(hv)ondt;H&ZW+!MT2RTsJPG`kszDo7pQ`LxS>xQo9c&uau(sgxJ;kZv~$D@c8`Nm@^8Odt3AQ^y!nLv?a2-mz#z99v0;~Fvo3A zMGjZ6mnZ1$`PhDA;dS|?cGVo)G(O0P=^@)z<*4j#TXTEa~?+f3h71ok0<>qTXJW-XuWl|jVI5+UL zqB+&=D!JhK^NQPX9=Y8L4;@qjZ-h1@Ewb12ha^50?jEd)cN-99s;UNOQ{G4WA4+H! z_q%g6X87fr8+Pd$6Cbk0>S|V3yO5#U8Exs8T-^ourZC@>0ZiyODm$Wo?V)aV1mXHhaOLdYT>1(Eq*HGUr6^rE%95HoqJDTa+bK~NhTT7Y5`fK(m z$KB0d=iSDcV<~b@H&E(A?y1NWv9AJemQKjlh38o*sc&Jk+{dZB|GKF69{NLZ4y6Al z>!|KjCN1%BX$~6-Q=mfP0HI{l#N+IfJJbo8Tu(c<<*IwW%oIEHu|e+s3VjLr>n1bK zJba!FJ;!xlTe;tPIIqX%`BCQ@uDw@F@yfSDeu!ZmC=zx6p7p6#Gccj zwbQ=wD##JCFR(4JPOsC5Dq6Fyj;hC)piZ171h*E%6RG z$<}|_){LJ#9a#nq{IRv415>}ra^RS2s%N5A2*C@RTZ}tQdaQ~FsCZXbH&=5TZ9)%5 z{~GU;HT{UwyYwtRb6tygld=3PzMgyMBM0Wt1q!A5dK+6;J`V&5louO2p9H*K-qKxK zZ*tSVD!b@lr&xnsMFJ1gNKN|J(eMJJE9dEb5-onIIg%0=Zj98n9&k;w&`e$HP?mG_ z6xWgF+b@=mAj_7UMcBVq6pI@#U%l{#W9@=X3(AkwjxcX2T3qrD%W7FWb=s+Bf86FO zPeHeb$G;e8Tbdu(6|>~S0%U#DqgBdNXe<8D>xxfe!$0YLcC6P{sf+p&U6N-bA7=Kl z)HvX2)W(x67i9Trjr<040;xWpu^psB&a)MT`qk|mwSvvu%2%04dt$`*y|y^A(F{LU zo_=y5fYnxI>$|EkPPfw+4{J;?6UwWO*O! zsiCd9P$Nt&u7Bv@+mXAD;*5iM=BI^7L<)Z=DvILFU z-&B(9EN(r_l5>tzd$i+{-X_cb>>Iu5Dh&@+ueX|~H#y~OJ{~%FYEtM{C&E$gx*XSA zvkL=Xu4cUPs~p?5`%2B?3@yUy1mTV*Q}r@M@^54T?~} zMGV^U`tHK+Wp>dd52g)`$ERr2Q<5i2#_*SL%e?PrX`Wxa5mOq!IyRM0BH#8)<&dZR zgLMmUxs86=X?LsZ)h5T4Zy#RVapcMFU1vxtHhIrOsJz$lsv$d3S8<9pPs%sTHS}HE z6%k{hhpa$9%^c&J*GEonP$HDvc$sAM%sA*ey0b0?aKWxnPmQ)En<_&*4I+U_p#cKM5$HTFKY*T)_D zeDy}Lt+U@jV@sbtraev>dIGOn0^J^6+?yp4mB?6pDt%Ghb&WEnN5&FG*Ox1NIP^=1 z>w0oN&q=eC?&t-}niXT?4N4|XnQ~)iz)$YZ#Ls4IrV=ggx9><_{JZIj%+c&`F+qW` zLbp|Stq)+W$~h`1TlBv7XV8&W@yQ2lL~l!(ALkI2u&-F{y6vl6d)%%5d16JG$JSME z-)@)nNN+8yHoFU~&29qs3c|*KfJ0NPU^(FA5L}E2bo`6f@}PeyM`rD7nrP!@s(D`E zzdp-5q-qdrEf)i-qk;`+Fmi>pni(C=LJXB*9nP8?TKh+9?L!^;bVfs!=!f%) zy#K7se-$IL&Cbs~rD7x710>u+3Rpth`gM* zX=U_%&&4G?d)`cJxMcgx%P!5@e|Ot2BdMFyzVv^a$oTEtv68>ve2Ve~R+kzaHQ7*g zBj6ub#9arQ;~}|olS@2wHu|~i9u#3I+@JDOWnXdbsXorVn;FABLsD)QU$2*oc46#` zlAM^$JAaABopebJm0cFzv+{l`hGb{C3c9cSxP9G;7oBnbb1z-+Dcc$z!nrY%(WS0t zVPa14Kd#+x9Mfzw|DHbR=vh}cdEe}0q>4yJu|O8RC@J6-N{yU%CnM^=&XzL?B3Qb zFf^{+bNXfVev1N^ixWPcSZ&p1#gSBhvmiOxxA$FB?ow8PgN1Ba0$XH5jUzTMEJ*vb zu!-q3u(M+V9?tr)aQOAK7b$ZA$F_h=Vzv30nWUJ3r$6snXH?3RmH+I`o6?~F$IQ+q z&w9V0vDu)pVM*NpSJOySxWFUZfa|TEU3Oh#pd`T$9~v<>F*7xaf(rl-@Bj-Klo(hc z#$kXfVSxL{47i{MfaVt1fdmti_*y7KNF5@3Wx5VX)(lB6lOgDI7uWzFXjF%30dOxW zNIx_CLWPAeHe=fY0_ssXS~~1YaHu2 zyLEGUh|m4lgortTIpz(SPiFg1OK%I%uX3LCBkyc>@bbq3a|GICmQOf-k$Ji4DW?4Y zHyzG&NOeBm=d!8eoX+My6XvCJm)a+8Xg<{~&(EXkbK6IsIcM|x>=Uacg zHnH#m$5c4M$$9d?MkjbE7Tb^x@=$COqnCjcd?XII>Xa8{tPNvq_02o`45>pdYDs?c z^ZM`D8*(=1Reb9gKCZa=!aGC#C~1SnUq}Wp=^Hk_GH85m(D(#+dK-%Zqo2cL3qg_E znNPczlJw{P)t6tsY5!T#kmnkqU+#PKCKxo%7zFtMd%ywL+&*`)EZ0rFVz}b&jeYlT z{yueYGxvqMj@jW&b+e}(*|`2Wa)Jc*DnV;*iwl=>POWe&-?{eN;*14zI(Wm%WEVNv z>ABb6{j}%BF}cRQhrfsD&7S>8He|Vq7ijmJM+4$Ho>2>qW?+2cq(Rng#^_HGm z_th+kzy;R}UF_MW>~Ne`;*|L}Ji(jsxw*yqrY9n;+UJD?>~@5{>c9U+!7f$IiX&X} z#!I=hlQFi()^1$kFH?G1;zQ<_N0Xg21gG+^p1jn9rT){}AIHx6&A1fyPyYOUy%sKR zK7mQoH5HBcJ{evuPCRMnW#ZMaM^|)$_|AK}qm1%^pDs?aqoGRZ5w-i zm64I?;&p`;pNr;S(rwZUkb7Si(WiD%&uGK)i!VjnCNw?_sLQjNkiB}}nIpT3*K!p# zZQrf!21no7`}EH|J@M|EsgHl^bM3$L4b*W2 E0GFf)F8}}l literal 0 HcmV?d00001 diff --git a/common/windivert/assets/WinDivert64.sys b/common/windivert/assets/WinDivert64.sys new file mode 100644 index 0000000000000000000000000000000000000000..218ccaf423ef0a67696226f9ef3a09149e4441d0 GIT binary patch literal 94144 zcmeFa3wTu3)%ZQRGLVE5gwa?pM2$6yVr;x526TqZz!{xL6cMbV(Q3rjD#eM!8zxLf zm>x!{V)d<7X=_`pz7?%PK!pq-3837piq`_#ml;L{?OO;?ng4I?J!g_|vF-Q0-}C?e z-}CWsa?W1+w)Wa~YpHkaxY8frcEgRs zi;4!6rHdZ7c>RM%eYoT!`#I;;_K#hJkN?fN9}Q9OGd_~=Q6Db-XgKe4US0a}@#_8h z$4995V)bqbFaG#Q!r`#zBYC%kUsK`BZvMe!Df_^d)cYKc8}4^HmK|~G5A3*|juRaP z*#jMpB|v_mp>2bB7pre~mb~OU+u<0%OP+j(f;t=>ydmPl?`8pMAfPkZuYsi?j5<_;nl}`5=;okI$7mj#S@@KHZimAhu9G>c z2slcq7+N{@^Yr@Xb~w6*Ptgfg8)>V%AlxF1WGN&22rL5SD1j|Y$kgv4z3)A}AwDy=a zZ#5NGu8MaZ=Wrx8L&kO|++s$GW=g5iqKUj3BYERgC~t-eolz^HNS>Eh%5{GS7)Oi7 z5(q@|<`|OAb%La@*2Uj@&f%ze!5X$8yNP^t9V<5w^m*?v7pba*)+{YKdOTV+O$^9wLt{eNC8_!V7rVD*mxPgg&jxNt{vfuN9lsT~=VMgUS( z*`(@Ct7u-TNR`?xubSBHBg|o4X8T)kr~Csu6`xvV?%rZrd(GI6JTuj4S~-GLK1>>^ z?WVGFGqfYCDc-xWx&U2D=<|tn)}<8zYj)Wz7|COKN}kCw+J4D*UAV(&n=9O9UF!5z zerOawG>1^5aog%fWBU3=u+Y<)m0v1!UNLdGQGD5yKqOG+ z_FJn15pP+QXORdPFYHipU?Hn1L?X()ktfP#lZq1G7C>oau0qTw{h>eOop+~e(EL&| z-i+Q-w#aP#d>$F$)d__Vta61J@ttQ{qoxF`jRDK!3|N!y^IKO|0N!m{Gdffy#bznt zlr_p-xiRAFM{~N2L3P{A$oRov6wK$NAX#Mjc0_yyojyZU2#-;0HUK}#PHZPUCY|t@ zPTx>Fp*tY0B};uhe_Eu{M&pfDxhik*CMj!5s5={4ZW3B{H5$Ivjgh)x3Z~Ktu{ELX z$TN%ud`@3a<~chlFw~vZ=rN{u2u$cIMW`$QrE_N0ok`Rt(^}&t$`jg>rHCCUD^GWb z;alblcLbuo-NxKyk;}_U{nllr@zzEmX5D#a<(u($ekxy~2CNZ*Y`d=|Y zdSF#g54q?ksqwWZX|Gjj=Pb!S!i-qvp(jCF)4*CEzSJ6A3=qQ6;crUc4 zAUGfp8K1+mFZtboRreQ=+-F)eb7&hlFp(Jx3I~{>?OC2#&bp{O>&`DJxk1Y5UBF-p zC`gtCEZ<+u==g%h#!P+>`Qg@h)~o?$^s=lwcaR-^9am};kE=H$_mqKYsDp}n_Zn&X zb{fpOT6aOD?wCV&ok|j^J1*UKM)AXT*QI++CFt92Lv^>U^&QY{gwq|i%|nm*Dpz@> z*W&ALP(5~v^w>#H>mCzbCf#G#UNdPqUG$7Vti12_Pj^vUnT+ay)mFBNw+I7|21wuZOl~K`K;&%ggFPtW*S&FY3vT-~O0TWwxS)(C^b6r-nQ zW)J9^Av9D0)(DSiNzDeh-2)G9J2 zE%znSp{TqDi4CV5hQc}VL5~WBb)$Pz`p#eVqI3^^AIk4TUy&ceo77;ls!h`WH}T|U z2!;%${%JG=`rith~FJO(o zPsnEv=d&5kqDaWd?VzB_4{7`y`Lc&bpJ|uLc8zCOELW7GY*C65NsFw(lWhHC(qZ(ENeAj5lMbVQXhBe4uO%!xg;cNSFB!>5SM&`fh?TVMt8X9=>dx?6 zYm5cVxzpFf+>CNjA-o9rwEi$_{F%(EyL*Y#oOs#8($uuVkX9n*pj7rz9^hj4_;Cm7 z4*!vD_W-(ssBCvuShY%?-l#g@Qaa$}GMU#ZchQuGI`}%M1J;O&WB5Mt>{^~1Ea=Qp z^goXL$}1NoeEsM%??qL-eQrdLLNI2@NNF%6sdC>Ej+eVZJwJM+*= zMV~>CA0J9Nwg&w$2Dby5|1kWIuB2Blu-v82%9PO6Uc6}bJ)Ghk9n+&Xn~{5A+MR^^0w3`ZI?9Kd4_YCzi5Q%ZqpYR8zh~HCG!b z&K7OCw_zeZ#t)%iRPVKW;6VN2xK8}<|N2EA{Q`@)^jLoU$cXef!ddlkWTI0f?&M;>)on(mlmsFTI|Gr)@jztC=0K!*Q_AH+ zaGBP$Vlff6z!m`#MlW%uTyH+>a9Av%?ltu#yZXFyPQTCGcGpp zY9q|@$zetYnURZ})x(W2^3D-j@*021wUfW5_2ewUEIPZagQ-+pi)}f6_r}OMvS;@OPi*t?XyduDtwTMO zP&bxzQ6#WQ(R=&y*!t1lXx?Q%#jr6NubURzaX#0)lsG{Fk>n97)$A z*)e_VRJ)-m*L29M>DM%H@dWS9cc5IRToV=1i(X`1#Gb)J7*ITm+KZ(q+5r}R>)n*= z?%`qqf4Pjbyq_-jB2L$<_}9wj3A}Yh2QS$}u>Y~YGEtVY zLYX@2eS2y4dkG4~p5#O3WZD~m1WGTg5(yCsk3BmN+q5r%Mt@+T64yOFAncCe@28;sb94J28#h%~AG#wdGJR z>zq1ky={nRtHid&zi&5A)8HUcJtME$kPnJ30UA?$WSt16o?0u_%&x+` z>mXMEbNTmky$r*HyI1w^|IYm;Mq}@_#J6ra*-RUsa9yTbMNF1R|CElO z;JLz#K3`TWTcNF=??i}VRsZNKUb6KRjx(DW#d(i2%~^S`D09gBMsbmZBjL?5)kLE$ zbhsG}cM$6IJ#(U?4B6?M&t{&L_Z;brK`JT|Zdb{}^K_BTQdq>-VGN3{S2^3i<2csw zHHF`*GfBFyiJi|o<=XITwefhi6&CAxs5^gJLDMOr*F~0PPd&afFYjyl0)xC9`Eoei zoq2z!8T@N8qjis^Tnpz>p;gp{DEBO(Oo!RNZuhuPfBX1~(ty=z?*8#(W^8Xi(^_W6 z;uo4Z>-~oJUAF5O_|28Oz;q50!P~!S4ztARVTJoFGwPcs36Kn=G9#|Lg>dW%11n`t zYA%&(B26tR*UcwqIjl{Lk6AC}Nt*HV*b5K5E1yN@fKtkJ+lkCTWUOH4vuVJqth754 z_p7u;Lj>pcBcw2EeaaO*iIkRD#M)(!qlCYmAtlt=aQ2s;0s66m-ef~9HqVMy0SkBB zS&(vVqrSumQb9ED)uGaNUF8a0-}*olX$pA7@dExlSA#cEY!}es#|ShIvBB53S<`5u zpzfAGru=D;Ka))>@5&vLis_z=@0_T=WzWK0MRwj3r5N54S2SNmC8r1o23CXRZ zqf{&2R}n=62ncpn?7={DRpCV0R;Z9lL@7`Od*SVg^5<#+D~!|@LK-c>C*^v8BBe|2 zM2(UHtfJ>n7c#Cr0m3-JhyA{2ox>X)H8FnIgMtgw3(c55Ug32gi8Y}jKGY^3J_MaU z$3#UH+NJP0Z0&H)PKhk-^ocu#!#k^~sMD9Lh~cZpCj-xgCWdH}u+d>?b#9r>QrE%; z3OmqfryZXSlXkpIJ2cB(XvY)RrQ(h#&2lU)q+;lN>z54f_ZCUr@fGN}I!^(4qGJoJ z_egO$DdNv&qC`qW$L>;59*KG&6ZKB7sNoWIdnRhJiqZuhBT*AGQBU=Xa!S<5OjH|D za9F3WS!rw$AB;49O696f-;|cDVxe18{C%VVUL9Y4!AoYKj(?<5ve7=C?)EjHba(o0 zklc}`$(_E2maIZY{1-aQayv_uEOhO|gc;_tO)s4jTAjl4Wix3tO?D$TAmNWmRk<<# z=L~!u@c4ww>|`Pd@uT=1*7|rX1O0=B?iA>A8R%RMJ%1k1M>0@cOQeK0f!>#aPS()1 z0=+o{y-Y((eg$+~20B7RxBeFB$P8318=^(0{|@LW8ECOid#;o)C3Z8f0KcVk_Gf@fkxBN=DUEne*@%h z8!{Ql0|L3(hA@2V3Gz+BChLqBczhfsO?wYAv);!R(5G(PY z1tpv;B>sMh-^b2k_`^H%;w6Ykalwholqh?vGs7}^o+y*i;J2=-@LRW6`K?bq*IcWe z3NqTU_AhXH7SlwBvG4SvfJ$hMm@WV_{cpdvVSN7H;w4#++r1 zyVq+SUCsC7@oxxyj+o2)CLt|gdWXdW*867r&%EdT^H zmQ`kWWAL1Sb%fde+Yet!tlQV|ifMh{8K_)u-BoP1e>LcUUPF;O$O&dOa|SD9sX z1tPg`M$J44N6oCdN{=(tku5-vFs(RRFR9-RRK@_WNP5lKmeN4whU7@utZktt z8r5`+8Qv8vOcxxe%ugOAyNgqZ3cCE3v!@JQW2$2bQ;EY`J51Iz#!Q5Uqh{^Yv7s$F z-l#j@qn4@OVXI_EJhPt?HTVC`W89Xz*6PYx*=X#D{mr-!Jfr46{#FZkr7!Bn`Njs< z#O`=Y@UNi8Xl~iCoz}Z`(cCUj^hXQV8be{v<+znN&B`n@>Ua7hZd|Ii<4zT@3d=l8 zNB;4pWu7H7S0}@!^_Ce+l(vse{`nOAv8=ZOR)D?Byr!8jACSkS`Gt(9pjR5;p zvaY9+b#^7v>Te~OX?5EzF>|^#oy^!aR@`d{_b%Vls}=IymsSK-e{|3vU6M_8LRY}* zN*AB*F@?|;#2_e=E{2=oax>OZXy$bJMFsBYXD;~xj4@5v0@#M-{+ux2`CEH(|n;}ijO*MvgpFngfy)#e>ArO%+ff$Pj9Dj>P4S_ojf)l zfzf&!qphXFPu3a<+msXzN@FCXx0Av`2V%5ASVL!n$QiynkR=XFqeFL?8at|EZ5l}j zq%o3#b0~~}sb1?dKyl*D6J(TWvVgflazts_76ga?(r5-vS+H5hx zw!>vxVKHPyz(VwJ{8o(Y#|0`$xvb9^qYdpZ-gL#0DyPP1S-Pu10u0Dk z>StcbMm1vuG6B39&zsIX#?;qKn}PsBaI41XBx*Ltzj;V5-jzNfuR!^YyxB~u4kzIph{4tN&d`U~lTV4kdiL8qx* zrjQB>&o;u>Q8`L$_(|eCp_q7$-H~01ujfkYSiYM&BBQetHKZbA2N{hvX%v}rjc{2y zv)KGi%Qs&NKzmr}>pZ{p#uX$>wQl8)Lye@BMzPx&+fr-{^3CJ3#Df(nP}oZE+TCVC^$6GQMMRImM7=e*9b;nLn!9nz|G+Ycw*LpdSS+E%~!#SU9>c z7S9cBHhj^-R-xso_~R8Wf=G^tZyw*p5#Q4uV_~fEZCp2ee~5Z=8?hcZ@{{$dA=U3E<|3h+O>)RL@qYMOZlP!{VF>Wi}(~8{~HY&bZ~vDiB(%4Ds^3`P={F+l4$uCsS)P8X@a2G-xq+ysoZLUj_gdc=3%X<5oUypGpgYuA zUh|FD7?tuwCv0M6am|ucDzrD-m?55$xF$@@;?K&FEG!(x%xqxBg0`%uz370mTj%m( zojHsw%v~u_m3-mPjPQpfuGwJBc$+t)!kz7Tjf@U!C-rZNed)ATdSahCPwochE!Y(L zudHNowD6ZQnv(sZg&)g@WXE63fwZa^w!w-aH+{n4-^ZBXw?0T*uP|9wW{k))W}J@T zpo!Viy#ATC84Et3d7m1wPqSA(0@wWK34{`P!G=(xAXpvRbE*-RO{zdO z9dER~$fuI*9%wLySvvAQ) zNhV_Fq*!C9yMF3Lx`bFFSLC~hc5lsUuQ6t|RG6tNIZV$pwDNdRntF5^9rRiooupGG zsk2tsg}*S`WG|=Y8{aH8FPQ2glMu_R>mqgK#-lO(&T76f9_q;HC}1A+<(^CjA-75j zx=a=l&Hjk5xOEG#90@wZ8v&HaXK|h3r6^x193$ZpzH#eYW<1j2iPRx$v+5$gs_-r* zrSZ7OJ+jQx`e|w?43V?ZJ8Z9~fbg~yeHq(a8$6E*RG%2_FXna~#^XU@xz;;1wd2S&*;7)dDEz)DAl^m!y(*Xh+>`}~%O zy;3p0bRasd_htUNz_Xa8xFfv2>1fu2K34UEV(~mrXm7sZ?ewHphnEMpFDWI389ieO z1IuHLb9!pV6;2)Cu?bXB8!ddu%M$Y>bYQf%!Tz@svPZUMD8-qEcWoeN6;v%e)ibNG z*aNy!i_xc+;nFS)JM<98|G{$-p5Lt@1~w7h;kTL5{_EP5gdJ zGo$zAs_y<=no=c=sz{<)T~U}EAb(qz$#g8KCu+P#XR{Jp4+FL)_TT4l1OuTxS-~@e z69kh}ZVUoZ7n zO2#2Q^)t>(uS)#x8`@`=o<>2(%qE5`x6^-ijk-Vq*q-Ad;nyEF^zHA<{`Df03h&5ubdn<9UyV zLtDNbrj_THts#6*JAIv1R9zkdpbFiDec*#GLQegYG+X^$y2Tz&E*p9lpmPFE6OX{@b}a0hk#fI5=n&iEOv2o4FwbA#81x}CvE;)l_ZXi<563r(xp z8L&VyQ`Ij-4}XMm|8|5%-Y!=any(OV=A@&I^qVA0xh|1|b+Q@ed%=vr!oC+GSR;Ai zi33H7en%?S41SJZy0&wQnH{3RfFA_xWg#l~Rt}URw0rS=w@K5UD04vOw6BNfTCBjm z)}tBzyqw+E(C76W_4Kex!rW?N>a~i_R(S0ykU}xl?2KCz*?rX})!)Gm-PoyWbEz8rHzl=|_ zX1~$aDj)2jRRki>m$eXp{!--Ye6|FlCqK|6{yJkyjfE39T|2fU{wysV6)i0Hgx_j9 zc_hv32p$jC(+9CDxoCAi*)l=Hz5#{7dMD)?tD12kRW&UV+XOSHN zOQUL5pKG+qQc}rR)4IT{p2m`r9Xd!d8Xu<$BkWRvemW*g0@cl=*-b7plCN5XUlnur zC6lc&$NEnl-JEj$6at#om|~9F-jinz+my^Vhrx22%gk|6RJCFZO{y}&^gA*6ItJHWGEX7Rf6G^-U%E-n(xe9Z9fTl; zYRh10%aF8-lkMqr$d;{*AK#!`c8T4x<9b@QHszW?NLu!2E5-M2*%d^jTh=sD8WKKF zT4g=?n4l*NlyaS>a4Nc;p_bk^v))d*av3!+$RwcbhNoNsIi;5_I_26A3De~Uk4cxa z6{J}ucxLCA-q-8-!5~q8iGZ8)QdXes?I0$(Kj4}JlA-xw#T5-FWdnuZ_Ros zM21|9d?{B*GJ#mBute0v)$Tf~X{}k7t{2~<&Ky~jrCe^QKeWOv2XACNv(8NGm+0UK zlfBm4Y}J;MmO(+c%AhcVn#mn5PMxkYC%DY*hyCY8}H z2CoD;UcNRyNEgw>iSWMZZ5>eXDP@007SZ+`I3Md*y<)GtB5Adn1M%F8%R1MD7E9O3 zXxxg*<+t8}A}=7z;cpHEV(V%z%@OpNxVz)ir|@}m(c_e|p21$Zv9sSiK|b=Jj2^^s zxf!{(*!R6%fyixaSx;!-nQV5BnIczJ@ZD^7&TJtidb5MiIebVtkw>55%ZxlaU%%S* ztAkhM(JuZrMjm}r!^B~c&>UW*e@wsTskjNHKNSC0V;>|U zSi3>3swaqlfQpfQ7}VHZX7oWC>@Z{hm22iOOg1xt>RsTOLe=PlbL1s4D>E@a2ISel z($liPaO$hddti?89vGpfh|B5b=u9G@#)ph!$b-d<6f4bu` zG=E(y!j=vx>aX%WIFDEny0>7=VIsv{f`ggz836VO+m&?`j}vqX%(2&S2~+053Repq-l(4 zWhn>~k?CwY%Z%jc=?%x^le(1NvoBY-=RSD%zh(v04M((g34v zHJyi}#xR|60bo&NpOT)9x;klH{A?Zdxa5esRtV?>4VtAvF#!$Kpe7AkBA|V`s_QlA zRRP5{Xsiac3+NpU8lge22xyrGovJ}E3Frk4I#PpP5YQ7El%qk<3n-#NJ69`IpB2y@ z8uYOS{Z2qX(4f^C^cw-yYtTXsnkS%<8uYXV{Zc@uY0%F!=obPyN`t0r(4zv%)u10~ z&|Cq1b))Kqi5m1X0ezxDJ`H+6Kx;JUdm2=8H=yS=Xeglg1AL=r7t)=EgxhpnjwBCp zug&!m=0k7sq~~AT-hmGz>pn)Huac#HqK*X#S2TR(3$JQE!o&J@dL4grreDb4+@NP^ z8<#bi*4(lVM1z;zZL8>I#=lRT!GDCgRwmg7F;PsGnR$xvzS8HHSKrwJ)@~c$a%X9yf4UW=ZlS%*^Eo{K}19Ws0`CI9HIB za=me

_))<%L%@4Opt2|0besS?==CH(Aq941JT^G^7TzD)&abi)FRjXLZJZ3sEXR zWUs1nSMrB7Xcrq){8~GHJR3HM%6_W2WP!J4U;9Z2O>&2{YdZ4vNtuqVzp?ZU7RJ8J z^~Ao+jwN%F12F9JofAvu?ta&^`(2E@9-iHAbNCWD_gtr7tVsw3f*d+Xxz0_ALcxDi z_D77ia>m&%eXl^4rQOb(288zX3+7?uPv(n1j8MBxdC^yHte#sYvfLkWL~}pp!@;Ex zo>^x9(AKX!vnD&)qUv-P3cRudOVqhJ8>`!tA3o;K(B>@#>nl4HNR$c%ie*6a(xCtP zCbYS=U~R$r*cKN%P)=j<&f?fs%%ICK!M3If*tYxXsDcfiNWIgu(p}&X%1;P=lN~%c z^i58%RQh0o;M*eQ6{95#asBhI_zlcVy?%adk$(d;6V9u*r$ zWYSWv_@qwi6cto&;8WJ$B9k7Tb#>X*z?kbNDN_gU-}CGUZ!~7@I9j$>vZfDb zdxQ4x%?Tb6+RL`%d%y#crX30KKjq{1i5K+83yI(HLH3zEkMb<#=~Q*{@f6k7g;Pe` zqvZEmHu$% z?KKvx4sA19w(N{;%Wm1@Ho_N347(?du`afsIy~Gg6+R7E%T}XhFWVl+^XA!EUc^nMc)hRkHEf1^#BbAycT z*)wMGQYOtF%A&Z@5a`=4{vsv%*jNrfAem+8hWRiSPRZ7e(u-2vR2gY{I87u^^ooAT znnM(o6>p>{N(hcI7MNL{mVZ<(c2bl_E{cMX-u1={8$!#R(ZcgRHE&Hjkp`Apdq6`q zPq#FF97WI#M%#Che*vp$g4eontk>EhurQ()DhA4~R+~u6C{;F*u9Qur@2O3szXCF= zCp~JkeaM$L>fa?uePO$dJ!XprW8vz?le!v1d+KlRr`SGuRcw2%*ZMNF)$k765iR6|AI`J6)io=< z#^{7MXGijUTn}c7A?u7oqBBIJvs5OC-S7L3`N9+WtCM_Eo2HoN$jy_qc2jdC?HcxX z7N-r>lO7HhR{!!rYQ!oCoVy?xL?F(XO?jOv+T`LraRMEh|`#i4{xMyw;3&Tn^u_Pw+?FrPH2T)Y&G>0 z*-*DGlp1f$eB1@uqeYd!5@Y;IW5Habh5qqaZ@#^`ZN0S|fh}y;M5^eRzlXa0#>^)v z3r5Z&huG#vV3dN9kL9Ip^VaxnDEv*+G2A`nm%Fj*AW_}5vSeXW_H~M+EQhp6Az5zh zc~_XJZXHW-FBAMkvz_wgU&<_4&plY%l4sY2Hztn}-buRnyA&B+J*hLcS zmkf>(+yRhC9U-OmfbmL6B!OA0b*+El+hg5XXf3zu9z-acv7ByW*3Y3x)RU^|HXgM2 z45y~E!V`aJ&19`t^&T{Mnxse7ZNOzPIE@94S-!E!6M9&gM67eiiqQ0vN^I;T%-6hg zVEM<8LVC$qP{)v$ZW00}D#2BQP*gI|;||#Z8MRT*erdsdhiJjl@Aqy&X4q%NpC>{v z8(=KN*1(NV$&-2-A*$XDT3lwuXOQySn{hcQq#4N^5!4I0NU8^IV?ivo*%?dVu#&iH z(4qQ3x8NU#Yr!{Hl1cfLis15C?`kyDJ@Hp!r5XJ!{A?EX?g)BeA*rP!I!N^GEqI6& znHE^%7h0=T3zTPZ>lQ{0UP1&m7vrUq!ewX~4N`qY+i2K~V%;#0^+peO$;3PR9Ja91 z40aly2QomZG%h2Rdl+nBx>?`OVDX6ux663oIaI0MOuT?phhVa`#^G>2PvNXs>hwe4 zd<*6E;M|L)t|a1LW2rL_iTI)e5dUU^Mm+JXD2j~TgUNe|LRIlj?IBQ&QNrPQ=UHO^ zDettA=s?~v9$0?>s+ZZQb`W8WpJpv*k66aCF}EXe{Xh|g)AZP$paKu;z(oW+p?_o< zGk-{LAQahv6bb4~R1nh>Hggk0RlfUlz9S?MoymCDVYwezrd2Vyl3$i1k8~uq7p3C? zSNk3n(0hQS!s#{q%?_T%-y9?S1=&$XgJ|i#-?ubZWhu!V%uW}V_!YS%eh2X{BtOwO zN|{Tg%#dB?a8>5fy3GI3Wj4Kfc`1S_&QCtxQ%t86Gr}(B0#(cjx|qu- zhI#JyX;fP7tjZUX8YBk%!U5oJ7EEfwPs7uM*aDd1vZnVbdbcj4SJ8u{=$A>{w`c|a zr2L74)Xo4e7{qgqfeX>;Oo=gd2!xZ#FR@pVN!yP7KGh3;}UK z(?|&F1==Nz)AVK`5ndsQrx&%o1?!bI+l$;pHQSC(TB;0cc798{3i`Bbtex4e*(}5rgQT60Y5P2KFY{T?a+8HtC_~ zRR={M(K~ts(fIl|D2gP86N1~G(jDKoV~$A2CFY`TNTNh&pZ-XvHuHNI{AphAg5M#e z*Iukgy{P_P94vzuh7#F9!&B2`gg?=x|DI(alQlxXYG<AM3Ryma6jAu+ob%5%q~V zlql+ltm-GJIf2ERy>635o206qG>v5tPVvcHafin#OvEU&0KV?{nXruWv~E_)^?DC3 z`P&hHGCB0f*wb_^k4h~ADa~kmmPRKw?oF|_C#e|yl=ifSmHd0wy(}9i{Y6&&=|w#P zN^nVR#W-_t`$tIoOLhCHh?f7}GRv&ya$0^mEw4z3>zyV@AMkecPP_4^eM{rHs)hoD z011B7Lb6Cpj5ap`)tde^gdz0*sF3`2v42uVJ|0T!pOVN`7``d&@;xEfF`+$KM)(Ej zDde(Q@9f0wVibjSdLY+{y;S6kbRK4zX{__}5jT!xo|q-GS*-&(S|B>pf{x6p@nrI< zL2p$mv0<5DT~NYyca|8KF$Xv7ka z=;%(Ojkdxa>Au_8JIPd%FtiRJ(=3f_hD_%s0=lRSna)j|4=LqBJL>h-Q}p=P=x78@%OU2ec&XfCg$POf zCR3*7h`zDmZ?E8FRl&V|D{wpel)*V%HFn(2icazt{+Wi)Tgd@oY4;MzLZy4J5 zE?3G2Pv;J!A>Nuzx99HuAbIYzvk4Y^Yu2maDbr3SSXk4oLPt+K%7+^>jZ(n@d|=!h zv&`arRaPus5ZXL6)SYL{oK00&49jMVgqz*6`Hr-`FualEb32mXRUeOZB#*O}C682r zCp(hG>f$TnzjXvXrJID0#% zRrY0JmzulWTAm!FY`CHQSvshW*F8~IAsy1XLaf@oogW^#+Tpl}XD^S#cEbJVZ%39+ zlgX0#_~&{5Sqg{jIQ)p`Cp^=5?&X=mGnnV+JiY&A>3>qDoFB;T`!7%a^*i7nNfb0c zKfiI#BFbBOgTwJQ&lf!XZge=#;klUS8lF8omp3{bFY$hkcL&egJYVq`H#r>TJRTl# zehBi+lnCJpMz6<9VJXJRkBD|H$Dufv1wEn&(!Y z**wqjyu=gd$-af3CE_`kr=I7BJZ(Hr^1ROT5zjuJ{y%m&PUacTa~{v-JSE_NGLLI2 zKgL7&`db~2C-7lfK80VG`3Y?WzLaOc?WE=Df-Vyq9FFf?N4Y$&T<>t)`~!Zn2O9PM zeus2hc}^g06;FU?Gief(@iosro)nLh?W#dM_6Dfzq!i2V)8J0TrA4wI;p}mC%ZQ|8G14mxF~Sa!_lkgNE) zo7h%w9%q-5-?54oK1*k{%e^5fSB*00;xxsTd};@?m24dJ{JU~)zE90$Z?Ri3 zZWa^5PzucT2coCHK@CXP+zv0}U!Pat#09${a!}-CM>lTSj&ebQyMY|s@enA_j<#mE zFh0Ap?ik$Ysab2>j~fwQyK9V)?9Gg^0_B|SS-7evnWzXWY1sW+q%P(}vu3#w`m+iw zn0rKemqppH)K<$I7;0pueu)%#Tt!DM@>*|rt&gp5EcRWT(@qXxV6U&WE+1CtF&1;` za>guDMrWMDUj4{R9NCU(cY7ggv}p3olw3d2HfZwQDATiBPv?c(*`IP-+suz1#P|R|du|EW)TZ-NAk85_b+5kdtHaF zL-N0*Kd)GXsnL-fyC@|ubCDdGN_rN_W>@n3rEKju7D?li!vPtNBBR8@i`_h-2Cr@-B8vJ9!LLDfHDWXC~Q{JZhRtbqv5^PH#=80%qUA1ZK?IrW9^U zpx~wpYRNXFX64i?jhSnSkYI7m8&fYbW-d(!3ptZnW6YeN4mxXMQ%^T${)%9-Oe%Le znvRusp`&StMy9Dy-a{Nl+ubTtAy=ilZVKZxvBl9Xr=U|SJq4>@hZ>IBNa2}QthO?@ zTrqfMtTvK+CcNz}&u*-Zc+bpM-?{2Lx7G@rnOj>4Q&SK$u02b_!VQwtDPM*1Rj67a zU&Zn@1U_FYp&>FdW<0>%J$|b*v4gc=D2$iBHl))iNv}BKaF;>n)tmJQ;5&{s?ORhN7BZq%i-ndsW@W~ zxLiil#m>%t`K9tTSNkt-Q`WBkmE;M%`kElb#h^+Sh32a|UKK||6EFYG z66C&lnb1uNX_2=T5?@m}1kjxZ%TSlY1CkqkDaeV>yQ`9HU*C;$3#Waec~gCo5#xir z*m*j)SPEo=m2D6Fygwuq)%P){Tm!)oZyEa4QTc@zzkerRbr$!|pp)S#nkE5aovWI% zdAT{hAub_R6j&j*ca>xPDWeW0ZB3vNKW)3Mhq~Bo|zmZ&+Y}P1;65@~v zx{*>!z0@E`x+$(oiaX%W&|BCazWF<{ecn{ySu~m^B}6MSck6qpr2!IAFA={bU==<7 z8(cG|P?9}Cd`J2VjaV0Pg`~V&ioh}2?mTbgzS8z{HDwo_&6^9wnWAQw#pOFPrYzmL z%2&eh#?8ps5)0pIxFEE-n_d4bU(Oevf-ef<=~kn|7Q`dBm2=Z&ZkaJ?46a4RfijNw zk9G3}<$z&a237&AA>|5>>tPxZT^XkNBLQRKT*X^*geHxY_?Z2Eq|oKgQ+m4fJWA;~ z7jZNp&!XFlZ!Nask$&s*iHuUy;wNwP@sLfIX6jud^UhUf>f}NSk}RUH zIK?B8`bM!@#T&6%oLi>5ew7>BYp#RgR<+s4<9T z66ZfC(kNC?@h@iyDFcmLy6WcmEuR}7c!FHnmbZtARKA-5o8lCY6Hbd1GBJL(qd3Pn)we#r?Lx^07DPhl_vrA?KLRw!x058K zmR#a@&Iu%66DI}gqLfY_(h{BYsU;{w8RY_O} zDo>1C=gW97W^EdmG+KLkC(K*E(d&J%hK_^&!x!ks|f<8mDlu3DGFBhonkE5TW594wI^_MT_+6E z5YN9rri*oT;(GF)A7gl()#;CI&ttB~Jtn`-`U_@={&m(ie=Odw&f4LRB@6wr&j%nE z{jo0!MEUZ^b`*Iy1BHV{S;HNe7b}<(Ir0S^vdb60j1{n*MqbgmEklc^! z<>f0XeBm!_=t|~{4D>@AI)zy?15E`_)Fpf$$&!Xr-kX#MJd+Ilay*EFhpw<_4X8+L zKq@5mJYswBDLOrkkBYYODf)RD9|g7XDRQUrQBY_kdc4A^=(}m0h+#eO7-g#Sp0xc) ze{^(}kbNBGkpM!{v>NBI<9mu_bx4Zq;-H5VJ?e$B~ZANdt8L&QLGHGVWjQT}- z$Z*9f_wxSjVqwFgkAV84`Ir=qlU>p4#HU>MAL?#EH6ZA!W*sm%#k z9~Eg)|8*WoLihG#8T}d)4BfxYuH;xZhPM%BYrMmPTln$J} zvrFHx!7hESlunUi2E|c!ck6zA47@vs?ry6(`%hz zwx1+f#$7qix|F*G?xZX8J)NVSROa9QT6Une-HTz;}>av9+x=?QPOqM6awp8zMUOQmqHwNNMq z8ylVdq-`lzfAR@``jqh7Ed!4`72>CTF1dEHUNYK#4Nt_ zjYdw%*}|bI*A6eq=|VzBY25ykSWQboB6xht^)Qh1qJRUMO@wrRTu+;f1-_Igv_H2o zGINeJk?Rx8$jo;AIW#h}gD-2Qv?DTeg@VqMe)mR}mOvpqfICUvMTD_%rtk$3D|iL0 z`-&-Yz2t~2)C=#YWPzP>l*>z^SBtP`Cr%){ zYD#2b2L&gO=p86X2VNsaXDlG2AebzM!Tt)^YyCuxKT~iJzWR#Z)O}}!mte>dv%^%6 zAaD+y#2~m!_$zup2aK@U@)zI0n*hO1phi!$P-@EcYl=ywP4ONyDY-m;4yGlhPT6vLP~UU?`zVv0iR zrBXTHrbiGbQ{p$3Yq_j0i@2Ty>U1g7K|Wj}lfBf}b5Zc!?!!#>%(w1waxcF+(>BGl z^6IEp>B+3DnDo~&9aesb)ExN{Uvgvqn=(OK+qo^DwZ0qxvNp)ls!BGO&s#c5)!SjM zYpgB_4lucl%|yv<`ih0DQf3X`@*sC3>`CUB);>{R%p6RaDoO#2S-XX>q0VeEA&K-8 zuXK*>vi1@Kjl)q&?RY%laG!F`r#iJ>T0B5f%5mA_YGts9;Xbr78)L$;?a*1y9A9*y z<3cszn@gNP8d*PH!U5Kc%$!fm)_pqxO`B#cz>DE6*0Ypra(bO6{Q%aNia%ToQuYF#z{ z<+6zk81DHHV}=Yp?0p`t0!^FsLKhHs3FeU5>gbyh&dw$^Yw}W(($KR*uZtYYYU+<% z=!AcgV**ial){i9Lq#H1N`xpRugFJ5?>vtSWH3=L@j?2getgT3zh1?}_e#s6bxyeg zK_BT$xxVjVSl;Up8O|+}d5DnLWmFS5ihvdF;@h*xWk}>U{S>)C!v{*;y0}seW&244ns)1`b z&)EV{aIFU_oa^OQ7YVxw%bu{Z&WcU7LI%4Y{8$;T?o0|GJ6&<+vt7cOzwN`B z_bx*`q)$wvC84EwLSv7`baYe6enfdzG5uQ(v_~opW`UhJ1CU-r>swBH*3-&9Ec@B4 zUon!Yv+K&b{I%@;3_Kr9qKcoo#rn_hzttd()4M&YYc4s~v>tAe4y##h%xs2j^u<8k za>-G*TyoSYyAd_7&LWNR;AxT|=i+8kPve@o+R0|B!>p;DV%)z(%CPRPr=EJVdMwKn zHP_2keANWDt7xuVk{A}G9Iccqmym6U3G6#2%ABxS>f*^APlRpcxs>)LLOiMCDXi)=suYA8Td_mOr7~LcV(n8Faa)S?2qt5yYtA^M=P!Ydsa&q!8lfytG+(iTO|+6v@QYKk-Combkr~?Rvet`5w*Wgo zKPnlE=Op{b5_z%hIT!(UZ`i#NYd%)|-Rl_wUW#x+pU{@CL{an3y1m2``z*&>u*Ey8 z_Gn+uXQ=tdTI;H3D?J)}QE1B+BCT~q0TkXb9swBZ1r#n)z*ZXwHUilCP3so%bQbVz zHJS)RcB!UdOSI6z1Xi#*w&lo^@eAP&2{0)JKj}ZNlPDogM1ERuw$k*Z@uI4EBR^oh zP>cA#*cKUc&Y@fRwa=dM_AU@T-h#*V43>6Dht@gyQ~8mjL?x4q9*j8WE?EI3v6@%2 zN#68k>kmr6d%u|)-C(v(Z*VY8-Ej^(d2XsGVTwt(mE zIVP8MCS>d+JHzE>q<}`y>9mF4`?G#NPZd{mV@$@p7zldM&Jwg`$lR+O^Vh|n*~$Q% zLg0`x*p`&~t>k5TmSn^8eJz)mAycjwS^DZ}L*FF_=;wgcHm9C1)GV<}c|J(}HG3`Z<`lO~erJ47N>vklTw6hsm9#=x1C8)srL^d^^;oVm`g-*-MjMMPYN<4< zuc6~!fr-+)>qht)72p;@iBUf&rEU@YsemKwgifSyF5%k1#65hxVGfI-AmAN0>XYoq#*GNL(xceCBBW|XOXs2Szvw3+m}#?1c9 z0hsvHZFX@6h!bJ1Frta-7Gm$rYAOji(mHl6bdm6K~y zMAcvpwhW<@%yD649=4yW+6(bpdxFF06Jxx*oE1p5_K3l;;w0)2Rap!bEZ@-uwcO^f3) ztYhW&KFm0B@54kW6WN!wbI%jA9*Z9&7vXRVY@KUSM>Y{wA4od^%%sAHo5*E zt|%TG&3&35`^gRDa9ca(=Gqd^EL_@aOXS-8S?>OVSEVT@1(|w1XK_ZX2axTMC=ZG? z=_@&l=*V?c#`u*HNmSV-d7KV@KBZTJ;sX2l+<5C$)m(>R2D^{WURT=64~Mr}ZBx2ycXr7Rq6Pg5MB}4ai+gaT)W| zJkk)0ROoNCJuCsMT2EgesDPTjjIfrE>9q>%>CkJH7vw9wRwF-_kBWxP5l@z&)r&{nv z2&P=efH$*T@M6=-Ozbb5(R+X<2#HuSP1%)%brR!IiVi6wY>|RyJkg63!lwDO{)K7O zT8t=P>}pexUS<=u_P+y>;@)-DtZ-_uPZox3)z5ak*btEx7Zh z03+&VdC9$m-^bv6g9EmxzYvWj*yL$N=_-_AS(5WHlg zGh0<5i$*mqrwJ;*H==%SL|>Vu?m$i0YL}=M>^G4y&40EE6C9j!jga!v$0G1}nZ#@m zp0DtGLMePAXE#gOdfhgGmI`a9mtiT_J~6dg$IZT*S2QmyubNFP6jUNyY{*;5HL5SG zZWa-}9!TRHk)r&z-Jcqi{(MYbbct+NAxh0U&h7-|2iZG$RV~gXIUg1LJ|^N;C?PgF zC+~RspF|}$+V&Hm?rQ3$o_U1)qIH7eV=88@Hq2r*eVT9zdmVnApzXI7s5V7ss|2{< zE|KlCl!4_VRmAjCo1E<9VKxHdQ=ir4DLbuZR13Q)P zC}yVD)PjK47u=;OAO1C(yf`;)lGNhlBt?);=-#&hCg>i(5>nu1l~H%U5q_Tz>I|;{ zCdlDGrrP*cJjpNg^kJO7159ll;PR(0?`{2D3RRv^X+pr0J}X|yEhg-RF2xk;w?->U zDl*Y^($y)~FNra&b1t}3aL+qWUNt+hR}*P(Du^?%v{kI@(#E>z@6^s`^pBdi&J{5% z4VRKru5qeF&EuKw{iWt@+ZFi~(uhS+Ry=j$-7AXls!F-^K)&UyYZXsLbX=~@lw#H^ zjeqw=&5{2_RS3fWFpaAG_5t&rs!rx= z`oOvh!pk0goq}?=w-8yoc;Np6P}X@3C!qtp9k9AW+nvEPxh)dQK~!Dn9N@lT>r$s5 zn&LAe^;HkRv51dAB-_s=o5>5(DY2~ue+a5UunAN*ko-#_A@?aOKq*J?2?ru4m=Tn{ z2>**NB}TFhRJ-il=)Nw#0OdZkuYc2(bjemE2Ydn#!zXf0cBv`5Z-4p&R$sPC^S+}B z7F)KZYm(z;f$FNJ`vQ?_KR3i@D(b#nFj*Jx;xbjcZ(at*eWzs-bKDCAq*KEB_h5t% z^JW5*f6RL%3Pcf!hqbhRo9<_;fSO4Y$y%kRR= zN7~68MY0NJ#M4}^L(=n*@^6>1XZcH7_GC{zsh4KC^TbS4b9w}cz^i;Vq&LfMJQiuHQaQItS$&2g9Vu|cP0nX1CSMfPGe(3|L)b3jP z$;r=^U_k&LzxQW)V)!AfB0^cDkx)~HJn`}a95cX5pSLjD^l9lX&Nm296mfAKrynG} zRrKCZ=mG4a;9D-cbn(v%wMl~ud&PKXjVm9K)pR{6ZzLs+!9@{zyFD^7I!wb?G#qxib%|MI=0_n1-CjdIF3KZ- zb4}ifsw`$l(->7$1Ux+nv0?^yF~o?Bi~A@eIfkqor=+tAhUDKW8X;L%15an=C-i!< zR#>%X3pU`T3UaEp-^mgdGR5NsLPZ>WhMgcEnbpKUN_+SDxhhhQ*YOiaavw6i-&cRJ z&nsZIm`6k9?qzEWzYS%@mE+~ep#pm?ZHI4Diig*lP~zFO?0>QM9&k-POTg$!2%!to zL_~;)2&fpUV5LJ81u2S%N=Yb6GXxMDO%Xv98)A>rv0*{6gIMq{Dk36wR8$ZPHpI%C zJvo7Z2D#pQ-}k-WyXeX6nX@}PJ3Bi&Th1O5hsuA&NpR}(9x%Y0`jlOM45}#+Uv7W} z-a@zmF^KE~kS5w72LRlKI>^-W_y>9PYzJ}&Km^E(t_QqhY831#s720nBYum26NTtH zw&o%_JO`m&X%-DdUxo7rG6WGjPljoGSX($D0!blYVRRX~`8M8-83bE+L0DN}f_5RV z#yKBm&g+n;eGr}@ub>Jb-?ivSkF>-~E8M6W6g3bidv_A^K*XE@4|5r?jtW%GkOjlN zTEqv^Z5mn=D}ILBNhGYV%e>!%3L<(F)-5fMlN3-S&>P4EiVph*b5Ox}@<2WYk{9xF zkwCyLa^6^zn8 zb*OQ3ec9#HKs&%8L~pJjJ3xc)zrZ83Rp@%7wgj57s)y}I@Qr}$jf3H~DGv&6)XGTP zpAHFOROJi_(ME4%=Rv|i+6B_%q;UGrAwIlBVgZv#mZ=)}7AsjfTUciS6=_#ehqxe_ zAt*Hr@2@w$1vdj`B+U>4jK%fFYCzyTEYNDd$Vi(l&PcojpAWkrmk1w>NI`GF!YcLk z#&|qkj5rD}f-s7S*u?n*ZjnND*^l1I@9u%PC?cH@U#S{6>zBkJKF*|ByHX590462T z6SKy0c*NrbaRFPrD2S7y<^kl)AP1X(I&t079F%b`O;dja6|2k4+?Hhroi zmP~Q~P{3b8XT9@2v+y+^QFDV|$5heRb5Z&#$mA)G&e;w3ysB*BeSIMjGUD5nI1q|M z+H4E7Uj#mwz;Q?&<6u4qJc11LfH%7fZaRJgvr2QsBV+J|kq8HvQ%Bl`Ppso#=FtMO zpY5R&!${~06tI49tx=^O-lxT@a0k31%VPru+U-j11Nb~}RuE!s02E4d{sqMR-6D5a zAED9#4YNF8a@D!ZIp93&3lpP`_*V`cR|aMxKN?`1Lz;6D8ksm-roOgJ{b8H>3?`J@ zl_C%?_{IX%RFk0q=shua!1DIXTEh6n{A4MCABXUFpdWA21w0I{4uhDn)Wiz-bx7S> z1i&VlXJw$Ny0t4wLxR?FSU3F$cFQu&?&nytY*HhHp^7Y^@!^Za_}*8&EFgreX7V+7 zoGN1lRVgK72ES-(IXv+)F{uo1{jkwN`i>n3>4VMfm``4;O2mh6Xu)^QA|+35LEJ7v zB<e!~XfivKZG;4tka0^>a;D}c=4HAb+1D^(!P0)aJvgmt%mhtGF zZ7EOa^SjrNY-{Vfqw4k~<82RJHLPU6>AZ=_MqwEDtLw~bObrMGh+hO%^;sE?`M#79a zh@)p0E%iF?p&eKT*6y>tIPRk9@)mjS!CU^&^9;b9O*H(I=c1)F@FXOFt)`Vw3iM$D zln0V94NnF0_RvIldpKp6`5?~+?GC+;BW37;@GK3VyGOnh>cF8(MQ?s?EjpaIWWf2`$l6vwH|cc|od^pN2qzBK z3m{KR_)ZwWPS5QYh_z`AB8(TG4E%`>f~2rP`V^ia(qj*xi=0FYfVc++^G^hA9pu5` zv}uirK&#nKAe{=44(m*3@mro05l;kfv_^ZN1AW*K+ zNd(+WeB*cXTt;de-kO@}CkI2LYMx<}3SJ5bAUX?IKAY5k98x#nE=9Z2;GLiVrOl)8 zv^cB0Pq;)w)xoFN(6&`zxEPJ;VYZzA2^&zKe*i_ib_jCkdkg~K-x2T{1l9w`$mp_Q zOy1X_tW3`Ta~Z7hkkiW@$}Y<_q4tEnPXl8frWOq(0^bgnp+My0pWz8~8ZAuJgS>z6z}J@HdlfB?A{CBCkjv$n zP2-^p6et!1+lQzs0#`%S@LzTO*9`yF6TtVu!&UfiHU3+J|JLHab@=aH{P#Zo`w;)F z$A6#Tzi#-iDgFz`J3x-Se5LVlDE`|94j>=T<40Q{K98T_I7T1;rQ^Sc@Ko9O?`r%v z9e&U82b_3uJL|8#zH9k;cb(5C8{KOk@)-zMG7_(lnm@oVx%oBxiZs7~UrO_1_!Vuw zN2E(*KM1x@u(q3t9?iU${hbg_Z^N65B0}i_z17VF!+!)deeoG*ya@BF*&sY zjH?Jo8Sh^lQYo-6zYM*zw8Pid+aupY>B{}_KmPuJ z^o~D=#IJJf@XvSP+(=Fz$6;k3tYpI|s&H<7tO!Ho3NKetM*J6y2Hn|;C=dx`rSSnG zNy3k#B=~@3G=JGx%X;`>v;aeqPnw)SL3tT=aMU&b)-qX67BGo8Bmt*zAKB8pwj*{2 zdmRCX_^%k)Ep|Nbv;!52q^ydKq=iF#Ssd`xUJ?1qA-)O4D1m+l6X4HZ_5}$p*gTb3 zW&wB`$$Fge7C<7VUocuA^)*I(BTCJPZ$ltrb4#4G7R*i~p~IVC=napi80dgxHBfd~ z2U~c6AyR=?k`up@V9TV9WE;FR!K)zBuk~Xi+3wJO885!@`hmJ>i3T?lb{@%c`aIdoq z$w`)9O}zRk6Re*O{!l%j%3vT)c{L;loJ&ih2f*|TiC)&vQplfZSs*j05pni#*`w7A z_(}!uj{zSO0knODHzv1frfqCa2zj_t8ZlYcY-*%5-9y^kv59JB;>oj%fp7bc8vj zsij)BKXq8y*J0&Un6{h>quiS(hIY}SXdMmg!uvV!lpwg6mUU+V_|HJ1FQ%dDKB{K zW-!PmfCeDK(2ZFQ_U%#d|#Pl)PhV#EaTmtLR zrY?cMYsCFa0+P{s0Q@Z_;A{epC!i()`6RqEdGVtNIE8?t3CNG?n?+*L%`<*#EAUl31~&&w-NjvAz(ED-w;rops!6p8v=$BFqwe4 z1mu_JC?PKn0lf$~k$|HKs7XL+0)E6l1chM_30O_Qa|FyMU@`%N3FtvU3j(SW@H<-Y zhrd?@tRf(v&x%u+1&UEmK>BIkJ?z24@tNg)Y!9e^Vl1!MTqC1vJ}7=&LBdLrCzJP& z$eubwS(|!M>}1iHF+VL6Q|m@OD2cSRa|pOyJbL|>-`6!f<+0lK!{8fRt&du8#$IyW zVVdA;O5U8FXT7m}kIRa~@9$6C_1!xu-r7#*-z5;tgWL|*}Vx% zo8KH*)UvE-(prtBz9S;EhAUUc#h*T`Xt6mc>yhkupXJQmCPPwRRWh<`6XS2s+4Fo~ z!F$H+I34Sgch^lXRvB&4XUNGiv4?vs6W;7w)UI&NBxVR{q30#t$;a#|`^GLZ403%i zRV{lS!*b;&wVKqa1D4d^j`M_fWQ+_T#SU*1Ff2e=j!nwzy9#q0{#ua z79ySVLteZh0%DJN_ud37BA^FBpH9%P((RePTs=XDfTaYKd(6X!6R?zk4pbih76kpc z;XTu*J>fl%BVauN=}&q1bOP2B(1GCJnV?@ZtY`Wb4Fnwmwh++b84u4TU<&~~2>!hZ z`spKjrtkcmphH0H1@GROfJFqv8hQ9kf_^wbpQg|=eQzRNDFNwCy!hz^Y$2fYOCH{g zpwA`f%k}G-ei4yQu9+7vj)3(9w0OnCa|yVBpkGALcUI|{KK7dT+?#-<1eANj!-o^F zlz?>v{ZfK{QU9Ll)86u)#}Tlefb5qt+IaE930Ory+7})^nV>I6(C5;6rXNS7t0Ex% zD=&T=0jmf|`^Liu67r!D^l6$s(+?-ol@gHlofkixfTaYK`$5nr_@@)}Ee7{Y-{g@6_!JbW8LKaSAPdIcW+qMT^yb1+vd67z+zm=6|%kD?ebuJeU) zRU|+rjM7*zwu^>)HZ~7qV9qe!G6u53=+UqUoL+DTJsQvvq(Q@U@UdfO9<&a+x*ZG) zqdy0;2Fk(k4{^wWl)d4$Cw~39(EB@Xh0~vd*+FT8fm1X}?k%^%>CrHM0eXMSt#JCk z>Vp$5=g!&^rV$-ghX;h!U+?sTSGS#c{;DCu=y%tT-f}OD9u4L@1oiEA68|%|J;_^8 zKYGipaC(2%kKS-Aoc^!+@n@Yj{V&-IQ|!NCFA%rF%ikM)2!__`~|t#JDPs=eTG zE1ce+^`ST13a9_8KJ-S1`CQ|o{)4=G)1L^_=}wsoYu|rkFM8JJzqc2i-1a2zf7M?6 zNuU4JpZr<2J++r#^}`PLC8EKqvj1!Tg@2bk_$|=*FYHC{xD{Ssy|EX6=2kfUuJ)q0 z+zO}nXM52bZiUnT8-3`xz4gXE{GR9DwYRSR=AR|}r}p+|`|xLOdy=o9edsN>!s-33 zJ_yS8U$YM`;3-Go-aQ*%YqEs92=I9WpjQonXQ8;)&mV{Idk|sv{dfK*yIT%4utmS- zfyOEP@lL0o@u&Q{_a}c>nx5*TyZ-!LX@tqEt3CN!?t9|DYk%{1+zY4wt9|Kx8F($1 zSHr^O^PkYC?ro#z_R-b;{9S2;%j?(v>0hl8UTK8M>u>c*ur7L%Pj~zDcian?&!6=P z)krX0$cC^wEC%0B@S!mI^iH2psXFucon;b6|4;Tv827^H^+sR*%xzEd7VN(ecY=L^ zFm8p@`?G%ZhFjtE|4u);YqxOv|DJwyS{h;X`8WE3xEDt6-_egwZiUnD*`EAPKX|eh z&hMZ4BVqa>$Zb#jcAsCh#OKojVIDIKd>J%1>h~LuMmWE}`nx^^MI?Jcn!mFXf3ip2 z^`Up%3NQb!`tUd03a8&yA9~BJaC+VCMQ^zkR$sr`hn{%svOe4$-`NFW^t8~s4s3#0e%=tn2F!s-9(`oWX6aDKb` z553n9L2i5Ex4Zq|_aBa6KO*pT#n9ha4dMLuHVq? zoxNR8+zKmy*ZS`*cRlgfwf=j@op5@;+5_P{3R(eS<^5gz{}Yd0HIrB8!svHz|Gnc@ zc=`YJ`sQ&foIkgZ{%Sh znrOzelj`qUx-j~}>bqxddy;4O`OHr3>^--_>HogH|A|}S^t$^4f5)wG`oi14aGrba zPkXZWf08g?dwb&d_x8SLZiUn9sr~n!TjBKoRej*wSYhqq@AaWmwmtc8zv_b>?#oAG zws`OF1ADc2{a_%pZRT%0fu8O6@9c#?kpm4VA`hc`9{-NL=q>lc<=x$0{0+Cg)sNnC zE1X_;{rEd>h135#{orf$pZ&RiPd_?!Tv&Z}?@xLo@t*D7-|I&ww>`;Q(4Xinx5DZD zy?*ex^$QE-uoiIUL=aWUX3d`?h?@j!rcBr~h(-&(FvEa$UBIc0@YdaE*vqK{-o+UB zXHUQ}up4v+?2`BuZv`>XDyS)DhUe5m?86PIwU_snQks}%GvB@H(`e}{hO|#=n_t*% z8mLXO-Lm!{txCQ8$5S3pIl}reaL5(8Y^{fv7e45x=y2q8!MDqvh65fu=ekSI`DaS5 zYx1Wt+J&pG2D_egw=bBq^YfS2Z9&`MTUr^5F7+SHIHr*}+;my>s-euNBfFn0v^%}@ z-qitho)VjeAIYNZWZRW2z1DO-aY$_3wPznIUn@*W8F^gM?WE|UYhI?DqDobZvt=cl zjrT28da?8B}Tbb z?B#J|dRlp&sF7p3Q|zfX&-)}Ct{xPg{V3GpK=a(MpI`L(F~*!dai_s4n`PxE-0mMf zdSu?7!&yv|RSs+WeVCG$a86#=4+Y?LV%T?!VMoMC*b#L66a4X?!TB{9`syV`f`n1t;g9g2kDbB)Xvmqy z+jrfaXv+=We(N>@c7KkaGV%-ZKZ|g2wQ(Iiu2%G)>{pIQChHy__-68V^iG@gXfNK| ztMEGO2t$4N=M@wh!SM+W_L}TE&DzT+glTHr8O79CKb+~yXH5>nXmAdQK5A8fgZFZ< zq5|SxDy=WrKm!XQMbg9q@ws=G|*8T)2?w3(+t+DH7Hw3HSxPco>a@ zr6W4TZ93dTQ84{^AxwY2V4sKxUwFe;3n*e37oMSa-n%`+bua(-&)@_Lkb@viPE0r} zC^U!zBujsb_rt0HA2@r~e_r*P0`cxM^Jjh79J106-4xJj0?~7*%53% zEjm;Z$OCY7sLpvfJP%cUDNIxuqYh-Uuz|)*Y#?bi)~8-NT*?{40#CwW$51C^Dg~oL z9I7FiuBt7Ci9=Y$n1-p4R50ll$$If{F>fjnzX4m=;MM zQ)yAISFDmRm4mpddNP0xp$kZYA>sUR&2C?{D?9FsGa$K=*Y*Ylrh zijy(%kA1PyNf1Xt|`jH ziyBK}Tvw0-Tx!yYCJ3Y(j;tsNGLoYzGNmy^Hz`aJo-4BD!pUlVFiGkNsv3MaSk0|3 zre-XMsjcgfLO7Wsf{~OZsT3kR4v-Xq0s!KK!RsI%YT zP`wMrBTG^+NszJRI%;SALppe;6Dx*GkXCmbq9usq)vX`SmnuyR*D-l$2l7x(ep^XL zGS`&AD3DgsSQS%*XM*~VKkrY*#7IhbeWH6@&y5u^cn=ZV0#^}HKBNnRIKD0jrd1sv zit`}>`H<;6y}|QP6T$i*oO36RmyZet@AF}9a4Ev~u25a0a(2O?w#Zb);JrNT7F^$n zc&JQ*I8`-X-re(2RTsm{F9-R9#?gP}K@o#@&af1?1k2h5hj;+l!n#mSfBi72dWll8a4KDMlsGo(qcS$MMgx;$s$=3HsM19k zHf}M7jg>?4g5C$7WAGe<=SYxUpF`b(oCI-X69rtxCLa|sg9asRM2#}0d%hnwgsFz9 zyA1#vfb>KKlL?3RAq(vQ=}Z;BwSs<(6vu7c$d3atT7w$MWdO!40^L0br$*pBLB|Bo zFnEUj@{DI61>=+ZQL%m`C?B%V-crsyz0ktAaG2L?xafVMoX{4HYt%61^8+z)jyjG% zl8#{(=o#oNir0D1D|46hNKTL*gY+1r$Bett2fgc#!}}QON;@r(ix$X53*@2&vKZJh zpuT@qzfvV6AKWhQ|J5!#AX|%RJ8=47=1dukx{hc2QQv_2IAmwY%7#=~rUcj-aZJ_) z>X>?qTD@wON~v5?O6gJ4PlN+*F zr@cmYjZBpQJ1>pfn$CN|?kkEx9jjq7Eud*tplRMvuPRhb<-95;2W1rp`PAoOm{mUb zQ1ZNXDFM6-;>a>$P#77i4Dd>=qLiX`r71_jVw zD9#*+;~U`FcbqmaUgtd-x50wxRR>Tp>1y#7YCW`IXEGhx_dvd_V)nyY(l9J}HK@M= zk5?pTK^&eQWFhUsvwkQ&4W;i(!TNp#|D-_@vjDF~YZQiUAZQ@_B8Ve{?MF6H)R=_V zI|?guD3~l$1e0_lV>q4)yhHtJio$(^I96{5=SINw9nqA9xEwJ|lu5;L(e^O*P(=Ll z;&k4lx|>9u4gK^gYA#if23Z#T0A)4mDCoAj^ifixn3S(rm1wC5^r%oSbxFM4tA9j& zD%d0r75J0E>MbyAge5cqJYQrxD9}zt0Re6Hm*>!qs9dUG8dWt4*5^IXiXs_$1Mixs zPiyD{<&wqd<1j1$uKPHBGH!PTapXm^U^~XzE&3>jMbv-~!BGJpPaf-AFI$RwWGOo6 zIqs80YpXE9A8`YJ1nQ0NlR$RRQlTEGl2lQU0U5q@Mezk3Sp?S^)aUT}Md+`sP;7qu zMr{YEFWx;F>hS=TgwjGBlA<`K$b`!c+634cMMJr865dbqd;*dz_yL0c0MSQ*%Yq4B zh&KAu;Q_3K7}ujQ5dKH&I{xFwt03YbKOFt(bc<)W`_YKa!{9iyS+r{Zn)Y@mBK}uL zq(eMNz*PjMZ57<4PwkoyFZQp$Fs`vcY5({4AC`a|jDO(DS0Z6%0Ot^4Z-5DXp`HN0 z4B&pa&O^8e;81y9SO?%fxT+8yprQf~kH*dq;R*y_4Br37yx;^x4us)dZ4Az%z|tX% zpe-m-BZLM0G~~l=f`dLw0gvDTxN0B_=Rjb!;F6&DaApIh1l{9z2n+gN;7(x6K(BPb z=R$BZTw@_D=yxHX%oN-YXTT#k8Mz`5b_SS1!>|P?K0tX*;1j|$fY3eQ`3m}2$p7*O z-)b+!L2xNtr4SbMuaNImKLq#(Jc6I#YJ{+$4`rf_Ve5v1?7$a6umCO{2*dmG*hQFl z<3bqDXu&pt?_LGt3qk)J`SSyzt=$Jag1g}Q2w?>G!X=L61~3|Yb(B`nA1?;n54dz7 z4uWwbz(0X7f~j!1K^V?H!KUc*>d_hCL72Hf^ADu}$3Xv62mOR#pM&}%>2Z)Z;1Se; z>pX-7Z7Q<2&K5Af0-K9q09@jrKlK1dSn_N=f(bTImkmoEjVUI zW9OT2)j=EtTi|*HVFbrIflfl$0^n-6_#iDj*yOP-+F}C*mLA(==#nNLB#({ttqS)&M_HcL;8PD-Obf{;;6G zjQr*oYe7H3mqzd{T;UK#@CRIR5JoU>BhZ2{f+yfQ0^urv6`P>GP#EC*ZEz1^Y&+-% zT$#Wpg4H{r>=3R8I6oW43Lx`1fKzgSCdfz7*F!#I16;^AL@+83*+CKx_d%ErVFat; zvVgFlPm282mIBZ@NQ)p=2(|*k2;PNj0fg%TR_%ed0AU2N{U9sg4M9)1PJ#Tr0j?_o z{|w>?j#bbYXWk*OK~UESmcWHUc@ew?ml}lY5&j6&3(zSA_zf;&s3WWx4rM$Faz=Fr zPzwB~RS<^;a02qBKqeLd1K&O!uQ?Lx6(x!V@e~*x&!ox8%w1|1alT){41sf*G(j9>8L|g}H|Y z`GqlAt{iqyXaIH?J+o(Vrm@%&L1CfTL6J!;OTYP%L2MQy)Rp7oH*abrE0V=@4G#)+ z4D*`@qan&9mJ2JG^Zg*oJc0aCVcx6CjKogL{-S3SWdU6xceF&1QvioY`T1ELf7;D8dMv&JN8SVj5++qi`D z@n=XfUGW2cpT;pLf`g~a`!!w1vq}=m&xOT7GUy;{1FM{%PHYY)$)nATLWTIv^Ah|y zNDU?7@rq5cVYuqM&VVt&R?IFsJi;2*ufrlaF026Hh{dLZ4Pay@di2Gye1!sau9JJBo}X26PVc@)xWF;z@}>4v;E3j6Khl70R@Xhza$hV@Cv@ zLd%KvVe_9H6L>zaTceekWfb)7H~BJ#R#zt4G0QlIfb)A z>6o4X#yU7G0>ui$ac*p%&8Zx|aM*yDI;4N*Z% z7Tc1^gt#-i;B3I=a9Biz!Y&@fpKGMAqfZP!eqa||*ZBSMXN2D1 zpjtdmj07-UqGJBh;q&~0Lpk~pF%jsKEz_C)gei3oMY{|zn6m7|S>YJ5ZWwE{B>`pt zJLB=v^6=Ab>>O=ec-k+lNBpLv|K|5Bv>Yg7OOz z=#emlPlpiNgJC0(LnGX$^XH)Pn9x2AIJW3QJQ+jUA#i>>4StQ_R|nc3It?D}lt*WI zFklzSICzH6{DGD04$A2w#t@zcy~`ZGJ@IKtjQ8l>gGzi^;E8XF1!(C+K` z@Ee2Yq7S*Sp@bu0;t7?N*+GQ^lu_Cc;B^+znF+iH!99968O21j4WOhZkgF$N6TX07 z2)hwkaBl|mnZWrBpy3WO3jql^!rnteNIwPgvI3kR{&W^zMjfb!PFYL?8f;und>Qah z`5%k*fq1AMcqcK2f;@u-B>bz4^np$cTrfw7sXcGeq`IUP!F0R0RK8Nxrb4-@g_M9@O=L45J+7TuvUZ+^Fbw3EMym=e%%1{z@? zyGW1^N1$ZCOZ)r!M*GYWg#Z2h-ynf13|mZsxwlf5s&eXb>T|GM z@m#rFm0Vh`PA)yyEY~8}J2x;lJa<8ETyAo1dTwSeH@7smBDX5HF1J3nF}EeREf>q9 z<>}@Hza+mjzaqaXzb?N%A1e?qkSkCrpcUv8&N((9qstW1~>I)hRS_;|^QOrBt0h%CL`KpQGFat&{WwiXlBgM3pN-CXs;!M1)|CBnk{HGLWK6 zP(|D+R0XoVmB9dde{>@*EpLyHMcK5;Fy0D;;R%avAV=?m9*Rkex%vddc#>(LN>@P` zN$I}esfRJcXx5fA#~^UoSxkdLbPe=OL|Vnt(voHk8;OJbgV5NX=E`D61;Ox{uHH|E zZVUx8pyPj@{bUTy;81AzZ(?p@?)iU~xdf7?ps=8}L=s4_(;HH9|$!)un$F`39 zYmz?1qeD8bVG{k9l=`78PEYB&wg{+X*xEhqJGo_RV|(`3+C>*_;;uQJztD<#ROTK% zrN{`RL^VEtGwT&u*1I;%S;a|v3hC+dqMSEpPjTEV&;Ix>UvkOG^W#cy(OByszBxlA;5kMzh7UZbxEk8+n- z@zB*G*yHW@qkCAPl-2?F$JTo)UEFc!-3$l%AeKn*tENE_ueOTpsvotFtE95#>k0C; zyx|3M3d3ujiEHoC+~nPsKATgsdyDJo_XUCVyF<_24nCMAyW?zg;EQSR_rHHS%xtto zeDS>Hx6h1tJwf%@A-^Y|e1|90%*8C7-~lu4a$P;Li+y#BVuSkH!(C3Vnn8OwQwqQWn`8l6=Yq}{7gVthrt>_ABLWY zC}{DYI7j*h2l>&gVcwJG8Wa!;GoLhpP}kdb{LgMA>Au(TH&?{1M9=G-)C=HU{iY4*~I;|k{Q@i`oE zc&wE4+j~aSpPext^>ovKSwA%7$0dkFJ^wbrzj}i~(ASW|#(T>aF1WwW`91Se;Rwo} z^0_bTN9N_-553*kes|Cwi?hey$n3ayXUFEZ7awXANJkwWO50U?;z!&IQuW*B1l^6{ zY3(PKuK&CtYr0$C$uON`H_zTgAzIYP$xpvz#N)H9Jjy(ky7^x##ovdl+$?u+-@Wm} z8wyp5E{c&N10IGn`T41v#s;lCQ)oLr)N{_7T{Ew2rzg=HMGjkBl#w;N8j$ar{I!J6 z`WklW#lVA!oS^Z0&j*e<_&mU9%Te+AI8g_SH?0XwyZlcPMbwS&gIPPzy3bVGHeL17 z0=vcGg&PJW^xM0l>FDJ?@oE;e%Kdb|du>x0_R{v#+tarKoOEW3zDajn_~PQns_$Q~ zjyJSZpqZvnULGqHv)d-Kezb)DW)CmjtmD$TbhEKlcBUqax1D%Xe(Q^5)Yqk=11$bo zXVP?Q;f>;0Y=1#|;e}YzHo1e!-z!5^?uYsQ%yrHySyZxJX~IvMbYz95r^3em=~$EM zt#e85p00j1=i@V;eM}(9Lyyr1?+XVGrhxXv{oY1)noz^$bz9m^G8{(gSXhPAOxJq# zTmHP82~GOdgeo8tI)oxkm!yh=OCc&MCL%%~fRHqbB3+(NrBWb>(Z#9MfOu(eIq25t zp$26vee}O!bdCABkw_FBx;9;le@`bRsSu_(%r7Fmixux?KX8*Pe|Fc~RL+)6o01^A z;yqfD(%C5iC8Oid+NLaV5z9~0m@;U&dDgtyBWOuy1R)oLjF!r&)lGgU|0lY6AkSM8j%cQ5uYooae?CE4xL zcaLwu*qfi3x%wM-NxSN5gw^%Wb=vWHuf)q)2^Uij#O0licW_x|aM$paQbVrexM1^$ z8mZ;xwEE?$fsgYX%nELp4v8*H>#uwv+0lQ=NWW{DjX{t0j?JI7{j%E3{dFHka;}#w zZM~hiy>Eu^mi@L0cVq9{e|uE!vhz9By=nWZ_oIEMo_eJvt6Sp{x2(Z-k!Kw}fvN^3 zmqM6aqub*7Yb{*+Kgl_H)BO0p&~755|F^6fxMW~WjSLMttf`SH+;+HR(|c=8_36WT z)^tcu))f3j+i5fx$RmNYTd<5gcWVuDw^G2}ipNgg_^E!(YUYAZ>3REyC%#J?bd#R6 ze9@zuSMMlK+PnP1Nz(Nvt91IZDN)1YFYJ+DXfg4ETw}bV`G7f2Vy#e?Sd$^oN+ZTu zNNi}#Z)-M}a=&#{I#qpW+r#miWoO^A-bB1%J)IE z(4o!w*0;wb`Ry3RRLjKFwScWES``M`PwcgWpKKK2@b|Z5>+Vy6Rkn zo{d51nKdOB$*m_7C`vKXF0{tXq4Ry-j%;Wne>0j<{IT5q#iLen&HYDjNX&Re+8MF( zn6=Zb_jgWDn<4SwSijcuoVXJ!?DkUc-J{6dnsVaJDhcEM8wWfOSD*}ew5H_tGNV<& zxzf#nCpJ1QwrQ*hN*fYWKIolEbeq#rdH0P~(;8LCx8B55B<%`%H(<&Jg}GwBEmiY2 z=WKG`p=Pv8{f$qmXrS8aPj9r-6Kj9YU!J$+y^P{`(_;C3(UA))U)Y(>O?SO=al`zF zVxtFrsWl9!-xlhY#AR~fQ@*&nmq7A^8ky6@oc-2bb(*ty!*!O!bgtS}oZo#>&s_eM zL%{^aU(A_2vPHN@C{Cw>M+p5V{kIlPm1ofu=zTjZniy3SSu_#)B%VRDps9|_7xG2UVJ9#anZe< zS}R3rA9`iiAF4fa{i)fl;~|wpYp*f;Hp;Q1Zhon68osiLar5w|465YgR>=y>Eo*1R z*O^9V{u5`pDZg#)1izhySKZtu#l8I0&;RCz`9F62us3+xq?$DF<{4V`go7?~4y`}E zYNJFZd(B4D$4u*-iQg2p3%8q|Wfy(Fe|nkBtJlW)vo{ViQ}z>$ywhr+uQcz;hC_PQ z9FyYAEm=R?hK)ShZ_?0U&*#UZX5USEu!mVY_hq6=@&JQ`PsxWj3@IHnQap(K)bXUF zUSE21d<^xmq**a2B+CO()r1qIR z7A6W|`*TOBE^OSVHq}o|ZyGiD^-`7Ar>~e1xgp}FsRv{??~k0PZ!HgSyW8?!gIBkLlef1x%b@e{M!+jiNaOWCEK4B z4iy)zyjE)3XHtKsD!(luXD{fw8fe~&bdFA{dfztp%{=OW@yiDf+f=c1;bu$e2OhC~ z^2_DNjW*d@zkT?se(`QL`3nbTZqJCF;gRdcYM*K))n?y(z3tgq=pSA||4>8p4|LhE zy$}2|Ig1PIO!F3uINnWV=<&${hT7fL02&N3$Knc-Q&`coiLh+XiZKAc03pQ`q<9FL zbr2`UK%K6Ju#yT&Zhn~S@v8;w{KH0ZSK_%n871USU5fPGRfOqzNR^b^c+4_S~8;7qjA{o!-^+ zo|o_4p2e8Vo+f+$@>AE$(R%9NuX!+Tp8a5c@_6=OtG!i9<*_?Ptp{m)YQQ?7POs2(57>ol~+hPKVc$R)=mM8tHzc-x%XTi#Cxq9Egp(extAC z^ktW0r`4af+m>)Z2)8^k8vB9xo)!3VgySJUzZolbD zyW}0T)kD(lqv(qt27A9$Ebn)zM7Mvo?nt|*5ALm*!sX ze{!dcVvWjqdL}1f&~@Kw#e*W`)qfQJBkr*xI$No^zk7R?x!VZsH@2#y!>-<5)^fW= z=YkV6kzDib$-{T2$T}ZG?hZ|%R%fhu*f_6G%giNBQ9 zE?Y3|qRo&;RQWB`5j#a@|47{J8t~7-uW7rOmy_4cxRP&Idii~B`;zJRwva%uD2Ve4fzpP3zL}xyGFJEF3oR zx?9}ix~-+#-yah3zalUdqPnZir$0@$+%no$XY~z}IaGOy``>na;&}4~S6-419VFq( zJA-C&D3k{`43|ZFqP|-cTC3>QtPK_D*(UW_eF(X|!*yl>I^SviazU;)_T9og*`J`dm z7Z1zp@2sB5Eom>}Xso&DCGo{*_Dimswuje;BYkCW-70@_>XyfXvkp;@Z305p6j{s3 zrZs#q^F6CTy0ARU@j~SxSNg{6+K(C2#7nMv#ga14uwCYku^P2ld4c)*^&7SxyniUf zG9=*Y<*@W|v9|280fTGLxi9l?IDEmM+WMh(((HX%!>m#Ut^1iD<~bzXH$K8|Huq%R z{o507jysS@HIpBxwPHlWcXI9cXv!&yY4MBbE8Fzb2CE#;I%hwyWmfI3i0Pt%TkHEwqP1vFYOX~4ja?gZrxcEp_#_&9teWXrr>A*t()M@evmGBgjN59Qu|;Ce z>K~sEx~#o6VP&h@uHu47jrnJ1TF+vQbB+()(^7Tw`=P@cM>l6^e;gZ=>cyUWE2(t6SRi+7WgPUyZl;)xwxwdd&<$;)oDYL3t4 z>^SYPKNI@9R_N~@^7^~JSh{)amgs>yE5d%ve>l2Mt|(l(htp_5c9a^cuK#@ z@=fQ`H?UuOjt(PP$3}SS?9^J!HL;+6-Ecs1SK?8HhDl1)*>h89#`71Q9I|i1Ov!AG zk-YrT*S4vD ze;K~y`I1S2*F1H7ZgRbb9L^aUnC0=~(};Qh%wb-drz3rL?*hw*>)y&1#D(qhw=|$Xq9mu|N?zqJ+gOf)@vPLK_(t2@ZrqYu9rXTxm ztdm(h&XgN;=ff8I-MvAzoatHcx}~&;Cmt=I->SVx9Ybr zzun6gF_2rV`fY6gxEoU8$KB`HpD|}%J9c-lo8eQtLxy$Lx=ELnX2oqhK>l{*hW>W5 z#U}N#YOe>_J8aXO>z;7u{RwfF$g_949~@F7NRGKN?{^28Yh@C? zpPsz=_2uWmraz;tHeTqav=-H>O$4>2#(p3qJ-?eQ*sPc?SiV_lAp6@^Ft@I%26FotU0t+y~#7NJ_ zx&PXOYYfdOE>lO{AA)%bc6{*mH~qghBS&;;f(t7~Xq|=MrN#flUoI9^kT*9lLF>4q zCR>iOHJf2*Hp&zqa!L`a+hd_+E0C?H7!1Vcq$#oriq7nyD6|^v#18NY4O-}f_R0!$ zk`Py(E{A$OacPNXTem(tw*AGncmqAU4#G)F4|<-Q-jsOy*@2|zaoeAz?|qTC_IXO& z^KC1eHm5&ZzW3Rh5`J8fUc~)lpSh*XZ;(w*gxHq@eszh}sY{|<+{^4g`%Lyqc*R;b z(|=J4OS4Hn$#<#YXP@=lf99OsnLql}gyy9?t3JEe7_8D9u2GU-*6K6;S`yi#ct^^Y zRVGoNSM3gcs4rxR{-TeF;Q&u`dr&~c!{fIfP;z^Hg{aihKeG)|kCKADkeu%uyHVd3eRg(JLoZDM@-@=iTXr8XrX z;LVok*Gm*NYg{Ic)jaX$!Eu(()5A`yNfmX6L^Z6^^QLUjwC4Ajwa{l)cj+^KJHOf4 zXWpNnbZ0>RmXL65IkWtk52DJF`pn->d@^_)CQNO1?J%jN-gcP(xp`7+ zOWSEMXa66cC+(!$%ihxg38gQ^l0JdkG13$l;r6HZlUA~xGU8XG6tC#Kc1Ud(0c!+)$cS-;qbnSCjK6DwoTcx>r} zlwI-?-?YU4Giwa6RCW z`-JgR+1zJ^v>5$GgK>$;{!`Zy&2>UzJ%t`_gfpB@TLu<^HXKS;NO2u$Z## zu5ZSHwvi29iw69-Jn?xIcSS~^htB;Wl#S1~N?YB{o^$N(QR7F^o6mEOmCEdrbM#vL zc(MD6%2Qt%j5oR!zS$1?&!1iKy1Zh5V}0Z%CzgHV(n0IjEhJn-;7aEaDAY8_pz@0)G7X}ukM~1lf2yeY39irkL!XI z)V57Ec@=qV{_;_+JBBPb9WV2=MmOO-wlDe6mupUQPYz9w9^kjEFKKMtwK(^wj_ap3 zh%}gOo@0=pA3#sg_anourFs9gZjLPXyAD=hj~3Sv_C)dbOi73sNDFMxqR>ZnKB5?C zQfMAKd{>E3jjd*~hA8GZ>`6SIQ)C*;{U`38iuKn6GC8vZ6%E$7C<$aNRjj>X+1mDn z%RgouA4^Tinsm5qU8dwl|J|!>E4e9e$`$(=k5QxgHhaFTrS6zM_uv`%W}8K6`t#;m zU$%T?xptJL+%ZX$9j1f#T3&UgjWL^ldh5W8tMANsEaQ}s@&=23F*{=b95>UavEPwm zsgT-$Q5qHviEn2njBOh371&DA(R^S(ccaO73#k*&+(Kl6t1jG_GRr+i?@(-vV$DG7 z+UxIL>E3MRXp4T??5HdE#?NxX=pXBqGHz<5R>+2}Ii~q^aLe7H+9{=1(jU$xub%7o zYK?;S>!h7_=L^C|j$9|}ydvwvpu$r-ca&Uxb#9kl{bR9n3ie*}ze&GK_#CD4{Sb?@ zRzYXrlCMwRU!pqNs7`y=xTkE^iRI;~@8BRLy^D+HUY6c87gNiaKK}UFqJl@w=QsP5 zZH(KgqBQvK`ct(jR}^HN3*XtxN?v1Ky*KnpU9QZd%+x^zE=Sq#gDJw0e_%8c+;%|_z!wL3~TUEb9-$b2$9i_M{akeDTvw&_;XWp%Hp zQ_DOiDb2Fnvsfnk*7-#(c_A9KRmZV{{7?&zlo4VF>Va(0~BZ? zQXMy9Bv?%7cp_>bi%*l7!%~2;iLn7}@te^3lwx2=p?}$S&*1!wNZH_JlBNqAhQ7b} z@yLgYdt;Ros~!}~j6K)1J9xXb^s+2(!>O7d5>w8O_)U9S;Qn6L>8`C+`irmkjP7_9 zEzfLSP%-K2$CwWX7}dUO%CbKB&Dv&_w~rnzaxkp)smSI%TerMO>#LSUJu>m; zp+`pRHkM7i_^FlQhhaQs%OV5mt20F6xUzpp)s~{W1ZHA zH(wYFZe`v1X8WMN?$VqOQ%4^Vf6>RWV6U2*%Xza6M@PxWPJgE%zh~X0We3=~#Vb}S z?er;^a^I1WaKF{B;>e}s8Aju;UY$5=mHW13qPjCny(L4EPP`dYmG{$4@|E_<8NOsd7Z9aNS1qZdyZFCq0Qjs)4oXdOTC~ycgDejEPaDX zt)|9T*Bp@+Gb(SWJ9l(**4AUK6E}=qS9WET!H@iNzD?iTuFu*h>ODwyO~CyJ`oq@K z3iZMcY_rZgvUw0=-<4p;S&|8lH1moT8V@Ypv+zrr=bEO-p%d38lT~Hv8C%tN%J_`7 zd{X{RgJJWmFmxf=@&3!>5~0N@@7JFTdD&-rbyfqr8P5KD^;txz<-ARb)Gd$UB9a!b zztIzPMt0h03dtZrM+0yJIy4RK$<>htlsL4n|JV2A%GAKr=30^k ztS#%tFGhQq;Z9Ji(HJj&wZO=z%WB?qpNK$M$l}1*iN8HpLCl543<(Qm3asQ!4)SA% zMTGftU^5##JdDj7ZJE(c(O^nMVPxlXIQEwr79Bxz3k+g2Y0mKKWelVV2@eZ}*Ir@0 zjAlqTFh*;7Fx_cNA7x-h2P$ait`XeAsvdqzk6-&=VHhTOcnll<%CG~x$p~*VvT$yp ze7-@!u=+^D7x^Os;cd@9ZRpLl@q6ZQW8cf#M`oNk|7r$RL^3~`Y@&3Hn{RV@neM^j+w|h?(XXPn}Zlb$Y8eG4= z@91b9ajWW<+pjH74>Y07IP~hFaow5u$Ace+EMI7?nmdGXWd?Q7)3iB78CI?<-%dC+ zx7bI2>)k<(T9@Q)uAJ7W8g%5an^tV>70Q{h6;AG|bq0eb+>6>KF?Ni)c1`ltEjyVf zH!iPK8vkTFx4rGC&f*)7roBy3v&xE#*)cV`e^ctm*4%2zw0pA$r%pG~SLq+>rZJPg zVp*(rN#*=8J{Kh|(raRGd|oR1(s9MjQEDmi{2@*yNU8$gy#xEn&}1Bci@KzUD3w2; zhcJmhVFnw`I>$SfYrZWwqM;Gnf3>N+c$H?6tlw0;P`IpB+9;q!vR!XwaLImXQ^+BeyplOPaAS?Dp0O+B37J6SG`27uGH= zm6D9^)9QO*9A~t9lJ&lo*Nc>9^pnWl>N~&S=&WNZnJ=zvW?mc>a-Op?k(8@oka;Aj zSmno^+sE1Cu79^tCkGmRzN9}i(_8!UqP5GolvKGJKLhRe+r^UmO;{KtFZCks+0obOi99C{E&`Q0e{^5gz)Yb{Rjg{A(8ZAFC zZjfE_=hG{0y_$Cabe~++v%{BGA2a7hrtd4xs3Aqn+Bj#HOB0B$_>dN@Csg40iU2!*fsRMzdX)fDIjbWIARrcwLor>hRpJjBb zKivY<@g+(s?gRiB)d9eG<|q9jvm-O{j(g!baiBkBf6$*lb(8#`^alt7_g`*Y8txzc zz=6N|#}KvE4_UP_nzn`qLQXPaVWbYExM*6VkET=3U0i8n$`FMc*-7~mu4&q!iWE{K# zA`pdqIa8v-7A^B9H=x;V{uXRRS8Kh+2 zJLBL*_+c3YCo9U4(_Q~c&4IkkYPj?UssRqXSCDY>E-ds@k4y4-3heU|sh7FJh z(oJ%NDo1ha(d!AxgyF51ar3Uj#YRM=iZC~`HZ9)U#2yMV2*qSC=m{#!OG;f`evZ)_ zEw>-gWna8D)be4bTV}h&eI7T<$*wUj-?QQ9@MVp%0?KI6=oKLejUv==k6J?H?2yrF zA=32d9G5C?5$O2CVNPZuhGZp z9ZS1s9)%rC%jz>L^_vu-{jd;ACW`t`UH5;Na#)Js;3^xZXqDMxd8Dd{)Wuy zK>NiqI6q12FLlmxU*}lyOrS>JeIXj(2Gp>^D#89h1_|J?LlpuB)DC?df2xBrfL~ic z)5_Jw&CcBgeVO0Z-D#hId^;^#Fdiu|$?VsExpp4s9Q4Tk2~ZqV_WZ(@zFz}&=zH(q z(Cv(x&oaJb+an$yZ8Z4o7GNII-CuzP?*dd6s+^&iAs;6D-Dl$XN1Z+pX;-w1rH92= zr3AVQptAs6EN1(?10Pg~4~{rcV#C9q3<&XlU7gZl8xUfNzY<~ri$fLulYP7Yu6I1p z)WnC6#b7JG4w)BbI^vu9t}+Zl!| zJ=?2uIOi6jm$Lc5N%1EBY&NoRIlwur(V>9I=H$J`4GXA~ENYizP5myg53I7-aJbyy{dekA9UU z=UilI%hRiNa~s(e_)e0>iE3uVYpLmR0E@j6?dF4gwl-9ljcfN1L$(P^#_d~vttP7I zNO!`(d{vw;W40XBUZ-_DIJ*mZnIiod-rMn}yPrg0iAK(J`L$ng6m@78rt(Jb>KIs> zBP94ITKML82e(PECta!rNkej{THKvaC&1G(cDo-yqWZ*2;AN$^s+{o8Z|{*&z_1K8 zP%J~m0kO&ZuTvrLpJkbc6bE3D8JK>+Wk&L?I+U0J4+iu3BgTAEOGFGO_dk|DY%Oe!+2Z0Zejq6N){$_8UxcbOu;71q|N) z5tCp+Is``!9~MfWdKn9n!NY>202u>Ddx43%4+JB`fCLu=^vu7ld&XluL!o!cP~pMH zG@B!J*V~k4GBtKK^~#^Ih#`q~3Hez#!2}jm%F5PXAT&UB1&8hvLnKrXF2pYgM;b!$ zg214)49v?MOb-_37U{*4@Uzv}5Aon_Ltz36BKuoEpUR#`1IQRq3O5T7gpF2W}$ zBPav=VMzWF?(Q2=_zs*Xhh@(dLf`9h8rJ#NTWgs?H5my5oTIe}1U< z36bA_OE{#DN$B1BQ4ds!7S^|hidCWBYZrH*R&MWEGf@p&dnBgQ8!NHStnbz zCSv-sGdY;O`gtNnQmP%O%M_Qwb8&aHna(wLr6t8PJZqM2gk<-OvbL?WH1?s=RzORy zM6PVV&E61^xID4T=85*_t_h@D9#_ZLuaZYLC0jdw(s0RBjRsQR9r_RimBRE~TpF=$#d}Qt#IJ$cE)$ z_Gk(6LM7H|2`?Ve@5~_%;64A|$Tfnyx>WaJeURC(wK);r+bJqBT_1tOyk@VVrRwj>g3ZT*REKZT;jD^9jX3u zcJzvJI`+nOUOX?C!GRm|T!YfHma%pQqJ5{MAD9meX9h(+>`i6r~;6H`% zERI!y_&52Xp)OP8(zvT@pABV$f5WoU^XW`*OfQ)Pug&yY){kyxA7dRdD<~I?`yg0$JPeM3VA&jM3QSA!&4334%j$o7tiyQu zD`5r>@RvvXJ)ru9+kxnJz1u;AeS!KjY;{^>;K=gL)Ei759IV&HJ!Bx@+)XzVrPf6u%4}8H9 zt{Sv%D4HyONmZY^`09gqQOtGEZu{;P4P6er6&xo`s^?&s)iwSiCKur*^1PxgS_Hi% zJjF7s(C4~G4}Tqntfd|qy<>OfZ404?*vZvk-DC2e9*R2j))@U(?ve(=HH8ZrgdlX% z`pY2pt(`L+Omisk)aG84B418RZ|7x*II;C?**teb@H$1x(XGslLvD9@rH}6u}(1;U64z`hOV-ve0 zcOwb&-POCZ@ZIk`mgMu4ELG(A%vhmcbhLy<%&#wJ9gk{!V<7NWUF@6{`zahcDF|4a zE9*Lq+DMhid44dNfOnk)rypOg8F(h_{&AtgMvlUmf_uy%k?yK+2FUA+o>VD_6m6ZU z*~`c47>m-iGJHhpc#N_JE$rj?d?za>KJK8{UY?$-w!s+OlbKL!bb9@Y;F|O5Oui{{ zWqu49wM<7)q6N(FVrI|_M~Y)oTWnppl)rQ%(&c$p*iuLbF6%MjUt8Q|pjHMyG!cicb#IjeJ!<8mE&Kelh@eF`CA zZ=s`_N5@m7-UcYpQE76zu3CcL9IhfIzY*Aw@Sz+RF0kWVFb-{FL)y* zWZvB0UNcyxGA~ljfsl5!)pI>eeY8oY17)lM6V4zx;~cRxNHKIu%TMGY*r(;r4%+S> zQ&I7TP(AXL<*S=wHOG7rB|%03Hz<#%YVKCdUphHzPx0JjZ8uWu`M|qSUDXd`wGlb5 zW1`ALcr{WMT>;dngxBr4#ccGT!+e4U$4wqm#B>Xvfu`fv*u zcfJjlos)BZ3h!?WgBoApoxW7oT|FIf^-W?iNa;~Ms*v8u#(aD;Hinr=e24Y9oBz36 zavt3+W`bkX;R8Flto{LChSOvxD$oKW{$@cnBXKNBvnT4nKrNX54nk-S8GCus6>KmrEU2s7eh4UrhDq_A%+)0 zZclBpOb59qZ%Q!E7dGQ1_Vl_@Qd`ts~(VSA__W zkP!KiQO2H~vX{qO9X538jx~JWDrnOt_th4kGDY zBwMl0e8u!tRN7IrUE5vX%1R0QY5tN!H5&ueY#68+JY+!tqQXD;4j3f*z6`uS4b16$ z&0WiuTlLtOvvNr8=ahQ_c<+Cb!C$JJ@xIEj0yR=v(Ek9z`lZbLKi2wz%zlZrQg!^l zoGzK-Ww8OlIE<#!(8^y5T^5aLDrU+2Z@jfUIP|K=(pMwJ+g5EStTu9V?|e$A;L_oI!BHob(ZEnaO-*GY z&N(TSc&7N%7I*mLTKfCiq0;4FNYkz}gwHJ>vBxxH7^E3AJ+28gGd;fO#?R~E>nzDj z>`l86Q&wm+p_uJXTKg_0s8@VeNy1v}!lSUy@MFV?Yf&39pp9p3?mHVQXcx*N4`{34 zeV-xqT@w8Bv`b>$#EMQzJrx4}SWBN;s7 z1>c4}3LJK|&{Ba7S?nYL=K}1pJmk3GW4LrUjR7B+xnpsKgL%hJcUu^}O1&o&n1Km~VuEr4 z{$Z*9X*d^{z76nR&Gv=pKu>*c_r@{z`T0+(lvf(w;%^xyexA{* z`}u(c(AmBPR~r1x(tfuDTu#THH>#h>sqk*`Xj*F4I}=dSiP9-U5_iC5-D0l-(~T5;71#LO^cBHHNF@b}ZcuE}Lfwl26Q-SNiU$%msq=*2 zSK5b{4$d}LR6E~jK|j}G<3{KE(5^mYTk4vWIO?Y!>Av)E0+}#2H|ZK3K{C6bt&9?* z+;eHPTq5KmP7fbV2|~%fMh1<=G~2D1KTcn4dH$sSVnR_ozwwf|RBX2b=bi9Lr6$^q z6r9frGn+bN^BGLsSbOYkiQ)#*@mHRHW+tjmTP8&VwL%U<%`d}k31iu8*N$_GtSXn2 z>6Lr5b(-q=Ms_NH?h0MpUNmM4ZEFmSb}Ew=y&WkaTmYXD)V82z|8iVRq#?T9K*|4u z>M>Q6GNTr2Lj7w}RE-tkCQArw0<-&(#pB69kxNqWgunL3i7C3dW2z}U){r))kMv1}iN=>*d3y^?XKZn+I Mok_m={2+h)7u^}>0RR91 literal 0 HcmV?d00001 diff --git a/common/windivert/assets_386.go b/common/windivert/assets_386.go new file mode 100644 index 0000000000..0cbf35ed5c --- /dev/null +++ b/common/windivert/assets_386.go @@ -0,0 +1,14 @@ +//go:build windows && 386 + +package windivert + +import _ "embed" + +//go:embed assets/WinDivert32.sys +var sysBytes []byte + +func assetFiles() []assetFile { + return []assetFile{{"WinDivert32.sys", sysBytes}} +} + +func driverSysName() string { return "WinDivert32.sys" } diff --git a/common/windivert/assets_amd64.go b/common/windivert/assets_amd64.go new file mode 100644 index 0000000000..2c9fb6c6ad --- /dev/null +++ b/common/windivert/assets_amd64.go @@ -0,0 +1,14 @@ +//go:build windows && amd64 + +package windivert + +import _ "embed" + +//go:embed assets/WinDivert64.sys +var sysBytes []byte + +func assetFiles() []assetFile { + return []assetFile{{"WinDivert64.sys", sysBytes}} +} + +func driverSysName() string { return "WinDivert64.sys" } diff --git a/common/windivert/assets_unsupported.go b/common/windivert/assets_unsupported.go new file mode 100644 index 0000000000..04698953fa --- /dev/null +++ b/common/windivert/assets_unsupported.go @@ -0,0 +1,7 @@ +//go:build windows && !amd64 && !386 + +package windivert + +func assetFiles() []assetFile { return nil } + +func driverSysName() string { return "" } diff --git a/common/windivert/driver_windows.go b/common/windivert/driver_windows.go new file mode 100644 index 0000000000..d6bc59f893 --- /dev/null +++ b/common/windivert/driver_windows.go @@ -0,0 +1,212 @@ +//go:build windows + +package windivert + +import ( + "errors" + "os" + "path/filepath" + "runtime" + "strconv" + "sync" + + E "github.com/sagernet/sing/common/exceptions" + + "golang.org/x/sys/windows" +) + +const ( + driverServiceName = "WinDivert" + driverDeviceName = `\\.\WinDivert` +) + +var ( + driverOnce sync.Once + driverErr error + // driverDevName is ASCII-safe and must be available before ensureDriver + // so Open can try CreateFile first and only install on FILE_NOT_FOUND. + driverDevName, _ = windows.UTF16PtrFromString(driverDeviceName) +) + +// Requires SeLoadDriverPrivilege (Administrator). Running the 386 build +// under WOW64 on a 64-bit kernel is rejected — use the amd64 build. +func ensureDriver() error { + driverOnce.Do(func() { + driverErr = installDriver() + }) + return driverErr +} + +func installDriver() error { + if runtime.GOARCH == "386" { + var isWow64 bool + err := windows.IsWow64Process(windows.CurrentProcess(), &isWow64) + if err == nil && isWow64 { + return E.New("windivert: 386 build detected running under WOW64 on a 64-bit kernel; use the amd64 build") + } + } + + dir, err := ensureExtracted() + if err != nil { + return err + } + sysPath := filepath.Join(dir, driverSysName()) + sysPathW, err := windows.UTF16PtrFromString(sysPath) + if err != nil { + return E.Cause(err, "windivert: utf16 driver path") + } + + // Serialize driver install across concurrent processes. + mutexName, _ := windows.UTF16PtrFromString("WinDivertDriverInstallMutex") + mutex, err := windows.CreateMutex(nil, false, mutexName) + if err != nil { + return E.Cause(err, "windivert: create install mutex") + } + defer windows.CloseHandle(mutex) + _, err = windows.WaitForSingleObject(mutex, windows.INFINITE) + if err != nil { + return E.Cause(err, "windivert: wait install mutex") + } + defer windows.ReleaseMutex(mutex) + + manager, err := windows.OpenSCManager(nil, nil, windows.SC_MANAGER_ALL_ACCESS) + if err != nil { + return E.Cause(err, "windivert: open SCM") + } + defer windows.CloseServiceHandle(manager) + + serviceNameW, _ := windows.UTF16PtrFromString(driverServiceName) + service, err := windows.OpenService(manager, serviceNameW, windows.SERVICE_ALL_ACCESS) + if err != nil { + service, err = windows.CreateService( + manager, + serviceNameW, + serviceNameW, + windows.SERVICE_ALL_ACCESS, + windows.SERVICE_KERNEL_DRIVER, + windows.SERVICE_DEMAND_START, + windows.SERVICE_ERROR_NORMAL, + sysPathW, + nil, nil, nil, nil, nil, + ) + if err != nil { + if errors.Is(err, windows.ERROR_SERVICE_EXISTS) { + service, err = windows.OpenService(manager, serviceNameW, windows.SERVICE_ALL_ACCESS) + } + if err != nil { + return wrapDriverInstallError(err) + } + } + } + defer windows.CloseServiceHandle(service) + + err = windows.StartService(service, 0, nil) + if err != nil && errors.Is(err, windows.ERROR_SERVICE_DISABLED) { + // A prior process called DeleteService on a still-running kernel + // driver: SCM marks the record for deletion and flips START_TYPE + // to DISABLED until the last handle closes. Re-enable so we can + // start it instead of waiting for a reboot. + err = windows.ChangeServiceConfig( + service, + windows.SERVICE_NO_CHANGE, + windows.SERVICE_DEMAND_START, + windows.SERVICE_NO_CHANGE, + nil, nil, nil, nil, nil, nil, nil, + ) + if err != nil { + return E.Cause(err, "windivert: re-enable disabled service") + } + err = windows.StartService(service, 0, nil) + } + if err == nil { + // Mark for deletion so the driver unregisters when the last handle + // closes or on next reboot. Matches the upstream DLL's behavior: + // only the process that actually started the service takes on the + // cleanup responsibility. If another process already started it, + // we leave DeleteService to them. + _ = windows.DeleteService(service) + } else if !errors.Is(err, windows.ERROR_SERVICE_ALREADY_RUNNING) { + return E.Cause(err, "windivert: start service") + } + return nil +} + +func wrapDriverInstallError(err error) error { + if errors.Is(err, windows.ERROR_ACCESS_DENIED) { + return E.Cause(err, "windivert: installing the kernel driver requires Administrator privileges") + } + return E.Cause(err, "windivert: create service") +} + +type assetFile struct { + name string + data []byte +} + +var ( + extractOnce sync.Once + extractErr error + extractDir string +) + +// The on-disk copy is protected by Windows Authenticode signature +// enforcement, which rejects any tampered .sys at StartService time. +func ensureExtracted() (string, error) { + extractOnce.Do(func() { + extractDir, extractErr = extractImpl() + }) + return extractDir, extractErr +} + +func extractImpl() (string, error) { + files := assetFiles() + if len(files) == 0 { + return "", E.New("windivert: unsupported architecture ", runtime.GOARCH) + } + + base, err := os.UserCacheDir() + if err != nil { + return "", E.Cause(err, "windivert: locate user cache dir") + } + dir := filepath.Join(base, "sing-box", "windivert", "v"+AssetVersion) + err = os.MkdirAll(dir, 0o755) + if err != nil { + return "", E.Cause(err, "windivert: mkdir ", dir) + } + + for _, asset := range files { + err = ensureAsset(dir, asset) + if err != nil { + return "", err + } + } + return dir, nil +} + +// Concurrent sing-box processes race on os.Rename (atomic on NTFS); +// whichever wins creates the final file. Writers that lose the race +// silently discard their temp copy. +func ensureAsset(dir string, asset assetFile) error { + target := filepath.Join(dir, asset.name) + _, err := os.Stat(target) + if err == nil { + return nil + } + if !os.IsNotExist(err) { + return E.Cause(err, "windivert: stat ", asset.name) + } + tmp := target + ".tmp-" + strconv.Itoa(os.Getpid()) + err = os.WriteFile(tmp, asset.data, 0o644) + if err != nil { + return E.Cause(err, "windivert: write ", asset.name) + } + err = os.Rename(tmp, target) + if err != nil { + os.Remove(tmp) + if _, statErr := os.Stat(target); statErr == nil { + return nil + } + return E.Cause(err, "windivert: rename ", asset.name) + } + return nil +} diff --git a/common/windivert/filter.go b/common/windivert/filter.go new file mode 100644 index 0000000000..5c8fb5adcd --- /dev/null +++ b/common/windivert/filter.go @@ -0,0 +1,182 @@ +package windivert + +import ( + "encoding/binary" + "net/netip" + + E "github.com/sagernet/sing/common/exceptions" +) + +// WINDIVERT_FILTER VM instruction layout (24 bytes, #pragma pack(1)): +// +// word 0 (LE): field:11 | test:5 | success:16 +// word 1 (LE): failure:16 | neg:1 | reserved:15 +// words 2..5: arg[4] (native-endian uint32 each) +// +// The driver walks this as a decision tree: evaluate the test at inst i; +// on success jump to success; on failure jump to failure. Continuations +// 0x7FFE and 0x7FFF are ACCEPT and REJECT terminals. +const ( + filterInstBytes = 24 + filterMaxInsts = 256 + + fieldZero = 0 + fieldOutbound = 2 + fieldIP = 5 + fieldIPv6 = 6 + fieldTCP = 8 + fieldIPSrcAddr = 21 + fieldIPDstAddr = 22 + fieldIPv6SrcAddr = 28 + fieldIPv6DstAddr = 29 + fieldTCPSrcPort = 38 + fieldTCPDstPort = 39 + + testEQ = 0 + + resultAccept uint16 = 0x7FFE + resultReject uint16 = 0x7FFF +) + +// Filter flags passed to IOCTL_WINDIVERT_STARTUP alongside the compiled +// filter. These tell the driver what *kinds* of packets the filter might +// match, used as a kernel-side fast-reject. +const ( + filterFlagOutbound uint64 = 0x0020 + filterFlagIP uint64 = 0x0040 + filterFlagIPv6 uint64 = 0x0080 +) + +type filterInst struct { + field uint16 // 11 bits used + test uint8 // 5 bits used + success uint16 + failure uint16 + neg bool + arg [4]uint32 +} + +// Filter is a typed specification of packets to capture. It replaces +// WinDivert's filter string language. +// +// Zero value = "reject all" (match nothing), suitable for send-only handles. +type Filter struct { + insts []filterInst + flags uint64 // filter flags for STARTUP ioctl +} + +// reject returns a filter that matches no packet. The empty insts slice +// is encoded as a single rejecting instruction by encode(). +func reject() *Filter { + return &Filter{} +} + +// OutboundTCP returns a filter matching outbound TCP packets on the given +// 5-tuple. Both addresses must share an address family (IPv4 or IPv6). +func OutboundTCP(src, dst netip.AddrPort) (*Filter, error) { + if !src.IsValid() || !dst.IsValid() { + return nil, E.New("windivert: filter: invalid address port") + } + if src.Addr().Is4() != dst.Addr().Is4() { + return nil, E.New("windivert: filter: mixed IPv4/IPv6") + } + f := &Filter{ + flags: filterFlagOutbound, + } + // Insts chain as AND: each test's failure = REJECT, success = next inst. + // The final inst's success = ACCEPT. + f.add(fieldOutbound, testEQ, argUint32(1)) + if src.Addr().Is4() { + f.flags |= filterFlagIP + f.add(fieldIP, testEQ, argUint32(1)) + f.add(fieldTCP, testEQ, argUint32(1)) + f.add(fieldIPSrcAddr, testEQ, argIPv4(src.Addr())) + f.add(fieldIPDstAddr, testEQ, argIPv4(dst.Addr())) + } else { + f.flags |= filterFlagIPv6 + f.add(fieldIPv6, testEQ, argUint32(1)) + f.add(fieldTCP, testEQ, argUint32(1)) + f.add(fieldIPv6SrcAddr, testEQ, argIPv6(src.Addr())) + f.add(fieldIPv6DstAddr, testEQ, argIPv6(dst.Addr())) + } + f.add(fieldTCPSrcPort, testEQ, argUint32(uint32(src.Port()))) + f.add(fieldTCPDstPort, testEQ, argUint32(uint32(dst.Port()))) + return f, nil +} + +func (f *Filter) add(field uint16, test uint8, arg [4]uint32) { + f.insts = append(f.insts, filterInst{field: field, test: test, arg: arg}) +} + +func argUint32(v uint32) [4]uint32 { return [4]uint32{v, 0, 0, 0} } + +// argIPv4 encodes an IPv4 address for IP_SRCADDR/IP_DSTADDR. The driver +// compares against an IPv4-mapped-IPv6 form: {host_order_u32, 0x0000FFFF, +// 0, 0} (see sys/windivert.c windivert_get_ipv4_addr and the IPv4_SRCADDR +// val-word construction). Omitting the 0x0000FFFF marker causes the EQ +// test to fail for every packet. +func argIPv4(addr netip.Addr) [4]uint32 { + b := addr.As4() + return [4]uint32{binary.BigEndian.Uint32(b[:]), 0x0000FFFF, 0, 0} +} + +// argIPv6 encodes an IPv6 address for IPV6_SRCADDR/IPV6_DSTADDR. The +// driver stores the address as four host-order uint32s in REVERSED word +// order: val[0]=low (bytes 12..15), val[3]=high (bytes 0..3). See +// sys/windivert.c windivert_outbound_network_v6_classify val-word +// construction. +func argIPv6(addr netip.Addr) [4]uint32 { + b := addr.As16() + return [4]uint32{ + binary.BigEndian.Uint32(b[12:16]), + binary.BigEndian.Uint32(b[8:12]), + binary.BigEndian.Uint32(b[4:8]), + binary.BigEndian.Uint32(b[0:4]), + } +} + +// encode serializes the Filter to the on-wire WINDIVERT_FILTER[] format +// plus the filter_flags for STARTUP ioctl. +func (f *Filter) encode() ([]byte, uint64, error) { + if len(f.insts) == 0 { + // "Reject all" — one instruction, ZERO == 0 is always true, but we + // invert by setting both success and failure to REJECT. + return encodeInst(filterInst{ + field: fieldZero, + test: testEQ, + success: resultReject, + failure: resultReject, + }), 0, nil + } + if len(f.insts) > filterMaxInsts-1 { + return nil, 0, E.New("windivert: filter too long") + } + buf := make([]byte, 0, filterInstBytes*len(f.insts)) + for i, inst := range f.insts { + if i == len(f.insts)-1 { + inst.success = resultAccept + } else { + inst.success = uint16(i + 1) + } + inst.failure = resultReject + buf = append(buf, encodeInst(inst)...) + } + return buf, f.flags, nil +} + +func encodeInst(inst filterInst) []byte { + out := make([]byte, filterInstBytes) + word0 := uint32(inst.field&0x7FF) | uint32(inst.test&0x1F)<<11 | + uint32(inst.success)<<16 + word1 := uint32(inst.failure) + if inst.neg { + word1 |= 1 << 16 + } + binary.LittleEndian.PutUint32(out[0:4], word0) + binary.LittleEndian.PutUint32(out[4:8], word1) + binary.LittleEndian.PutUint32(out[8:12], inst.arg[0]) + binary.LittleEndian.PutUint32(out[12:16], inst.arg[1]) + binary.LittleEndian.PutUint32(out[16:20], inst.arg[2]) + binary.LittleEndian.PutUint32(out[20:24], inst.arg[3]) + return out +} diff --git a/common/windivert/filter_test.go b/common/windivert/filter_test.go new file mode 100644 index 0000000000..babac3e86a --- /dev/null +++ b/common/windivert/filter_test.go @@ -0,0 +1,140 @@ +package windivert + +import ( + "encoding/binary" + "net/netip" + "testing" +) + +func TestRejectFilter(t *testing.T) { + t.Parallel() + bin, flags, err := reject().encode() + if err != nil { + t.Fatal(err) + } + if len(bin) != filterInstBytes { + t.Fatalf("reject filter len: got %d, want %d", len(bin), filterInstBytes) + } + if flags != 0 { + t.Fatalf("reject filter flags: got %x, want 0", flags) + } + // word0: field=ZERO=0, test=EQ=0, success=REJECT=0x7FFF + word0 := binary.LittleEndian.Uint32(bin[0:4]) + if word0 != uint32(resultReject)<<16 { + t.Fatalf("reject word0 = %08x", word0) + } + // word1: failure=REJECT + word1 := binary.LittleEndian.Uint32(bin[4:8]) + if word1 != uint32(resultReject) { + t.Fatalf("reject word1 = %08x", word1) + } +} + +func TestOutboundTCPFilterIPv4(t *testing.T) { + t.Parallel() + src := netip.MustParseAddrPort("10.1.2.3:54321") + dst := netip.MustParseAddrPort("1.2.3.4:443") + f, err := OutboundTCP(src, dst) + if err != nil { + t.Fatal(err) + } + bin, flags, err := f.encode() + if err != nil { + t.Fatal(err) + } + if want := filterFlagOutbound | filterFlagIP; flags != want { + t.Fatalf("flags: got %x, want %x", flags, want) + } + // 7 instructions: OUTBOUND, IP, TCP, IP_SRCADDR, IP_DSTADDR, TCP_SRCPORT, TCP_DSTPORT + const wantInsts = 7 + if len(bin) != wantInsts*filterInstBytes { + t.Fatalf("instruction count: got %d, want %d", len(bin)/filterInstBytes, wantInsts) + } + + // Inst 0: OUTBOUND == 1, success=1, failure=REJECT + checkInst(t, bin[0*filterInstBytes:], 0, fieldOutbound, testEQ, 1, resultReject, 1) + // Inst 1: IP == 1, success=2 + checkInst(t, bin[1*filterInstBytes:], 1, fieldIP, testEQ, 2, resultReject, 1) + // Inst 2: TCP == 1, success=3 + checkInst(t, bin[2*filterInstBytes:], 2, fieldTCP, testEQ, 3, resultReject, 1) + // Inst 3: IP_SRCADDR == 10.1.2.3 (host-order uint32 = 0x0A010203, arg[1]=0x0000FFFF marker) + checkInst(t, bin[3*filterInstBytes:], 3, fieldIPSrcAddr, testEQ, 4, resultReject, 0x0A010203) + checkArg1(t, bin[3*filterInstBytes:], 3, 0x0000FFFF) + // Inst 4: IP_DSTADDR == 1.2.3.4 + checkInst(t, bin[4*filterInstBytes:], 4, fieldIPDstAddr, testEQ, 5, resultReject, 0x01020304) + checkArg1(t, bin[4*filterInstBytes:], 4, 0x0000FFFF) + // Inst 5: TCP_SRCPORT == 54321 + checkInst(t, bin[5*filterInstBytes:], 5, fieldTCPSrcPort, testEQ, 6, resultReject, 54321) + // Last inst 6: TCP_DSTPORT == 443, success=ACCEPT + checkInst(t, bin[6*filterInstBytes:], 6, fieldTCPDstPort, testEQ, resultAccept, resultReject, 443) +} + +func TestOutboundTCPFilterIPv6(t *testing.T) { + t.Parallel() + src := netip.MustParseAddrPort("[2001:db8::1]:54321") + dst := netip.MustParseAddrPort("[2001:db8::2]:443") + f, err := OutboundTCP(src, dst) + if err != nil { + t.Fatal(err) + } + bin, flags, err := f.encode() + if err != nil { + t.Fatal(err) + } + if want := filterFlagOutbound | filterFlagIPv6; flags != want { + t.Fatalf("flags: got %x, want %x", flags, want) + } + // Inst 3: IPv6_SRCADDR. The driver stores the address in reversed + // word order: arg[0]=low (bytes 12..15)=1, arg[3]=high (bytes 0..3)=0x20010db8. + off := 3 * filterInstBytes + a0 := binary.LittleEndian.Uint32(bin[off+8:]) + a1 := binary.LittleEndian.Uint32(bin[off+12:]) + a2 := binary.LittleEndian.Uint32(bin[off+16:]) + a3 := binary.LittleEndian.Uint32(bin[off+20:]) + if a0 != 1 || a1 != 0 || a2 != 0 || a3 != 0x20010db8 { + t.Fatalf("ipv6 src arg=[%08x %08x %08x %08x], want [1 0 0 0x20010db8]", a0, a1, a2, a3) + } +} + +func TestOutboundTCPFilterMixedFamily(t *testing.T) { + t.Parallel() + src := netip.MustParseAddrPort("10.0.0.1:1234") + dst := netip.MustParseAddrPort("[2001:db8::1]:443") + if _, err := OutboundTCP(src, dst); err == nil { + t.Fatal("expected error for mixed families") + } +} + +func checkArg1(t *testing.T, raw []byte, idx int, arg1 uint32) { + t.Helper() + got := binary.LittleEndian.Uint32(raw[12:16]) + if got != arg1 { + t.Errorf("inst %d arg[1]: got %08x, want %08x", idx, got, arg1) + } +} + +func checkInst(t *testing.T, raw []byte, idx int, field uint16, test uint8, success, failure uint16, arg0 uint32) { + t.Helper() + word0 := binary.LittleEndian.Uint32(raw[0:4]) + word1 := binary.LittleEndian.Uint32(raw[4:8]) + a0 := binary.LittleEndian.Uint32(raw[8:12]) + gotField := uint16(word0 & 0x7FF) + gotTest := uint8((word0 >> 11) & 0x1F) + gotSuccess := uint16(word0 >> 16) + gotFailure := uint16(word1 & 0xFFFF) + if gotField != field { + t.Errorf("inst %d field: got %d, want %d", idx, gotField, field) + } + if gotTest != test { + t.Errorf("inst %d test: got %d, want %d", idx, gotTest, test) + } + if gotSuccess != success { + t.Errorf("inst %d success: got %d, want %d", idx, gotSuccess, success) + } + if gotFailure != failure { + t.Errorf("inst %d failure: got %d, want %d", idx, gotFailure, failure) + } + if a0 != arg0 { + t.Errorf("inst %d arg[0]: got %08x, want %08x", idx, a0, arg0) + } +} diff --git a/common/windivert/handle_windows.go b/common/windivert/handle_windows.go new file mode 100644 index 0000000000..e7f5ae6736 --- /dev/null +++ b/common/windivert/handle_windows.go @@ -0,0 +1,320 @@ +//go:build windows + +package windivert + +import ( + "encoding/binary" + "errors" + "runtime" + "sync" + "unsafe" + + E "github.com/sagernet/sing/common/exceptions" + + "golang.org/x/sys/windows" +) + +// Handle owns a WinDivert kernel device handle plus a private event for +// overlapped I/O. Methods on *Handle are not safe for concurrent use +// across goroutines (there is a single shared event per Handle). +// +// addr is a per-Handle Address buffer the IOCTL struct embeds a pointer +// to. It lives on the heap (as a field of a heap-allocated Handle) so +// the pointer value stored as bytes in the ioctl buffer remains valid +// across stack growth between buildIoctl* and the DeviceIoControl +// syscall — stack-local Address values are not safe for this pattern +// because Go's escape analysis does not see the pointer through the +// unsafe.Pointer → uintptr → bytes conversion. +type Handle struct { + device windows.Handle + event windows.Handle + closing sync.Once + closeErr error + addr Address +} + +// Filter may be nil for "reject all", suitable for send-only handles. +// Requires Administrator on first call per process (installs the kernel +// driver via SCM); subsequent calls reuse the running driver. +func Open(filter *Filter, layer Layer, priority int16, flags Flag) (*Handle, error) { + err := validateOpenArgs(layer, priority, flags) + if err != nil { + return nil, err + } + if filter == nil { + filter = reject() + } + filterBin, filterFlags, err := filter.encode() + if err != nil { + return nil, err + } + device, err := openDevice() + if err != nil { + if !errors.Is(err, windows.ERROR_FILE_NOT_FOUND) && + !errors.Is(err, windows.ERROR_PATH_NOT_FOUND) { + if errors.Is(err, windows.ERROR_ACCESS_DENIED) { + return nil, E.Cause(err, "windivert: open device (administrator required)") + } + return nil, E.Cause(err, "windivert: open device") + } + // Device node missing: kernel driver not loaded. Install + retry. + // Matches WinDivertOpen's lazy-install path; avoids racing StartService + // against a still-loaded driver whose SCM record is marked for deletion. + err = ensureDriver() + if err != nil { + return nil, err + } + device, err = openDevice() + if err != nil { + if errors.Is(err, windows.ERROR_ACCESS_DENIED) { + return nil, E.Cause(err, "windivert: open device (administrator required)") + } + return nil, E.Cause(err, "windivert: open device") + } + } + event, err := windows.CreateEvent(nil, 1, 0, nil) // manual reset, unsignaled + if err != nil { + windows.CloseHandle(device) + return nil, E.Cause(err, "windivert: create event") + } + h := &Handle{device: device, event: event} + + err = h.initialize(layer, priority, flags) + if err != nil { + h.Close() + return nil, err + } + err = h.startup(filterBin, filterFlags) + if err != nil { + h.Close() + return nil, err + } + return h, nil +} + +func openDevice() (windows.Handle, error) { + return windows.CreateFile( + driverDevName, + windows.GENERIC_READ|windows.GENERIC_WRITE, + 0, nil, + windows.OPEN_EXISTING, + windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_OVERLAPPED, + 0, + ) +} + +func validateOpenArgs(layer Layer, priority int16, flags Flag) error { + if layer != LayerNetwork { + return E.New("windivert: invalid layer ", uint32(layer)) + } + if priority < PriorityLowest || priority > PriorityHighest { + return E.New("windivert: priority out of range") + } + if flags&^FlagSendOnly != 0 { + return E.New("windivert: unknown flag bits") + } + return nil +} + +func (h *Handle) initialize(layer Layer, priority int16, flags Flag) error { + in := buildIoctlInitialize(layer, priority, flags) + // WINDIVERT_VERSION is a 64-byte packed struct; only the first 20 + // bytes (magic, major, minor, bits) carry data, the rest is reserved. + var outBuf [versionStructSize]byte + binary.LittleEndian.PutUint64(outBuf[0:8], magicDLL) + binary.LittleEndian.PutUint32(outBuf[8:12], versionMajor) + binary.LittleEndian.PutUint32(outBuf[12:16], versionMinor) + binary.LittleEndian.PutUint32(outBuf[16:20], uint32(unsafe.Sizeof(uintptr(0))*8)) + _, err := doIoctl(h.device, ioctlInitialize, in[:], outBuf[:], h.event) + if err != nil { + return E.Cause(err, "windivert: initialize ioctl") + } + gotMagic := binary.LittleEndian.Uint64(outBuf[0:8]) + if gotMagic != magicSYS { + return E.New("windivert: driver magic mismatch (got ", gotMagic, ")") + } + gotMajor := binary.LittleEndian.Uint32(outBuf[8:12]) + if gotMajor < versionMajor { + gotMinor := binary.LittleEndian.Uint32(outBuf[12:16]) + return E.New("windivert: driver version too old: ", gotMajor, ".", gotMinor) + } + return nil +} + +func (h *Handle) startup(filterBin []byte, filterFlags uint64) error { + in := buildIoctlStartup(filterFlags) + _, err := doIoctl(h.device, ioctlStartup, in[:], filterBin, h.event) + if err != nil { + return E.Cause(err, "windivert: startup ioctl") + } + return nil +} + +// If the handle is closed mid-Recv the error wraps ERROR_OPERATION_ABORTED. +func (h *Handle) Recv(buf []byte) (int, Address, error) { + if len(buf) == 0 { + return 0, Address{}, E.New("windivert: recv: zero-length buffer") + } + h.addr = Address{} + in := buildIoctlRecv(&h.addr) + n, err := doIoctl(h.device, ioctlRecv, in[:], buf, h.event) + runtime.KeepAlive(h) + if err != nil { + return 0, Address{}, err + } + return int(n), h.addr, nil +} + +// The address's Outbound flag controls whether the packet is sent toward +// the wire (outbound=true) or delivered up the stack (outbound=false). +// IfIdx and SubIfIdx can stay zero — the driver uses the routing table +// when IfIdx=0. +func (h *Handle) Send(packet []byte, addr *Address) (int, error) { + if len(packet) == 0 { + return 0, E.New("windivert: send: empty packet") + } + if addr == nil { + return 0, E.New("windivert: send: nil address") + } + h.addr = *addr + in := buildIoctlSend(&h.addr) + n, err := doIoctl(h.device, ioctlSend, in[:], packet, h.event) + runtime.KeepAlive(h) + if err != nil { + return 0, err + } + return int(n), nil +} + +// Idempotent. Aborts any in-flight I/O on the handle. +func (h *Handle) Close() error { + h.closing.Do(func() { + var errs []error + if h.device != 0 { + err := windows.CloseHandle(h.device) + if err != nil { + errs = append(errs, err) + } + h.device = 0 + } + if h.event != 0 { + err := windows.CloseHandle(h.event) + if err != nil { + errs = append(errs, err) + } + h.event = 0 + } + h.closeErr = E.Errors(errs...) + }) + return h.closeErr +} + +// IOCTL codes from windivert_device.h. CTL_CODE macro layout: +// +// (DeviceType << 16) | (Access << 14) | (Function << 2) | Method +const ( + fileDeviceNetwork uint32 = 0x12 + accessReadWrite uint32 = 3 // FILE_READ_DATA | FILE_WRITE_DATA + accessRead uint32 = 1 + + methodInDirect uint32 = 1 + methodOutDirect uint32 = 2 +) + +func ctlCode(deviceType, access, function, method uint32) uint32 { + return (deviceType << 16) | (access << 14) | (function << 2) | method +} + +var ( + ioctlInitialize = ctlCode(fileDeviceNetwork, accessReadWrite, 0x921, methodOutDirect) + ioctlStartup = ctlCode(fileDeviceNetwork, accessReadWrite, 0x922, methodInDirect) + ioctlRecv = ctlCode(fileDeviceNetwork, accessRead, 0x923, methodOutDirect) + ioctlSend = ctlCode(fileDeviceNetwork, accessReadWrite, 0x924, methodInDirect) +) + +// Magic numbers exchanged during INITIALIZE. DLL sends magicDLL in the +// version struct; driver returns magicSYS on success. +const ( + magicDLL uint64 = 0x4C4C447669645724 // "$WdivDLL" in LE bytes + magicSYS uint64 = 0x5359537669645723 // "#WdivSYS" in LE bytes +) + +const ( + versionMajor uint32 = 2 + versionMinor uint32 = 2 +) + +// Size of the WINDIVERT_IOCTL union on wire (packed). +const ioctlSize = 16 + +// Size of WINDIVERT_VERSION on wire (packed). Only the first 20 bytes +// carry data; the rest is reserved zero padding. +const versionStructSize = 64 + +// doIoctl performs a single synchronous (blocking) overlapped +// DeviceIoControl. The handle is opened with FILE_FLAG_OVERLAPPED so +// DeviceIoControl returns ERROR_IO_PENDING; we then wait for completion +// via GetOverlappedResult. Event is passed in so callers can reuse it +// across calls on the same handle (avoids per-call CreateEvent). +func doIoctl(handle windows.Handle, code uint32, in []byte, out []byte, event windows.Handle) (uint32, error) { + var overlapped windows.Overlapped + overlapped.HEvent = event + _ = windows.ResetEvent(event) + + var inPtr *byte + var inLen uint32 + if len(in) > 0 { + inPtr = &in[0] + inLen = uint32(len(in)) + } + var outPtr *byte + var outLen uint32 + if len(out) > 0 { + outPtr = &out[0] + outLen = uint32(len(out)) + } + var returned uint32 + err := windows.DeviceIoControl(handle, code, inPtr, inLen, outPtr, outLen, &returned, &overlapped) + if err != nil && !errors.Is(err, windows.ERROR_IO_PENDING) { + return 0, err + } + err = windows.GetOverlappedResult(handle, &overlapped, &returned, true) + if err != nil { + return 0, err + } + return returned, nil +} + +func buildIoctlInitialize(layer Layer, priority int16, flags Flag) [ioctlSize]byte { + var buf [ioctlSize]byte + binary.LittleEndian.PutUint32(buf[0:4], uint32(layer)) + // The driver expects priority + WINDIVERT_PRIORITY_HIGHEST (30000) so + // the low range maps to non-negative integers. + binary.LittleEndian.PutUint32(buf[4:8], uint32(int32(priority)+int32(PriorityHighest))) + binary.LittleEndian.PutUint64(buf[8:16], uint64(flags)) + return buf +} + +func buildIoctlStartup(filterFlags uint64) [ioctlSize]byte { + var buf [ioctlSize]byte + binary.LittleEndian.PutUint64(buf[0:8], filterFlags) + return buf +} + +// buildIoctlRecv packs a user-space pointer to a WINDIVERT_ADDRESS into +// the ioctl struct. The driver dereferences it to write the address for +// the received packet. Caller must keep the Address alive via +// runtime.KeepAlive. +func buildIoctlRecv(addr *Address) [ioctlSize]byte { + var buf [ioctlSize]byte + binary.LittleEndian.PutUint64(buf[0:8], uint64(uintptr(unsafe.Pointer(addr)))) + binary.LittleEndian.PutUint64(buf[8:16], 0) + return buf +} + +func buildIoctlSend(addr *Address) [ioctlSize]byte { + var buf [ioctlSize]byte + binary.LittleEndian.PutUint64(buf[0:8], uint64(uintptr(unsafe.Pointer(addr)))) + binary.LittleEndian.PutUint64(buf[8:16], uint64(unsafe.Sizeof(Address{}))) + return buf +} diff --git a/common/windivert/handle_windows_test.go b/common/windivert/handle_windows_test.go new file mode 100644 index 0000000000..dd05ce7b0c --- /dev/null +++ b/common/windivert/handle_windows_test.go @@ -0,0 +1,106 @@ +//go:build windows + +package windivert + +import ( + "encoding/binary" + "testing" + "unsafe" + + "github.com/stretchr/testify/require" +) + +// CTL_CODE macro from Windows DDK: +// +// (DeviceType<<16) | (Access<<14) | (Function<<2) | Method +func TestCtlCodeMatchesDDK(t *testing.T) { + t.Parallel() + // FILE_DEVICE_NETWORK=0x12, FILE_READ_DATA|FILE_WRITE_DATA=3, METHOD_OUT_DIRECT=2 + require.Equal(t, uint32(0x12E486), ctlCode(0x12, 3, 0x921, 2)) + // FILE_READ_DATA=1, METHOD_OUT_DIRECT=2 + require.Equal(t, uint32(0x12648E), ctlCode(0x12, 1, 0x923, 2)) +} + +// Baked-in against windivert_device.h @ v2.2.2. A mismatch here means the +// kernel will reject every ioctl with ERROR_INVALID_FUNCTION. +func TestIoctlCodesMatchUpstream(t *testing.T) { + t.Parallel() + require.Equal(t, uint32(0x12E486), ioctlInitialize) + require.Equal(t, uint32(0x12E489), ioctlStartup) + require.Equal(t, uint32(0x12648E), ioctlRecv) + require.Equal(t, uint32(0x12E491), ioctlSend) +} + +func TestBuildIoctlInitialize(t *testing.T) { + t.Parallel() + buf := buildIoctlInitialize(LayerNetwork, 100, FlagSendOnly) + require.Equal(t, uint32(LayerNetwork), binary.LittleEndian.Uint32(buf[0:4])) + // Driver expects priority+PriorityHighest(30000) so the range is non-negative. + require.Equal(t, uint32(30100), binary.LittleEndian.Uint32(buf[4:8])) + require.Equal(t, uint64(FlagSendOnly), binary.LittleEndian.Uint64(buf[8:16])) +} + +func TestBuildIoctlInitializePriorityRange(t *testing.T) { + t.Parallel() + lowest := buildIoctlInitialize(LayerNetwork, PriorityLowest, 0) + require.Equal(t, uint32(0), binary.LittleEndian.Uint32(lowest[4:8])) + highest := buildIoctlInitialize(LayerNetwork, PriorityHighest, 0) + require.Equal(t, uint32(60000), binary.LittleEndian.Uint32(highest[4:8])) + zero := buildIoctlInitialize(LayerNetwork, 0, 0) + require.Equal(t, uint32(30000), binary.LittleEndian.Uint32(zero[4:8])) +} + +func TestBuildIoctlStartup(t *testing.T) { + t.Parallel() + flags := filterFlagOutbound | filterFlagIP + buf := buildIoctlStartup(flags) + require.Equal(t, flags, binary.LittleEndian.Uint64(buf[0:8])) + // The second quad-word is unused for STARTUP. + require.Equal(t, uint64(0), binary.LittleEndian.Uint64(buf[8:16])) +} + +func TestBuildIoctlRecvEmbedsAddressPointer(t *testing.T) { + t.Parallel() + addr := &Address{Timestamp: 0xCAFEBABE} + buf := buildIoctlRecv(addr) + require.Equal(t, uint64(uintptr(unsafe.Pointer(addr))), + binary.LittleEndian.Uint64(buf[0:8])) + // RECV does not carry an address length; driver writes full Address back. + require.Equal(t, uint64(0), binary.LittleEndian.Uint64(buf[8:16])) +} + +func TestBuildIoctlSendEmbedsAddressPointerAndSize(t *testing.T) { + t.Parallel() + addr := &Address{} + buf := buildIoctlSend(addr) + require.Equal(t, uint64(uintptr(unsafe.Pointer(addr))), + binary.LittleEndian.Uint64(buf[0:8])) + require.Equal(t, uint64(unsafe.Sizeof(Address{})), + binary.LittleEndian.Uint64(buf[8:16])) + require.Equal(t, uint64(80), binary.LittleEndian.Uint64(buf[8:16])) +} + +func TestValidateOpenArgsLayer(t *testing.T) { + t.Parallel() + require.NoError(t, validateOpenArgs(LayerNetwork, 0, 0)) + require.Error(t, validateOpenArgs(Layer(1), 0, 0)) + require.Error(t, validateOpenArgs(Layer(42), 0, 0)) +} + +func TestValidateOpenArgsPriorityBounds(t *testing.T) { + t.Parallel() + require.NoError(t, validateOpenArgs(LayerNetwork, PriorityHighest, 0)) + require.NoError(t, validateOpenArgs(LayerNetwork, PriorityLowest, 0)) + require.NoError(t, validateOpenArgs(LayerNetwork, 0, 0)) + require.Error(t, validateOpenArgs(LayerNetwork, PriorityHighest+1, 0)) + require.Error(t, validateOpenArgs(LayerNetwork, PriorityLowest-1, 0)) +} + +func TestValidateOpenArgsFlags(t *testing.T) { + t.Parallel() + require.NoError(t, validateOpenArgs(LayerNetwork, 0, 0)) + require.NoError(t, validateOpenArgs(LayerNetwork, 0, FlagSendOnly)) + // Unknown flag bits must be rejected to surface caller mistakes early. + require.Error(t, validateOpenArgs(LayerNetwork, 0, Flag(0x10))) + require.Error(t, validateOpenArgs(LayerNetwork, 0, FlagSendOnly|Flag(0x10))) +} diff --git a/common/windivert/integration_windows_test.go b/common/windivert/integration_windows_test.go new file mode 100644 index 0000000000..00ab897093 --- /dev/null +++ b/common/windivert/integration_windows_test.go @@ -0,0 +1,88 @@ +//go:build windows + +package windivert + +import ( + "errors" + "net/netip" + "testing" + "time" + + "github.com/stretchr/testify/require" + "golang.org/x/sys/windows" +) + +func openHandle(t *testing.T, filter *Filter, flags Flag) *Handle { + t.Helper() + h, err := Open(filter, LayerNetwork, 0, flags) + require.NoError(t, err) + return h +} + +// A send-only handle installs+opens the driver but does not attach a +// receive filter, so it exercises the full driver-install path without +// diverting any live traffic on the host. +func TestIntegrationOpenSendOnly(t *testing.T) { + h := openHandle(t, nil, FlagSendOnly) + require.NoError(t, h.Close()) +} + +// Close is idempotent per the doc contract. +func TestIntegrationCloseTwice(t *testing.T) { + h := openHandle(t, nil, FlagSendOnly) + require.NoError(t, h.Close()) + require.NoError(t, h.Close()) +} + +// Recv must unblock when the handle is closed concurrently. Without this, +// the spoofer's run goroutine could deadlock on shutdown. +func TestIntegrationRecvAbortsOnClose(t *testing.T) { + // A filter no live traffic will match, so Recv blocks indefinitely + // until Close aborts the overlapped I/O. + filter, err := OutboundTCP( + netip.MustParseAddrPort("10.255.255.254:1"), + netip.MustParseAddrPort("10.255.255.253:2"), + ) + require.NoError(t, err) + h := openHandle(t, filter, 0) + + errCh := make(chan error, 1) + go func() { + buf := make([]byte, MTUMax) + _, _, recvErr := h.Recv(buf) + errCh <- recvErr + }() + + // Let Recv reach the blocking DeviceIoControl before Close races in. + time.Sleep(200 * time.Millisecond) + require.NoError(t, h.Close()) + + select { + case err := <-errCh: + require.Error(t, err) + require.True(t, errors.Is(err, windows.ERROR_OPERATION_ABORTED), + "Recv should return ERROR_OPERATION_ABORTED, got %v", err) + case <-time.After(3 * time.Second): + t.Fatal("Recv did not unblock within 3s after Close") + } +} + +// Two concurrent Open calls must both succeed: the first wins the driver +// install race, the second reuses the already-running service. +func TestIntegrationConcurrentOpen(t *testing.T) { + errCh := make(chan error, 2) + handles := make(chan *Handle, 2) + for i := 0; i < 2; i++ { + go func() { + h, err := Open(nil, LayerNetwork, 0, FlagSendOnly) + handles <- h + errCh <- err + }() + } + for i := 0; i < 2; i++ { + err := <-errCh + h := <-handles + require.NoError(t, err) + require.NoError(t, h.Close()) + } +} diff --git a/common/windivert/windivert.go b/common/windivert/windivert.go new file mode 100644 index 0000000000..e9a8fc9545 --- /dev/null +++ b/common/windivert/windivert.go @@ -0,0 +1,71 @@ +// Package windivert provides a pure-Go binding to the WinDivert kernel +// driver on Windows (amd64 and 386). User-mode WinDivert calls are +// reimplemented in Go; only the signed kernel driver is embedded as an +// asset, since SCM-installed drivers must live on disk and their +// Authenticode signature forbids modification. +// +// Administrator is required for the first Open in a process so SCM can +// load the driver. Upstream: https://github.com/basil00/WinDivert v2.2.2, +// redistributed under its LGPL v3 option; see assets/LICENSE.txt. +package windivert + +import "unsafe" + +const AssetVersion = "2.2.2" + +// MTUMax is WINDIVERT_MTU_MAX from windivert.h (40 + 0xFFFF). Suitable as +// a single-packet receive buffer size. +const MTUMax = 40 + 0xFFFF + +type Layer uint32 + +const LayerNetwork Layer = 0 + +type Flag uint64 + +const FlagSendOnly Flag = 0x0008 + +const ( + PriorityHighest int16 = 30000 + PriorityLowest int16 = -30000 +) + +// Address mirrors WINDIVERT_ADDRESS from windivert.h (80 bytes, +// little-endian on both amd64 and 386): +// +// 0: INT64 Timestamp +// 8: UINT32 bitfield: Layer:8 | Event:8 | flags | Reserved1:8 +// 12: UINT32 Reserved2 +// 16: 64 bytes union (WINDIVERT_DATA_NETWORK / FLOW / SOCKET / REFLECT) +type Address struct { + Timestamp int64 + bits uint32 + Reserved2 uint32 + union [64]byte +} + +var _ [80]byte = [unsafe.Sizeof(Address{})]byte{} + +// Bit positions inside the Address's packed flags word. +const ( + addrBitIPv6 = 20 + addrBitIPChecksum = 21 + addrBitTCPChecksum = 22 +) + +func getFlagBit(bits uint32, pos uint) bool { return bits&(1< Date: Wed, 15 Apr 2026 20:50:21 +0800 Subject: [PATCH 35/93] Fix legacy rule-set download_detour blocked by empty direct check --- common/dialer/detour.go | 24 ++++++++++++------------ common/dialer/dialer.go | 20 ++++++++++---------- common/httpclient/client.go | 15 ++++++++------- option/http.go | 21 +++++++++++---------- route/rule/rule_set_remote.go | 11 ++++++----- 5 files changed, 47 insertions(+), 44 deletions(-) diff --git a/common/dialer/detour.go b/common/dialer/detour.go index dc1777022c..b2fc3efa0b 100644 --- a/common/dialer/detour.go +++ b/common/dialer/detour.go @@ -17,20 +17,20 @@ type DirectDialer interface { } type DetourDialer struct { - outboundManager adapter.OutboundManager - detour string - defaultOutbound bool - legacyDNSDialer bool - dialer N.Dialer - initOnce sync.Once - initErr error + outboundManager adapter.OutboundManager + detour string + defaultOutbound bool + disableEmptyDirectCheck bool + dialer N.Dialer + initOnce sync.Once + initErr error } -func NewDetour(outboundManager adapter.OutboundManager, detour string, legacyDNSDialer bool) N.Dialer { +func NewDetour(outboundManager adapter.OutboundManager, detour string, disableEmptyDirectCheck bool) N.Dialer { return &DetourDialer{ - outboundManager: outboundManager, - detour: detour, - legacyDNSDialer: legacyDNSDialer, + outboundManager: outboundManager, + detour: detour, + disableEmptyDirectCheck: disableEmptyDirectCheck, } } @@ -66,7 +66,7 @@ func (d *DetourDialer) init() { } else { dialer = d.outboundManager.Default() } - if !d.defaultOutbound && !d.legacyDNSDialer { + if !d.defaultOutbound && !d.disableEmptyDirectCheck { if directDialer, isDirect := dialer.(DirectDialer); isDirect { if directDialer.IsEmpty() { d.initErr = E.New("detour to an empty direct outbound makes no sense") diff --git a/common/dialer/dialer.go b/common/dialer/dialer.go index 08257a04a7..f78aa9f3d9 100644 --- a/common/dialer/dialer.go +++ b/common/dialer/dialer.go @@ -17,15 +17,15 @@ import ( ) type Options struct { - Context context.Context - Options option.DialerOptions - RemoteIsDomain bool - DirectResolver bool - ResolverOnDetour bool - NewDialer bool - LegacyDNSDialer bool - DirectOutbound bool - DefaultOutbound bool + Context context.Context + Options option.DialerOptions + RemoteIsDomain bool + DirectResolver bool + ResolverOnDetour bool + NewDialer bool + DisableEmptyDirectCheck bool + DirectOutbound bool + DefaultOutbound bool } // TODO: merge with NewWithOptions @@ -49,7 +49,7 @@ func NewWithOptions(options Options) (N.Dialer, error) { if outboundManager == nil { return nil, E.New("missing outbound manager") } - dialer = NewDetour(outboundManager, dialOptions.Detour, options.LegacyDNSDialer) + dialer = NewDetour(outboundManager, dialOptions.Detour, options.DisableEmptyDirectCheck) } else if options.DefaultOutbound { outboundManager := service.FromContext[adapter.OutboundManager](options.Context) if outboundManager == nil { diff --git a/common/httpclient/client.go b/common/httpclient/client.go index c8eb0fef8e..a6fde9c02d 100644 --- a/common/httpclient/client.go +++ b/common/httpclient/client.go @@ -16,13 +16,14 @@ import ( func NewTransport(ctx context.Context, logger logger.ContextLogger, tag string, options option.HTTPClientOptions) (*ManagedTransport, error) { rawDialer, err := dialer.NewWithOptions(dialer.Options{ - Context: ctx, - Options: options.DialerOptions, - RemoteIsDomain: true, - DirectResolver: options.DirectResolver, - ResolverOnDetour: options.ResolveOnDetour, - NewDialer: options.ResolveOnDetour, - DefaultOutbound: options.DefaultOutbound, + Context: ctx, + Options: options.DialerOptions, + RemoteIsDomain: true, + DirectResolver: options.DirectResolver, + ResolverOnDetour: options.ResolveOnDetour, + NewDialer: options.ResolveOnDetour, + DisableEmptyDirectCheck: options.DisableEmptyDirectCheck, + DefaultOutbound: options.DefaultOutbound, }) if err != nil { return nil, err diff --git a/option/http.go b/option/http.go index fc7e16df96..1a97270443 100644 --- a/option/http.go +++ b/option/http.go @@ -25,16 +25,17 @@ type QUICOptions struct { } type _HTTPClientOptions struct { - Tag string `json:"tag,omitempty"` - Engine string `json:"engine,omitempty"` - Version int `json:"version,omitempty"` - DisableVersionFallback bool `json:"disable_version_fallback,omitempty"` - Headers badoption.HTTPHeader `json:"headers,omitempty"` - HTTP2Options HTTP2Options `json:"-"` - HTTP3Options QUICOptions `json:"-"` - DefaultOutbound bool `json:"-"` - ResolveOnDetour bool `json:"-"` - DirectResolver bool `json:"-"` + Tag string `json:"tag,omitempty"` + Engine string `json:"engine,omitempty"` + Version int `json:"version,omitempty"` + DisableVersionFallback bool `json:"disable_version_fallback,omitempty"` + Headers badoption.HTTPHeader `json:"headers,omitempty"` + HTTP2Options HTTP2Options `json:"-"` + HTTP3Options QUICOptions `json:"-"` + DefaultOutbound bool `json:"-"` + DisableEmptyDirectCheck bool `json:"-"` + ResolveOnDetour bool `json:"-"` + DirectResolver bool `json:"-"` OutboundTLSOptionsContainer DialerOptions } diff --git a/route/rule/rule_set_remote.go b/route/rule/rule_set_remote.go index 24066d75af..90b699e7eb 100644 --- a/route/rule/rule_set_remote.go +++ b/route/rule/rule_set_remote.go @@ -294,11 +294,12 @@ func (s *RemoteRuleSet) resolveTransport() (adapter.HTTPTransport, error) { } if s.options.RemoteOptions.DownloadDetour != "" { //nolint:staticcheck deprecated.Report(s.ctx, deprecated.OptionLegacyRuleSetDownloadDetour) - var httpClientOptions option.HTTPClientOptions - httpClientOptions.DialerOptions = option.DialerOptions{ - Detour: s.options.RemoteOptions.DownloadDetour, //nolint:staticcheck - } - return httpClientManager.ResolveTransport(s.ctx, s.logger, httpClientOptions) + return httpClientManager.ResolveTransport(s.ctx, s.logger, option.HTTPClientOptions{ + DialerOptions: option.DialerOptions{ + Detour: s.options.RemoteOptions.DownloadDetour, //nolint:staticcheck + }, + DisableEmptyDirectCheck: true, + }) } defaultTransport := httpClientManager.DefaultTransport() if defaultTransport == nil { From 90a642ef96b6329a9c0cc8d69e359b22e6effdc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 15 Apr 2026 21:02:40 +0800 Subject: [PATCH 36/93] Reject pure-IP rule-set references without match_response DNS rules referencing rule-sets that contain only ip_cidr predicates silently stopped matching when legacy DNS mode was disabled, because the IP-CIDR branch cannot match against an in-flight DNS query. The existing validation intentionally let every rule_set through on the premise that mixed sets still work via their non-IP branches, which is only true when such a branch exists. Track whether a rule-set carries any non-IP-CIDR predicate and reject pure-IP references the same way bare ip_cidr fields are already rejected. --- adapter/router.go | 6 ++ dns/router.go | 39 ++++++---- dns/router_test.go | 160 ++++++++++++++++++++++++++++++++++++++++- docs/migration.md | 4 +- docs/migration.zh.md | 4 +- route/rule/rule_set.go | 11 +++ 6 files changed, 206 insertions(+), 18 deletions(-) diff --git a/adapter/router.go b/adapter/router.go index 26f4612578..24d45af006 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -70,4 +70,10 @@ type RuleSetMetadata struct { ContainsWIFIRule bool ContainsIPCIDRRule bool ContainsDNSQueryTypeRule bool + // ContainsNonIPCIDRRule signals that the rule-set carries at least one sub-rule + // with a predicate other than destination ip_cidr / ip_set, so it can contribute + // to DNS pre-response matching. A rule-set where this is false and + // ContainsIPCIDRRule is true is "pure-IP" and matches nothing before a DNS + // response is available. + ContainsNonIPCIDRRule bool } diff --git a/dns/router.go b/dns/router.go index b9fc8f9775..adde3bac0c 100644 --- a/dns/router.go +++ b/dns/router.go @@ -186,7 +186,7 @@ func (r *Router) buildRules(startRules bool) ([]adapter.DNSRule, bool, dnsRuleMo return nil, false, dnsRuleModeFlags{}, err } if !legacyDNSMode { - err = validateLegacyDNSModeDisabledRules(r.rawRules) + err = validateLegacyDNSModeDisabledRules(router, r.rawRules, nil) if err != nil { return nil, false, dnsRuleModeFlags{}, err } @@ -248,7 +248,7 @@ func (r *Router) ValidateRuleSetMetadataUpdate(tag string, metadata adapter.Rule return err } if !candidateLegacyDNSMode { - return validateLegacyDNSModeDisabledRules(r.rawRules) + return validateLegacyDNSModeDisabledRules(router, r.rawRules, overrides) } return nil } @@ -258,7 +258,7 @@ func (r *Router) ValidateRuleSetMetadataUpdate(tag string, metadata adapter.Rule } if legacyDNSMode { if !candidateLegacyDNSMode && flags.disabled { - err := validateLegacyDNSModeDisabledRules(r.rawRules) + err := validateLegacyDNSModeDisabledRules(router, r.rawRules, overrides) if err != nil { return err } @@ -269,7 +269,7 @@ func (r *Router) ValidateRuleSetMetadataUpdate(tag string, metadata adapter.Rule if candidateLegacyDNSMode { return E.New(deprecated.OptionLegacyDNSAddressFilter.MessageWithLink()) } - return nil + return validateLegacyDNSModeDisabledRules(router, r.rawRules, overrides) } func (r *Router) matchDNS(ctx context.Context, rules []adapter.DNSRule, allowFakeIP bool, ruleIndex int, isAddressQuery bool, options *adapter.DNSQueryOptions) (adapter.DNSTransport, adapter.DNSRule, int) { @@ -1025,10 +1025,10 @@ func referencedDNSRuleSetTags(rules []option.DNSRule) []string { return tags } -func validateLegacyDNSModeDisabledRules(rules []option.DNSRule) error { +func validateLegacyDNSModeDisabledRules(router adapter.Router, rules []option.DNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) error { var seenEvaluate bool for i, rule := range rules { - requiresPriorEvaluate, err := validateLegacyDNSModeDisabledRuleTree(rule) + requiresPriorEvaluate, err := validateLegacyDNSModeDisabledRuleTree(router, rule, metadataOverrides) if err != nil { return E.Cause(err, "validate dns rule[", i, "]") } @@ -1063,14 +1063,14 @@ func validateEvaluateFakeIPRules(rules []option.DNSRule, transportManager adapte return nil } -func validateLegacyDNSModeDisabledRuleTree(rule option.DNSRule) (bool, error) { +func validateLegacyDNSModeDisabledRuleTree(router adapter.Router, rule option.DNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) (bool, error) { switch rule.Type { case "", C.RuleTypeDefault: - return validateLegacyDNSModeDisabledDefaultRule(rule.DefaultOptions) + return validateLegacyDNSModeDisabledDefaultRule(router, rule.DefaultOptions, metadataOverrides) case C.RuleTypeLogical: requiresPriorEvaluate := dnsRuleActionType(rule) == C.RuleActionTypeRespond for i, subRule := range rule.LogicalOptions.Rules { - subRequiresPriorEvaluate, err := validateLegacyDNSModeDisabledRuleTree(subRule) + subRequiresPriorEvaluate, err := validateLegacyDNSModeDisabledRuleTree(router, subRule, metadataOverrides) if err != nil { return false, E.Cause(err, "sub rule[", i, "]") } @@ -1082,16 +1082,25 @@ func validateLegacyDNSModeDisabledRuleTree(rule option.DNSRule) (bool, error) { } } -func validateLegacyDNSModeDisabledDefaultRule(rule option.DefaultDNSRule) (bool, error) { +func validateLegacyDNSModeDisabledDefaultRule(router adapter.Router, rule option.DefaultDNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) (bool, error) { hasResponseRecords := hasResponseMatchFields(rule) if (hasResponseRecords || len(rule.IPCIDR) > 0 || rule.IPIsPrivate || rule.IPAcceptAny) && !rule.MatchResponse { return false, E.New("Response Match Fields (ip_cidr, ip_is_private, ip_accept_any, response_rcode, response_answer, response_ns, response_extra) require match_response to be enabled") } - // Intentionally do not reject rule_set here. A referenced rule set may mix - // destination-IP predicates with pre-response predicates such as domain items. - // When match_response is false, those destination-IP branches fail closed during - // pre-response evaluation instead of consuming DNS response state, while sibling - // non-response branches remain matchable. + // rule_set entries are only rejected when every referenced set is pure-IP; + // mixed sets still fall through because their non-IP branches remain matchable + // before a DNS response is available. + if !rule.MatchResponse && len(rule.RuleSet) > 0 { + for _, tag := range rule.RuleSet { + metadata, err := lookupDNSRuleSetMetadata(router, tag, metadataOverrides) + if err != nil { + return false, err + } + if metadata.ContainsIPCIDRRule && !metadata.ContainsNonIPCIDRRule { + return false, E.New(deprecated.OptionLegacyDNSAddressFilter.MessageWithLink()) + } + } + } if rule.RuleSetIPCIDRAcceptEmpty { //nolint:staticcheck return false, E.New(deprecated.OptionRuleSetIPCIDRAcceptEmpty.MessageWithLink()) } diff --git a/dns/router_test.go b/dns/router_test.go index 54213b23c3..206eae73bd 100644 --- a/dns/router_test.go +++ b/dns/router_test.go @@ -761,7 +761,8 @@ func TestValidateRuleSetMetadataUpdateAllowsRuleSetThatKeepsNonLegacyDNSMode(t * require.False(t, router.legacyDNSMode) err := router.ValidateRuleSetMetadataUpdate("dynamic-set", adapter.RuleSetMetadata{ - ContainsIPCIDRRule: true, + ContainsIPCIDRRule: true, + ContainsNonIPCIDRRule: true, }) require.NoError(t, err) } @@ -808,6 +809,163 @@ func TestValidateRuleSetMetadataUpdateAllowsRelaxingLegacyRequirement(t *testing require.NoError(t, err) } +func TestInitializeRejectsPureIPRuleSetWhenLegacyDNSModeDisabled(t *testing.T) { + t.Parallel() + + fakeSet := &fakeRuleSet{ + metadata: adapter.RuleSetMetadata{ + ContainsIPCIDRRule: true, + }, + } + routerService := &fakeRouter{ + ruleSets: map[string]adapter.RuleSet{ + "pure-ip": fakeSet, + }, + } + ctx := service.ContextWith[adapter.Router](context.Background(), routerService) + router := &Router{ + ctx: ctx, + logger: log.NewNOPFactory().NewLogger("dns"), + transport: &fakeDNSTransportManager{}, + client: &fakeDNSClient{}, + rawRules: make([]option.DNSRule, 0, 2), + rules: make([]adapter.DNSRule, 0, 2), + defaultDomainStrategy: C.DomainStrategyAsIS, + } + err := router.Initialize([]option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + QueryType: badoption.Listable[option.DNSQueryType]{option.DNSQueryType(mDNS.TypeA)}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + RuleSet: badoption.Listable[string]{"pure-ip"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + }) + require.ErrorContains(t, err, "Address Filter Fields") +} + +func TestInitializeAllowsMixedRuleSetWhenLegacyDNSModeDisabled(t *testing.T) { + t.Parallel() + + fakeSet := &fakeRuleSet{ + metadata: adapter.RuleSetMetadata{ + ContainsIPCIDRRule: true, + ContainsNonIPCIDRRule: true, + }, + } + routerService := &fakeRouter{ + ruleSets: map[string]adapter.RuleSet{ + "mixed": fakeSet, + }, + } + ctx := service.ContextWith[adapter.Router](context.Background(), routerService) + router := newTestRouterWithContext(t, ctx, []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + QueryType: badoption.Listable[option.DNSQueryType]{option.DNSQueryType(mDNS.TypeA)}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + RuleSet: badoption.Listable[string]{"mixed"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + }, &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + }, + }, &fakeDNSClient{}) + require.False(t, router.legacyDNSMode) +} + +func TestValidateRuleSetMetadataUpdateRejectsRuleSetFlippingToPureIP(t *testing.T) { + t.Parallel() + + fakeSet := &fakeRuleSet{ + metadata: adapter.RuleSetMetadata{ + ContainsIPCIDRRule: true, + ContainsNonIPCIDRRule: true, + }, + } + routerService := &fakeRouter{ + ruleSets: map[string]adapter.RuleSet{ + "mixed": fakeSet, + }, + } + ctx := service.ContextWith[adapter.Router](context.Background(), routerService) + router := newTestRouterWithContext(t, ctx, []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + QueryType: badoption.Listable[option.DNSQueryType]{option.DNSQueryType(mDNS.TypeA)}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + RuleSet: badoption.Listable[string]{"mixed"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + }, &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + }, + }, &fakeDNSClient{}) + require.False(t, router.legacyDNSMode) + + err := router.ValidateRuleSetMetadataUpdate("mixed", adapter.RuleSetMetadata{ + ContainsIPCIDRRule: true, + }) + require.ErrorContains(t, err, "Address Filter Fields") +} + func TestCloseWaitsForInFlightLookupUntilContextCancellation(t *testing.T) { t.Parallel() diff --git a/docs/migration.md b/docs/migration.md index 867f903b69..be26827f03 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -82,7 +82,9 @@ See [ACME](/configuration/shared/certificate-provider/acme/) for fields newly ad ### Migrate address filter fields to response matching Legacy Address Filter Fields (`ip_cidr`, `ip_is_private` without `match_response`) in DNS rules are deprecated, -along with the Legacy `rule_set_ip_cidr_accept_empty` DNS rule item. +along with the Legacy `rule_set_ip_cidr_accept_empty` DNS rule item. A DNS rule that references a rule-set +containing only `ip_cidr` items (for example, a GeoIP rule-set) without `match_response` is also rejected +at startup when legacy DNS mode is disabled. In sing-box 1.14.0, use the [`evaluate`](/configuration/dns/rule_action/#evaluate) action to fetch a DNS response, then match against it explicitly with `match_response`. diff --git a/docs/migration.zh.md b/docs/migration.zh.md index 54dec47e4b..4d003e1ecd 100644 --- a/docs/migration.zh.md +++ b/docs/migration.zh.md @@ -82,7 +82,9 @@ sing-box 1.14.0 新增字段参阅 [ACME](/zh/configuration/shared/certificate-p ### 迁移地址筛选字段到响应匹配 旧版地址筛选字段(不使用 `match_response` 的 `ip_cidr`、`ip_is_private`)已废弃, -旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项也已废弃。 +旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项也已废弃。当旧版 DNS 模式被禁用时, +引用仅包含 `ip_cidr` 项的规则集(例如 GeoIP 规则集)且未设置 `match_response` 的 DNS 规则 +也将在启动时被拒绝。 在 sing-box 1.14.0 中,请使用 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作 获取 DNS 响应,然后通过 `match_response` 显式匹配。 diff --git a/route/rule/rule_set.go b/route/rule/rule_set.go index 3c2d9eda2a..6720e788b7 100644 --- a/route/rule/rule_set.go +++ b/route/rule/rule_set.go @@ -2,6 +2,7 @@ package rule import ( "context" + "reflect" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" @@ -75,12 +76,22 @@ func isDNSQueryTypeHeadlessRule(rule option.DefaultHeadlessRule) bool { return len(rule.QueryType) > 0 } +func isNonIPCIDRHeadlessRule(rule option.DefaultHeadlessRule) bool { + ipOnly := option.DefaultHeadlessRule{ + IPCIDR: rule.IPCIDR, + IPSet: rule.IPSet, + Invert: rule.Invert, + } + return !reflect.DeepEqual(rule, ipOnly) +} + func buildRuleSetMetadata(headlessRules []option.HeadlessRule) adapter.RuleSetMetadata { return adapter.RuleSetMetadata{ ContainsProcessRule: HasHeadlessRule(headlessRules, isProcessHeadlessRule), ContainsWIFIRule: HasHeadlessRule(headlessRules, isWIFIHeadlessRule), ContainsIPCIDRRule: HasHeadlessRule(headlessRules, isIPCIDRHeadlessRule), ContainsDNSQueryTypeRule: HasHeadlessRule(headlessRules, isDNSQueryTypeHeadlessRule), + ContainsNonIPCIDRRule: HasHeadlessRule(headlessRules, isNonIPCIDRHeadlessRule), } } From fee6afdbf597bd1c2f9263791cd1d7677c6e73b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 16 Apr 2026 00:27:14 +0800 Subject: [PATCH 37/93] Fix use-after-free of pooled value buffers in bbolt Batch writes --- experimental/cachefile/dns_cache.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/experimental/cachefile/dns_cache.go b/experimental/cachefile/dns_cache.go index 914c7e5adc..55718c59a1 100644 --- a/experimental/cachefile/dns_cache.go +++ b/experimental/cachefile/dns_cache.go @@ -52,6 +52,10 @@ func (c *CacheFile) LoadDNSCache(transportName string, qName string, qType uint1 } func (c *CacheFile) SaveDNSCache(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time) error { + value := buf.Get(8 + len(rawMessage)) + defer buf.Put(value) + binary.BigEndian.PutUint64(value[:8], uint64(expireAt.Unix())) + copy(value[8:], rawMessage) return c.batch(func(tx *bbolt.Tx) error { bucket, err := c.createBucket(tx, bucketDNSCache) if err != nil { @@ -65,10 +69,6 @@ func (c *CacheFile) SaveDNSCache(transportName string, qName string, qType uint1 binary.BigEndian.PutUint16(key, qType) copy(key[2:], qName) defer buf.Put(key) - value := buf.Get(8 + len(rawMessage)) - defer buf.Put(value) - binary.BigEndian.PutUint64(value[:8], uint64(expireAt.Unix())) - copy(value[8:], rawMessage) return bucket.Put(key, value) }) } From 4a68bc9274b3ff47c44da1062ed8c163676cae3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 16 Apr 2026 18:00:13 +0800 Subject: [PATCH 38/93] Reject IP literal server name with TLS spoof --- common/tls/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/tls/client.go b/common/tls/client.go index 00020ee2c9..35c628c11e 100644 --- a/common/tls/client.go +++ b/common/tls/client.go @@ -30,7 +30,7 @@ func parseTLSSpoofOptions(serverName string, options option.OutboundTLSOptions) if !tlsspoof.PlatformSupported { return "", 0, E.New("`spoof` is not supported on this platform") } - if options.DisableSNI || serverName == "" { + if options.DisableSNI || serverName == "" || M.ParseAddr(serverName).IsValid() { return "", 0, E.New("`spoof` requires TLS ClientHello with SNI") } method, err := tlsspoof.ParseMethod(options.SpoofMethod) From 9bffb64bbdc086d4d16c777dcdf9e32351f043d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 17 Apr 2026 12:13:41 +0800 Subject: [PATCH 39/93] Fix macOS tlsspoof --- common/tlsspoof/README.md | 3 ++ common/tlsspoof/integration_test.go | 12 +++++- common/tlsspoof/integration_unix_test.go | 49 +++++++++++++++++++++- common/tlsspoof/packet.go | 30 ++++++++++---- common/tlsspoof/raw_darwin.go | 52 ++++++++++++++++-------- 5 files changed, 119 insertions(+), 27 deletions(-) create mode 100644 common/tlsspoof/README.md diff --git a/common/tlsspoof/README.md b/common/tlsspoof/README.md new file mode 100644 index 0000000000..de684e15cd --- /dev/null +++ b/common/tlsspoof/README.md @@ -0,0 +1,3 @@ +# tls spoof + +idea from https://github.com/therealaleph/sni-spoofing-rust diff --git a/common/tlsspoof/integration_test.go b/common/tlsspoof/integration_test.go index e365929089..b7b07d54be 100644 --- a/common/tlsspoof/integration_test.go +++ b/common/tlsspoof/integration_test.go @@ -84,8 +84,16 @@ func tcpdumpObserver(t *testing.T, iface string, port uint16, needle string, do } func dialLocalEchoServer(t *testing.T) (client net.Conn, serverPort uint16) { + return dialLocalEchoServerFamily(t, "tcp4", "127.0.0.1:0") +} + +func dialLocalEchoServerIPv6(t *testing.T) (client net.Conn, serverPort uint16) { + return dialLocalEchoServerFamily(t, "tcp6", "[::1]:0") +} + +func dialLocalEchoServerFamily(t *testing.T, network, address string) (client net.Conn, serverPort uint16) { t.Helper() - listener, err := net.Listen("tcp4", "127.0.0.1:0") + listener, err := net.Listen(network, address) require.NoError(t, err) accepted := make(chan net.Conn, 1) @@ -97,7 +105,7 @@ func dialLocalEchoServer(t *testing.T) (client net.Conn, serverPort uint16) { close(accepted) }() addr := listener.Addr().(*net.TCPAddr) - client, err = net.Dial("tcp4", addr.String()) + client, err = net.Dial(network, addr.String()) require.NoError(t, err) server := <-accepted require.NotNil(t, server) diff --git a/common/tlsspoof/integration_unix_test.go b/common/tlsspoof/integration_unix_test.go index c734ed891a..9ec5760c75 100644 --- a/common/tlsspoof/integration_unix_test.go +++ b/common/tlsspoof/integration_unix_test.go @@ -48,11 +48,56 @@ func TestIntegrationSpoofer_WrongSequence(t *testing.T) { require.True(t, captured, "injected fake ClientHello must be observable on loopback") } +func TestIntegrationSpoofer_IPv6_WrongChecksum(t *testing.T) { + requireRoot(t) + client, serverPort := dialLocalEchoServerIPv6(t) + spoofer, err := NewSpoofer(client, MethodWrongChecksum) + require.NoError(t, err) + defer spoofer.Close() + + payload, err := hex.DecodeString(realClientHello) + require.NoError(t, err) + fake, err := rewriteSNI(payload, "letsencrypt.org") + require.NoError(t, err) + + captured := tcpdumpObserver(t, loopbackInterface, serverPort, "letsencrypt.org", func() { + require.NoError(t, spoofer.Inject(fake)) + }, 3*time.Second) + require.True(t, captured, "injected fake ClientHello must be observable on loopback") +} + +func TestIntegrationSpoofer_IPv6_WrongSequence(t *testing.T) { + requireRoot(t) + client, serverPort := dialLocalEchoServerIPv6(t) + spoofer, err := NewSpoofer(client, MethodWrongSequence) + require.NoError(t, err) + defer spoofer.Close() + + payload, err := hex.DecodeString(realClientHello) + require.NoError(t, err) + fake, err := rewriteSNI(payload, "letsencrypt.org") + require.NoError(t, err) + + captured := tcpdumpObserver(t, loopbackInterface, serverPort, "letsencrypt.org", func() { + require.NoError(t, spoofer.Inject(fake)) + }, 3*time.Second) + require.True(t, captured, "injected fake ClientHello must be observable on loopback") +} + // Loopback bypasses TCP checksum validation, so wrong-sequence is used instead. func TestIntegrationConn_InjectsThenForwardsRealCH(t *testing.T) { requireRoot(t) + runInjectsThenForwardsRealCH(t, "tcp4", "127.0.0.1:0") +} + +func TestIntegrationConn_IPv6_InjectsThenForwardsRealCH(t *testing.T) { + requireRoot(t) + runInjectsThenForwardsRealCH(t, "tcp6", "[::1]:0") +} - listener, err := net.Listen("tcp4", "127.0.0.1:0") +func runInjectsThenForwardsRealCH(t *testing.T, network, address string) { + t.Helper() + listener, err := net.Listen(network, address) require.NoError(t, err) serverReceived := make(chan []byte, 1) @@ -69,7 +114,7 @@ func TestIntegrationConn_InjectsThenForwardsRealCH(t *testing.T) { addr := listener.Addr().(*net.TCPAddr) serverPort := uint16(addr.Port) - client, err := net.Dial("tcp4", addr.String()) + client, err := net.Dial(network, addr.String()) require.NoError(t, err) t.Cleanup(func() { client.Close() diff --git a/common/tlsspoof/packet.go b/common/tlsspoof/packet.go index d84fc4b12c..9bdf7a59d9 100644 --- a/common/tlsspoof/packet.go +++ b/common/tlsspoof/packet.go @@ -74,18 +74,34 @@ func encodeTCP(frame []byte, ipHeaderLen int, src, dst netip.AddrPort, seqNum, a } func buildSpoofFrame(method Method, src, dst netip.AddrPort, sendNext, receiveNext uint32, payload []byte) ([]byte, error) { - var sequence uint32 - corrupt := false + sequence, corrupt, err := resolveSpoofSequence(method, sendNext, payload) + if err != nil { + return nil, err + } + return buildTCPSegment(src, dst, sequence, receiveNext, payload, corrupt), nil +} + +// buildSpoofTCPSegment returns a TCP segment without an IP header, for +// platforms where the kernel synthesises the IP header (darwin IPv6). +func buildSpoofTCPSegment(method Method, src, dst netip.AddrPort, sendNext, receiveNext uint32, payload []byte) ([]byte, error) { + sequence, corrupt, err := resolveSpoofSequence(method, sendNext, payload) + if err != nil { + return nil, err + } + segment := make([]byte, tcpHeaderLen+len(payload)) + encodeTCP(segment, 0, src, dst, sequence, receiveNext, payload, corrupt) + return segment, nil +} + +func resolveSpoofSequence(method Method, sendNext uint32, payload []byte) (uint32, bool, error) { switch method { case MethodWrongSequence: - sequence = sendNext - uint32(len(payload)) + return sendNext - uint32(len(payload)), false, nil case MethodWrongChecksum: - sequence = sendNext - corrupt = true + return sendNext, true, nil default: - return nil, E.New("tls_spoof: unknown method ", method) + return 0, false, E.New("tls_spoof: unknown method ", method) } - return buildTCPSegment(src, dst, sequence, receiveNext, payload, corrupt), nil } func applyTCPChecksum(tcp header.TCP, srcAddr, dstAddr netip.Addr, payload []byte, corrupt bool) { diff --git a/common/tlsspoof/raw_darwin.go b/common/tlsspoof/raw_darwin.go index 170561a872..99c9a5c665 100644 --- a/common/tlsspoof/raw_darwin.go +++ b/common/tlsspoof/raw_darwin.go @@ -59,7 +59,7 @@ func newRawSpoofer(conn net.Conn, method Method) (Spoofer, error) { if err != nil { return nil, err } - fd, sockaddr, err := openDarwinRawSocket(dst) + fd, sockaddr, err := openDarwinRawSocket(src, dst) if err != nil { return nil, err } @@ -119,31 +119,51 @@ func readDarwinTCPSequence(src, dst netip.AddrPort) (uint32, uint32, error) { return 0, 0, E.New("tls_spoof: connection ", src, "->", dst, " not found in pcblist_n") } -func openDarwinRawSocket(dst netip.AddrPort) (int, unix.Sockaddr, error) { - if !dst.Addr().Is4() { - // macOS does not expose IPV6_HDRINCL; raw AF_INET6 injection would - // require either BPF link-layer writes or kernel-side IPv6 header - // synthesis, neither of which is implemented here. - return -1, nil, E.New("tls_spoof: IPv6 not supported on darwin") +func openDarwinRawSocket(src, dst netip.AddrPort) (int, unix.Sockaddr, error) { + if dst.Addr().Is4() { + return openIPv4RawSocket(dst) } - return openIPv4RawSocket(dst) + // macOS does not accept IPV6_HDRINCL on AF_INET6 SOCK_RAW IPPROTO_TCP + // sockets, so the kernel builds the IPv6 header itself. Bind to the real + // connection's source address so in6_selectsrc returns it, and rely on + // in6p_cksum defaulting to -1 so the user-supplied TCP checksum is + // preserved (including deliberately corrupted ones). + fd, err := unix.Socket(unix.AF_INET6, unix.SOCK_RAW, unix.IPPROTO_TCP) + if err != nil { + return -1, nil, E.Cause(err, "open AF_INET6 SOCK_RAW") + } + err = unix.Bind(fd, &unix.SockaddrInet6{Addr: src.Addr().As16()}) + if err != nil { + unix.Close(fd) + return -1, nil, E.Cause(err, "bind AF_INET6 SOCK_RAW") + } + sockaddr := &unix.SockaddrInet6{Port: int(dst.Port()), Addr: dst.Addr().As16()} + return fd, sockaddr, nil } func (s *darwinSpoofer) Inject(payload []byte) error { + if !s.src.Addr().Is4() { + segment, err := buildSpoofTCPSegment(s.method, s.src, s.dst, s.sendNext, s.receiveNext, payload) + if err != nil { + return err + } + err = unix.Sendto(s.rawFD, segment, 0, s.rawSockAddr) + if err != nil { + return E.Cause(err, "sendto raw socket") + } + return nil + } frame, err := buildSpoofFrame(s.method, s.src, s.dst, s.sendNext, s.receiveNext, payload) if err != nil { return err } // Darwin inherits the historical BSD quirk: with IP_HDRINCL the kernel // expects ip_len and ip_off in host byte order, not network byte order. - // Apple's rip_output swaps them back before transmission. This does not - // apply to IPv6. - if s.src.Addr().Is4() { - totalLen := binary.BigEndian.Uint16(frame[2:4]) - binary.NativeEndian.PutUint16(frame[2:4], totalLen) - fragOff := binary.BigEndian.Uint16(frame[6:8]) - binary.NativeEndian.PutUint16(frame[6:8], fragOff) - } + // Apple's rip_output swaps them back before transmission. + totalLen := binary.BigEndian.Uint16(frame[2:4]) + binary.NativeEndian.PutUint16(frame[2:4], totalLen) + fragOff := binary.BigEndian.Uint16(frame[6:8]) + binary.NativeEndian.PutUint16(frame[6:8], fragOff) err = unix.Sendto(s.rawFD, frame, 0, s.rawSockAddr) if err != nil { return E.Cause(err, "sendto raw socket") From 4b15fccb787d915dfdf8a43bb230ce3f5553e987 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 17 Apr 2026 13:29:31 +0800 Subject: [PATCH 40/93] Scope HTTP/2 fallback and HTTP/3 broken state per authority --- common/httpclient/helpers.go | 30 ++++++ common/httpclient/helpers_test.go | 51 ++++++++++ common/httpclient/http2_fallback_transport.go | 48 +++++---- .../http2_fallback_transport_test.go | 37 +++++++ common/httpclient/http3_transport.go | 70 ++++++++----- common/httpclient/http3_transport_test.go | 99 +++++++++++++++++++ 6 files changed, 295 insertions(+), 40 deletions(-) create mode 100644 common/httpclient/helpers_test.go create mode 100644 common/httpclient/http2_fallback_transport_test.go create mode 100644 common/httpclient/http3_transport_test.go diff --git a/common/httpclient/helpers.go b/common/httpclient/helpers.go index cffc797198..7cc78cc6e1 100644 --- a/common/httpclient/helpers.go +++ b/common/httpclient/helpers.go @@ -12,6 +12,8 @@ import ( E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" + + "golang.org/x/net/idna" ) func dialTLS(ctx context.Context, rawDialer N.Dialer, baseTLSConfig tls.Config, destination M.Socksaddr, nextProtos []string, expectProto string) (net.Conn, error) { @@ -73,6 +75,34 @@ func mustGetBody(request *http.Request) io.ReadCloser { return body } +func requestAuthority(request *http.Request) string { + if request == nil || request.URL == nil || request.URL.Host == "" { + return "" + } + host, port, err := net.SplitHostPort(request.URL.Host) + if err != nil { + host = request.URL.Host + port = "" + } + if port == "" { + if request.URL.Scheme == "http" { + port = "80" + } else { + port = "443" + } + } + if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { + return host + ":" + port + } + ascii, idnaErr := idna.Lookup.ToASCII(host) + if idnaErr == nil { + host = ascii + } else { + host = strings.ToLower(host) + } + return net.JoinHostPort(host, port) +} + func buildSTDTLSConfig(baseTLSConfig tls.Config, destination M.Socksaddr, nextProtos []string) (*stdTLS.Config, error) { if baseTLSConfig == nil { return nil, nil diff --git a/common/httpclient/helpers_test.go b/common/httpclient/helpers_test.go new file mode 100644 index 0000000000..2c451e0a58 --- /dev/null +++ b/common/httpclient/helpers_test.go @@ -0,0 +1,51 @@ +package httpclient + +import ( + "net/http" + "net/url" + "testing" +) + +func TestRequestAuthority(t *testing.T) { + testCases := []struct { + name string + url string + expect string + }{ + {name: "https default port", url: "https://example.com/foo", expect: "example.com:443"}, + {name: "http default port", url: "http://example.com/foo", expect: "example.com:80"}, + {name: "https explicit port", url: "https://example.com:8443/foo", expect: "example.com:8443"}, + {name: "https uppercase host", url: "https://EXAMPLE.COM/foo", expect: "example.com:443"}, + {name: "https ipv6 default port", url: "https://[2001:db8::1]/foo", expect: "[2001:db8::1]:443"}, + {name: "https ipv6 explicit port", url: "https://[2001:db8::1]:8443/foo", expect: "[2001:db8::1]:8443"}, + {name: "https ipv4", url: "https://192.0.2.1/foo", expect: "192.0.2.1:443"}, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + parsed, err := url.Parse(testCase.url) + if err != nil { + t.Fatalf("parse url: %v", err) + } + got := requestAuthority(&http.Request{URL: parsed}) + if got != testCase.expect { + t.Fatalf("got %q, want %q", got, testCase.expect) + } + }) + } + + t.Run("nil request", func(t *testing.T) { + if got := requestAuthority(nil); got != "" { + t.Fatalf("got %q, want empty", got) + } + }) + t.Run("nil URL", func(t *testing.T) { + if got := requestAuthority(&http.Request{}); got != "" { + t.Fatalf("got %q, want empty", got) + } + }) + t.Run("empty host", func(t *testing.T) { + if got := requestAuthority(&http.Request{URL: &url.URL{Scheme: "https"}}); got != "" { + t.Fatalf("got %q, want empty", got) + } + }) +} diff --git a/common/httpclient/http2_fallback_transport.go b/common/httpclient/http2_fallback_transport.go index 5b16dff187..682b1ebadf 100644 --- a/common/httpclient/http2_fallback_transport.go +++ b/common/httpclient/http2_fallback_transport.go @@ -6,7 +6,7 @@ import ( "errors" "net" "net/http" - "sync/atomic" + "sync" "github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/option" @@ -20,35 +20,47 @@ import ( var errHTTP2Fallback = E.New("fallback to HTTP/1.1") type http2FallbackTransport struct { - h2Transport *http2.Transport - h1Transport *http1Transport - h2Fallback *atomic.Bool + h2Transport *http2.Transport + h1Transport *http1Transport + fallbackAccess sync.RWMutex + fallbackAuthority map[string]struct{} } func newHTTP2FallbackTransport(rawDialer N.Dialer, baseTLSConfig tls.Config, options option.HTTP2Options) (*http2FallbackTransport, error) { h1 := newHTTP1Transport(rawDialer, baseTLSConfig) - var fallback atomic.Bool h2Transport, err := ConfigureHTTP2Transport(options) if err != nil { return nil, err } h2Transport.DialTLSContext = func(ctx context.Context, network, addr string, _ *stdTLS.Config) (net.Conn, error) { - conn, dialErr := dialTLS(ctx, rawDialer, baseTLSConfig, M.ParseSocksaddr(addr), []string{http2.NextProtoTLS, "http/1.1"}, http2.NextProtoTLS) - if dialErr != nil { - if errors.Is(dialErr, errHTTP2Fallback) { - fallback.Store(true) - } - return nil, dialErr - } - return conn, nil + return dialTLS(ctx, rawDialer, baseTLSConfig, M.ParseSocksaddr(addr), []string{http2.NextProtoTLS, "http/1.1"}, http2.NextProtoTLS) } return &http2FallbackTransport{ - h2Transport: h2Transport, - h1Transport: h1, - h2Fallback: &fallback, + h2Transport: h2Transport, + h1Transport: h1, + fallbackAuthority: make(map[string]struct{}), }, nil } +func (t *http2FallbackTransport) isH2Fallback(authority string) bool { + if authority == "" { + return false + } + t.fallbackAccess.RLock() + _, found := t.fallbackAuthority[authority] + t.fallbackAccess.RUnlock() + return found +} + +func (t *http2FallbackTransport) markH2Fallback(authority string) { + if authority == "" { + return + } + t.fallbackAccess.Lock() + t.fallbackAuthority[authority] = struct{}{} + t.fallbackAccess.Unlock() +} + func (t *http2FallbackTransport) RoundTrip(request *http.Request) (*http.Response, error) { return t.roundTrip(request, true) } @@ -57,7 +69,8 @@ func (t *http2FallbackTransport) roundTrip(request *http.Request, allowHTTP1Fall if request.URL.Scheme != "https" || requestRequiresHTTP1(request) { return t.h1Transport.RoundTrip(request) } - if t.h2Fallback.Load() { + authority := requestAuthority(request) + if t.isH2Fallback(authority) { if !allowHTTP1Fallback { return nil, errHTTP2Fallback } @@ -70,6 +83,7 @@ func (t *http2FallbackTransport) roundTrip(request *http.Request, allowHTTP1Fall if !errors.Is(err, errHTTP2Fallback) || !allowHTTP1Fallback { return nil, err } + t.markH2Fallback(authority) return t.h1Transport.RoundTrip(cloneRequestForRetry(request)) } diff --git a/common/httpclient/http2_fallback_transport_test.go b/common/httpclient/http2_fallback_transport_test.go new file mode 100644 index 0000000000..2c2085c863 --- /dev/null +++ b/common/httpclient/http2_fallback_transport_test.go @@ -0,0 +1,37 @@ +package httpclient + +import ( + "testing" +) + +func TestHTTP2FallbackAuthorityIsolation(t *testing.T) { + transport := &http2FallbackTransport{fallbackAuthority: make(map[string]struct{})} + + transport.markH2Fallback("a.example:443") + if !transport.isH2Fallback("a.example:443") { + t.Fatal("a.example:443 should be marked") + } + if transport.isH2Fallback("b.example:443") { + t.Fatal("b.example:443 must remain unmarked after marking a.example") + } + + transport.markH2Fallback("b.example:443") + if !transport.isH2Fallback("b.example:443") { + t.Fatal("b.example:443 should be marked after explicit mark") + } + if !transport.isH2Fallback("a.example:443") { + t.Fatal("a.example:443 mark must survive marking another authority") + } +} + +func TestHTTP2FallbackEmptyAuthorityNoOp(t *testing.T) { + transport := &http2FallbackTransport{fallbackAuthority: make(map[string]struct{})} + + transport.markH2Fallback("") + if len(transport.fallbackAuthority) != 0 { + t.Fatalf("empty authority must not be stored, got %d entries", len(transport.fallbackAuthority)) + } + if transport.isH2Fallback("") { + t.Fatal("isH2Fallback must be false for empty authority") + } +} diff --git a/common/httpclient/http3_transport.go b/common/httpclient/http3_transport.go index 0b8855d7cd..d3eb5bc155 100644 --- a/common/httpclient/http3_transport.go +++ b/common/httpclient/http3_transport.go @@ -24,13 +24,17 @@ type http3Transport struct { h3Transport *http3.Transport } +type http3BrokenEntry struct { + until time.Time + backoff time.Duration +} + type http3FallbackTransport struct { h3Transport *http3.Transport h2Fallback innerTransport fallbackDelay time.Duration brokenAccess sync.Mutex - brokenUntil time.Time - brokenBackoff time.Duration + broken map[string]http3BrokenEntry } func newHTTP3RoundTripper( @@ -114,6 +118,7 @@ func newHTTP3FallbackTransport( h3Transport: newHTTP3RoundTripper(rawDialer, baseTLSConfig, options), h2Fallback: h2Fallback, fallbackDelay: fallbackDelay, + broken: make(map[string]http3BrokenEntry), }, nil } @@ -138,31 +143,32 @@ func (t *http3FallbackTransport) RoundTrip(request *http.Request) (*http.Respons } func (t *http3FallbackTransport) roundTripHTTP3(request *http.Request) (*http.Response, error) { - if t.h3Broken() { + authority := requestAuthority(request) + if t.h3Broken(authority) { return t.h2FallbackRoundTrip(request) } response, err := t.h3Transport.RoundTripOpt(request, http3.RoundTripOpt{OnlyCachedConn: true}) if err == nil { - t.clearH3Broken() + t.clearH3Broken(authority) return response, nil } if !errors.Is(err, http3.ErrNoCachedConn) { - t.markH3Broken() + t.markH3Broken(authority) return t.h2FallbackRoundTrip(cloneRequestForRetry(request)) } if !requestReplayable(request) { response, err = t.h3Transport.RoundTrip(request) if err == nil { - t.clearH3Broken() + t.clearH3Broken(authority) return response, nil } - t.markH3Broken() + t.markH3Broken(authority) return nil, err } - return t.roundTripHTTP3Race(request) + return t.roundTripHTTP3Race(request, authority) } -func (t *http3FallbackTransport) roundTripHTTP3Race(request *http.Request) (*http.Response, error) { +func (t *http3FallbackTransport) roundTripHTTP3Race(request *http.Request, authority string) (*http.Response, error) { ctx, cancel := context.WithCancel(request.Context()) defer cancel() type result struct { @@ -215,13 +221,13 @@ func (t *http3FallbackTransport) roundTripHTTP3Race(request *http.Request) (*htt received++ if raceResult.err == nil { if raceResult.h3 { - t.clearH3Broken() + t.clearH3Broken(authority) } drainRemaining() return raceResult.response, nil } if raceResult.h3 { - t.markH3Broken() + t.markH3Broken(authority) h3Err = raceResult.err if goroutines == 1 { goroutines++ @@ -269,29 +275,47 @@ func (t *http3FallbackTransport) Close() error { return t.h3Transport.Close() } -func (t *http3FallbackTransport) h3Broken() bool { +func (t *http3FallbackTransport) h3Broken(authority string) bool { + if authority == "" { + return false + } t.brokenAccess.Lock() defer t.brokenAccess.Unlock() - return !t.brokenUntil.IsZero() && time.Now().Before(t.brokenUntil) + entry, found := t.broken[authority] + if !found { + return false + } + if entry.until.IsZero() || !time.Now().Before(entry.until) { + delete(t.broken, authority) + return false + } + return true } -func (t *http3FallbackTransport) clearH3Broken() { +func (t *http3FallbackTransport) clearH3Broken(authority string) { + if authority == "" { + return + } t.brokenAccess.Lock() - t.brokenUntil = time.Time{} - t.brokenBackoff = 0 + delete(t.broken, authority) t.brokenAccess.Unlock() } -func (t *http3FallbackTransport) markH3Broken() { +func (t *http3FallbackTransport) markH3Broken(authority string) { + if authority == "" { + return + } t.brokenAccess.Lock() defer t.brokenAccess.Unlock() - if t.brokenBackoff == 0 { - t.brokenBackoff = 5 * time.Minute + entry := t.broken[authority] + if entry.backoff == 0 { + entry.backoff = 5 * time.Minute } else { - t.brokenBackoff *= 2 - if t.brokenBackoff > 48*time.Hour { - t.brokenBackoff = 48 * time.Hour + entry.backoff *= 2 + if entry.backoff > 48*time.Hour { + entry.backoff = 48 * time.Hour } } - t.brokenUntil = time.Now().Add(t.brokenBackoff) + entry.until = time.Now().Add(entry.backoff) + t.broken[authority] = entry } diff --git a/common/httpclient/http3_transport_test.go b/common/httpclient/http3_transport_test.go new file mode 100644 index 0000000000..600e88db06 --- /dev/null +++ b/common/httpclient/http3_transport_test.go @@ -0,0 +1,99 @@ +//go:build with_quic + +package httpclient + +import ( + "testing" + "time" +) + +func TestHTTP3BrokenAuthorityIsolation(t *testing.T) { + transport := &http3FallbackTransport{broken: make(map[string]http3BrokenEntry)} + + transport.markH3Broken("a.example:443") + if !transport.h3Broken("a.example:443") { + t.Fatal("a.example:443 should be broken after mark") + } + if transport.h3Broken("b.example:443") { + t.Fatal("b.example:443 must not be affected by marking a.example") + } +} + +func TestHTTP3BrokenBackoffPerAuthority(t *testing.T) { + transport := &http3FallbackTransport{broken: make(map[string]http3BrokenEntry)} + + transport.markH3Broken("a.example:443") + if transport.broken["a.example:443"].backoff != 5*time.Minute { + t.Fatalf("first mark should set backoff to 5m, got %v", transport.broken["a.example:443"].backoff) + } + transport.markH3Broken("a.example:443") + if transport.broken["a.example:443"].backoff != 10*time.Minute { + t.Fatalf("second mark should double backoff to 10m, got %v", transport.broken["a.example:443"].backoff) + } + transport.markH3Broken("a.example:443") + if transport.broken["a.example:443"].backoff != 20*time.Minute { + t.Fatalf("third mark should double to 20m, got %v", transport.broken["a.example:443"].backoff) + } + + if _, found := transport.broken["b.example:443"]; found { + t.Fatal("marking a.example must not leak into b.example backoff state") + } + + transport.markH3Broken("b.example:443") + if transport.broken["b.example:443"].backoff != 5*time.Minute { + t.Fatalf("b.example first mark should start at 5m independent of a.example, got %v", transport.broken["b.example:443"].backoff) + } +} + +func TestHTTP3BrokenBackoffCap(t *testing.T) { + transport := &http3FallbackTransport{broken: make(map[string]http3BrokenEntry)} + + transport.broken["a.example:443"] = http3BrokenEntry{backoff: 48 * time.Hour, until: time.Now().Add(48 * time.Hour)} + transport.markH3Broken("a.example:443") + if transport.broken["a.example:443"].backoff != 48*time.Hour { + t.Fatalf("backoff must cap at 48h, got %v", transport.broken["a.example:443"].backoff) + } +} + +func TestHTTP3BrokenClearDeletesEntry(t *testing.T) { + transport := &http3FallbackTransport{broken: make(map[string]http3BrokenEntry)} + + transport.markH3Broken("a.example:443") + transport.markH3Broken("b.example:443") + transport.clearH3Broken("a.example:443") + + if _, found := transport.broken["a.example:443"]; found { + t.Fatal("clearH3Broken must delete the entry") + } + if !transport.h3Broken("b.example:443") { + t.Fatal("clearing a.example must not affect b.example") + } +} + +func TestHTTP3BrokenExpiredEntryGarbageCollected(t *testing.T) { + transport := &http3FallbackTransport{broken: make(map[string]http3BrokenEntry)} + + transport.broken["a.example:443"] = http3BrokenEntry{ + backoff: 5 * time.Minute, + until: time.Now().Add(-time.Second), + } + if transport.h3Broken("a.example:443") { + t.Fatal("expired entry must report not broken") + } + if _, found := transport.broken["a.example:443"]; found { + t.Fatal("expired entry must be garbage-collected on read") + } +} + +func TestHTTP3BrokenEmptyAuthorityNoOp(t *testing.T) { + transport := &http3FallbackTransport{broken: make(map[string]http3BrokenEntry)} + + transport.markH3Broken("") + if len(transport.broken) != 0 { + t.Fatalf("markH3Broken must ignore empty authority, got %d entries", len(transport.broken)) + } + if transport.h3Broken("") { + t.Fatal("h3Broken must return false for empty authority") + } + transport.clearH3Broken("") +} From 32f56cce079a413b5d26db476a6b164ecc956b96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 17 Apr 2026 16:26:51 +0800 Subject: [PATCH 41/93] Defer implicit default HTTP client fallback to first use --- common/httpclient/manager.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/common/httpclient/manager.go b/common/httpclient/manager.go index 2b4f9d5be3..614e4f83bd 100644 --- a/common/httpclient/manager.go +++ b/common/httpclient/manager.go @@ -69,21 +69,25 @@ func (m *Manager) Start(stage adapter.StartStage) error { return E.Cause(err, "resolve default http client") } m.defaultTransport = sharedTransport - } else if m.defaultTransportFallback != nil { + } + return nil +} + +func (m *Manager) DefaultTransport() adapter.HTTPTransport { + m.access.Lock() + defer m.access.Unlock() + if m.defaultTransport == nil && m.defaultTransportFallback != nil { transport, err := m.defaultTransportFallback() if err != nil { - return E.Cause(err, "create default http client") + m.logger.Error(E.Cause(err, "create default http client")) + return nil } - m.trackTransport(transport) + m.managedTransports = append(m.managedTransports, transport) m.defaultTransport = &sharedManagedTransport{ managed: transport, shared: &sharedState{}, } } - return nil -} - -func (m *Manager) DefaultTransport() adapter.HTTPTransport { if m.defaultTransport == nil { return nil } From 3c9c4aee0754e118e4344f9dc3a3f3288b562890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 17 Apr 2026 18:03:42 +0800 Subject: [PATCH 42/93] Strip EDNS padding from upstream DNS responses --- dns/client.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/dns/client.go b/dns/client.go index 37ba98a84f..53ab4ccd62 100644 --- a/dns/client.go +++ b/dns/client.go @@ -536,11 +536,24 @@ func (c *Client) prepareExchangeMessage(message *dns.Msg, options adapter.DNSQue return message } +func stripDNSPadding(response *dns.Msg) { + for _, record := range response.Extra { + opt, isOpt := record.(*dns.OPT) + if !isOpt { + continue + } + opt.Option = common.Filter(opt.Option, func(it dns.EDNS0) bool { + return it.Option() != dns.EDNS0PADDING + }) + } +} + func (c *Client) exchangeToTransport(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg) (*dns.Msg, error) { ctx, cancel := context.WithTimeout(ctx, c.timeout) defer cancel() response, err := transport.Exchange(ctx, message) if err == nil { + stripDNSPadding(response) return response, nil } var rcodeError RcodeError From a8b8f15a7645a5eb0e6919c8c50258a1430342ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 18 Apr 2026 11:39:01 +0800 Subject: [PATCH 43/93] Fix Apple TLS metadata capture --- common/tls/apple_client_platform_darwin.m | 55 ++++++++--------------- 1 file changed, 18 insertions(+), 37 deletions(-) diff --git a/common/tls/apple_client_platform_darwin.m b/common/tls/apple_client_platform_darwin.m index c4a6c19f67..d03f9fff93 100644 --- a/common/tls/apple_client_platform_darwin.m +++ b/common/tls/apple_client_platform_darwin.m @@ -285,26 +285,10 @@ static bool box_apple_tls_state_copy(const box_apple_tls_state_t *source, box_ap return false; } -static bool box_apple_tls_state_load(nw_connection_t connection, box_apple_tls_state_t *state, char **error_out) { - box_apple_tls_state_reset(state); - if (connection == nil) { - box_set_error_message(error_out, "apple TLS: invalid client"); - return false; - } - - nw_protocol_definition_t tls_definition = nw_protocol_copy_tls_definition(); - nw_protocol_metadata_t metadata = nw_connection_copy_protocol_metadata(connection, tls_definition); - if (metadata == NULL || !nw_protocol_metadata_is_tls(metadata)) { - box_set_error_message(error_out, "apple TLS: metadata unavailable"); - return false; - } - - sec_protocol_metadata_t sec_metadata = nw_tls_copy_sec_protocol_metadata(metadata); - if (sec_metadata == NULL) { - box_set_error_message(error_out, "apple TLS: metadata unavailable"); - return false; - } - +// Captures TLS negotiation results from the verify block. The sec_metadata +// exposed here is live for the duration of the handshake; the one retrieved +// after nw_connection_state_ready may return stale ALPN/server_name buffers. +static void box_apple_tls_state_load(sec_protocol_metadata_t sec_metadata, box_apple_tls_state_t *state) { state->version = (uint16_t)sec_protocol_metadata_get_negotiated_tls_protocol_version(sec_metadata); state->cipher_suite = (uint16_t)sec_protocol_metadata_get_negotiated_tls_ciphersuite(sec_metadata); state->alpn = box_apple_tls_metadata_copy_negotiated_protocol(sec_metadata); @@ -329,15 +313,11 @@ static bool box_apple_tls_state_load(nw_connection_t connection, box_apple_tls_s }); if (chain_data.length > 0) { state->peer_cert_chain = malloc(chain_data.length); - if (state->peer_cert_chain == NULL) { - box_set_error_message(error_out, "apple TLS: out of memory"); - box_apple_tls_state_reset(state); - return false; + if (state->peer_cert_chain != NULL) { + memcpy(state->peer_cert_chain, chain_data.bytes, chain_data.length); + state->peer_cert_chain_len = chain_data.length; } - memcpy(state->peer_cert_chain, chain_data.bytes, chain_data.length); - state->peer_cert_chain_len = chain_data.length; } - return true; } box_apple_tls_client_t *box_apple_tls_client_create( @@ -388,15 +368,12 @@ static bool box_apple_tls_state_load(nw_connection_t connection, box_apple_tls_s sec_protocol_options_add_tls_application_protocol(sec_options, protocol.UTF8String); } sec_protocol_options_set_peer_authentication_required(sec_options, !insecure); - if (insecure) { - sec_protocol_options_set_verify_block(sec_options, ^(sec_protocol_metadata_t metadata, sec_trust_t trust, sec_protocol_verify_complete_t complete) { - complete(true); - }, box_apple_tls_client_queue(client)); - } else if (verifyDate != nil || anchors.count > 0 || anchor_only) { - sec_protocol_options_set_verify_block(sec_options, ^(sec_protocol_metadata_t metadata, sec_trust_t trust, sec_protocol_verify_complete_t complete) { - complete(box_evaluate_trust(trust, anchors, anchor_only, verifyDate)); - }, box_apple_tls_client_queue(client)); - } + sec_protocol_options_set_verify_block(sec_options, ^(sec_protocol_metadata_t metadata, sec_trust_t trust, sec_protocol_verify_complete_t complete) { + if (client->state.version == 0) { + box_apple_tls_state_load(metadata, &client->state); + } + complete(insecure || box_evaluate_trust(trust, anchors, anchor_only, verifyDate)); + }, box_apple_tls_client_queue(client)); }, NW_PARAMETERS_DEFAULT_CONFIGURATION); nw_connection_t connection = box_apple_tls_create_connection(connected_socket, parameters); @@ -420,7 +397,11 @@ static bool box_apple_tls_state_load(nw_connection_t connection, box_apple_tls_s switch (state) { case nw_connection_state_ready: if (!atomic_load(&client->ready_done)) { - atomic_store(&client->ready, box_apple_tls_state_load(connection, &client->state, &client->ready_error)); + if (client->state.version == 0) { + box_set_error_message(&client->ready_error, "apple TLS: metadata unavailable"); + } else { + atomic_store(&client->ready, true); + } atomic_store(&client->ready_done, true); dispatch_semaphore_signal(box_apple_tls_ready_semaphore(client)); } From 6ecd3deef5b63ce9891ff5446d7841115364a64b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 17 Apr 2026 16:51:53 +0800 Subject: [PATCH 44/93] Fix tls-spoof --- common/tls/client.go | 10 +- common/tls/client_test.go | 154 +++++++++++ common/tls/std_client.go | 2 +- common/tls/utls_client.go | 2 +- common/tls/utls_client_test.go | 73 ++++++ common/tlsspoof/client_hello.go | 103 ++------ common/tlsspoof/client_hello_test.go | 102 ++++---- common/tlsspoof/conn_test.go | 267 +++++++++++++++++++- common/tlsspoof/integration_test.go | 33 ++- common/tlsspoof/integration_tls_test.go | 118 +++++++++ common/tlsspoof/integration_unix_test.go | 97 +++++-- common/tlsspoof/integration_windows_test.go | 27 +- common/tlsspoof/packet_test.go | 59 +++++ common/tlsspoof/raw_darwin.go | 39 ++- common/tlsspoof/raw_linux.go | 24 +- common/tlsspoof/raw_stub.go | 2 +- common/tlsspoof/raw_windows.go | 32 ++- common/tlsspoof/spoof.go | 56 ++-- common/windivert/handle_windows.go | 6 +- common/windivert/handle_windows_test.go | 3 + common/windivert/windivert.go | 9 +- 21 files changed, 980 insertions(+), 238 deletions(-) create mode 100644 common/tls/client_test.go create mode 100644 common/tls/utls_client_test.go create mode 100644 common/tlsspoof/integration_tls_test.go diff --git a/common/tls/client.go b/common/tls/client.go index 35c628c11e..b5b975bf23 100644 --- a/common/tls/client.go +++ b/common/tls/client.go @@ -6,6 +6,7 @@ import ( "errors" "net" "os" + "strings" "github.com/sagernet/sing-box/common/badtls" "github.com/sagernet/sing-box/common/tlsspoof" @@ -33,6 +34,9 @@ func parseTLSSpoofOptions(serverName string, options option.OutboundTLSOptions) if options.DisableSNI || serverName == "" || M.ParseAddr(serverName).IsValid() { return "", 0, E.New("`spoof` requires TLS ClientHello with SNI") } + if strings.EqualFold(options.Spoof, serverName) { + return "", 0, E.New("`spoof` must differ from `server_name`") + } method, err := tlsspoof.ParseMethod(options.SpoofMethod) if err != nil { return "", 0, err @@ -44,11 +48,7 @@ func applyTLSSpoof(conn net.Conn, spoof string, method tlsspoof.Method) (net.Con if spoof == "" { return conn, nil } - spoofer, err := tlsspoof.NewSpoofer(conn, method) - if err != nil { - return nil, err - } - return tlsspoof.NewConn(conn, spoofer, spoof), nil + return tlsspoof.NewConn(conn, method, spoof) } func NewDialerFromOptions(ctx context.Context, logger logger.ContextLogger, dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) { diff --git a/common/tls/client_test.go b/common/tls/client_test.go new file mode 100644 index 0000000000..5bc939e29e --- /dev/null +++ b/common/tls/client_test.go @@ -0,0 +1,154 @@ +package tls + +import ( + "context" + "crypto/tls" + "net" + "testing" + + tf "github.com/sagernet/sing-box/common/tlsfragment" + "github.com/sagernet/sing-box/common/tlsspoof" + "github.com/sagernet/sing-box/option" + + "github.com/stretchr/testify/require" +) + +func TestParseTLSSpoofOptions_Disabled(t *testing.T) { + t.Parallel() + spoof, method, err := parseTLSSpoofOptions("example.com", option.OutboundTLSOptions{}) + require.NoError(t, err) + require.Empty(t, spoof) + require.Equal(t, tlsspoof.MethodWrongSequence, method) +} + +func TestParseTLSSpoofOptions_MethodWithoutSpoof(t *testing.T) { + t.Parallel() + _, _, err := parseTLSSpoofOptions("example.com", option.OutboundTLSOptions{ + SpoofMethod: tlsspoof.MethodNameWrongChecksum, + }) + require.Error(t, err) +} + +func TestParseTLSSpoofOptions_IPLiteralRejected(t *testing.T) { + t.Parallel() + _, _, err := parseTLSSpoofOptions("1.2.3.4", option.OutboundTLSOptions{ + Spoof: "example.com", + }) + require.Error(t, err) +} + +func TestParseTLSSpoofOptions_EmptyServerNameRejected(t *testing.T) { + t.Parallel() + _, _, err := parseTLSSpoofOptions("", option.OutboundTLSOptions{ + Spoof: "example.com", + }) + require.Error(t, err) +} + +func TestParseTLSSpoofOptions_DisableSNIRejected(t *testing.T) { + t.Parallel() + _, _, err := parseTLSSpoofOptions("example.com", option.OutboundTLSOptions{ + Spoof: "decoy.com", + DisableSNI: true, + }) + require.Error(t, err) +} + +// TestParseTLSSpoofOptions_RejectsSameSNI is the primary regression test for +// the "spoofed packet contains the original SNI" bug report: when a user +// configures spoof equal to server_name, the rewriter produces a byte-identical +// record, so the fake and real ClientHellos on the wire look the same. Reject +// at parse time. +func TestParseTLSSpoofOptions_RejectsSameSNI(t *testing.T) { + t.Parallel() + _, _, err := parseTLSSpoofOptions("example.com", option.OutboundTLSOptions{ + Spoof: "example.com", + }) + require.Error(t, err) + + _, _, err = parseTLSSpoofOptions("example.com", option.OutboundTLSOptions{ + Spoof: "EXAMPLE.com", + }) + require.Error(t, err, "comparison must be case-insensitive") +} + +func TestParseTLSSpoofOptions_UnknownMethodRejected(t *testing.T) { + t.Parallel() + _, _, err := parseTLSSpoofOptions("example.com", option.OutboundTLSOptions{ + Spoof: "decoy.com", + SpoofMethod: "nonsense", + }) + require.Error(t, err) +} + +func TestParseTLSSpoofOptions_DistinctSNIAccepted(t *testing.T) { + t.Parallel() + if !tlsspoof.PlatformSupported { + t.Skip("tlsspoof not supported on this platform") + } + spoof, method, err := parseTLSSpoofOptions("example.com", option.OutboundTLSOptions{ + Spoof: "decoy.com", + SpoofMethod: tlsspoof.MethodNameWrongSequence, + }) + require.NoError(t, err) + require.Equal(t, "decoy.com", spoof) + require.Equal(t, tlsspoof.MethodWrongSequence, method) +} + +// The following tests guard the wrap gate in STDClientConfig.Client(): +// tf.Conn must wrap the underlying connection whenever either `fragment` or +// `record_fragment` is set, so that TLS fragmentation coexists with features +// like tls_spoof that layer on top of tf.Conn. + +func newSTDClientConfigForGateTest(fragment, recordFragment bool) *STDClientConfig { + return &STDClientConfig{ + ctx: context.Background(), + config: &tls.Config{ServerName: "example.com", InsecureSkipVerify: true}, + fragment: fragment, + recordFragment: recordFragment, + } +} + +func TestSTDClient_Client_NoFragment_DoesNotWrap(t *testing.T) { + t.Parallel() + client, server := net.Pipe() + defer client.Close() + defer server.Close() + wrapped, err := newSTDClientConfigForGateTest(false, false).Client(client) + require.NoError(t, err) + _, isTF := wrapped.NetConn().(*tf.Conn) + require.False(t, isTF, "no fragment flags: must not wrap with tf.Conn") +} + +func TestSTDClient_Client_FragmentOnly_Wraps(t *testing.T) { + t.Parallel() + client, server := net.Pipe() + defer client.Close() + defer server.Close() + wrapped, err := newSTDClientConfigForGateTest(true, false).Client(client) + require.NoError(t, err) + _, isTF := wrapped.NetConn().(*tf.Conn) + require.True(t, isTF, "fragment=true: must wrap with tf.Conn") +} + +func TestSTDClient_Client_RecordFragmentOnly_Wraps(t *testing.T) { + t.Parallel() + client, server := net.Pipe() + defer client.Close() + defer server.Close() + wrapped, err := newSTDClientConfigForGateTest(false, true).Client(client) + require.NoError(t, err) + _, isTF := wrapped.NetConn().(*tf.Conn) + require.True(t, isTF, "record_fragment=true: must wrap with tf.Conn") +} + +func TestSTDClient_Client_BothFragment_Wraps(t *testing.T) { + t.Parallel() + client, server := net.Pipe() + defer client.Close() + defer server.Close() + wrapped, err := newSTDClientConfigForGateTest(true, true).Client(client) + require.NoError(t, err) + _, isTF := wrapped.NetConn().(*tf.Conn) + require.True(t, isTF, "both fragment flags: must wrap with tf.Conn") +} diff --git a/common/tls/std_client.go b/common/tls/std_client.go index f38981c687..031a256f7d 100644 --- a/common/tls/std_client.go +++ b/common/tls/std_client.go @@ -75,7 +75,7 @@ func (c *STDClientConfig) STDConfig() (*STDConfig, error) { } func (c *STDClientConfig) Client(conn net.Conn) (Conn, error) { - if c.recordFragment { + if c.fragment || c.recordFragment { conn = tf.NewConn(conn, c.ctx, c.fragment, c.recordFragment, c.fragmentFallbackDelay) } conn, err := applyTLSSpoof(conn, c.spoof, c.spoofMethod) diff --git a/common/tls/utls_client.go b/common/tls/utls_client.go index a8b91973c2..1cc41554fa 100644 --- a/common/tls/utls_client.go +++ b/common/tls/utls_client.go @@ -83,7 +83,7 @@ func (c *UTLSClientConfig) STDConfig() (*STDConfig, error) { } func (c *UTLSClientConfig) Client(conn net.Conn) (Conn, error) { - if c.recordFragment { + if c.fragment || c.recordFragment { conn = tf.NewConn(conn, c.ctx, c.fragment, c.recordFragment, c.fragmentFallbackDelay) } conn, err := applyTLSSpoof(conn, c.spoof, c.spoofMethod) diff --git a/common/tls/utls_client_test.go b/common/tls/utls_client_test.go new file mode 100644 index 0000000000..48c1e327ec --- /dev/null +++ b/common/tls/utls_client_test.go @@ -0,0 +1,73 @@ +//go:build with_utls + +package tls + +import ( + "context" + "net" + "testing" + + tf "github.com/sagernet/sing-box/common/tlsfragment" + + utls "github.com/metacubex/utls" + "github.com/stretchr/testify/require" +) + +// Guards the wrap gate in UTLSClientConfig.Client(): tf.Conn must wrap the +// underlying connection whenever either `fragment` or `record_fragment` is +// set. Mirrors the STDClientConfig gate tests to keep both code paths in +// lockstep. + +func newUTLSClientConfigForGateTest(fragment, recordFragment bool) *UTLSClientConfig { + return &UTLSClientConfig{ + ctx: context.Background(), + config: &utls.Config{ServerName: "example.com", InsecureSkipVerify: true}, + id: utls.HelloChrome_Auto, + fragment: fragment, + recordFragment: recordFragment, + } +} + +func TestUTLSClient_Client_NoFragment_DoesNotWrap(t *testing.T) { + t.Parallel() + client, server := net.Pipe() + defer client.Close() + defer server.Close() + wrapped, err := newUTLSClientConfigForGateTest(false, false).Client(client) + require.NoError(t, err) + _, isTF := wrapped.NetConn().(*tf.Conn) + require.False(t, isTF, "no fragment flags: must not wrap with tf.Conn") +} + +func TestUTLSClient_Client_FragmentOnly_Wraps(t *testing.T) { + t.Parallel() + client, server := net.Pipe() + defer client.Close() + defer server.Close() + wrapped, err := newUTLSClientConfigForGateTest(true, false).Client(client) + require.NoError(t, err) + _, isTF := wrapped.NetConn().(*tf.Conn) + require.True(t, isTF, "fragment=true: must wrap with tf.Conn") +} + +func TestUTLSClient_Client_RecordFragmentOnly_Wraps(t *testing.T) { + t.Parallel() + client, server := net.Pipe() + defer client.Close() + defer server.Close() + wrapped, err := newUTLSClientConfigForGateTest(false, true).Client(client) + require.NoError(t, err) + _, isTF := wrapped.NetConn().(*tf.Conn) + require.True(t, isTF, "record_fragment=true: must wrap with tf.Conn") +} + +func TestUTLSClient_Client_BothFragment_Wraps(t *testing.T) { + t.Parallel() + client, server := net.Pipe() + defer client.Close() + defer server.Close() + wrapped, err := newUTLSClientConfigForGateTest(true, true).Client(client) + require.NoError(t, err) + _, isTF := wrapped.NetConn().(*tf.Conn) + require.True(t, isTF, "both fragment flags: must wrap with tf.Conn") +} diff --git a/common/tlsspoof/client_hello.go b/common/tlsspoof/client_hello.go index 0ca7c5a9f2..abdfa31753 100644 --- a/common/tlsspoof/client_hello.go +++ b/common/tlsspoof/client_hello.go @@ -1,86 +1,37 @@ package tlsspoof import ( - "encoding/binary" + "bytes" + "context" + "crypto/tls" - tf "github.com/sagernet/sing-box/common/tlsfragment" + "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" ) -const ( - recordLengthOffset = 3 - handshakeLengthOffset = 6 -) - -// server_name extension layout (RFC 6066 §3). Offsets are relative to the -// SNI host name (index returned by the parser): -// -// ... uint16 extension_type = 0x0000 (host_name - 9) -// ... uint16 extension_data_length (host_name - 7) -// ... uint16 server_name_list_length (host_name - 5) -// ... uint8 name_type = host_name (host_name - 3) -// ... uint16 host_name_length (host_name - 2) -// sni host_name (host_name) -const ( - extensionDataLengthOffsetFromSNI = -7 - listLengthOffsetFromSNI = -5 - hostNameLengthOffsetFromSNI = -2 -) - -func rewriteSNI(record []byte, fakeSNI string) ([]byte, error) { - if len(fakeSNI) > 0xFFFF { - return nil, E.New("fake SNI too long: ", len(fakeSNI), " bytes") - } - serverName := tf.IndexTLSServerName(record) - if serverName == nil { - return nil, E.New("not a ClientHello with SNI") - } - - delta := len(fakeSNI) - serverName.Length - out := make([]byte, len(record)+delta) - copy(out, record[:serverName.Index]) - copy(out[serverName.Index:], fakeSNI) - copy(out[serverName.Index+len(fakeSNI):], record[serverName.Index+serverName.Length:]) - - err := patchUint16(out, recordLengthOffset, delta) - if err != nil { - return nil, E.Cause(err, "patch record length") +// buildFakeClientHello drives crypto/tls against a write-only in-memory conn +// to capture a generated ClientHello. CurvePreferences pins classical groups +// to suppress Go's default X25519MLKEM768 hybrid key share; without this the +// post-quantum public key alone (~1184 bytes) pushes the record past one MSS, +// and middleboxes do not reassemble fragmented ClientHellos. The handshake +// error is discarded because the stub conn's Read returns immediately. +func buildFakeClientHello(sni string) ([]byte, error) { + if sni == "" { + return nil, E.New("empty sni") } - err = patchUint24(out, handshakeLengthOffset, delta) - if err != nil { - return nil, E.Cause(err, "patch handshake length") - } - for _, off := range []int{ - serverName.ExtensionsListLengthIndex, - serverName.Index + extensionDataLengthOffsetFromSNI, - serverName.Index + listLengthOffsetFromSNI, - serverName.Index + hostNameLengthOffsetFromSNI, - } { - err = patchUint16(out, off, delta) - if err != nil { - return nil, E.Cause(err, "patch length at offset ", off) - } - } - return out, nil -} - -func patchUint16(data []byte, offset, delta int) error { - patched := int(binary.BigEndian.Uint16(data[offset:])) + delta - if patched < 0 || patched > 0xFFFF { - return E.New("uint16 out of range: ", patched) - } - binary.BigEndian.PutUint16(data[offset:], uint16(patched)) - return nil -} - -func patchUint24(data []byte, offset, delta int) error { - original := int(data[offset])<<16 | int(data[offset+1])<<8 | int(data[offset+2]) - patched := original + delta - if patched < 0 || patched > 0xFFFFFF { - return E.New("uint24 out of range: ", patched) + var buf bytes.Buffer + tlsConn := tls.Client(bufio.NewWriteOnlyConn(&buf), &tls.Config{ + ServerName: sni, + // Order matches what browsers advertised before post-quantum. + CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256, tls.CurveP384}, + MinVersion: tls.VersionTLS12, + MaxVersion: tls.VersionTLS13, + NextProtos: []string{"h2", "http/1.1"}, + InsecureSkipVerify: true, + }) + _ = tlsConn.HandshakeContext(context.Background()) + if buf.Len() == 0 { + return nil, E.New("tls ClientHello not produced") } - data[offset] = byte(patched >> 16) - data[offset+1] = byte(patched >> 8) - data[offset+2] = byte(patched) - return nil + return buf.Bytes(), nil } diff --git a/common/tlsspoof/client_hello_test.go b/common/tlsspoof/client_hello_test.go index 746d0482ad..3eb7a2e040 100644 --- a/common/tlsspoof/client_hello_test.go +++ b/common/tlsspoof/client_hello_test.go @@ -1,8 +1,9 @@ package tlsspoof import ( + "bytes" "encoding/binary" - "encoding/hex" + "strings" "testing" tf "github.com/sagernet/sing-box/common/tlsfragment" @@ -10,70 +11,73 @@ import ( "github.com/stretchr/testify/require" ) -// realClientHello is a captured Chrome ClientHello for github.com, -// reused from common/tlsfragment/index_test.go. -const realClientHello = "16030105f8010005f403036e35de7389a679c54029cf452611f2211c70d9ac3897271de589ab6155f8e4ab20637d225f1ef969ad87ed78bfb9d171300bcb1703b6f314ccefb964f79b7d0961002a0a0a130213031301c02cc02bcca9c030c02fcca8c00ac009c014c013009d009c0035002fc008c012000a01000581baba00000000000f000d00000a6769746875622e636f6d00170000ff01000100000a000e000c3a3a11ec001d001700180019000b000201000010000e000c02683208687474702f312e31000500050100000000000d00160014040308040401050308050805050108060601020100120000003304ef04ed3a3a00010011ec04c0aeb2250c092a3463161cccb29d9183331a424964248579507ed23a180b0ceab2a5f5d9ce41547e497a89055471ea572867ba3a1fc3c9e45025274a20f60c6b60e62476b6afed0403af59ab83660ef4112ae20386a602010d0a5d454c0ed34c84ed4423e750213e6a2baab1bf9c4367a6007ab40a33d95220c2dcaa44f257024a5626b545db0510f4311b1a60714154909c6a61fdfca011fb2626d657aeb6070bf078508babe3b584555013e34acc56198ed4663742b3155a664a9901794c4586820a7dc162c01827291f3792e1237f801a8d1ef096013c181c4a58d2f6859ba75022d18cc4418bd4f351d5c18f83a58857d05af860c4b9ac018a5b63f17184e591532c6bc2cf2215d4a282c8a8a4f6f7aee110422c8bc9ebd3b1d609c568523aaae555db320e6c269473d87af38c256cbb9febc20aea6380c32a8916f7a373c8b1e37554e3260bf6621f6b804ee80b3c516b1d01985bf4c603b6daa9a5991de6a7a29f3a7122b8afb843a7660110fce62b43c615f5bcc2db688ba012649c0952b0a2c031e732d2b454c6b2968683cb8d244be2c9a7fa163222979eaf92722b92b862d81a3d94450c2b60c318421ebb4307c42d1f0473592a5c30e42039cc68cda9721e61aa63f49def17c15221680ed444896340133bbee67556f56b9f9d78a4df715f926a12add0cc9c862e46ea8b7316ae468282c18601b2771c9c9322f982228cf93effaacd3f80cbd12bce5fc36f56e2a3caf91e578a5fae00c9b23a8ed1a66764f4433c3628a70b8f0a6196adc60a4cb4226f07ba4c6b363fe9065563bfc1347452946386bab488686e837ab979c64f9047417fca635fe1bb4f074f256cc8af837c7b455e280426547755af90a61640169ef180aea3a77e662bb6dac1b6c3696027129b1a5edf495314e9c7f4b6110e16378ec893fa24642330a40aba1a85326101acb97c620fd8d71389e69eaed7bdb01bbe1fd428d66191150c7b2cd1ad4257391676a82ba8ce07fb2667c3b289f159003a7c7bc31d361b7b7f49a802961739d950dfcc0fa1c7abce5abdd2245101da391151490862028110465950b9e9c03d08a90998ab83267838d2e74a0593bc81f74cdf734519a05b351c0e5488c68dd810e6e9142ccc1e2f4a7f464297eb340e27acc6b9d64e12e38cce8492b3d939140b5a9e149a75597f10a23874c84323a07cdd657274378f887c85c4259b9c04cd33ba58ed630ef2a744f8e19dd34843dff331d2a6be7e2332c599289cd248a611c73d7481cd4a9bd43449a3836f14b2af18a1739e17999e4c67e85cc5bcecabb14185e5bcaff3c96098f03dc5aba819f29587758f49f940585354a2a780830528d68ccd166920dadcaa25cab5fc1907272a826aba3f08bc6b88757776812ecb6c7cec69a223ec0a13a7b62a2349a0f63ed7a27a3b15ba21d71fe6864ec6e089ae17cadd433fa3138f7ee24353c11365818f8fc34f43a05542d18efaac24bfccc1f748a0cc1a67ad379468b76fd34973dba785f5c91d618333cd810fe0700d1bbc8422029782628070a624c52c5309a4a64d625b11f8033ab28df34a1add297517fcc06b92b6817b3c5144438cf260867c57bde68c8c4b82e6a135ef676a52fbae5708002a404e6189a60e2836de565ad1b29e3819e5ed49f6810bcb28e1bd6de57306f94b79d9dae1cc4624d2a068499beef81cd5fe4b76dcbfff2a2008001d002001976128c6d5a934533f28b9914d2480aab2a8c1ab03d212529ce8b27640a716002d00020101002b000706caca03040303001b00030200015a5a000100" +// x25519MLKEM768 is the IANA code point for the post-quantum hybrid named +// group (0x11EC). The fake ClientHello must never carry it — its 1184-byte +// key share is the reason kernel-generated ClientHellos exceed one MSS, and +// the reason this builder has to force CurvePreferences. +const x25519MLKEM768 uint16 = 0x11EC -func decodeClientHello(t *testing.T) []byte { - t.Helper() - payload, err := hex.DecodeString(realClientHello) +func TestBuildFakeClientHello_ParsesWithSNI(t *testing.T) { + t.Parallel() + record, err := buildFakeClientHello("example.com") require.NoError(t, err) - return payload -} -func assertConsistent(t *testing.T, payload []byte, expectedSNI string) { - t.Helper() - serverName := tf.IndexTLSServerName(payload) - require.NotNil(t, serverName, "parser should find SNI in rewritten payload") - require.Equal(t, expectedSNI, serverName.ServerName) - require.Equal(t, expectedSNI, string(payload[serverName.Index:serverName.Index+serverName.Length])) - // Record length must equal len(payload) - 5. - recordLen := binary.BigEndian.Uint16(payload[3:5]) - require.Equal(t, len(payload)-5, int(recordLen), "record length must equal payload - 5") - // Handshake length must equal len(payload) - 5 - 4. - handshakeLen := int(payload[6])<<16 | int(payload[7])<<8 | int(payload[8]) - require.Equal(t, len(payload)-5-4, handshakeLen, "handshake length must equal payload - 9") + serverName := tf.IndexTLSServerName(record) + require.NotNil(t, serverName, "output must parse as a ClientHello") + require.Equal(t, "example.com", serverName.ServerName) + + recordLen := binary.BigEndian.Uint16(record[3:5]) + require.Equal(t, len(record)-5, int(recordLen), + "record length header must match on-wire record size") + handshakeLen := int(record[6])<<16 | int(record[7])<<8 | int(record[8]) + require.Equal(t, len(record)-5-4, handshakeLen, + "handshake length header must match handshake body size") } -func TestRewriteSNI_ShorterReplacement(t *testing.T) { +// TestBuildFakeClientHello_FitsOneSegment is the regression guard for the +// whole point of the rewrite: the fake must never need fragmenting on a +// standard 1500-byte path MTU. 1200 leaves ~260 bytes for IP+TCP headers and +// a generous safety margin — the X25519MLKEM768 ClientHello this replaces +// hit ~1400+. +func TestBuildFakeClientHello_FitsOneSegment(t *testing.T) { t.Parallel() - payload := decodeClientHello(t) - out, err := rewriteSNI(payload, "a.io") - require.NoError(t, err) - require.Len(t, out, len(payload)-6) // original "github.com" is 10 bytes, "a.io" is 4 bytes. - assertConsistent(t, out, "a.io") + for _, sni := range []string{"a.io", "example.com", strings.Repeat("a", 253)} { + record, err := buildFakeClientHello(sni) + require.NoError(t, err, "sni=%q", sni) + require.Less(t, len(record), 1200, "sni=%q built %d bytes", sni, len(record)) + } } -func TestRewriteSNI_SameLengthReplacement(t *testing.T) { +// TestBuildFakeClientHello_NoPostQuantumKeyShare catches regressions that +// would accidentally pull an X25519MLKEM768 key share (the reason the prior +// implementation had to fragment) back into the fake — e.g. if CurvePreferences +// stopped being respected by a future Go version. +func TestBuildFakeClientHello_NoPostQuantumKeyShare(t *testing.T) { t.Parallel() - payload := decodeClientHello(t) - out, err := rewriteSNI(payload, "example.co") + record, err := buildFakeClientHello("example.com") require.NoError(t, err) - require.Len(t, out, len(payload)) - assertConsistent(t, out, "example.co") + + var needle [2]byte + binary.BigEndian.PutUint16(needle[:], x25519MLKEM768) + require.False(t, bytes.Contains(record, needle[:]), + "output must not contain the X25519MLKEM768 code point (0x%04x)", x25519MLKEM768) } -func TestRewriteSNI_LongerReplacement(t *testing.T) { +// TestBuildFakeClientHello_RandomizesPerCall ensures crypto/tls generates a +// fresh random + session_id + key_share on every call, as required to avoid +// trivial fingerprinting of the spoof. +func TestBuildFakeClientHello_RandomizesPerCall(t *testing.T) { t.Parallel() - payload := decodeClientHello(t) - out, err := rewriteSNI(payload, "letsencrypt.org") + first, err := buildFakeClientHello("example.com") require.NoError(t, err) - require.Len(t, out, len(payload)+5) // "letsencrypt.org" is 15, original 10, delta 5. - assertConsistent(t, out, "letsencrypt.org") + second, err := buildFakeClientHello("example.com") + require.NoError(t, err) + require.NotEqual(t, first, second, + "repeated calls must produce distinct bytes (random/session_id/key_share must vary)") } -func TestRewriteSNI_NoSNIReturnsError(t *testing.T) { +func TestBuildFakeClientHello_RejectsEmpty(t *testing.T) { t.Parallel() - // Truncated payload — not a valid ClientHello. - _, err := rewriteSNI([]byte{0x16, 0x03, 0x01, 0x00, 0x01, 0x01}, "x.com") + _, err := buildFakeClientHello("") require.Error(t, err) } - -func TestRewriteSNI_DoesNotMutateInput(t *testing.T) { - t.Parallel() - payload := decodeClientHello(t) - original := append([]byte(nil), payload...) - _, err := rewriteSNI(payload, "letsencrypt.org") - require.NoError(t, err) - require.Equal(t, original, payload, "input payload must not be mutated") -} diff --git a/common/tlsspoof/conn_test.go b/common/tlsspoof/conn_test.go index 981f1a49c3..b41cf54753 100644 --- a/common/tlsspoof/conn_test.go +++ b/common/tlsspoof/conn_test.go @@ -1,19 +1,36 @@ package tlsspoof import ( + "bytes" + "context" + "encoding/binary" "encoding/hex" "io" "net" "testing" + "time" tf "github.com/sagernet/sing-box/common/tlsfragment" "github.com/stretchr/testify/require" ) +// realClientHello is a captured Chrome ClientHello for github.com. Tests that +// stack tlsspoof.Conn on top of tf.Conn still need a parseable payload to +// exercise the fragment transform. +const realClientHello = "16030105f8010005f403036e35de7389a679c54029cf452611f2211c70d9ac3897271de589ab6155f8e4ab20637d225f1ef969ad87ed78bfb9d171300bcb1703b6f314ccefb964f79b7d0961002a0a0a130213031301c02cc02bcca9c030c02fcca8c00ac009c014c013009d009c0035002fc008c012000a01000581baba00000000000f000d00000a6769746875622e636f6d00170000ff01000100000a000e000c3a3a11ec001d001700180019000b000201000010000e000c02683208687474702f312e31000500050100000000000d00160014040308040401050308050805050108060601020100120000003304ef04ed3a3a00010011ec04c0aeb2250c092a3463161cccb29d9183331a424964248579507ed23a180b0ceab2a5f5d9ce41547e497a89055471ea572867ba3a1fc3c9e45025274a20f60c6b60e62476b6afed0403af59ab83660ef4112ae20386a602010d0a5d454c0ed34c84ed4423e750213e6a2baab1bf9c4367a6007ab40a33d95220c2dcaa44f257024a5626b545db0510f4311b1a60714154909c6a61fdfca011fb2626d657aeb6070bf078508babe3b584555013e34acc56198ed4663742b3155a664a9901794c4586820a7dc162c01827291f3792e1237f801a8d1ef096013c181c4a58d2f6859ba75022d18cc4418bd4f351d5c18f83a58857d05af860c4b9ac018a5b63f17184e591532c6bc2cf2215d4a282c8a8a4f6f7aee110422c8bc9ebd3b1d609c568523aaae555db320e6c269473d87af38c256cbb9febc20aea6380c32a8916f7a373c8b1e37554e3260bf6621f6b804ee80b3c516b1d01985bf4c603b6daa9a5991de6a7a29f3a7122b8afb843a7660110fce62b43c615f5bcc2db688ba012649c0952b0a2c031e732d2b454c6b2968683cb8d244be2c9a7fa163222979eaf92722b92b862d81a3d94450c2b60c318421ebb4307c42d1f0473592a5c30e42039cc68cda9721e61aa63f49def17c15221680ed444896340133bbee67556f56b9f9d78a4df715f926a12add0cc9c862e46ea8b7316ae468282c18601b2771c9c9322f982228cf93effaacd3f80cbd12bce5fc36f56e2a3caf91e578a5fae00c9b23a8ed1a66764f4433c3628a70b8f0a6196adc60a4cb4226f07ba4c6b363fe9065563bfc1347452946386bab488686e837ab979c64f9047417fca635fe1bb4f074f256cc8af837c7b455e280426547755af90a61640169ef180aea3a77e662bb6dac1b6c3696027129b1a5edf495314e9c7f4b6110e16378ec893fa24642330a40aba1a85326101acb97c620fd8d71389e69eaed7bdb01bbe1fd428d66191150c7b2cd1ad4257391676a82ba8ce07fb2667c3b289f159003a7c7bc31d361b7b7f49a802961739d950dfcc0fa1c7abce5abdd2245101da391151490862028110465950b9e9c03d08a90998ab83267838d2e74a0593bc81f74cdf734519a05b351c0e5488c68dd810e6e9142ccc1e2f4a7f464297eb340e27acc6b9d64e12e38cce8492b3d939140b5a9e149a75597f10a23874c84323a07cdd657274378f887c85c4259b9c04cd33ba58ed630ef2a744f8e19dd34843dff331d2a6be7e2332c599289cd248a611c73d7481cd4a9bd43449a3836f14b2af18a1739e17999e4c67e85cc5bcecabb14185e5bcaff3c96098f03dc5aba819f29587758f49f940585354a2a780830528d68ccd166920dadcaa25cab5fc1907272a826aba3f08bc6b88757776812ecb6c7cec69a223ec0a13a7b62a2349a0f63ed7a27a3b15ba21d71fe6864ec6e089ae17cadd433fa3138f7ee24353c11365818f8fc34f43a05542d18efaac24bfccc1f748a0cc1a67ad379468b76fd34973dba785f5c91d618333cd810fe0700d1bbc8422029782628070a624c52c5309a4a64d625b11f8033ab28df34a1add297517fcc06b92b6817b3c5144438cf260867c57bde68c8c4b82e6a135ef676a52fbae5708002a404e6189a60e2836de565ad1b29e3819e5ed49f6810bcb28e1bd6de57306f94b79d9dae1cc4624d2a068499beef81cd5fe4b76dcbfff2a2008001d002001976128c6d5a934533f28b9914d2480aab2a8c1ab03d212529ce8b27640a716002d00020101002b000706caca03040303001b00030200015a5a000100" + +func decodeClientHello(t *testing.T) []byte { + t.Helper() + payload, err := hex.DecodeString(realClientHello) + require.NoError(t, err) + return payload +} + type fakeSpoofer struct { injected [][]byte err error + closeErr error } func (f *fakeSpoofer) Inject(payload []byte) error { @@ -25,7 +42,7 @@ func (f *fakeSpoofer) Inject(payload []byte) error { } func (f *fakeSpoofer) Close() error { - return nil + return f.closeErr } func readAll(t *testing.T, conn net.Conn) []byte { @@ -37,12 +54,12 @@ func readAll(t *testing.T, conn net.Conn) []byte { func TestConn_Write_InjectsThenForwards(t *testing.T) { t.Parallel() - payload, err := hex.DecodeString(realClientHello) - require.NoError(t, err) + payload := decodeClientHello(t) client, server := net.Pipe() spoofer := &fakeSpoofer{} - wrapped := NewConn(client, spoofer, "letsencrypt.org") + wrapped, err := newConn(client, spoofer, "letsencrypt.org") + require.NoError(t, err) serverRead := make(chan []byte, 1) go func() { @@ -66,12 +83,12 @@ func TestConn_Write_InjectsThenForwards(t *testing.T) { func TestConn_Write_SecondWriteDoesNotInject(t *testing.T) { t.Parallel() - payload, err := hex.DecodeString(realClientHello) - require.NoError(t, err) + payload := decodeClientHello(t) client, server := net.Pipe() spoofer := &fakeSpoofer{} - wrapped := NewConn(client, spoofer, "letsencrypt.org") + wrapped, err := newConn(client, spoofer, "letsencrypt.org") + require.NoError(t, err) serverRead := make(chan []byte, 1) go func() { @@ -89,18 +106,244 @@ func TestConn_Write_SecondWriteDoesNotInject(t *testing.T) { require.Len(t, spoofer.injected, 1) } -func TestConn_Write_NonClientHelloReturnsError(t *testing.T) { +// TestConn_Write_SurfacesCloseError guards against the defer pattern silently +// dropping the spoofer's Close() error on the success path. +func TestConn_Write_SurfacesCloseError(t *testing.T) { t.Parallel() + client, server := net.Pipe() defer client.Close() defer server.Close() + spoofer := &fakeSpoofer{closeErr: errSpoofClose} + wrapped, err := newConn(client, spoofer, "letsencrypt.org") + require.NoError(t, err) + + go func() { _, _ = io.ReadAll(server) }() + + _, err = wrapped.Write([]byte("trigger inject")) + require.ErrorIs(t, err, errSpoofClose, + "Close() error must be wrapped into Write's return") +} + +func TestConn_NewConn_RejectsEmptySNI(t *testing.T) { + t.Parallel() + client, server := net.Pipe() + defer client.Close() + defer server.Close() + _, err := newConn(client, &fakeSpoofer{}, "") + require.Error(t, err, "empty SNI must fail at construction") +} + +var errSpoofClose = errTest("spoof-close-failed") + +type errTest string + +func (e errTest) Error() string { return string(e) } + +// recordingConn intercepts each Write call so tests can assert how many +// downstream writes occurred and in what order with respect to spoof +// injection. It does not implement WithUpstream, so tf.Conn's +// N.UnwrapReader(conn).(*net.TCPConn) returns nil and fragment-mode falls +// back to its plain Write + time.Sleep path — which is what we want to +// exercise over a net.Pipe. +type recordingConn struct { + net.Conn + writes [][]byte + timeline *[]string +} +func (c *recordingConn) Write(p []byte) (int, error) { + c.writes = append(c.writes, append([]byte(nil), p...)) + if c.timeline != nil { + *c.timeline = append(*c.timeline, "write") + } + return c.Conn.Write(p) +} + +type tlsRecord struct { + contentType byte + payload []byte +} + +func parseTLSRecords(t *testing.T, data []byte) []tlsRecord { + t.Helper() + var records []tlsRecord + for len(data) > 0 { + require.GreaterOrEqual(t, len(data), 5, "record header incomplete") + recordLen := int(binary.BigEndian.Uint16(data[3:5])) + require.GreaterOrEqual(t, len(data), 5+recordLen, "record payload truncated") + records = append(records, tlsRecord{ + contentType: data[0], + payload: append([]byte(nil), data[5:5+recordLen]...), + }) + data = data[5+recordLen:] + } + return records +} + +// TestConn_StackedWithRecordFragment mirrors the wrapping order that +// STDClientConfig.Client() produces when record_fragment is enabled: +// tls.Client → tlsspoof.Conn → tf.Conn → raw conn. +// Asserts the decoy is injected and the real handshake arrives split into +// multiple TLS records whose payloads reassemble to the original. +func TestConn_StackedWithRecordFragment(t *testing.T) { + t.Parallel() + payload := decodeClientHello(t) + + client, server := net.Pipe() + defer server.Close() + + fragConn := tf.NewConn(client, context.Background(), false, true, time.Millisecond) spoofer := &fakeSpoofer{} - wrapped := NewConn(client, spoofer, "letsencrypt.org") + wrapped, err := newConn(fragConn, spoofer, "letsencrypt.org") + require.NoError(t, err) + + serverRead := make(chan []byte, 1) + go func() { serverRead <- readAll(t, server) }() + + _, err = wrapped.Write(payload) + require.NoError(t, err) + require.NoError(t, wrapped.Close()) + forwarded := <-serverRead + + require.Len(t, spoofer.injected, 1, "spoof must inject exactly once") + injected := tf.IndexTLSServerName(spoofer.injected[0]) + require.NotNil(t, injected, "injected payload must parse as ClientHello") + require.Equal(t, "letsencrypt.org", injected.ServerName) + + records := parseTLSRecords(t, forwarded) + require.Greater(t, len(records), 1, "record_fragment must produce multiple records") + var reassembled []byte + for _, r := range records { + require.Equal(t, byte(0x16), r.contentType, "all records must be handshake") + reassembled = append(reassembled, r.payload...) + } + require.Equal(t, payload[5:], reassembled, "record payloads must reassemble to original handshake") +} + +// TestConn_StackedWithPacketFragment is the primary regression test for the +// fragment-only gate fix in STDClientConfig.Client(). It verifies that +// packet-level fragmentation combined with spoof produces: +// - one spoof injection carrying the decoy SNI, +// - multiple separate writes to the underlying conn, +// - an unmodified byte stream when those writes are concatenated +// (no extra record framing). +func TestConn_StackedWithPacketFragment(t *testing.T) { + t.Parallel() + payload := decodeClientHello(t) + + client, server := net.Pipe() + defer server.Close() + + rc := &recordingConn{Conn: client} + fragConn := tf.NewConn(rc, context.Background(), true, false, time.Millisecond) + spoofer := &fakeSpoofer{} + wrapped, err := newConn(fragConn, spoofer, "letsencrypt.org") + require.NoError(t, err) + + serverRead := make(chan []byte, 1) + go func() { serverRead <- readAll(t, server) }() + + _, err = wrapped.Write(payload) + require.NoError(t, err) + require.NoError(t, wrapped.Close()) + forwarded := <-serverRead + + require.Len(t, spoofer.injected, 1, "spoof must inject exactly once") + injected := tf.IndexTLSServerName(spoofer.injected[0]) + require.NotNil(t, injected) + require.Equal(t, "letsencrypt.org", injected.ServerName) + + require.Greater(t, len(rc.writes), 1, "fragment must split the ClientHello into multiple writes") + require.Equal(t, payload, bytes.Join(rc.writes, nil), + "concatenated writes must equal original bytes (no extra framing)") + require.Equal(t, payload, forwarded) +} + +// TestConn_StackedWithBothFragment exercises the combination that produces +// the strongest obfuscation: each chunk becomes its own TLS record and its +// own TCP write. +func TestConn_StackedWithBothFragment(t *testing.T) { + t.Parallel() + payload := decodeClientHello(t) + + client, server := net.Pipe() + defer server.Close() + + rc := &recordingConn{Conn: client} + fragConn := tf.NewConn(rc, context.Background(), true, true, time.Millisecond) + spoofer := &fakeSpoofer{} + wrapped, err := newConn(fragConn, spoofer, "letsencrypt.org") + require.NoError(t, err) + + serverRead := make(chan []byte, 1) + go func() { serverRead <- readAll(t, server) }() + + _, err = wrapped.Write(payload) + require.NoError(t, err) + require.NoError(t, wrapped.Close()) + forwarded := <-serverRead + + require.Len(t, spoofer.injected, 1) + injected := tf.IndexTLSServerName(spoofer.injected[0]) + require.NotNil(t, injected) + require.Equal(t, "letsencrypt.org", injected.ServerName) + + require.Greater(t, len(rc.writes), 1, "split-packet must produce multiple writes") + records := parseTLSRecords(t, forwarded) + require.Greater(t, len(records), 1, "split-record must produce multiple records") + var reassembled []byte + for _, r := range records { + require.Equal(t, byte(0x16), r.contentType) + reassembled = append(reassembled, r.payload...) + } + require.Equal(t, payload[5:], reassembled, + "record payloads must reassemble to the original handshake") +} + +// trackingSpoofer adds the spoof injection to a shared event timeline so +// TestConn_StackedInjectionOrder can prove the decoy precedes the first +// downstream write. +type trackingSpoofer struct { + injected [][]byte + timeline *[]string +} + +func (s *trackingSpoofer) Inject(payload []byte) error { + s.injected = append(s.injected, append([]byte(nil), payload...)) + *s.timeline = append(*s.timeline, "inject") + return nil +} + +func (s *trackingSpoofer) Close() error { return nil } + +// TestConn_StackedInjectionOrder asserts the documented wire order: the +// decoy injection happens before any write reaches the underlying conn. +func TestConn_StackedInjectionOrder(t *testing.T) { + t.Parallel() + payload := decodeClientHello(t) + + client, server := net.Pipe() + defer server.Close() + + var timeline []string + rc := &recordingConn{Conn: client, timeline: &timeline} + fragConn := tf.NewConn(rc, context.Background(), true, true, time.Millisecond) + spoofer := &trackingSpoofer{timeline: &timeline} + wrapped, err := newConn(fragConn, spoofer, "letsencrypt.org") + require.NoError(t, err) + + serverRead := make(chan []byte, 1) + go func() { serverRead <- readAll(t, server) }() + + _, err = wrapped.Write(payload) + require.NoError(t, err) + require.NoError(t, wrapped.Close()) + <-serverRead - _, err := wrapped.Write([]byte("not a ClientHello")) - require.Error(t, err) - require.Empty(t, spoofer.injected) + require.NotEmpty(t, timeline) + require.Equal(t, "inject", timeline[0], "decoy must be injected before any downstream write") + require.Contains(t, timeline[1:], "write", "at least one downstream write must follow the inject") } func TestParseMethod(t *testing.T) { diff --git a/common/tlsspoof/integration_test.go b/common/tlsspoof/integration_test.go index b7b07d54be..23a83ff174 100644 --- a/common/tlsspoof/integration_test.go +++ b/common/tlsspoof/integration_test.go @@ -11,7 +11,7 @@ import ( "os" "os/exec" "strings" - "sync/atomic" + "sync" "testing" "time" @@ -21,11 +21,20 @@ import ( func requireRoot(t *testing.T) { t.Helper() if os.Geteuid() != 0 { - t.Fatal("integration test requires root") + t.Skip("integration test requires root; re-run with `go test -exec sudo`") } } func tcpdumpObserver(t *testing.T, iface string, port uint16, needle string, do func(), wait time.Duration) bool { + t.Helper() + return tcpdumpObserverMulti(t, iface, port, []string{needle}, do, wait)[needle] +} + +// tcpdumpObserverMulti captures tcpdump output while do() executes and reports +// which of the provided needles were observed in the raw ASCII dump. Use this +// to assert that distinct payloads (e.g. fake vs real ClientHello) are both on +// the wire. +func tcpdumpObserverMulti(t *testing.T, iface string, port uint16, needles []string, do func(), wait time.Duration) map[string]bool { t.Helper() ctx, cancel := context.WithTimeout(context.Background(), wait) defer cancel() @@ -62,16 +71,22 @@ func tcpdumpObserver(t *testing.T, iface string, port uint16, needle string, do t.Fatal("tcpdump did not attach within 2s") } - var found atomic.Bool + var access sync.Mutex + found := make(map[string]bool, len(needles)) readerDone := make(chan struct{}) go func() { defer close(readerDone) scanner := bufio.NewScanner(stdout) scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) for scanner.Scan() { - if strings.Contains(scanner.Text(), needle) { - found.Store(true) + line := scanner.Text() + access.Lock() + for _, needle := range needles { + if !found[needle] && strings.Contains(line, needle) { + found[needle] = true + } } + access.Unlock() } }() @@ -80,7 +95,13 @@ func tcpdumpObserver(t *testing.T, iface string, port uint16, needle string, do time.Sleep(200 * time.Millisecond) _ = cmd.Process.Signal(os.Interrupt) <-readerDone - return found.Load() + access.Lock() + defer access.Unlock() + result := make(map[string]bool, len(needles)) + for _, needle := range needles { + result[needle] = found[needle] + } + return result } func dialLocalEchoServer(t *testing.T) (client net.Conn, serverPort uint16) { diff --git a/common/tlsspoof/integration_tls_test.go b/common/tlsspoof/integration_tls_test.go new file mode 100644 index 0000000000..d179c3841c --- /dev/null +++ b/common/tlsspoof/integration_tls_test.go @@ -0,0 +1,118 @@ +//go:build linux || darwin + +package tlsspoof + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "io" + "math/big" + "net" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +// generateSelfSignedCert returns a TLS certificate valid for the given SAN. +func generateSelfSignedCert(t *testing.T, commonName string, sans ...string) tls.Certificate { + t.Helper() + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + require.NoError(t, err) + template := x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{CommonName: commonName}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + DNSNames: sans, + } + der, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + require.NoError(t, err) + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}) + keyDER, err := x509.MarshalPKCS8PrivateKey(priv) + require.NoError(t, err) + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}) + cert, err := tls.X509KeyPair(certPEM, keyPEM) + require.NoError(t, err) + return cert +} + +// TestIntegrationConn_RealTLSHandshake drives a real crypto/tls ClientHello +// through the spoofer and asserts the on-wire fake packet carries the fake SNI +// while the server receives the real SNI. This exercises the full +// `tls.Client(wrapped, config).Handshake()` path rather than a static hex +// payload, matching what user-facing code hits. +func TestIntegrationConn_RealTLSHandshake(t *testing.T) { + requireRoot(t) + const realSNI = "real.test" + const fakeSNI = "fake.test" + + serverCert := generateSelfSignedCert(t, realSNI, realSNI) + tlsConfig := &tls.Config{Certificates: []tls.Certificate{serverCert}} + + listener, err := tls.Listen("tcp4", "127.0.0.1:0", tlsConfig) + require.NoError(t, err) + t.Cleanup(func() { listener.Close() }) + + serverSNI := make(chan string, 1) + go func() { + conn, err := listener.Accept() + if err != nil { + return + } + defer conn.Close() + tlsConn := conn.(*tls.Conn) + _ = tlsConn.SetDeadline(time.Now().Add(3 * time.Second)) + if handshakeErr := tlsConn.Handshake(); handshakeErr != nil { + serverSNI <- "handshake-error:" + handshakeErr.Error() + return + } + serverSNI <- tlsConn.ConnectionState().ServerName + _, _ = io.Copy(io.Discard, conn) + }() + + addr := listener.Addr().(*net.TCPAddr) + serverPort := uint16(addr.Port) + raw, err := net.Dial("tcp4", addr.String()) + require.NoError(t, err) + t.Cleanup(func() { raw.Close() }) + + wrapped, err := NewConn(raw, MethodWrongSequence, fakeSNI) + require.NoError(t, err) + + clientConfig := &tls.Config{ + ServerName: realSNI, + InsecureSkipVerify: true, + } + tlsClient := tls.Client(wrapped, clientConfig) + t.Cleanup(func() { tlsClient.Close() }) + + seen := tcpdumpObserverMulti(t, loopbackInterface, serverPort, + []string{realSNI, fakeSNI}, func() { + _ = tlsClient.SetDeadline(time.Now().Add(3 * time.Second)) + err := tlsClient.Handshake() + require.NoError(t, err, "TLS handshake must succeed (wrong-sequence fake is dropped by peer)") + }, 4*time.Second) + + require.True(t, seen[realSNI], + "real ClientHello on the wire must contain original SNI %q", realSNI) + require.True(t, seen[fakeSNI], + "fake ClientHello on the wire must contain fake SNI %q", fakeSNI) + + select { + case sniOnServer := <-serverSNI: + require.Equal(t, realSNI, sniOnServer, + "TLS server must see the real SNI (fake packet dropped by peer TCP stack)") + case <-time.After(3 * time.Second): + t.Fatal("TLS server did not complete handshake") + } +} diff --git a/common/tlsspoof/integration_unix_test.go b/common/tlsspoof/integration_unix_test.go index 9ec5760c75..0f4585fd82 100644 --- a/common/tlsspoof/integration_unix_test.go +++ b/common/tlsspoof/integration_unix_test.go @@ -15,13 +15,11 @@ import ( func TestIntegrationSpoofer_WrongChecksum(t *testing.T) { requireRoot(t) client, serverPort := dialLocalEchoServer(t) - spoofer, err := NewSpoofer(client, MethodWrongChecksum) + spoofer, err := newRawSpoofer(client, MethodWrongChecksum) require.NoError(t, err) defer spoofer.Close() - payload, err := hex.DecodeString(realClientHello) - require.NoError(t, err) - fake, err := rewriteSNI(payload, "letsencrypt.org") + fake, err := buildFakeClientHello("letsencrypt.org") require.NoError(t, err) captured := tcpdumpObserver(t, loopbackInterface, serverPort, "letsencrypt.org", func() { @@ -33,13 +31,11 @@ func TestIntegrationSpoofer_WrongChecksum(t *testing.T) { func TestIntegrationSpoofer_WrongSequence(t *testing.T) { requireRoot(t) client, serverPort := dialLocalEchoServer(t) - spoofer, err := NewSpoofer(client, MethodWrongSequence) + spoofer, err := newRawSpoofer(client, MethodWrongSequence) require.NoError(t, err) defer spoofer.Close() - payload, err := hex.DecodeString(realClientHello) - require.NoError(t, err) - fake, err := rewriteSNI(payload, "letsencrypt.org") + fake, err := buildFakeClientHello("letsencrypt.org") require.NoError(t, err) captured := tcpdumpObserver(t, loopbackInterface, serverPort, "letsencrypt.org", func() { @@ -51,13 +47,11 @@ func TestIntegrationSpoofer_WrongSequence(t *testing.T) { func TestIntegrationSpoofer_IPv6_WrongChecksum(t *testing.T) { requireRoot(t) client, serverPort := dialLocalEchoServerIPv6(t) - spoofer, err := NewSpoofer(client, MethodWrongChecksum) + spoofer, err := newRawSpoofer(client, MethodWrongChecksum) require.NoError(t, err) defer spoofer.Close() - payload, err := hex.DecodeString(realClientHello) - require.NoError(t, err) - fake, err := rewriteSNI(payload, "letsencrypt.org") + fake, err := buildFakeClientHello("letsencrypt.org") require.NoError(t, err) captured := tcpdumpObserver(t, loopbackInterface, serverPort, "letsencrypt.org", func() { @@ -69,13 +63,11 @@ func TestIntegrationSpoofer_IPv6_WrongChecksum(t *testing.T) { func TestIntegrationSpoofer_IPv6_WrongSequence(t *testing.T) { requireRoot(t) client, serverPort := dialLocalEchoServerIPv6(t) - spoofer, err := NewSpoofer(client, MethodWrongSequence) + spoofer, err := newRawSpoofer(client, MethodWrongSequence) require.NoError(t, err) defer spoofer.Close() - payload, err := hex.DecodeString(realClientHello) - require.NoError(t, err) - fake, err := rewriteSNI(payload, "letsencrypt.org") + fake, err := buildFakeClientHello("letsencrypt.org") require.NoError(t, err) captured := tcpdumpObserver(t, loopbackInterface, serverPort, "letsencrypt.org", func() { @@ -95,6 +87,76 @@ func TestIntegrationConn_IPv6_InjectsThenForwardsRealCH(t *testing.T) { runInjectsThenForwardsRealCH(t, "tcp6", "[::1]:0") } +// TestIntegrationConn_FakeAndRealHaveDistinctSNIs asserts that the on-wire fake +// packet carries the fake SNI (letsencrypt.org) AND the real packet still +// carries the original SNI (github.com). If the builder regresses to producing +// empty or mismatched bytes, the fake-SNI needle will be missing. +func TestIntegrationConn_FakeAndRealHaveDistinctSNIs(t *testing.T) { + requireRoot(t) + runFakeAndRealHaveDistinctSNIs(t, "tcp4", "127.0.0.1:0", "letsencrypt.org") +} + +func TestIntegrationConn_IPv6_FakeAndRealHaveDistinctSNIs(t *testing.T) { + requireRoot(t) + runFakeAndRealHaveDistinctSNIs(t, "tcp6", "[::1]:0", "letsencrypt.org") +} + +func runFakeAndRealHaveDistinctSNIs(t *testing.T, network, address, fakeSNI string) { + t.Helper() + const originalSNI = "github.com" + require.NotEqual(t, originalSNI, fakeSNI) + + listener, err := net.Listen(network, address) + require.NoError(t, err) + + serverReceived := make(chan []byte, 1) + go func() { + conn, err := listener.Accept() + if err != nil { + return + } + defer conn.Close() + _ = conn.SetReadDeadline(time.Now().Add(2 * time.Second)) + got, _ := io.ReadAll(conn) + serverReceived <- got + }() + + addr := listener.Addr().(*net.TCPAddr) + serverPort := uint16(addr.Port) + client, err := net.Dial(network, addr.String()) + require.NoError(t, err) + t.Cleanup(func() { + client.Close() + listener.Close() + }) + + wrapped, err := NewConn(client, MethodWrongSequence, fakeSNI) + require.NoError(t, err) + + payload, err := hex.DecodeString(realClientHello) + require.NoError(t, err) + + seen := tcpdumpObserverMulti(t, loopbackInterface, serverPort, + []string{originalSNI, fakeSNI}, func() { + n, err := wrapped.Write(payload) + require.NoError(t, err) + require.Equal(t, len(payload), n) + }, 3*time.Second) + require.True(t, seen[originalSNI], + "real ClientHello must carry original SNI %q on the wire", originalSNI) + require.True(t, seen[fakeSNI], + "fake ClientHello must carry fake SNI %q on the wire", fakeSNI) + + _ = wrapped.Close() + select { + case got := <-serverReceived: + require.Equal(t, payload, got, + "server must receive real ClientHello unchanged (wrong-sequence fake must be dropped)") + case <-time.After(2 * time.Second): + t.Fatal("echo server did not receive real ClientHello") + } +} + func runInjectsThenForwardsRealCH(t *testing.T, network, address string) { t.Helper() listener, err := net.Listen(network, address) @@ -121,9 +183,8 @@ func runInjectsThenForwardsRealCH(t *testing.T, network, address string) { listener.Close() }) - spoofer, err := NewSpoofer(client, MethodWrongSequence) + wrapped, err := NewConn(client, MethodWrongSequence, "letsencrypt.org") require.NoError(t, err) - wrapped := NewConn(client, spoofer, "letsencrypt.org") payload, err := hex.DecodeString(realClientHello) require.NoError(t, err) diff --git a/common/tlsspoof/integration_windows_test.go b/common/tlsspoof/integration_windows_test.go index d3f823841e..b0461a31b2 100644 --- a/common/tlsspoof/integration_windows_test.go +++ b/common/tlsspoof/integration_windows_test.go @@ -12,11 +12,11 @@ import ( "github.com/stretchr/testify/require" ) -func newSpoofer(t *testing.T, conn net.Conn, method Method) Spoofer { +func newSpoofer(t *testing.T, conn net.Conn, method Method) rawSpoofer { t.Helper() - spoofer, err := NewSpoofer(conn, method) + s, err := newRawSpoofer(conn, method) require.NoError(t, err) - return spoofer + return s } // Basic lifecycle: opening a spoofer against a live TCP conn installs @@ -46,11 +46,10 @@ func TestIntegrationSpooferOpenClose(t *testing.T) { require.NoError(t, spoofer.Close()) } -// End-to-end: Conn.Write injects a fake ClientHello with a rewritten -// SNI, then forwards the real ClientHello. With wrong-sequence, the -// fake lands before the connection's send-next sequence — the peer TCP -// stack treats it as already-received and only surfaces the real bytes -// to the echo server. +// End-to-end: Conn.Write injects a fake ClientHello with a fresh SNI, then +// forwards the real ClientHello. With wrong-sequence, the fake lands before +// the connection's send-next sequence — the peer TCP stack treats it as +// already-received and only surfaces the real bytes to the echo server. func TestIntegrationConnInjectsThenForwardsRealCH(t *testing.T) { listener, err := net.Listen("tcp4", "127.0.0.1:0") require.NoError(t, err) @@ -72,8 +71,8 @@ func TestIntegrationConnInjectsThenForwardsRealCH(t *testing.T) { require.NoError(t, err) t.Cleanup(func() { client.Close() }) - spoofer := newSpoofer(t, client, MethodWrongSequence) - wrapped := NewConn(client, spoofer, "letsencrypt.org") + wrapped, err := NewConn(client, MethodWrongSequence, "letsencrypt.org") + require.NoError(t, err) payload, err := hex.DecodeString(realClientHello) require.NoError(t, err) @@ -94,7 +93,7 @@ func TestIntegrationConnInjectsThenForwardsRealCH(t *testing.T) { // Inject before any kernel payload: stages the fake, then Write flushes // the real CH. Same terminal expectation as the Conn variant but via the -// Spoofer primitive directly. +// raw spoofer primitive directly. func TestIntegrationSpooferInjectThenWrite(t *testing.T) { listener, err := net.Listen("tcp4", "127.0.0.1:0") require.NoError(t, err) @@ -119,12 +118,12 @@ func TestIntegrationSpooferInjectThenWrite(t *testing.T) { spoofer := newSpoofer(t, client, MethodWrongSequence) t.Cleanup(func() { spoofer.Close() }) - payload, err := hex.DecodeString(realClientHello) - require.NoError(t, err) - fake, err := rewriteSNI(payload, "letsencrypt.org") + fake, err := buildFakeClientHello("letsencrypt.org") require.NoError(t, err) require.NoError(t, spoofer.Inject(fake)) + payload, err := hex.DecodeString(realClientHello) + require.NoError(t, err) n, err := client.Write(payload) require.NoError(t, err) require.Equal(t, len(payload), n) diff --git a/common/tlsspoof/packet_test.go b/common/tlsspoof/packet_test.go index 992a96840e..5c6d5b6be4 100644 --- a/common/tlsspoof/packet_test.go +++ b/common/tlsspoof/packet_test.go @@ -75,3 +75,62 @@ func TestBuildTCPSegment_MixedFamilyPanics(t *testing.T) { buildTCPSegment(src, dst, 0, 0, nil, false) }) } + +func TestBuildSpoofFrame_WrongSequence(t *testing.T) { + t.Parallel() + src := netip.MustParseAddrPort("10.0.0.1:54321") + dst := netip.MustParseAddrPort("1.2.3.4:443") + payload := []byte("fake-client-hello") + const sendNext uint32 = 10_000 + frame, err := buildSpoofFrame(MethodWrongSequence, src, dst, sendNext, 20_000, payload) + require.NoError(t, err) + + tcp := header.TCP(frame[header.IPv4MinimumSize:]) + require.Equal(t, sendNext-uint32(len(payload)), tcp.SequenceNumber(), + "wrong-sequence places the fake at sendNext-len(payload)") + require.True(t, tcp.Flags().Contains(header.TCPFlagAck|header.TCPFlagPsh)) + + // Checksum must still be valid — only the sequence number is wrong. + payloadChecksum := checksum.Checksum(payload, 0) + require.True(t, tcp.IsChecksumValid( + tcpip.AddrFrom4(src.Addr().As4()), + tcpip.AddrFrom4(dst.Addr().As4()), + payloadChecksum, + uint16(len(payload)), + )) +} + +func TestBuildSpoofFrame_WrongChecksum(t *testing.T) { + t.Parallel() + src := netip.MustParseAddrPort("10.0.0.1:54321") + dst := netip.MustParseAddrPort("1.2.3.4:443") + payload := []byte("fake-client-hello") + const sendNext uint32 = 5_000 + frame, err := buildSpoofFrame(MethodWrongChecksum, src, dst, sendNext, 20_000, payload) + require.NoError(t, err) + + tcp := header.TCP(frame[header.IPv4MinimumSize:]) + require.Equal(t, sendNext, tcp.SequenceNumber(), + "wrong-checksum keeps the real sequence number") + + payloadChecksum := checksum.Checksum(payload, 0) + require.False(t, tcp.IsChecksumValid( + tcpip.AddrFrom4(src.Addr().As4()), + tcpip.AddrFrom4(dst.Addr().As4()), + payloadChecksum, + uint16(len(payload)), + )) + require.True(t, header.IPv4(frame[:header.IPv4MinimumSize]).IsChecksumValid(), + "IPv4 checksum must remain valid so the router forwards the packet") +} + +func TestBuildSpoofTCPSegment_EncodesWithoutIPHeader(t *testing.T) { + t.Parallel() + src := netip.MustParseAddrPort("[fe80::1]:54321") + dst := netip.MustParseAddrPort("[2606:4700::1]:443") + payload := []byte("fake-client-hello") + segment, err := buildSpoofTCPSegment(MethodWrongSequence, src, dst, 1000, 2000, payload) + require.NoError(t, err) + require.Equal(t, tcpHeaderLen+len(payload), len(segment), + "segment must be TCP header + payload, no IP header") +} diff --git a/common/tlsspoof/raw_darwin.go b/common/tlsspoof/raw_darwin.go index 99c9a5c665..ab31687692 100644 --- a/common/tlsspoof/raw_darwin.go +++ b/common/tlsspoof/raw_darwin.go @@ -9,6 +9,7 @@ import ( "sync" "syscall" + "github.com/sagernet/sing-tun/gtcpip/header" E "github.com/sagernet/sing/common/exceptions" "golang.org/x/sys/unix" @@ -34,14 +35,26 @@ const ( darwinXtcpcbRcvNxtOffset = 80 ) -var darwinStructSize = sync.OnceValue(func() int { - value, _ := syscall.Sysctl("kern.osrelease") - major, _, _ := strings.Cut(value, ".") - n, _ := strconv.ParseInt(major, 10, 64) +// darwinStructSize returns the size of xinpcb_n for the running Darwin kernel. +// Darwin 22 (macOS 13 Ventura) grew the struct from 384 to 408 bytes; there is +// no ABI-stable way to read it, so we key off the kernel version. +var darwinStructSize = sync.OnceValues(func() (int, error) { + value, err := syscall.Sysctl("kern.osrelease") + if err != nil { + return 0, E.Cause(err, "sysctl kern.osrelease") + } + major, _, ok := strings.Cut(value, ".") + if !ok { + return 0, E.New("unexpected kern.osrelease format: ", value) + } + n, err := strconv.ParseInt(major, 10, 64) + if err != nil { + return 0, E.Cause(err, "parse kern.osrelease major version: ", value) + } if n >= 22 { - return 408 + return 408, nil } - return 384 + return 384, nil }) type darwinSpoofer struct { @@ -54,7 +67,7 @@ type darwinSpoofer struct { receiveNext uint32 } -func newRawSpoofer(conn net.Conn, method Method) (Spoofer, error) { +func newRawSpoofer(conn net.Conn, method Method) (rawSpoofer, error) { _, src, dst, err := tcpEndpoints(conn) if err != nil { return nil, err @@ -87,7 +100,10 @@ func readDarwinTCPSequence(src, dst netip.AddrPort) (uint32, uint32, error) { if err != nil { return 0, 0, E.Cause(err, "sysctl net.inet.tcp.pcblist_n") } - structSize := darwinStructSize() + structSize, err := darwinStructSize() + if err != nil { + return 0, 0, err + } itemSize := structSize + darwinTCPExtraSize for i := darwinXinpgenSize; i+itemSize <= len(buffer); i += itemSize { inpcb := buffer[i : i+darwinXsocketOffset] @@ -160,10 +176,9 @@ func (s *darwinSpoofer) Inject(payload []byte) error { // Darwin inherits the historical BSD quirk: with IP_HDRINCL the kernel // expects ip_len and ip_off in host byte order, not network byte order. // Apple's rip_output swaps them back before transmission. - totalLen := binary.BigEndian.Uint16(frame[2:4]) - binary.NativeEndian.PutUint16(frame[2:4], totalLen) - fragOff := binary.BigEndian.Uint16(frame[6:8]) - binary.NativeEndian.PutUint16(frame[6:8], fragOff) + ip := header.IPv4(frame) + ip.SetTotalLengthDarwinRaw(ip.TotalLength()) + ip.SetFlagsFragmentOffsetDarwinRaw(ip.Flags(), ip.FragmentOffset()) err = unix.Sendto(s.rawFD, frame, 0, s.rawSockAddr) if err != nil { return E.Cause(err, "sendto raw socket") diff --git a/common/tlsspoof/raw_linux.go b/common/tlsspoof/raw_linux.go index cb694aba96..f82fbc9efb 100644 --- a/common/tlsspoof/raw_linux.go +++ b/common/tlsspoof/raw_linux.go @@ -29,7 +29,7 @@ type linuxSpoofer struct { receiveNext uint32 } -func newRawSpoofer(conn net.Conn, method Method) (Spoofer, error) { +func newRawSpoofer(conn net.Conn, method Method) (rawSpoofer, error) { tcpConn, src, dst, err := tcpEndpoints(conn) if err != nil { return nil, err @@ -66,22 +66,34 @@ func openLinuxRawSocket(dst netip.AddrPort) (int, unix.Sockaddr, error) { unix.Close(fd) return -1, nil, E.Cause(err, "set IPV6_HDRINCL") } - sockaddr := &unix.SockaddrInet6{Port: int(dst.Port())} - sockaddr.Addr = dst.Addr().As16() + // Linux raw IPv6 sockets interpret sin6_port as a nexthdr protocol number + // (see raw(7)); any value other than 0 or the socket's IPPROTO_TCP causes + // sendto to fail with EINVAL. The destination is already encoded in the + // user-supplied IPv6 header under IPV6_HDRINCL. + sockaddr := &unix.SockaddrInet6{Addr: dst.Addr().As16()} return fd, sockaddr, nil } // loadSequenceNumbers puts the socket briefly into TCP_REPAIR mode to read // snd_nxt and rcv_nxt from the kernel. TCP_REPAIR requires CAP_NET_ADMIN; // callers must run as root or grant both CAP_NET_RAW and CAP_NET_ADMIN. +// +// If the TCP_REPAIR_OFF revert fails, the socket would stay in TCP_REPAIR +// state and subsequent Write() calls would silently buffer instead of sending. +// Surface that error so callers can abort. func (s *linuxSpoofer) loadSequenceNumbers(tcpConn *net.TCPConn) error { - return control.Conn(tcpConn, func(raw uintptr) error { + return control.Conn(tcpConn, func(raw uintptr) (err error) { fd := int(raw) - err := unix.SetsockoptInt(fd, unix.IPPROTO_TCP, unix.TCP_REPAIR, unix.TCP_REPAIR_ON) + err = unix.SetsockoptInt(fd, unix.IPPROTO_TCP, unix.TCP_REPAIR, unix.TCP_REPAIR_ON) if err != nil { return E.Cause(err, "enter TCP_REPAIR (need CAP_NET_ADMIN)") } - defer unix.SetsockoptInt(fd, unix.IPPROTO_TCP, unix.TCP_REPAIR, unix.TCP_REPAIR_OFF) + defer func() { + offErr := unix.SetsockoptInt(fd, unix.IPPROTO_TCP, unix.TCP_REPAIR, unix.TCP_REPAIR_OFF) + if err == nil && offErr != nil { + err = E.Cause(offErr, "leave TCP_REPAIR") + } + }() err = unix.SetsockoptInt(fd, unix.IPPROTO_TCP, unix.TCP_REPAIR_QUEUE, tcpSendQueue) if err != nil { diff --git a/common/tlsspoof/raw_stub.go b/common/tlsspoof/raw_stub.go index a2da87d6b3..7edf2441a6 100644 --- a/common/tlsspoof/raw_stub.go +++ b/common/tlsspoof/raw_stub.go @@ -10,6 +10,6 @@ import ( const PlatformSupported = false -func newRawSpoofer(conn net.Conn, method Method) (Spoofer, error) { +func newRawSpoofer(conn net.Conn, method Method) (rawSpoofer, error) { return nil, E.New("tls_spoof: unsupported platform") } diff --git a/common/tlsspoof/raw_windows.go b/common/tlsspoof/raw_windows.go index b6961169f1..9f6553f1b8 100644 --- a/common/tlsspoof/raw_windows.go +++ b/common/tlsspoof/raw_windows.go @@ -25,11 +25,15 @@ const PlatformSupported = true // bounds the pathological case where the kernel buffers the packet. const closeGracePeriod = 2 * time.Second +// windowsSpoofer uses a single WinDivert handle for both capture and +// injection. Sequential Send() calls on one handle traverse one driver queue, +// so the fake provably precedes the released real on the wire — a guarantee +// two separate handles cannot make because cross-handle order depends on the +// scheduler. type windowsSpoofer struct { method Method src, dst netip.AddrPort divertH *windivert.Handle - injectH *windivert.Handle fakeReady chan []byte // buffered(1): staged by Inject done chan struct{} // closed by run() on exit @@ -37,12 +41,11 @@ type windowsSpoofer struct { runErr atomic.Pointer[error] } -func newRawSpoofer(conn net.Conn, method Method) (Spoofer, error) { +func newRawSpoofer(conn net.Conn, method Method) (rawSpoofer, error) { _, src, dst, err := tcpEndpoints(conn) if err != nil { return nil, err } - filter, err := windivert.OutboundTCP(src, dst) if err != nil { return nil, err @@ -51,17 +54,11 @@ func newRawSpoofer(conn net.Conn, method Method) (Spoofer, error) { if err != nil { return nil, E.Cause(err, "tls_spoof: open WinDivert") } - injectH, err := windivert.Open(nil, windivert.LayerNetwork, 0, windivert.FlagSendOnly) - if err != nil { - divertH.Close() - return nil, E.Cause(err, "tls_spoof: open WinDivert") - } s := &windowsSpoofer{ method: method, src: src, dst: dst, divertH: divertH, - injectH: injectH, fakeReady: make(chan []byte, 1), done: make(chan struct{}), } @@ -91,7 +88,6 @@ func (s *windowsSpoofer) Close() error { s.divertH.Close() <-s.done } - s.injectH.Close() }) if p := s.runErr.Load(); p != nil { return *p @@ -119,9 +115,17 @@ func (s *windowsSpoofer) run() { pkt := buf[:n] seq, ack, payloadLen, ok := parseTCPFields(pkt, addr.IPv6()) if !ok { - // Malformed / not TCP — shouldn't match our filter, but be safe. - _, _ = s.divertH.Send(pkt, &addr) - continue + // Our filter is OutboundTCP(src, dst); a non-TCP or truncated + // match means driver state is suspect. Re-inject so the kernel + // still sees the byte stream, then abort — continuing would risk + // reordering against an unknown reference point. + _, sendErr := s.divertH.Send(pkt, &addr) + if sendErr != nil { + s.recordErr(E.Cause(sendErr, "windivert re-inject malformed")) + return + } + s.recordErr(E.New("windivert received malformed packet matching spoof filter")) + return } if payloadLen == 0 { // Handshake ACK, keepalive, FIN — pass through unchanged. @@ -159,7 +163,7 @@ func (s *windowsSpoofer) run() { // Force both to 1 to keep our bytes intact. fakeAddr.SetIPChecksum(true) fakeAddr.SetTCPChecksum(true) - _, err = s.injectH.Send(frame, &fakeAddr) + _, err = s.divertH.Send(frame, &fakeAddr) if err != nil { s.recordErr(E.Cause(err, "windivert inject fake")) return diff --git a/common/tlsspoof/spoof.go b/common/tlsspoof/spoof.go index 2a27ec3280..1bca5693fe 100644 --- a/common/tlsspoof/spoof.go +++ b/common/tlsspoof/spoof.go @@ -40,40 +40,54 @@ func (m Method) String() string { } } -type Spoofer interface { +type rawSpoofer interface { Inject(payload []byte) error Close() error } -func NewSpoofer(conn net.Conn, method Method) (Spoofer, error) { - return newRawSpoofer(conn, method) -} - type Conn struct { net.Conn - spoofer Spoofer - fakeSNI string - injected bool + spoofer rawSpoofer + fakeHello []byte + injected bool } -func NewConn(conn net.Conn, spoofer Spoofer, fakeSNI string) *Conn { - return &Conn{ - Conn: conn, - spoofer: spoofer, - fakeSNI: fakeSNI, +func NewConn(conn net.Conn, method Method, fakeSNI string) (*Conn, error) { + spoofer, err := newRawSpoofer(conn, method) + if err != nil { + return nil, err } + result, err := newConn(conn, spoofer, fakeSNI) + if err != nil { + spoofer.Close() + return nil, err + } + return result, nil } -func (c *Conn) Write(b []byte) (int, error) { +func newConn(conn net.Conn, spoofer rawSpoofer, fakeSNI string) (*Conn, error) { + fakeHello, err := buildFakeClientHello(fakeSNI) + if err != nil { + return nil, E.Cause(err, "tls_spoof: build fake ClientHello") + } + return &Conn{ + Conn: conn, + spoofer: spoofer, + fakeHello: fakeHello, + }, nil +} + +func (c *Conn) Write(b []byte) (n int, err error) { if c.injected { return c.Conn.Write(b) } - defer c.spoofer.Close() - fake, err := rewriteSNI(b, c.fakeSNI) - if err != nil { - return 0, E.Cause(err, "tls_spoof: rewrite SNI") - } - err = c.spoofer.Inject(fake) + defer func() { + closeErr := c.spoofer.Close() + if err == nil && closeErr != nil { + err = E.Cause(closeErr, "tls_spoof: close spoofer") + } + }() + err = c.spoofer.Inject(c.fakeHello) if err != nil { return 0, E.Cause(err, "tls_spoof: inject") } @@ -83,7 +97,7 @@ func (c *Conn) Write(b []byte) (int, error) { func (c *Conn) Close() error { return E.Append(c.Conn.Close(), c.spoofer.Close(), func(e error) error { - return E.Cause(e, "close spoofer") + return E.Cause(e, "tls_spoof: close spoofer") }) } diff --git a/common/windivert/handle_windows.go b/common/windivert/handle_windows.go index e7f5ae6736..1d7aebfdef 100644 --- a/common/windivert/handle_windows.go +++ b/common/windivert/handle_windows.go @@ -110,9 +110,13 @@ func validateOpenArgs(layer Layer, priority int16, flags Flag) error { if priority < PriorityLowest || priority > PriorityHighest { return E.New("windivert: priority out of range") } - if flags&^FlagSendOnly != 0 { + const supportedFlags = FlagSniff | FlagSendOnly + if flags&^supportedFlags != 0 { return E.New("windivert: unknown flag bits") } + if flags&FlagSniff != 0 && flags&FlagSendOnly != 0 { + return E.New("windivert: FlagSniff and FlagSendOnly are mutually exclusive") + } return nil } diff --git a/common/windivert/handle_windows_test.go b/common/windivert/handle_windows_test.go index dd05ce7b0c..73dfbb166a 100644 --- a/common/windivert/handle_windows_test.go +++ b/common/windivert/handle_windows_test.go @@ -100,6 +100,9 @@ func TestValidateOpenArgsFlags(t *testing.T) { t.Parallel() require.NoError(t, validateOpenArgs(LayerNetwork, 0, 0)) require.NoError(t, validateOpenArgs(LayerNetwork, 0, FlagSendOnly)) + require.NoError(t, validateOpenArgs(LayerNetwork, 0, FlagSniff)) + // Sniff and send-only describe contradictory handle roles. + require.Error(t, validateOpenArgs(LayerNetwork, 0, FlagSniff|FlagSendOnly)) // Unknown flag bits must be rejected to surface caller mistakes early. require.Error(t, validateOpenArgs(LayerNetwork, 0, Flag(0x10))) require.Error(t, validateOpenArgs(LayerNetwork, 0, FlagSendOnly|Flag(0x10))) diff --git a/common/windivert/windivert.go b/common/windivert/windivert.go index e9a8fc9545..9d309886cb 100644 --- a/common/windivert/windivert.go +++ b/common/windivert/windivert.go @@ -23,7 +23,14 @@ const LayerNetwork Layer = 0 type Flag uint64 -const FlagSendOnly Flag = 0x0008 +const ( + // FlagSniff opens a passive observer: the driver copies matching packets + // to userspace without removing them from the network stack. Send is not + // required (and not allowed) on a sniffing handle. + FlagSniff Flag = 0x0001 + // FlagSendOnly opens a write-only injection handle; Recv is not allowed. + FlagSendOnly Flag = 0x0008 +) const ( PriorityHighest int16 = 30000 From b358fdd564a1ddb0eaad8af369bb9c6803ebd7aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 20 Apr 2026 09:39:32 +0800 Subject: [PATCH 45/93] Add search domain support for Tailscale DNS --- docs/configuration/dns/server/tailscale.md | 15 ++++- docs/configuration/dns/server/tailscale.zh.md | 15 ++++- option/tailscale.go | 1 + protocol/tailscale/dns_transport.go | 55 +++++++++++++++++-- 4 files changed, 80 insertions(+), 6 deletions(-) diff --git a/docs/configuration/dns/server/tailscale.md b/docs/configuration/dns/server/tailscale.md index 2677f2b821..b2169ed382 100644 --- a/docs/configuration/dns/server/tailscale.md +++ b/docs/configuration/dns/server/tailscale.md @@ -2,6 +2,10 @@ icon: material/new-box --- +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [accept_search_domain](#accept_search_domain) + !!! question "Since sing-box 1.12.0" # Tailscale @@ -17,7 +21,8 @@ icon: material/new-box "tag": "", "endpoint": "ts-ep", - "accept_default_resolvers": false + "accept_default_resolvers": false, + "accept_search_domain": false } ] } @@ -38,6 +43,14 @@ Indicates whether default DNS resolvers should be accepted for fallback queries if not enabled, `NXDOMAIN` will be returned for non-Tailscale domain queries. +#### accept_search_domain + +!!! question "Since sing-box 1.14.0" + +When enabled, single-label queries (e.g. `my-device`) are retried against each Tailscale search domain until one resolves. + +Default resolvers are not consulted for single-label queries regardless of `accept_default_resolvers`. + ### Examples === "MagicDNS only" diff --git a/docs/configuration/dns/server/tailscale.zh.md b/docs/configuration/dns/server/tailscale.zh.md index 10d84038c5..e0086653d6 100644 --- a/docs/configuration/dns/server/tailscale.zh.md +++ b/docs/configuration/dns/server/tailscale.zh.md @@ -2,6 +2,10 @@ icon: material/new-box --- +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [accept_search_domain](#accept_search_domain) + !!! question "自 sing-box 1.12.0 起" # Tailscale @@ -17,7 +21,8 @@ icon: material/new-box "tag": "", "endpoint": "ts-ep", - "accept_default_resolvers": false + "accept_default_resolvers": false, + "accept_search_domain": false } ] } @@ -38,6 +43,14 @@ icon: material/new-box 如果未启用,对于非 Tailscale 域名查询将返回 `NXDOMAIN`。 +#### accept_search_domain + +!!! question "自 sing-box 1.14.0 起" + +启用后,单标签查询(例如 `my-device`)将依次附加 Tailscale 搜索域进行重试,直到其中一个解析成功。 + +对于单标签查询,无论 `accept_default_resolvers` 是否启用,都不会使用默认 DNS 解析器。 + ### 示例 === "仅 MagicDNS" diff --git a/option/tailscale.go b/option/tailscale.go index f763c905d9..a078e9aa88 100644 --- a/option/tailscale.go +++ b/option/tailscale.go @@ -36,6 +36,7 @@ type TailscaleEndpointOptions struct { type TailscaleDNSServerOptions struct { Endpoint string `json:"endpoint,omitempty"` AcceptDefaultResolvers bool `json:"accept_default_resolvers,omitempty"` + AcceptSearchDomain bool `json:"accept_search_domain,omitempty"` } type TailscaleCertificateProviderOptions struct { diff --git a/protocol/tailscale/dns_transport.go b/protocol/tailscale/dns_transport.go index 4195235cf5..5bc4f793a4 100644 --- a/protocol/tailscale/dns_transport.go +++ b/protocol/tailscale/dns_transport.go @@ -4,6 +4,7 @@ package tailscale import ( "context" + "errors" "net" "net/http" "net/netip" @@ -28,6 +29,7 @@ import ( "github.com/sagernet/sing/service" nDNS "github.com/sagernet/tailscale/net/dns" "github.com/sagernet/tailscale/types/dnstype" + "github.com/sagernet/tailscale/util/dnsname" "github.com/sagernet/tailscale/wgengine/router" "github.com/sagernet/tailscale/wgengine/wgcfg" @@ -46,6 +48,7 @@ type DNSTransport struct { logger logger.ContextLogger endpointTag string acceptDefaultResolvers bool + acceptSearchDomain bool dnsRouter adapter.DNSRouter endpointManager adapter.EndpointManager endpoint *Endpoint @@ -53,6 +56,7 @@ type DNSTransport struct { routePrefixes []netip.Prefix routes map[string][]adapter.DNSTransport hosts map[string][]netip.Addr + searchDomains []string defaultResolvers []adapter.DNSTransport } @@ -66,6 +70,7 @@ func NewDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, logger: logger, endpointTag: options.Endpoint, acceptDefaultResolvers: options.AcceptDefaultResolvers, + acceptSearchDomain: options.AcceptSearchDomain, dnsRouter: service.FromContext[adapter.DNSRouter](ctx), endpointManager: service.FromContext[adapter.EndpointManager](ctx), }, nil @@ -129,6 +134,9 @@ func (t *DNSTransport) updateDNSServers(routeConfig *router.Config, dnsConfig *n for domain, addresses := range dnsConfig.Hosts { hosts[domain.WithTrailingDot()] = addresses } + searchDomains := common.Map(dnsConfig.SearchDomains, func(it dnsname.FQDN) string { + return it.WithTrailingDot() + }) var defaultResolvers []adapter.DNSTransport for _, resolver := range dnsConfig.DefaultResolvers { myResolver, err := t.createResolver(directDialerOnce, resolver) @@ -143,6 +151,7 @@ func (t *DNSTransport) updateDNSServers(routeConfig *router.Config, dnsConfig *n t.routePrefixes = routePrefixes t.routes = routes t.hosts = hosts + t.searchDomains = searchDomains t.defaultResolvers = defaultResolvers t.access.Unlock() @@ -151,10 +160,10 @@ func (t *DNSTransport) updateDNSServers(routeConfig *router.Config, dnsConfig *n } if len(defaultResolvers) > 0 { - t.logger.Info("updated ", len(routes), " routes, ", len(hosts), " hosts, default resolvers: ", + t.logger.Info("updated ", len(routes), " routes, ", len(hosts), " hosts, ", len(searchDomains), " search domains, default resolvers: ", strings.Join(common.Map(dnsConfig.DefaultResolvers, func(it *dnstype.Resolver) string { return it.Addr }), " ")) } else { - t.logger.Info("updated ", len(routes), " routes, ", len(hosts), " hosts") + t.logger.Info("updated ", len(routes), " routes, ", len(hosts), " hosts, ", len(searchDomains), " search domains") } return nil } @@ -250,13 +259,51 @@ func (t *DNSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M if len(message.Question) != 1 { return nil, os.ErrInvalid } + if t.acceptSearchDomain && mDNS.CountLabel(message.Question[0].Name) == 1 { + return t.exchangeWithSearchDomains(ctx, message) + } + t.access.RLock() + acceptDefaultResolvers := t.acceptDefaultResolvers + t.access.RUnlock() + return t.exchangeOnce(ctx, message, acceptDefaultResolvers) +} + +func (t *DNSTransport) exchangeWithSearchDomains(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { + t.access.RLock() + searchDomains := t.searchDomains + t.access.RUnlock() + singleLabel := strings.TrimSuffix(message.Question[0].Name, ".") + var lastErr error + for _, searchDomain := range searchDomains { + question := message.Question[0] + question.Name = singleLabel + "." + searchDomain + rewritten := *message + rewritten.Question = []mDNS.Question{question} + response, err := t.exchangeOnce(ctx, &rewritten, false) + if err == nil { + if response.Rcode == mDNS.RcodeNameError { + continue + } + return response, nil + } + if errors.Is(err, dns.RcodeNameError) { + continue + } + lastErr = err + } + if lastErr != nil { + return nil, lastErr + } + return nil, dns.RcodeNameError +} + +func (t *DNSTransport) exchangeOnce(ctx context.Context, message *mDNS.Msg, allowDefaultResolvers bool) (*mDNS.Msg, error) { question := message.Question[0] t.access.RLock() hosts := t.hosts routes := t.routes defaultResolvers := t.defaultResolvers - acceptDefaultResolvers := t.acceptDefaultResolvers t.access.RUnlock() addresses, hostsLoaded := hosts[question.Name] @@ -302,7 +349,7 @@ func (t *DNSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M return nil, lastErr } } - if acceptDefaultResolvers { + if allowDefaultResolvers { if len(defaultResolvers) > 0 { var lastErr error for _, resolver := range defaultResolvers { From 6b51bd67786dffd19b554d3e949ce99e0a58640d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 21 Apr 2026 13:28:14 +0800 Subject: [PATCH 46/93] Log DNS optimistic background refresh outcomes --- dns/client.go | 6 +++++- dns/client_log.go | 13 +++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/dns/client.go b/dns/client.go index 53ab4ccd62..318ee2322f 100644 --- a/dns/client.go +++ b/dns/client.go @@ -500,7 +500,7 @@ func (c *Client) backgroundRefreshDNS(transport adapter.DNSTransport, question d response, err := c.exchangeToTransport(ctx, transport, message) if err != nil { if c.logger != nil { - c.logger.Debug("optimistic refresh failed for ", FqdnToDomain(question.Name), ": ", err) + c.logger.DebugContext(ctx, "optimistic refresh failed for ", FqdnToDomain(question.Name), ": ", err) } return } @@ -512,6 +512,9 @@ func (c *Client) backgroundRefreshDNS(transport adapter.DNSTransport, question d rejected = !responseChecker(response) } if rejected { + if c.logger != nil { + c.logger.DebugContext(ctx, "optimistic refresh rejected for ", FqdnToDomain(question.Name)) + } if c.rdrc != nil { c.rdrc.SaveRDRCAsync(transport.Tag(), question.Name, question.Qtype, c.logger) } @@ -522,6 +525,7 @@ func (c *Client) backgroundRefreshDNS(transport adapter.DNSTransport, question d } timeToLive := applyResponseOptions(question, response, options) c.storeCache(transport, question, response, timeToLive) + logRefreshedResponse(c.logger, ctx, response, timeToLive) }() } diff --git a/dns/client_log.go b/dns/client_log.go index 129e273c4b..abc726d3c4 100644 --- a/dns/client_log.go +++ b/dns/client_log.go @@ -48,6 +48,19 @@ func logExchangedResponse(logger logger.ContextLogger, ctx context.Context, resp } } +func logRefreshedResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg, ttl uint32) { + if logger == nil || len(response.Question) == 0 { + return + } + domain := FqdnToDomain(response.Question[0].Name) + logger.DebugContext(ctx, "refreshed ", domain, " ", dns.RcodeToString[response.Rcode], " ", ttl) + for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { + for _, record := range recordList { + logger.InfoContext(ctx, "refreshed ", dns.Type(record.Header().Rrtype).String(), " ", FormatQuestion(record.String())) + } + } +} + func logRejectedResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg) { if logger == nil || len(response.Question) == 0 { return From acfb90a38e5c3f228f1aad1cb8c4ff37b9f9f8b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 21 Apr 2026 14:41:02 +0800 Subject: [PATCH 47/93] Fix Tailscale search domain response name mismatch --- protocol/tailscale/dns_transport.go | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/protocol/tailscale/dns_transport.go b/protocol/tailscale/dns_transport.go index 5bc4f793a4..adfe388bfd 100644 --- a/protocol/tailscale/dns_transport.go +++ b/protocol/tailscale/dns_transport.go @@ -272,11 +272,13 @@ func (t *DNSTransport) exchangeWithSearchDomains(ctx context.Context, message *m t.access.RLock() searchDomains := t.searchDomains t.access.RUnlock() - singleLabel := strings.TrimSuffix(message.Question[0].Name, ".") + originalQuestion := message.Question[0] + singleLabel := strings.TrimSuffix(originalQuestion.Name, ".") var lastErr error for _, searchDomain := range searchDomains { - question := message.Question[0] - question.Name = singleLabel + "." + searchDomain + expandedName := singleLabel + "." + searchDomain + question := originalQuestion + question.Name = expandedName rewritten := *message rewritten.Question = []mDNS.Question{question} response, err := t.exchangeOnce(ctx, &rewritten, false) @@ -284,6 +286,7 @@ func (t *DNSTransport) exchangeWithSearchDomains(ctx context.Context, message *m if response.Rcode == mDNS.RcodeNameError { continue } + restoreOriginalQuestion(response, expandedName, originalQuestion) return response, nil } if errors.Is(err, dns.RcodeNameError) { @@ -297,6 +300,17 @@ func (t *DNSTransport) exchangeWithSearchDomains(ctx context.Context, message *m return nil, dns.RcodeNameError } +// RFC 1035 §4.1.1 requires the response Question to match the request byte-for-byte, +// and stub resolvers discard Answer RRs whose owner name does not match the question. +func restoreOriginalQuestion(response *mDNS.Msg, expandedName string, originalQuestion mDNS.Question) { + response.Question = []mDNS.Question{originalQuestion} + for _, rr := range response.Answer { + if strings.EqualFold(rr.Header().Name, expandedName) { + rr.Header().Name = originalQuestion.Name + } + } +} + func (t *DNSTransport) exchangeOnce(ctx context.Context, message *mDNS.Msg, allowDefaultResolvers bool) (*mDNS.Msg, error) { question := message.Question[0] From 5cd421401d289851c0f7364436ff0ecfc12d4c7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 21 Apr 2026 22:30:58 +0800 Subject: [PATCH 48/93] Fix goroutine leak in networkquality tool Serialize probe rounds in startProber to eliminate unbounded fan-out of fire-and-forget probe goroutines (up to 100/sec per direction), and close HTTP/3 transports via transport.Close() in addition to CloseIdleConnections. --- common/networkquality/networkquality.go | 52 +++++++++++++++---------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/common/networkquality/networkquality.go b/common/networkquality/networkquality.go index a4c73472cb..8373035d88 100644 --- a/common/networkquality/networkquality.go +++ b/common/networkquality/networkquality.go @@ -227,7 +227,7 @@ type loadConnection struct { } func (c *loadConnection) run(ctx context.Context, onError func(error)) { - defer c.client.CloseIdleConnections() + defer closeMeasurementClient(c.client) markActive := func() { c.ready.Store(true) c.active.Store(true) @@ -451,29 +451,31 @@ func (r *directionRunner) startProber(ctx context.Context) { if conn == nil { continue } - go func(selfClient *http.Client) { - foreignClient, err := r.factory(r.plan.connectEndpoint, true, true, nil, nil) - if err != nil { - return - } - round, err := collectProbeRound(ctx, foreignClient, selfClient, r.plan.probeURL.String()) - foreignClient.CloseIdleConnections() - if err != nil { - return - } - r.recordProbeRound(probeRound{ - interval: int(r.currentInterval.Load()), - tcp: round.tcp, - tls: round.tls, - httpFirst: round.httpFirst, - httpLoaded: round.httpLoaded, - }) - }(conn.client) + r.runProbeRound(ctx, conn.client) ticker.Reset(r.probeInterval()) } }() } +func (r *directionRunner) runProbeRound(ctx context.Context, selfClient *http.Client) { + foreignClient, err := r.factory(r.plan.connectEndpoint, true, true, nil, nil) + if err != nil { + return + } + defer closeMeasurementClient(foreignClient) + round, err := collectProbeRound(ctx, foreignClient, selfClient, r.plan.probeURL.String()) + if err != nil { + return + } + r.recordProbeRound(probeRound{ + interval: int(r.currentInterval.Load()), + tcp: round.tcp, + tls: round.tls, + httpFirst: round.httpFirst, + httpLoaded: round.httpLoaded, + }) +} + func (r *directionRunner) probeInterval() time.Duration { interval := time.Second / time.Duration(settings.maxProbesPerSecond) capacity := r.currentCapacity.Load() @@ -945,7 +947,7 @@ func measureIdleLatency(ctx context.Context, factory MeasurementClientFactory, c return 0, 0, err } measurement, err := runProbe(ctx, client, config.smallURL.String(), false) - client.CloseIdleConnections() + closeMeasurementClient(client) if err != nil { return 0, 0, err } @@ -1274,6 +1276,16 @@ func newRequest(ctx context.Context, method string, rawURL string, body io.Reade return req, nil } +func closeMeasurementClient(client *http.Client) { + if client == nil { + return + } + client.CloseIdleConnections() + if closer, ok := client.Transport.(interface{ Close() error }); ok { + _ = closer.Close() + } +} + func validateResponse(resp *http.Response) error { if resp.StatusCode < 200 || resp.StatusCode >= 300 { return E.New("unexpected status: ", resp.Status) From 817f9c63647dc451b56d69293538d408e49f928e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 26 Mar 2026 15:19:26 +0800 Subject: [PATCH 49/93] Add ACME profile support for IP address certificates --- common/tls/acme.go | 11 +++++++++++ .../configuration/shared/certificate-provider/acme.md | 10 ++++++++++ .../shared/certificate-provider/acme.zh.md | 10 ++++++++++ option/acme.go | 1 + option/tls_acme.go | 1 + service/acme/service.go | 11 +++++++++++ 6 files changed, 44 insertions(+) diff --git a/common/tls/acme.go b/common/tls/acme.go index d576fc6b1e..7491255a16 100644 --- a/common/tls/acme.go +++ b/common/tls/acme.go @@ -69,10 +69,21 @@ func startACME(ctx context.Context, logger logger.Logger, options option.Inbound Storage: storage, Logger: zapLogger, } + profile := options.Profile + if profile == "" && acmeServer == certmagic.LetsEncryptProductionCA { + for _, domain := range options.Domain { + if certmagic.SubjectIsIP(domain) { + profile = "shortlived" + break + } + } + } + acmeConfig := certmagic.ACMEIssuer{ CA: acmeServer, Email: options.Email, Agreed: true, + Profile: profile, DisableHTTPChallenge: options.DisableHTTPChallenge, DisableTLSALPNChallenge: options.DisableTLSALPNChallenge, AltHTTPPort: int(options.AlternativeHTTPPort), diff --git a/docs/configuration/shared/certificate-provider/acme.md b/docs/configuration/shared/certificate-provider/acme.md index 5f167c2e0b..30a5ba8d6d 100644 --- a/docs/configuration/shared/certificate-provider/acme.md +++ b/docs/configuration/shared/certificate-provider/acme.md @@ -6,6 +6,7 @@ icon: material/new-box :material-plus: [account_key](#account_key) :material-plus: [key_type](#key_type) + :material-plus: [profile](#profile) :material-plus: [http_client](#http_client) # ACME @@ -37,6 +38,7 @@ icon: material/new-box }, "dns01_challenge": {}, "key_type": "", + "profile": "", "http_client": "" // or {} } ``` @@ -141,6 +143,14 @@ The private key type to generate for new certificates. | `rsa2048` | RSA | | `rsa4096` | RSA | +#### profile + +!!! question "Since sing-box 1.14.0" + +The ACME profile to use for certificate issuance. + +When empty and `provider` is Let's Encrypt, `shortlived` will be used automatically if any domain is an IP address. + #### http_client !!! question "Since sing-box 1.14.0" diff --git a/docs/configuration/shared/certificate-provider/acme.zh.md b/docs/configuration/shared/certificate-provider/acme.zh.md index 2c895f5fe7..e01986d5bc 100644 --- a/docs/configuration/shared/certificate-provider/acme.zh.md +++ b/docs/configuration/shared/certificate-provider/acme.zh.md @@ -6,6 +6,7 @@ icon: material/new-box :material-plus: [account_key](#account_key) :material-plus: [key_type](#key_type) + :material-plus: [profile](#profile) :material-plus: [http_client](#http_client) # ACME @@ -37,6 +38,7 @@ icon: material/new-box }, "dns01_challenge": {}, "key_type": "", + "profile": "", "http_client": "" // 或 {} } ``` @@ -136,6 +138,14 @@ ACME DNS01 质询字段。如果配置,将禁用其他质询方法。 | `rsa2048` | RSA | | `rsa4096` | RSA | +#### profile + +!!! question "自 sing-box 1.14.0 起" + +用于证书签发的 ACME profile。 + +当为空且 `provider` 为 Let's Encrypt 时,如果任意域名为 IP 地址,将自动使用 `shortlived`。 + #### http_client !!! question "自 sing-box 1.14.0 起" diff --git a/option/acme.go b/option/acme.go index 79260b5dff..31efffce14 100644 --- a/option/acme.go +++ b/option/acme.go @@ -24,6 +24,7 @@ type ACMECertificateProviderOptions struct { ExternalAccount *ACMEExternalAccountOptions `json:"external_account,omitempty"` DNS01Challenge *ACMEProviderDNS01ChallengeOptions `json:"dns01_challenge,omitempty"` KeyType ACMEKeyType `json:"key_type,omitempty"` + Profile string `json:"profile,omitempty"` HTTPClient *HTTPClientOptions `json:"http_client,omitempty"` } diff --git a/option/tls_acme.go b/option/tls_acme.go index 6dd8fa7083..636abc6ece 100644 --- a/option/tls_acme.go +++ b/option/tls_acme.go @@ -20,6 +20,7 @@ type InboundACMEOptions struct { AlternativeTLSPort uint16 `json:"alternative_tls_port,omitempty"` ExternalAccount *ACMEExternalAccountOptions `json:"external_account,omitempty"` DNS01Challenge *ACMEDNS01ChallengeOptions `json:"dns01_challenge,omitempty"` + Profile string `json:"profile,omitempty"` } type ACMEExternalAccountOptions struct { diff --git a/service/acme/service.go b/service/acme/service.go index b29be131f2..b73ffb9da1 100644 --- a/service/acme/service.go +++ b/service/acme/service.go @@ -112,11 +112,22 @@ func NewCertificateProvider(ctx context.Context, logger log.ContextLogger, tag s config.KeySource = certmagic.StandardKeyGenerator{KeyType: keyType} } + profile := options.Profile + if profile == "" && acmeServer == certmagic.LetsEncryptProductionCA { + for _, domain := range options.Domain { + if certmagic.SubjectIsIP(domain) { + profile = "shortlived" + break + } + } + } + acmeIssuer := certmagic.ACMEIssuer{ CA: acmeServer, Email: options.Email, AccountKeyPEM: options.AccountKey, Agreed: true, + Profile: profile, DisableHTTPChallenge: options.DisableHTTPChallenge, DisableTLSALPNChallenge: options.DisableTLSALPNChallenge, AltHTTPPort: int(options.AlternativeHTTPPort), From 0f719a0715d037542541bb1a7f8d04271f77245c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 21 Apr 2026 22:42:26 +0800 Subject: [PATCH 50/93] Fix ACME HTTP-01 challenge for IPv6 literal addresses --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index bfdf00193c..df438e6841 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.24.7 require ( github.com/anthropics/anthropic-sdk-go v1.26.0 github.com/anytls/sing-anytls v0.0.11 - github.com/caddyserver/certmagic v0.25.2 + github.com/caddyserver/certmagic v0.25.3-0.20260421143802-60d9d8b415d6 github.com/caddyserver/zerossl v0.1.5 github.com/coder/websocket v1.8.14 github.com/cretz/bine v0.2.0 diff --git a/go.sum b/go.sum index 328595092f..c3b416bdd0 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/anytls/sing-anytls v0.0.11 h1:w8e9Uj1oP3m4zxkyZDewPk0EcQbvVxb7Nn+rapE github.com/anytls/sing-anytls v0.0.11/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8= github.com/caddyserver/certmagic v0.25.2 h1:D7xcS7ggX/WEY54x0czj7ioTkmDWKIgxtIi2OcQclUc= github.com/caddyserver/certmagic v0.25.2/go.mod h1:llW/CvsNmza8S6hmsuggsZeiX+uS27dkqY27wDIuBWg= +github.com/caddyserver/certmagic v0.25.3-0.20260421143802-60d9d8b415d6 h1:LYSB6VgWzKtNrcxElw3c97BP40Oc7bizKxA9K1Vi/5k= +github.com/caddyserver/certmagic v0.25.3-0.20260421143802-60d9d8b415d6/go.mod h1:llW/CvsNmza8S6hmsuggsZeiX+uS27dkqY27wDIuBWg= github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE= 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= From fbc1ca36e6a5032789f87046543b51d0a83d762f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 22 Apr 2026 01:52:05 +0800 Subject: [PATCH 51/93] platform: Improve oom-killer --- experimental/libbox/oom_report.go | 85 +++++++++++++++++++++++++++-- service/oomkiller/service.go | 37 +++++++++++++ service/oomkiller/service_darwin.go | 2 + 3 files changed, 120 insertions(+), 4 deletions(-) diff --git a/experimental/libbox/oom_report.go b/experimental/libbox/oom_report.go index e96c3e875d..64afc4b523 100644 --- a/experimental/libbox/oom_report.go +++ b/experimental/libbox/oom_report.go @@ -64,19 +64,63 @@ type oomReporter struct{} var _ oomkiller.OOMReporter = (*oomReporter)(nil) func (r *oomReporter) WriteReport(memoryUsage uint64) error { - now := time.Now().UTC() + draftPath := filepath.Join(sWorkingPath, "oom_draft") + draftInfo, err := os.Stat(draftPath) + if err != nil { + if !os.IsNotExist(err) { + return err + } + draftInfo = nil + } reportsDir := filepath.Join(sWorkingPath, "oom_reports") - err := os.MkdirAll(reportsDir, 0o777) + err = os.MkdirAll(reportsDir, 0o777) if err != nil { return err } chownReport(reportsDir) - destPath, err := nextAvailableReportPath(reportsDir, now) + destPath, err := nextAvailableReportPath(reportsDir, time.Now().UTC()) if err != nil { return err } - err = os.MkdirAll(destPath, 0o777) + err = r.writeSnapshot(destPath, memoryUsage) + if err != nil { + return err + } + return discardDraftIfCurrent(draftPath, draftInfo) +} + +func (r *oomReporter) WriteDraft(memoryUsage uint64) error { + draftPath := filepath.Join(sWorkingPath, "oom_draft") + os.RemoveAll(draftPath) + return r.writeSnapshot(draftPath, memoryUsage) +} + +func (r *oomReporter) DiscardDraft() error { + draftPath := filepath.Join(sWorkingPath, "oom_draft") + return os.RemoveAll(draftPath) +} + +func discardDraftIfCurrent(draftPath string, draftInfo os.FileInfo) error { + if draftInfo == nil { + return nil + } + currentInfo, err := os.Stat(draftPath) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + if !os.SameFile(draftInfo, currentInfo) { + return nil + } + return os.RemoveAll(draftPath) +} + +func (r *oomReporter) writeSnapshot(destPath string, memoryUsage uint64) error { + now := time.Now().UTC() + err := os.MkdirAll(destPath, 0o777) if err != nil { return err } @@ -139,3 +183,36 @@ func writeOOMProfile(destPath string, name string) { } chownReport(filePath) } + +func promoteOOMDraftAt(workingPath string) { + draftPath := filepath.Join(workingPath, "oom_draft") + info, err := os.Stat(draftPath) + if err != nil || !info.IsDir() { + return + } + reportsDir := filepath.Join(workingPath, "oom_reports") + initReportDir(reportsDir) + destPath, err := nextAvailableReportPath(reportsDir, info.ModTime().UTC()) + if err != nil { + os.RemoveAll(draftPath) + return + } + err = os.Rename(draftPath, destPath) + if err != nil { + os.RemoveAll(draftPath) + return + } + chownReport(destPath) +} + +func promoteOOMDraft() { + promoteOOMDraftAt(sWorkingPath) +} + +func PromoteOOMDraft() { + promoteOOMDraft() +} + +func PromoteOOMDraftAt(workingPath string) { + promoteOOMDraftAt(workingPath) +} diff --git a/service/oomkiller/service.go b/service/oomkiller/service.go index ec3838d2bf..7c19562e36 100644 --- a/service/oomkiller/service.go +++ b/service/oomkiller/service.go @@ -15,6 +15,8 @@ import ( type OOMReporter interface { WriteReport(memoryUsage uint64) error + WriteDraft(memoryUsage uint64) error + DiscardDraft() error } func RegisterService(registry *boxService.Registry) { @@ -29,6 +31,7 @@ type Service struct { timerConfig timerConfig adaptiveTimer *adaptiveTimer lastReportTime atomic.Int64 + draftCancelled atomic.Bool } func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.OOMKillerServiceOptions) (adapter.Service, error) { @@ -81,3 +84,37 @@ func (s *Service) writeOOMReport(memoryUsage uint64) { s.logger.Info("OOM report saved") } } + +func (s *Service) writeOOMDraft(memoryUsage uint64) { + if s.draftCancelled.Load() { + return + } + reporter := service.FromContext[OOMReporter](s.ctx) + if reporter == nil { + return + } + err := reporter.WriteDraft(memoryUsage) + if s.draftCancelled.Load() { + reporter.DiscardDraft() + return + } + if err != nil { + s.logger.Warn("failed to write OOM draft: ", err) + } else { + s.logger.Warn("OOM draft saved") + } +} + +func (s *Service) discardOOMDraft() { + s.draftCancelled.Store(true) + reporter := service.FromContext[OOMReporter](s.ctx) + if reporter == nil { + return + } + err := reporter.DiscardDraft() + if err != nil { + s.logger.Warn("failed to discard OOM draft: ", err) + } else { + s.logger.Info("OOM draft discarded") + } +} diff --git a/service/oomkiller/service_darwin.go b/service/oomkiller/service_darwin.go index 1d51c1b480..a40daea10e 100644 --- a/service/oomkiller/service_darwin.go +++ b/service/oomkiller/service_darwin.go @@ -83,6 +83,7 @@ func (s *Service) Close() error { if isLast { C.stopMemoryPressureMonitor() } + s.discardOOMDraft() } return nil } @@ -100,6 +101,7 @@ func goMemoryPressureCallback(status C.ulong) { sample := readMemorySample(policyModeNetworkExtension) for _, s := range services { s.logger.Warn("memory pressure: critical, usage: ", byteformats.FormatMemoryBytes(sample.usage)) + s.writeOOMDraft(sample.usage) s.adaptiveTimer.notifyPressure() } } From d06d3bf2a41cb79d4c69b53a7b2302770a41809b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 23 Apr 2026 07:05:56 +0800 Subject: [PATCH 52/93] Fix darwin cgo DNS again --- dns/transport/local/local_darwin_cgo.go | 196 +++++------------------- 1 file changed, 39 insertions(+), 157 deletions(-) diff --git a/dns/transport/local/local_darwin_cgo.go b/dns/transport/local/local_darwin_cgo.go index 11adf76fb4..6468a31f1a 100644 --- a/dns/transport/local/local_darwin_cgo.go +++ b/dns/transport/local/local_darwin_cgo.go @@ -4,52 +4,23 @@ package local /* #include +#include #include -#include -static void *cgo_dns_open_super() { - return (void *)dns_open(NULL); -} - -static void cgo_dns_close(void *opaque) { - if (opaque != NULL) dns_free((dns_handle_t)opaque); -} - -static int cgo_dns_search(void *opaque, const char *name, int class, int type, - unsigned char *answer, int anslen) { - dns_handle_t handle = (dns_handle_t)opaque; +static int cgo_dns_search(const char *name, int class, int type, + unsigned char *answer, int anslen, int *out_h_errno) { + dns_handle_t handle = (dns_handle_t)dns_open(NULL); + if (handle == NULL) { + *out_h_errno = NO_RECOVERY; + return -1; + } struct sockaddr_storage from; uint32_t fromlen = sizeof(from); - return dns_search(handle, name, class, type, (char *)answer, anslen, (struct sockaddr *)&from, &fromlen); -} - -static void *cgo_res_init() { - res_state state = calloc(1, sizeof(struct __res_state)); - if (state == NULL) return NULL; - if (res_ninit(state) != 0) { - free(state); - return NULL; - } - return state; -} - -static void cgo_res_destroy(void *opaque) { - res_state state = (res_state)opaque; - res_ndestroy(state); - free(state); -} - -static int cgo_res_nsearch(void *opaque, const char *dname, int class, int type, - unsigned char *answer, int anslen, - int timeout_seconds, - int *out_h_errno) { - res_state state = (res_state)opaque; - state->retrans = timeout_seconds; - state->retry = 1; - int n = res_nsearch(state, dname, class, type, answer, anslen); - if (n < 0) { - *out_h_errno = state->res_h_errno; - } + h_errno = 0; + int n = dns_search(handle, name, class, type, (char *)answer, anslen, + (struct sockaddr *)&from, &fromlen); + *out_h_errno = h_errno; + dns_free(handle); return n; } */ @@ -58,7 +29,6 @@ import "C" import ( "context" "errors" - "time" "unsafe" boxC "github.com/sagernet/sing-box/constant" @@ -73,125 +43,38 @@ const ( darwinResolverTryAgain = 2 darwinResolverNoRecovery = 3 darwinResolverNoData = 4 - - darwinResolverMaxPacketSize = 65535 ) -var errDarwinNeedLargerBuffer = errors.New("darwin resolver response truncated") - -func darwinLookupSystemDNS(name string, class, qtype, timeoutSeconds int) (*mDNS.Msg, error) { - response, err := darwinSearchWithSystemRouting(name, class, qtype) - if err == nil { - return response, nil - } - fallbackResponse, fallbackErr := darwinSearchWithResolv(name, class, qtype, timeoutSeconds) - if fallbackErr == nil || fallbackResponse != nil { - return fallbackResponse, fallbackErr - } - return nil, E.Errors( - E.Cause(err, "dns_search"), - E.Cause(fallbackErr, "res_nsearch"), - ) -} - -func darwinSearchWithSystemRouting(name string, class, qtype int) (*mDNS.Msg, error) { - handle := C.cgo_dns_open_super() - if handle == nil { - return nil, E.New("dns_open failed") - } - defer C.cgo_dns_close(handle) - - cName := C.CString(name) - defer C.free(unsafe.Pointer(cName)) - - bufSize := 1232 - for { - answer := make([]byte, bufSize) - n := C.cgo_dns_search(handle, cName, C.int(class), C.int(qtype), - (*C.uchar)(unsafe.Pointer(&answer[0])), C.int(len(answer))) - if n <= 0 { - return nil, E.New("dns_search failed for ", name) - } - if int(n) > bufSize { - bufSize = int(n) - continue - } - return unpackDarwinResolverMessage(answer[:int(n)], "dns_search") - } -} - -func darwinSearchWithResolv(name string, class, qtype int, timeoutSeconds int) (*mDNS.Msg, error) { - state := C.cgo_res_init() - if state == nil { - return nil, E.New("res_ninit failed") - } - defer C.cgo_res_destroy(state) - +func darwinLookupSystemDNS(name string, class, qtype int) (*mDNS.Msg, error) { cName := C.CString(name) defer C.free(unsafe.Pointer(cName)) - bufSize := 1232 - for { - answer := make([]byte, bufSize) - var hErrno C.int - n := C.cgo_res_nsearch(state, cName, C.int(class), C.int(qtype), - (*C.uchar)(unsafe.Pointer(&answer[0])), C.int(len(answer)), - C.int(timeoutSeconds), - &hErrno) - if n >= 0 { - if int(n) > bufSize { - bufSize = int(n) - continue - } - return unpackDarwinResolverMessage(answer[:int(n)], "res_nsearch") - } - response, err := handleDarwinResolvFailure(name, answer, int(hErrno)) - if err == nil { - return response, nil - } - if errors.Is(err, errDarwinNeedLargerBuffer) && bufSize < darwinResolverMaxPacketSize { - bufSize *= 2 - if bufSize > darwinResolverMaxPacketSize { - bufSize = darwinResolverMaxPacketSize - } - continue - } - return nil, err + answer := make([]byte, 4096) + var hErrno C.int + n := C.cgo_dns_search(cName, C.int(class), C.int(qtype), + (*C.uchar)(unsafe.Pointer(&answer[0])), C.int(len(answer)), + &hErrno) + if n <= 0 { + return nil, darwinResolverHErrno(name, int(hErrno)) } -} - -func unpackDarwinResolverMessage(packet []byte, source string) (*mDNS.Msg, error) { var response mDNS.Msg - err := response.Unpack(packet) + err := response.Unpack(answer[:int(n)]) if err != nil { - return nil, E.Cause(err, "unpack ", source, " response") + return nil, E.Cause(err, "unpack dns_search response") } return &response, nil } -func handleDarwinResolvFailure(name string, answer []byte, hErrno int) (*mDNS.Msg, error) { - response, err := unpackDarwinResolverMessage(answer, "res_nsearch failure") - if err == nil && response.Response { - if response.Truncated && len(answer) < darwinResolverMaxPacketSize { - return nil, errDarwinNeedLargerBuffer - } - return response, nil - } - return nil, darwinResolverHErrno(name, hErrno) -} - func darwinResolverHErrno(name string, hErrno int) error { switch hErrno { case darwinResolverHostNotFound: return dns.RcodeNameError - case darwinResolverTryAgain: - return dns.RcodeServerFailure - case darwinResolverNoRecovery: - return dns.RcodeServerFailure case darwinResolverNoData: return dns.RcodeSuccess + case darwinResolverTryAgain, darwinResolverNoRecovery: + return dns.RcodeServerFailure default: - return E.New("res_nsearch: unknown error ", hErrno, " for ", name) + return E.New("dns_search: unknown h_errno ", hErrno, " for ", name) } } @@ -209,26 +92,13 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, return t.dhcpTransport.Exchange0(ctx, message, dhcpServers) } } - name := question.Name - timeoutSeconds := int(boxC.DNSTimeout / time.Second) - if deadline, hasDeadline := ctx.Deadline(); hasDeadline { - remaining := time.Until(deadline) - if remaining <= 0 { - return nil, context.DeadlineExceeded - } - seconds := int(remaining.Seconds()) - if seconds < 1 { - seconds = 1 - } - timeoutSeconds = seconds - } type resolvResult struct { response *mDNS.Msg err error } resultCh := make(chan resolvResult, 1) go func() { - response, err := darwinLookupSystemDNS(name, int(question.Qclass), int(question.Qtype), timeoutSeconds) + response, err := darwinLookupSystemDNS(question.Name, int(question.Qclass), int(question.Qtype)) resultCh <- resolvResult{response, err} }() var result resolvResult @@ -245,5 +115,17 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, return nil, result.err } result.response.Id = message.Id + // Workaround for a bug in Apple libresolv: res_query_mDNSResponder + // (libresolv/res_query.c), used when the resolver has + // DNS_FLAG_FORWARD_TO_MDNSRESPONDER set (typical inside a Network + // Extension), writes: + // + // ans->qr = 1; + // ans->qr = htons(ans->qr); + // + // HEADER.qr is a 1-bit bitfield (), so + // htons(1) == 0x0100 gets truncated back to 0, clearing the QR bit. + // Force it on so downstream clients see a valid response. + result.response.Response = true return result.response, nil } From 968bbd832c1d612ac307fdd301504957915e6a22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 24 Apr 2026 01:14:24 +0800 Subject: [PATCH 53/93] Fix stderr deprecated manager --- experimental/deprecated/stderr.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/experimental/deprecated/stderr.go b/experimental/deprecated/stderr.go index 0dfb935409..a999baea85 100644 --- a/experimental/deprecated/stderr.go +++ b/experimental/deprecated/stderr.go @@ -3,11 +3,13 @@ package deprecated import ( "os" "strconv" + "sync" "github.com/sagernet/sing/common/logger" ) type stderrManager struct { + access sync.Mutex logger logger.Logger reported map[string]bool } @@ -20,6 +22,8 @@ func NewStderrManager(logger logger.Logger) Manager { } func (f *stderrManager) ReportDeprecated(feature Note) { + f.access.Lock() + defer f.access.Unlock() if f.reported[feature.Name] { return } From 3d3c8a2fe911d082529cf273d52db7fd59851a78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 24 Apr 2026 08:54:40 +0800 Subject: [PATCH 54/93] Improve UDP batch support --- adapter/handler.go | 38 ++------- adapter/inbound.go | 4 +- adapter/upstream.go | 76 ++++++++--------- adapter/upstream_legacy.go | 109 +++++++++++++----------- common/interrupt/conn.go | 3 +- common/listener/listener.go | 12 +-- common/listener/listener_tcp.go | 2 +- common/listener/listener_udp.go | 116 ++++++++++++++++++++++---- common/mux/router.go | 2 +- go.mod | 2 +- go.sum | 4 +- protocol/anytls/inbound.go | 2 +- protocol/direct/inbound.go | 37 +++++++- protocol/dns/outbound.go | 4 +- protocol/group/selector.go | 18 ++-- protocol/group/urltest.go | 4 +- protocol/http/inbound.go | 4 +- protocol/mixed/inbound.go | 6 +- protocol/redirect/redirect.go | 2 +- protocol/redirect/tproxy.go | 4 +- protocol/shadowsocks/inbound.go | 10 +-- protocol/shadowsocks/inbound_multi.go | 8 +- protocol/shadowsocks/inbound_relay.go | 6 +- protocol/shadowtls/inbound.go | 2 +- protocol/socks/inbound.go | 4 +- protocol/trojan/inbound.go | 8 +- protocol/vless/inbound.go | 6 +- protocol/vmess/inbound.go | 6 +- route/route.go | 12 +-- service/resolved/service.go | 4 +- 30 files changed, 307 insertions(+), 208 deletions(-) diff --git a/adapter/handler.go b/adapter/handler.go index f8912110f9..4f54d18666 100644 --- a/adapter/handler.go +++ b/adapter/handler.go @@ -5,57 +5,31 @@ import ( "net" "github.com/sagernet/sing/common/buf" - E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) -// Deprecated type ConnectionHandler interface { - NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error + NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc) } -type ConnectionHandlerEx interface { - NewConnectionEx(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc) -} - -// Deprecated: use PacketHandlerEx instead type PacketHandler interface { - NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata InboundContext) error + NewPacket(buffer *buf.Buffer, source M.Socksaddr) } -type PacketHandlerEx interface { - NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) +type PacketBatchHandler interface { + NewPacketBatch(buffers []*buf.Buffer, sources []M.Socksaddr) } -// Deprecated: use OOBPacketHandlerEx instead type OOBPacketHandler interface { - NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, oob []byte, metadata InboundContext) error -} - -type OOBPacketHandlerEx interface { - NewPacketEx(buffer *buf.Buffer, oob []byte, source M.Socksaddr) + NewPacket(buffer *buf.Buffer, oob []byte, source M.Socksaddr) } -// Deprecated type PacketConnectionHandler interface { - NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error + NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc) } -type PacketConnectionHandlerEx interface { - NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc) -} - -// Deprecated: use TCPConnectionHandlerEx instead -// -//nolint:staticcheck type UpstreamHandlerAdapter interface { - N.TCPConnectionHandler - N.UDPConnectionHandler - E.Handler -} - -type UpstreamHandlerAdapterEx interface { N.TCPConnectionHandlerEx N.UDPConnectionHandlerEx } diff --git a/adapter/inbound.go b/adapter/inbound.go index 6f53b1222e..923a8e668d 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -22,12 +22,12 @@ type Inbound interface { type TCPInjectableInbound interface { Inbound - ConnectionHandlerEx + ConnectionHandler } type UDPInjectableInbound interface { Inbound - PacketConnectionHandlerEx + PacketConnectionHandler } type InboundRegistry interface { diff --git a/adapter/upstream.go b/adapter/upstream.go index 59c8f75f6d..348e0ca8dc 100644 --- a/adapter/upstream.go +++ b/adapter/upstream.go @@ -9,31 +9,31 @@ import ( ) type ( - ConnectionHandlerFuncEx = func(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc) - PacketConnectionHandlerFuncEx = func(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc) + ConnectionHandlerFunc = func(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc) + PacketConnectionHandlerFunc = func(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc) ) -func NewUpstreamHandlerEx( +func NewUpstreamHandler( metadata InboundContext, - connectionHandler ConnectionHandlerFuncEx, - packetHandler PacketConnectionHandlerFuncEx, -) UpstreamHandlerAdapterEx { - return &myUpstreamHandlerWrapperEx{ + connectionHandler ConnectionHandlerFunc, + packetHandler PacketConnectionHandlerFunc, +) UpstreamHandlerAdapter { + return &myUpstreamHandlerWrapper{ metadata: metadata, connectionHandler: connectionHandler, packetHandler: packetHandler, } } -var _ UpstreamHandlerAdapterEx = (*myUpstreamHandlerWrapperEx)(nil) +var _ UpstreamHandlerAdapter = (*myUpstreamHandlerWrapper)(nil) -type myUpstreamHandlerWrapperEx struct { +type myUpstreamHandlerWrapper struct { metadata InboundContext - connectionHandler ConnectionHandlerFuncEx - packetHandler PacketConnectionHandlerFuncEx + connectionHandler ConnectionHandlerFunc + packetHandler PacketConnectionHandlerFunc } -func (w *myUpstreamHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { +func (w *myUpstreamHandlerWrapper) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { myMetadata := w.metadata if source.IsValid() { myMetadata.Source = source @@ -44,7 +44,7 @@ func (w *myUpstreamHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn n w.connectionHandler(ctx, conn, myMetadata, onClose) } -func (w *myUpstreamHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { +func (w *myUpstreamHandlerWrapper) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { myMetadata := w.metadata if source.IsValid() { myMetadata.Source = source @@ -55,24 +55,24 @@ func (w *myUpstreamHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, w.packetHandler(ctx, conn, myMetadata, onClose) } -var _ UpstreamHandlerAdapterEx = (*myUpstreamContextHandlerWrapperEx)(nil) +var _ UpstreamHandlerAdapter = (*myUpstreamContextHandlerWrapper)(nil) -type myUpstreamContextHandlerWrapperEx struct { - connectionHandler ConnectionHandlerFuncEx - packetHandler PacketConnectionHandlerFuncEx +type myUpstreamContextHandlerWrapper struct { + connectionHandler ConnectionHandlerFunc + packetHandler PacketConnectionHandlerFunc } -func NewUpstreamContextHandlerEx( - connectionHandler ConnectionHandlerFuncEx, - packetHandler PacketConnectionHandlerFuncEx, -) UpstreamHandlerAdapterEx { - return &myUpstreamContextHandlerWrapperEx{ +func NewUpstreamContextHandler( + connectionHandler ConnectionHandlerFunc, + packetHandler PacketConnectionHandlerFunc, +) UpstreamHandlerAdapter { + return &myUpstreamContextHandlerWrapper{ connectionHandler: connectionHandler, packetHandler: packetHandler, } } -func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { +func (w *myUpstreamContextHandlerWrapper) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { _, myMetadata := ExtendContext(ctx) if source.IsValid() { myMetadata.Source = source @@ -83,7 +83,7 @@ func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, w.connectionHandler(ctx, conn, *myMetadata, onClose) } -func (w *myUpstreamContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { +func (w *myUpstreamContextHandlerWrapper) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { _, myMetadata := ExtendContext(ctx) if source.IsValid() { myMetadata.Source = source @@ -94,24 +94,24 @@ func (w *myUpstreamContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Co w.packetHandler(ctx, conn, *myMetadata, onClose) } -func NewRouteHandlerEx( +func NewRouteHandler( metadata InboundContext, router ConnectionRouterEx, -) UpstreamHandlerAdapterEx { - return &routeHandlerWrapperEx{ +) UpstreamHandlerAdapter { + return &routeHandlerWrapper{ metadata: metadata, router: router, } } -var _ UpstreamHandlerAdapterEx = (*routeHandlerWrapperEx)(nil) +var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil) -type routeHandlerWrapperEx struct { +type routeHandlerWrapper struct { metadata InboundContext router ConnectionRouterEx } -func (r *routeHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { +func (r *routeHandlerWrapper) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { if source.IsValid() { r.metadata.Source = source } @@ -121,7 +121,7 @@ func (r *routeHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Co r.router.RouteConnectionEx(ctx, conn, r.metadata, onClose) } -func (r *routeHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { +func (r *routeHandlerWrapper) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { if source.IsValid() { r.metadata.Source = source } @@ -131,21 +131,21 @@ func (r *routeHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn r.router.RoutePacketConnectionEx(ctx, conn, r.metadata, onClose) } -func NewRouteContextHandlerEx( +func NewRouteContextHandler( router ConnectionRouterEx, -) UpstreamHandlerAdapterEx { - return &routeContextHandlerWrapperEx{ +) UpstreamHandlerAdapter { + return &routeContextHandlerWrapper{ router: router, } } -var _ UpstreamHandlerAdapterEx = (*routeContextHandlerWrapperEx)(nil) +var _ UpstreamHandlerAdapter = (*routeContextHandlerWrapper)(nil) -type routeContextHandlerWrapperEx struct { +type routeContextHandlerWrapper struct { router ConnectionRouterEx } -func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { +func (r *routeContextHandlerWrapper) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { _, metadata := ExtendContext(ctx) if source.IsValid() { metadata.Source = source @@ -156,7 +156,7 @@ func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn r.router.RouteConnectionEx(ctx, conn, *metadata, onClose) } -func (r *routeContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { +func (r *routeContextHandlerWrapper) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { _, metadata := ExtendContext(ctx) if source.IsValid() { metadata.Source = source diff --git a/adapter/upstream_legacy.go b/adapter/upstream_legacy.go index 65402563a4..c9320ddb80 100644 --- a/adapter/upstream_legacy.go +++ b/adapter/upstream_legacy.go @@ -12,21 +12,30 @@ import ( type ( // Deprecated - ConnectionHandlerFunc = func(ctx context.Context, conn net.Conn, metadata InboundContext) error + LegacyConnectionHandlerFunc = func(ctx context.Context, conn net.Conn, metadata InboundContext) error // Deprecated - PacketConnectionHandlerFunc = func(ctx context.Context, conn N.PacketConn, metadata InboundContext) error + LegacyPacketConnectionHandlerFunc = func(ctx context.Context, conn N.PacketConn, metadata InboundContext) error ) // Deprecated // //nolint:staticcheck -func NewUpstreamHandler( +type LegacyUpstreamHandlerAdapter interface { + N.TCPConnectionHandler + N.UDPConnectionHandler + E.Handler +} + +// Deprecated +// +//nolint:staticcheck +func NewLegacyUpstreamHandler( metadata InboundContext, - connectionHandler ConnectionHandlerFunc, - packetHandler PacketConnectionHandlerFunc, + connectionHandler LegacyConnectionHandlerFunc, + packetHandler LegacyPacketConnectionHandlerFunc, errorHandler E.Handler, -) UpstreamHandlerAdapter { - return &myUpstreamHandlerWrapper{ +) LegacyUpstreamHandlerAdapter { + return &legacyUpstreamHandlerWrapper{ metadata: metadata, connectionHandler: connectionHandler, packetHandler: packetHandler, @@ -34,20 +43,20 @@ func NewUpstreamHandler( } } -var _ UpstreamHandlerAdapter = (*myUpstreamHandlerWrapper)(nil) +var _ LegacyUpstreamHandlerAdapter = (*legacyUpstreamHandlerWrapper)(nil) -// Deprecated: use myUpstreamHandlerWrapperEx instead. +// Deprecated: use NewUpstreamHandler instead. // //nolint:staticcheck -type myUpstreamHandlerWrapper struct { +type legacyUpstreamHandlerWrapper struct { metadata InboundContext - connectionHandler ConnectionHandlerFunc - packetHandler PacketConnectionHandlerFunc + connectionHandler LegacyConnectionHandlerFunc + packetHandler LegacyPacketConnectionHandlerFunc errorHandler E.Handler } -// Deprecated: use myUpstreamHandlerWrapperEx instead. -func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { +// Deprecated: use NewUpstreamHandler instead. +func (w *legacyUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { myMetadata := w.metadata if metadata.Source.IsValid() { myMetadata.Source = metadata.Source @@ -58,8 +67,8 @@ func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.C return w.connectionHandler(ctx, conn, myMetadata) } -// Deprecated: use myUpstreamHandlerWrapperEx instead. -func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { +// Deprecated: use NewUpstreamHandler instead. +func (w *legacyUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { myMetadata := w.metadata if metadata.Source.IsValid() { myMetadata.Source = metadata.Source @@ -70,8 +79,8 @@ func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn return w.packetHandler(ctx, conn, myMetadata) } -// Deprecated: use myUpstreamHandlerWrapperEx instead. -func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) { +// Deprecated: use NewUpstreamHandler instead. +func (w *legacyUpstreamHandlerWrapper) NewError(ctx context.Context, err error) { w.errorHandler.NewError(ctx, err) } @@ -83,28 +92,28 @@ func UpstreamMetadata(metadata InboundContext) M.Metadata { } } -// Deprecated: Use NewUpstreamContextHandlerEx instead. -type myUpstreamContextHandlerWrapper struct { - connectionHandler ConnectionHandlerFunc - packetHandler PacketConnectionHandlerFunc +// Deprecated: Use NewUpstreamContextHandler instead. +type legacyUpstreamContextHandlerWrapper struct { + connectionHandler LegacyConnectionHandlerFunc + packetHandler LegacyPacketConnectionHandlerFunc errorHandler E.Handler } -// Deprecated: Use NewUpstreamContextHandlerEx instead. -func NewUpstreamContextHandler( - connectionHandler ConnectionHandlerFunc, - packetHandler PacketConnectionHandlerFunc, +// Deprecated: Use NewUpstreamContextHandler instead. +func NewLegacyUpstreamContextHandler( + connectionHandler LegacyConnectionHandlerFunc, + packetHandler LegacyPacketConnectionHandlerFunc, errorHandler E.Handler, -) UpstreamHandlerAdapter { - return &myUpstreamContextHandlerWrapper{ +) LegacyUpstreamHandlerAdapter { + return &legacyUpstreamContextHandlerWrapper{ connectionHandler: connectionHandler, packetHandler: packetHandler, errorHandler: errorHandler, } } -// Deprecated: Use NewUpstreamContextHandlerEx instead. -func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { +// Deprecated: Use NewUpstreamContextHandler instead. +func (w *legacyUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { myMetadata := ContextFrom(ctx) if metadata.Source.IsValid() { myMetadata.Source = metadata.Source @@ -115,8 +124,8 @@ func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, con return w.connectionHandler(ctx, conn, *myMetadata) } -// Deprecated: Use NewUpstreamContextHandlerEx instead. -func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { +// Deprecated: Use NewUpstreamContextHandler instead. +func (w *legacyUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { myMetadata := ContextFrom(ctx) if metadata.Source.IsValid() { myMetadata.Source = metadata.Source @@ -127,18 +136,18 @@ func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Contex return w.packetHandler(ctx, conn, *myMetadata) } -// Deprecated: Use NewUpstreamContextHandlerEx instead. -func (w *myUpstreamContextHandlerWrapper) NewError(ctx context.Context, err error) { +// Deprecated: Use NewUpstreamContextHandler instead. +func (w *legacyUpstreamContextHandlerWrapper) NewError(ctx context.Context, err error) { w.errorHandler.NewError(ctx, err) } // Deprecated: Use ConnectionRouterEx instead. -func NewRouteHandler( +func NewLegacyRouteHandler( metadata InboundContext, router ConnectionRouter, logger logger.ContextLogger, -) UpstreamHandlerAdapter { - return &routeHandlerWrapper{ +) LegacyUpstreamHandlerAdapter { + return &legacyRouteHandlerWrapper{ metadata: metadata, router: router, logger: logger, @@ -146,29 +155,29 @@ func NewRouteHandler( } // Deprecated: Use ConnectionRouterEx instead. -func NewRouteContextHandler( +func NewLegacyRouteContextHandler( router ConnectionRouter, logger logger.ContextLogger, -) UpstreamHandlerAdapter { - return &routeContextHandlerWrapper{ +) LegacyUpstreamHandlerAdapter { + return &legacyRouteContextHandlerWrapper{ router: router, logger: logger, } } -var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil) +var _ LegacyUpstreamHandlerAdapter = (*legacyRouteHandlerWrapper)(nil) // Deprecated: Use ConnectionRouterEx instead. // //nolint:staticcheck -type routeHandlerWrapper struct { +type legacyRouteHandlerWrapper struct { metadata InboundContext router ConnectionRouter logger logger.ContextLogger } // Deprecated: Use ConnectionRouterEx instead. -func (w *routeHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { +func (w *legacyRouteHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { myMetadata := w.metadata if metadata.Source.IsValid() { myMetadata.Source = metadata.Source @@ -180,7 +189,7 @@ func (w *routeHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, } // Deprecated: Use ConnectionRouterEx instead. -func (w *routeHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { +func (w *legacyRouteHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { myMetadata := w.metadata if metadata.Source.IsValid() { myMetadata.Source = metadata.Source @@ -192,20 +201,20 @@ func (w *routeHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.Pa } // Deprecated: Use ConnectionRouterEx instead. -func (w *routeHandlerWrapper) NewError(ctx context.Context, err error) { +func (w *legacyRouteHandlerWrapper) NewError(ctx context.Context, err error) { w.logger.ErrorContext(ctx, err) } -var _ UpstreamHandlerAdapter = (*routeContextHandlerWrapper)(nil) +var _ LegacyUpstreamHandlerAdapter = (*legacyRouteContextHandlerWrapper)(nil) // Deprecated: Use ConnectionRouterEx instead. -type routeContextHandlerWrapper struct { +type legacyRouteContextHandlerWrapper struct { router ConnectionRouter logger logger.ContextLogger } // Deprecated: Use ConnectionRouterEx instead. -func (w *routeContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { +func (w *legacyRouteContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { myMetadata := ContextFrom(ctx) if metadata.Source.IsValid() { myMetadata.Source = metadata.Source @@ -217,7 +226,7 @@ func (w *routeContextHandlerWrapper) NewConnection(ctx context.Context, conn net } // Deprecated: Use ConnectionRouterEx instead. -func (w *routeContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { +func (w *legacyRouteContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { myMetadata := ContextFrom(ctx) if metadata.Source.IsValid() { myMetadata.Source = metadata.Source @@ -229,6 +238,6 @@ func (w *routeContextHandlerWrapper) NewPacketConnection(ctx context.Context, co } // Deprecated: Use ConnectionRouterEx instead. -func (w *routeContextHandlerWrapper) NewError(ctx context.Context, err error) { +func (w *legacyRouteContextHandlerWrapper) NewError(ctx context.Context, err error) { w.logger.ErrorContext(ctx, err) } diff --git a/common/interrupt/conn.go b/common/interrupt/conn.go index 6a6d31c68b..94caa915d4 100644 --- a/common/interrupt/conn.go +++ b/common/interrupt/conn.go @@ -3,6 +3,7 @@ package interrupt import ( "net" + "github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/x/list" ) @@ -71,5 +72,5 @@ func (c *PacketConn) WriterReplaceable() bool { } func (c *PacketConn) Upstream() any { - return c.PacketConn + return bufio.NewPacketConn(c.PacketConn) } diff --git a/common/listener/listener.go b/common/listener/listener.go index cc27a62e18..4ca82b6203 100644 --- a/common/listener/listener.go +++ b/common/listener/listener.go @@ -25,9 +25,9 @@ type Listener struct { logger logger.ContextLogger network []string listenOptions option.ListenOptions - connHandler adapter.ConnectionHandlerEx - packetHandler adapter.PacketHandlerEx - oobPacketHandler adapter.OOBPacketHandlerEx + connHandler adapter.ConnectionHandler + packetHandler adapter.PacketHandler + oobPacketHandler adapter.OOBPacketHandler threadUnsafePacketWriter bool disablePacketOutput bool setSystemProxy bool @@ -48,9 +48,9 @@ type Options struct { Logger logger.ContextLogger Network []string Listen option.ListenOptions - ConnectionHandler adapter.ConnectionHandlerEx - PacketHandler adapter.PacketHandlerEx - OOBPacketHandler adapter.OOBPacketHandlerEx + ConnectionHandler adapter.ConnectionHandler + PacketHandler adapter.PacketHandler + OOBPacketHandler adapter.OOBPacketHandler ThreadUnsafePacketWriter bool DisablePacketOutput bool SetSystemProxy bool diff --git a/common/listener/listener_tcp.go b/common/listener/listener_tcp.go index 54d84a6b7b..8cfdd6e6c3 100644 --- a/common/listener/listener_tcp.go +++ b/common/listener/listener_tcp.go @@ -106,6 +106,6 @@ func (l *Listener) loopTCPIn() { metadata.OriginDestination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap() ctx := log.ContextWithNewID(l.ctx) l.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) - go l.connHandler.NewConnectionEx(ctx, conn, metadata, nil) + go l.connHandler.NewConnection(ctx, conn, metadata, nil) } } diff --git a/common/listener/listener_udp.go b/common/listener/listener_udp.go index e689c8bb67..ed48a75553 100644 --- a/common/listener/listener_udp.go +++ b/common/listener/listener_udp.go @@ -11,6 +11,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/redir" "github.com/sagernet/sing/common/buf" + sBufio "github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" @@ -18,6 +19,8 @@ import ( "github.com/sagernet/sing/service" ) +const udpOutputBatchSize = 128 + func (l *Listener) ListenUDP() (net.PacketConn, error) { bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1})), l.listenOptions.ListenPort) var listenConfig net.ListenConfig @@ -98,6 +101,15 @@ func (l *Listener) PacketWriter() N.PacketWriter { func (l *Listener) loopUDPIn() { defer close(l.packetOutboundClosed) + if l.oobPacketHandler == nil { + if batchHandler, isBatchHandler := l.packetHandler.(adapter.PacketBatchHandler); isBatchHandler { + packetConn := sBufio.NewPacketConn(l.udpConn) + if readWaiter, created := sBufio.CreatePacketBatchReadWaiter(packetConn); created { + l.loopUDPInBatch(batchHandler, readWaiter) + return + } + } + } var buffer *buf.Buffer if !l.threadUnsafePacketWriter { buffer = buf.NewPacket() @@ -126,7 +138,7 @@ func (l *Listener) loopUDPIn() { return } buffer.Truncate(n) - l.oobPacketHandler.NewPacketEx(buffer, oob[:oobN], M.SocksaddrFromNetIP(addr).Unwrap()) + l.oobPacketHandler.NewPacket(buffer, oob[:oobN], M.SocksaddrFromNetIP(addr).Unwrap()) } } else { for { @@ -148,37 +160,82 @@ func (l *Listener) loopUDPIn() { return } buffer.Truncate(n) - l.packetHandler.NewPacketEx(buffer, M.SocksaddrFromNetIP(addr).Unwrap()) + l.packetHandler.NewPacket(buffer, M.SocksaddrFromNetIP(addr).Unwrap()) + } + } +} + +func (l *Listener) loopUDPInBatch(handler adapter.PacketBatchHandler, readWaiter N.PacketBatchReadWaiter) { + readWaitOptions := N.ReadWaitOptions{ + BatchSize: sBufio.DefaultPacketReadBatchSize, + } + readWaiter.InitializeReadWaiter(readWaitOptions) + for { + buffers, sources, err := readWaiter.WaitReadPackets() + if err != nil { + buf.ReleaseMulti(buffers) + if l.shutdown.Load() && E.IsClosed(err) { + return + } + l.udpConn.Close() + l.logger.Error("udp listener closed: ", err) + return } + handler.NewPacketBatch(buffers, sources) } } func (l *Listener) loopUDPOut() { + packetConn := sBufio.NewPacketConn(l.udpConn) + batchWriter := sBufio.NewPacketBatchWriter(packetConn) + packets := make([]*N.PacketBuffer, 0, udpOutputBatchSize) + buffers := make([]*buf.Buffer, 0, udpOutputBatchSize) + destinations := make([]M.Socksaddr, 0, udpOutputBatchSize) for { select { case packet := <-l.packetOutbound: - destination := packet.Destination.AddrPort() - _, err := l.udpConn.WriteToUDPAddrPort(packet.Buffer.Bytes(), destination) - packet.Buffer.Release() - N.PutPacketBuffer(packet) - if err != nil { - if l.shutdown.Load() && E.IsClosed(err) { - return - } - l.logger.Error("udp listener write back: ", destination, ": ", err) - continue - } - continue + packets = append(packets, packet) case <-l.packetOutboundClosed: + l.releasePacketOutbound() + return } - for { + drain: + for len(packets) < udpOutputBatchSize { select { case packet := <-l.packetOutbound: - packet.Buffer.Release() - N.PutPacketBuffer(packet) + packets = append(packets, packet) default: + break drain + } + } + for _, packet := range packets { + buffers = append(buffers, packet.Buffer) + destinations = append(destinations, packet.Destination) + } + err := batchWriter.WritePacketBatch(buffers, destinations) + for _, packet := range packets { + N.PutPacketBuffer(packet) + } + packets = packets[:0] + buffers = buffers[:0] + destinations = destinations[:0] + if err != nil { + if l.shutdown.Load() && E.IsClosed(err) { return } + l.logger.Error("udp listener write back: ", err) + } + } +} + +func (l *Listener) releasePacketOutbound() { + for { + select { + case packet := <-l.packetOutbound: + packet.Buffer.Release() + N.PutPacketBuffer(packet) + default: + return } } } @@ -203,5 +260,30 @@ func (w *packetWriter) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) } } +func (w *packetWriter) WritePacketBatch(buffers []*buf.Buffer, destinations []M.Socksaddr) error { + if len(buffers) == 0 || len(buffers) != len(destinations) { + buf.ReleaseMulti(buffers) + return os.ErrInvalid + } + for index, buffer := range buffers { + packet := N.NewPacketBuffer() + packet.Buffer = buffer + packet.Destination = destinations[index] + select { + case w.packetOutbound <- packet: + default: + buffer.Release() + N.PutPacketBuffer(packet) + buf.ReleaseMulti(buffers[index+1:]) + if w.shutdown.Load() { + return os.ErrClosed + } + w.logger.Trace("dropped packet batch to ", destinations[index]) + return nil + } + } + return nil +} + func (w *packetWriter) WriteIsThreadUnsafe() { } diff --git a/common/mux/router.go b/common/mux/router.go index ec78808600..6de6c4d6c7 100644 --- a/common/mux/router.go +++ b/common/mux/router.go @@ -42,7 +42,7 @@ func NewRouterWithOptions(router adapter.ConnectionRouterEx, logger logger.Conte return log.ContextWithNewID(ctx) }, Logger: logger, - HandlerEx: adapter.NewRouteContextHandlerEx(router), + HandlerEx: adapter.NewRouteContextHandler(router), Padding: options.Padding, Brutal: brutalOptions, }) diff --git a/go.mod b/go.mod index df438e6841..0e76fa5f73 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,7 @@ require ( github.com/sagernet/gomobile v0.1.12 github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 - github.com/sagernet/sing v0.8.10-0.20260421111925-3e730f2301b4 + github.com/sagernet/sing v0.8.10-0.20260424005254-7b2d7ac5204c github.com/sagernet/sing-cloudflared v0.1.0 github.com/sagernet/sing-mux v0.3.4 github.com/sagernet/sing-quic v0.6.2-0.20260412143638-8f65b6be7cd6 diff --git a/go.sum b/go.sum index c3b416bdd0..8ec6f38a6e 100644 --- a/go.sum +++ b/go.sum @@ -244,8 +244,8 @@ github.com/sagernet/nftables v0.3.0-mod.2 h1:ck2KMU02OxL1eDFgGaWYglMDpoOZ7OHzxje github.com/sagernet/nftables v0.3.0-mod.2/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4= -github.com/sagernet/sing v0.8.10-0.20260421111925-3e730f2301b4 h1:pqb1VEG6BXNTid2Llp/AT8ok7FZuCCis41glydWhgno= -github.com/sagernet/sing v0.8.10-0.20260421111925-3e730f2301b4/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing v0.8.10-0.20260424005254-7b2d7ac5204c h1:hSVSiYyv3x0wNn38mnlOwoTwod+vW4XE251KG/uaA4U= +github.com/sagernet/sing v0.8.10-0.20260424005254-7b2d7ac5204c/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing-cloudflared v0.1.0 h1:to+2fcCx8zu4X/DirRw9Ihc+FrEZ7oEyIqeCoJiwIpw= github.com/sagernet/sing-cloudflared v0.1.0/go.mod h1:bH2NKX+NpDTY1Zkxfboxw6MXB/ZywaNLmrDJYgKMJ2Y= github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s= diff --git a/protocol/anytls/inbound.go b/protocol/anytls/inbound.go index 52d773537a..e61e837a2a 100644 --- a/protocol/anytls/inbound.go +++ b/protocol/anytls/inbound.go @@ -96,7 +96,7 @@ func (h *Inbound) Close() error { return common.Close(h.listener, h.tlsConfig) } -func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { +func (h *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { if h.tlsConfig != nil { tlsConn, err := tls.ServerHandshake(ctx, conn, h.tlsConfig) if err != nil { diff --git a/protocol/direct/inbound.go b/protocol/direct/inbound.go index 81353b6599..fcb4671d89 100644 --- a/protocol/direct/inbound.go +++ b/protocol/direct/inbound.go @@ -3,6 +3,7 @@ package direct import ( "context" "net" + "os" "time" "github.com/sagernet/sing-box/adapter" @@ -80,11 +81,15 @@ func (i *Inbound) Close() error { return i.listener.Close() } -func (i *Inbound) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) { +func (i *Inbound) NewPacket(buffer *buf.Buffer, source M.Socksaddr) { i.udpNat.NewPacket([][]byte{buffer.Bytes()}, source, i.listener.UDPAddr(), nil) } -func (i *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { +func (i *Inbound) NewPacketBatch(buffers []*buf.Buffer, sources []M.Socksaddr) { + i.udpNat.NewPacketBatch(buffers, sources, i.listener.UDPAddr(), nil) +} + +func (i *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = i.Tag() metadata.InboundType = i.Type() destination := metadata.OriginDestination @@ -142,3 +147,31 @@ type directPacketWriter struct { func (w *directPacketWriter) WritePacket(buffer *buf.Buffer, addr M.Socksaddr) error { return w.writer.WritePacket(buffer, w.source) } + +func (w *directPacketWriter) CreatePacketBatchWriter() (N.PacketBatchWriter, bool) { + writer, created := bufio.CreatePacketBatchWriter(w.writer) + if !created { + return nil, false + } + return &directPacketBatchWriter{ + writer: writer, + source: w.source, + }, true +} + +type directPacketBatchWriter struct { + writer N.PacketBatchWriter + source M.Socksaddr +} + +func (w *directPacketBatchWriter) WritePacketBatch(buffers []*buf.Buffer, destinations []M.Socksaddr) error { + if len(buffers) == 0 || len(buffers) != len(destinations) { + buf.ReleaseMulti(buffers) + return os.ErrInvalid + } + sources := make([]M.Socksaddr, len(destinations)) + for index := range sources { + sources[index] = w.source + } + return w.writer.WritePacketBatch(buffers, sources) +} diff --git a/protocol/dns/outbound.go b/protocol/dns/outbound.go index 277d7454ea..747d578ea7 100644 --- a/protocol/dns/outbound.go +++ b/protocol/dns/outbound.go @@ -43,7 +43,7 @@ func (d *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (n return nil, os.ErrInvalid } -func (d *Outbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { +func (d *Outbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Destination = M.Socksaddr{} for { conn.SetReadDeadline(time.Now().Add(C.DNSTimeout)) @@ -58,6 +58,6 @@ func (d *Outbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata } } -func (d *Outbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { +func (d *Outbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { NewDNSPacketConnection(ctx, d.router, conn, nil, metadata) } diff --git a/protocol/group/selector.go b/protocol/group/selector.go index f3f7377b61..85bea2b964 100644 --- a/protocol/group/selector.go +++ b/protocol/group/selector.go @@ -25,9 +25,9 @@ func RegisterSelector(registry *outbound.Registry) { } var ( - _ adapter.OutboundGroup = (*Selector)(nil) - _ adapter.ConnectionHandlerEx = (*Selector)(nil) - _ adapter.PacketConnectionHandlerEx = (*Selector)(nil) + _ adapter.OutboundGroup = (*Selector)(nil) + _ adapter.ConnectionHandler = (*Selector)(nil) + _ adapter.PacketConnectionHandler = (*Selector)(nil) ) type Selector struct { @@ -156,21 +156,21 @@ func (s *Selector) ListenPacket(ctx context.Context, destination M.Socksaddr) (n return s.interruptGroup.NewPacketConn(conn, interrupt.IsExternalConnectionFromContext(ctx)), nil } -func (s *Selector) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { +func (s *Selector) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { ctx = interrupt.ContextWithIsExternalConnection(ctx) selected := s.selected.Load() - if outboundHandler, isHandler := selected.(adapter.ConnectionHandlerEx); isHandler { - outboundHandler.NewConnectionEx(ctx, conn, metadata, onClose) + if outboundHandler, isHandler := selected.(adapter.ConnectionHandler); isHandler { + outboundHandler.NewConnection(ctx, conn, metadata, onClose) } else { s.connection.NewConnection(ctx, selected, conn, metadata, onClose) } } -func (s *Selector) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { +func (s *Selector) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { ctx = interrupt.ContextWithIsExternalConnection(ctx) selected := s.selected.Load() - if outboundHandler, isHandler := selected.(adapter.PacketConnectionHandlerEx); isHandler { - outboundHandler.NewPacketConnectionEx(ctx, conn, metadata, onClose) + if outboundHandler, isHandler := selected.(adapter.PacketConnectionHandler); isHandler { + outboundHandler.NewPacketConnection(ctx, conn, metadata, onClose) } else { s.connection.NewPacketConnection(ctx, selected, conn, metadata, onClose) } diff --git a/protocol/group/urltest.go b/protocol/group/urltest.go index 26967279db..03d2e8afbb 100644 --- a/protocol/group/urltest.go +++ b/protocol/group/urltest.go @@ -161,12 +161,12 @@ func (s *URLTest) ListenPacket(ctx context.Context, destination M.Socksaddr) (ne return nil, err } -func (s *URLTest) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { +func (s *URLTest) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { ctx = interrupt.ContextWithIsExternalConnection(ctx) s.connection.NewConnection(ctx, s, conn, metadata, onClose) } -func (s *URLTest) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { +func (s *URLTest) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { ctx = interrupt.ContextWithIsExternalConnection(ctx) s.connection.NewPacketConnection(ctx, s, conn, metadata, onClose) } diff --git a/protocol/http/inbound.go b/protocol/http/inbound.go index e8a9a3daa5..fe573ea1e0 100644 --- a/protocol/http/inbound.go +++ b/protocol/http/inbound.go @@ -86,7 +86,7 @@ func (h *Inbound) Close() error { ) } -func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { +func (h *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { if h.tlsConfig != nil { tlsConn, err := tls.ServerHandshake(ctx, conn, h.tlsConfig) if err != nil { @@ -96,7 +96,7 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata a } conn = tlsConn } - err := http.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), h.authenticator, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, onClose) + err := http.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), h.authenticator, adapter.NewUpstreamHandler(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, onClose) if err != nil { N.CloseOnHandshakeFailure(conn, onClose, err) h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) diff --git a/protocol/mixed/inbound.go b/protocol/mixed/inbound.go index 64c3edb5b6..d35319473a 100644 --- a/protocol/mixed/inbound.go +++ b/protocol/mixed/inbound.go @@ -98,7 +98,7 @@ func (h *Inbound) Close() error { ) } -func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { +func (h *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { err := h.newConnection(ctx, conn, metadata, onClose) N.CloseOnHandshakeFailure(conn, onClose, err) if err != nil { @@ -125,9 +125,9 @@ func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata ada } switch headerBytes[0] { case socks4.Version, socks5.Version: - return socks.HandleConnectionEx(ctx, conn, reader, h.authenticator, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), h.listener, h.udpTimeout, metadata.Source, onClose) + return socks.HandleConnectionEx(ctx, conn, reader, h.authenticator, adapter.NewUpstreamHandler(metadata, h.newUserConnection, h.streamUserPacketConnection), h.listener, h.udpTimeout, metadata.Source, onClose) default: - return http.HandleConnectionEx(ctx, conn, reader, h.authenticator, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, onClose) + return http.HandleConnectionEx(ctx, conn, reader, h.authenticator, adapter.NewUpstreamHandler(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, onClose) } } diff --git a/protocol/redirect/redirect.go b/protocol/redirect/redirect.go index e04db8c4df..ff52bd7b4b 100644 --- a/protocol/redirect/redirect.go +++ b/protocol/redirect/redirect.go @@ -53,7 +53,7 @@ func (h *Redirect) Close() error { return h.listener.Close() } -func (h *Redirect) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { +func (h *Redirect) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { destination, err := redir.GetOriginalDestination(conn) if err != nil { conn.Close() diff --git a/protocol/redirect/tproxy.go b/protocol/redirect/tproxy.go index f0b82bb132..5f24162629 100644 --- a/protocol/redirect/tproxy.go +++ b/protocol/redirect/tproxy.go @@ -71,7 +71,7 @@ func (t *TProxy) Close() error { return t.listener.Close() } -func (t *TProxy) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { +func (t *TProxy) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = t.Tag() metadata.InboundType = t.Type() metadata.Destination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap() @@ -91,7 +91,7 @@ func (t *TProxy) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, s t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } -func (t *TProxy) NewPacketEx(buffer *buf.Buffer, oob []byte, source M.Socksaddr) { +func (t *TProxy) NewPacket(buffer *buf.Buffer, oob []byte, source M.Socksaddr) { destination, err := redir.GetOriginalDestinationFromOOB(oob) if err != nil { t.logger.Warn("process packet from ", source, ": get tproxy destination: ", err) diff --git a/protocol/shadowsocks/inbound.go b/protocol/shadowsocks/inbound.go index 52e2c52472..3fc9dc747a 100644 --- a/protocol/shadowsocks/inbound.go +++ b/protocol/shadowsocks/inbound.go @@ -75,11 +75,11 @@ func newInbound(ctx context.Context, router adapter.Router, logger log.ContextLo } switch { case options.Method == shadowsocks.MethodNone: - inbound.service = shadowsocks.NewNoneService(int64(udpTimeout.Seconds()), adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound)) + inbound.service = shadowsocks.NewNoneService(int64(udpTimeout.Seconds()), adapter.NewLegacyUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound)) case common.Contains(shadowaead.List, options.Method): - inbound.service, err = shadowaead.NewService(options.Method, nil, options.Password, int64(udpTimeout.Seconds()), adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound)) + inbound.service, err = shadowaead.NewService(options.Method, nil, options.Password, int64(udpTimeout.Seconds()), adapter.NewLegacyUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound)) case common.Contains(shadowaead_2022.List, options.Method): - inbound.service, err = shadowaead_2022.NewServiceWithPassword(options.Method, options.Password, int64(udpTimeout.Seconds()), adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound), ntp.TimeFuncFromContext(ctx)) + inbound.service, err = shadowaead_2022.NewServiceWithPassword(options.Method, options.Password, int64(udpTimeout.Seconds()), adapter.NewLegacyUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound), ntp.TimeFuncFromContext(ctx)) default: err = E.New("unsupported method: ", options.Method) } @@ -107,7 +107,7 @@ func (h *Inbound) Close() error { } //nolint:staticcheck -func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { +func (h *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { err := h.service.NewConnection(ctx, conn, adapter.UpstreamMetadata(metadata)) N.CloseOnHandshakeFailure(conn, onClose, err) if err != nil { @@ -120,7 +120,7 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata a } //nolint:staticcheck -func (h *Inbound) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) { +func (h *Inbound) NewPacket(buffer *buf.Buffer, source M.Socksaddr) { err := h.service.NewPacket(h.ctx, &stubPacketConn{h.listener.PacketWriter()}, buffer, M.Metadata{Source: source}) if err != nil { h.logger.Error(E.Cause(err, "process packet from ", source)) diff --git a/protocol/shadowsocks/inbound_multi.go b/protocol/shadowsocks/inbound_multi.go index 7ff9264693..706243e2d3 100644 --- a/protocol/shadowsocks/inbound_multi.go +++ b/protocol/shadowsocks/inbound_multi.go @@ -68,14 +68,14 @@ func newMultiInbound(ctx context.Context, router adapter.Router, logger log.Cont options.Method, options.Password, int64(udpTimeout.Seconds()), - adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound), + adapter.NewLegacyUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound), ntp.TimeFuncFromContext(ctx), ) } else if common.Contains(shadowaead.List, options.Method) { service, err = shadowaead.NewMultiService[int]( options.Method, int64(udpTimeout.Seconds()), - adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound), + adapter.NewLegacyUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound), ) } else { return nil, E.New("unsupported method: " + options.Method) @@ -138,7 +138,7 @@ func (h *MultiInbound) UpdateUsers(users []string, uPSKs []string) error { } //nolint:staticcheck -func (h *MultiInbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { +func (h *MultiInbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { err := h.service.NewConnection(ctx, conn, adapter.UpstreamMetadata(metadata)) N.CloseOnHandshakeFailure(conn, onClose, err) if err != nil { @@ -151,7 +151,7 @@ func (h *MultiInbound) NewConnectionEx(ctx context.Context, conn net.Conn, metad } //nolint:staticcheck -func (h *MultiInbound) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) { +func (h *MultiInbound) NewPacket(buffer *buf.Buffer, source M.Socksaddr) { err := h.service.NewPacket(h.ctx, &stubPacketConn{h.listener.PacketWriter()}, buffer, M.Metadata{Source: source}) if err != nil { h.logger.Error(E.Cause(err, "process packet from ", source)) diff --git a/protocol/shadowsocks/inbound_relay.go b/protocol/shadowsocks/inbound_relay.go index d7d7bcff72..c79eeb15eb 100644 --- a/protocol/shadowsocks/inbound_relay.go +++ b/protocol/shadowsocks/inbound_relay.go @@ -60,7 +60,7 @@ func newRelayInbound(ctx context.Context, router adapter.Router, logger log.Cont options.Method, options.Password, int64(udpTimeout.Seconds()), - adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound), + adapter.NewLegacyUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound), ) if err != nil { return nil, err @@ -98,7 +98,7 @@ func (h *RelayInbound) Close() error { } //nolint:staticcheck -func (h *RelayInbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { +func (h *RelayInbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { err := h.service.NewConnection(ctx, conn, adapter.UpstreamMetadata(metadata)) N.CloseOnHandshakeFailure(conn, onClose, err) if err != nil { @@ -111,7 +111,7 @@ func (h *RelayInbound) NewConnectionEx(ctx context.Context, conn net.Conn, metad } //nolint:staticcheck -func (h *RelayInbound) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) { +func (h *RelayInbound) NewPacket(buffer *buf.Buffer, source M.Socksaddr) { err := h.service.NewPacket(h.ctx, &stubPacketConn{h.listener.PacketWriter()}, buffer, M.Metadata{Source: source}) if err != nil { h.logger.Error(E.Cause(err, "process packet from ", source)) diff --git a/protocol/shadowtls/inbound.go b/protocol/shadowtls/inbound.go index 17afa26831..98d9ab0381 100644 --- a/protocol/shadowtls/inbound.go +++ b/protocol/shadowtls/inbound.go @@ -108,7 +108,7 @@ func (h *Inbound) Close() error { return h.listener.Close() } -func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { +func (h *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { err := h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, metadata.Source, metadata.Destination, onClose) N.CloseOnHandshakeFailure(conn, onClose, err) if err != nil { diff --git a/protocol/socks/inbound.go b/protocol/socks/inbound.go index 68e0ef5845..0f570b51f8 100644 --- a/protocol/socks/inbound.go +++ b/protocol/socks/inbound.go @@ -70,8 +70,8 @@ func (h *Inbound) Close() error { return h.listener.Close() } -func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { - err := socks.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), h.authenticator, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), h.listener, h.udpTimeout, metadata.Source, onClose) +func (h *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + err := socks.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), h.authenticator, adapter.NewUpstreamHandler(metadata, h.newUserConnection, h.streamUserPacketConnection), h.listener, h.udpTimeout, metadata.Source, onClose) N.CloseOnHandshakeFailure(conn, onClose, err) if err != nil { if E.IsClosedOrCanceled(err) { diff --git a/protocol/trojan/inbound.go b/protocol/trojan/inbound.go index 6e11c08897..920589b413 100644 --- a/protocol/trojan/inbound.go +++ b/protocol/trojan/inbound.go @@ -84,9 +84,9 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo } inbound.fallbackAddrTLSNextProto = fallbackAddrNextProto } - fallbackHandler = adapter.NewUpstreamContextHandlerEx(inbound.fallbackConnection, nil) + fallbackHandler = adapter.NewUpstreamContextHandler(inbound.fallbackConnection, nil) } - service := trojan.NewService[int](adapter.NewUpstreamContextHandlerEx(inbound.newConnection, inbound.newPacketConnection), fallbackHandler, logger) + service := trojan.NewService[int](adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection), fallbackHandler, logger) err := service.UpdateUsers(common.MapIndexed(options.Users, func(index int, it option.TrojanUser) int { return index }), common.Map(options.Users, func(it option.TrojanUser) string { @@ -164,7 +164,7 @@ func (h *Inbound) Close() error { ) } -func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { +func (h *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { if h.tlsConfig != nil && h.transport == nil { tlsConn, err := tls.ServerHandshake(ctx, conn, h.tlsConfig) if err != nil { @@ -258,5 +258,5 @@ func (h *inboundTransportHandler) NewConnectionEx(ctx context.Context, conn net. metadata.InboundDetour = h.listener.ListenOptions().Detour //nolint:staticcheck h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) - (*Inbound)(h).NewConnectionEx(ctx, conn, metadata, onClose) + (*Inbound)(h).NewConnection(ctx, conn, metadata, onClose) } diff --git a/protocol/vless/inbound.go b/protocol/vless/inbound.go index 75cd4124cd..1589bfb06a 100644 --- a/protocol/vless/inbound.go +++ b/protocol/vless/inbound.go @@ -58,7 +58,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo if err != nil { return nil, err } - service := vless.NewService[int](logger, adapter.NewUpstreamContextHandlerEx(inbound.newConnectionEx, inbound.newPacketConnectionEx)) + service := vless.NewService[int](logger, adapter.NewUpstreamContextHandler(inbound.newConnectionEx, inbound.newPacketConnectionEx)) service.UpdateUsers(common.MapIndexed(inbound.users, func(index int, _ option.VLESSUser) int { return index }), common.Map(inbound.users, func(it option.VLESSUser) string { @@ -147,7 +147,7 @@ func (h *Inbound) Close() error { ) } -func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { +func (h *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { if h.tlsConfig != nil && h.transport == nil { tlsConn, err := tls.ServerHandshake(ctx, conn, h.tlsConfig) if err != nil { @@ -218,5 +218,5 @@ func (h *inboundTransportHandler) NewConnectionEx(ctx context.Context, conn net. metadata.InboundDetour = h.listener.ListenOptions().Detour //nolint:staticcheck h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) - (*Inbound)(h).NewConnectionEx(ctx, conn, metadata, onClose) + (*Inbound)(h).NewConnection(ctx, conn, metadata, onClose) } diff --git a/protocol/vmess/inbound.go b/protocol/vmess/inbound.go index 4e9c763c93..8783d3e377 100644 --- a/protocol/vmess/inbound.go +++ b/protocol/vmess/inbound.go @@ -66,7 +66,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo if options.Transport != nil && options.Transport.Type != "" { serviceOptions = append(serviceOptions, vmess.ServiceWithDisableHeaderProtection()) } - service := vmess.NewService[int](adapter.NewUpstreamContextHandlerEx(inbound.newConnectionEx, inbound.newPacketConnectionEx), serviceOptions...) + service := vmess.NewService[int](adapter.NewUpstreamContextHandler(inbound.newConnectionEx, inbound.newPacketConnectionEx), serviceOptions...) inbound.service = service err = service.UpdateUsers(common.MapIndexed(options.Users, func(index int, it option.VMessUser) int { return index @@ -153,7 +153,7 @@ func (h *Inbound) Close() error { ) } -func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { +func (h *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { if h.tlsConfig != nil && h.transport == nil { tlsConn, err := tls.ServerHandshake(ctx, conn, h.tlsConfig) if err != nil { @@ -224,5 +224,5 @@ func (h *inboundTransportHandler) NewConnectionEx(ctx context.Context, conn net. metadata.InboundDetour = h.listener.ListenOptions().Detour //nolint:staticcheck h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) - (*Inbound)(h).NewConnectionEx(ctx, conn, metadata, onClose) + (*Inbound)(h).NewConnection(ctx, conn, metadata, onClose) } diff --git a/route/route.go b/route/route.go index 3dc3ea7669..0d5e1669a6 100644 --- a/route/route.go +++ b/route/route.go @@ -74,7 +74,7 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad metadata.LastInbound = metadata.Inbound metadata.Inbound = metadata.InboundDetour metadata.InboundDetour = "" - injectable.NewConnectionEx(ctx, conn, metadata, onClose) + injectable.NewConnection(ctx, conn, metadata, onClose) return nil } metadata.Network = N.NetworkTCP @@ -152,8 +152,8 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad for _, tracker := range r.trackers { conn = tracker.RoutedConnection(ctx, conn, metadata, selectedRule, selectedOutbound) } - if outboundHandler, isHandler := selectedOutbound.(adapter.ConnectionHandlerEx); isHandler { - outboundHandler.NewConnectionEx(ctx, conn, metadata, onClose) + if outboundHandler, isHandler := selectedOutbound.(adapter.ConnectionHandler); isHandler { + outboundHandler.NewConnection(ctx, conn, metadata, onClose) } else { r.connection.NewConnection(ctx, selectedOutbound, conn, metadata, onClose) } @@ -209,7 +209,7 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m metadata.LastInbound = metadata.Inbound metadata.Inbound = metadata.InboundDetour metadata.InboundDetour = "" - injectable.NewPacketConnectionEx(ctx, conn, metadata, onClose) + injectable.NewPacketConnection(ctx, conn, metadata, onClose) return nil } // TODO: move to UoT @@ -281,8 +281,8 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m if metadata.FakeIP { conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, metadata.Destination) } - if outboundHandler, isHandler := selectedOutbound.(adapter.PacketConnectionHandlerEx); isHandler { - outboundHandler.NewPacketConnectionEx(ctx, conn, metadata, onClose) + if outboundHandler, isHandler := selectedOutbound.(adapter.PacketConnectionHandler); isHandler { + outboundHandler.NewPacketConnection(ctx, conn, metadata, onClose) } else { r.connection.NewPacketConnection(ctx, selectedOutbound, conn, metadata, onClose) } diff --git a/service/resolved/service.go b/service/resolved/service.go index eaedc09d43..8f9740a06d 100644 --- a/service/resolved/service.go +++ b/service/resolved/service.go @@ -132,7 +132,7 @@ func (i *Service) Close() error { return i.listener.Close() } -func (i *Service) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { +func (i *Service) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = i.Tag() metadata.InboundType = i.Type() metadata.Destination = M.Socksaddr{} @@ -146,7 +146,7 @@ func (i *Service) NewConnectionEx(ctx context.Context, conn net.Conn, metadata a } } -func (i *Service) NewPacketEx(buffer *buf.Buffer, oob []byte, source M.Socksaddr) { +func (i *Service) NewPacket(buffer *buf.Buffer, oob []byte, source M.Socksaddr) { go i.exchangePacket(buffer, oob, source) } From 1610c18a2f998dd3019e8370ed16691cb599950c Mon Sep 17 00:00:00 2001 From: nekohasekai Date: Fri, 24 Apr 2026 09:04:27 +0800 Subject: [PATCH 55/93] Add Windows TLS engine --- adapter/certificate.go | 1 + adapter/certificate_darwin.go | 17 + common/certificate/anchors_darwin.h | 17 + common/certificate/anchors_darwin.m | 42 + common/certificate/store.go | 46 +- common/certificate/store_darwin.go | 167 ++ common/certificate/store_other.go | 13 + common/httpclient/apple_transport_darwin.go | 64 +- common/httpclient/apple_transport_darwin.h | 3 +- common/httpclient/apple_transport_darwin.m | 44 +- .../httpclient/apple_transport_darwin_test.go | 115 +- common/httpclient/client.go | 2 +- common/schannel/doc.go | 5 + common/schannel/schannel_windows.go | 719 +++++ common/schannel/schannel_windows_test.go | 188 ++ common/schannel/syscall_windows.go | 28 + common/schannel/types_windows.go | 161 ++ common/schannel/zsyscall_windows.go | 99 + common/tls/apple_client.go | 209 +- common/tls/apple_client_platform.go | 268 +- .../apple_client_platform_benchmark_test.go | 278 ++ common/tls/apple_client_platform_darwin.h | 8 +- common/tls/apple_client_platform_darwin.m | 403 ++- .../apple_client_platform_dispatch_test.go | 50 + ...ent_platform_dispatch_testhelper_darwin.go | 44 + common/tls/apple_client_platform_test.go | 325 ++- common/tls/client.go | 4 +- common/tls/std_client.go | 14 +- common/tls/system_client.go | 218 ++ common/tls/windows_client.go | 848 ++++++ common/tls/windows_client_stub.go | 15 + common/tls/windows_client_test.go | 2505 +++++++++++++++++ constant/tls.go | 2 + docs/configuration/shared/tls.md | 40 +- docs/configuration/shared/tls.zh.md | 39 +- 35 files changed, 6513 insertions(+), 488 deletions(-) create mode 100644 adapter/certificate_darwin.go create mode 100644 common/certificate/anchors_darwin.h create mode 100644 common/certificate/anchors_darwin.m create mode 100644 common/certificate/store_darwin.go create mode 100644 common/certificate/store_other.go create mode 100644 common/schannel/doc.go create mode 100644 common/schannel/schannel_windows.go create mode 100644 common/schannel/schannel_windows_test.go create mode 100644 common/schannel/syscall_windows.go create mode 100644 common/schannel/types_windows.go create mode 100644 common/schannel/zsyscall_windows.go create mode 100644 common/tls/apple_client_platform_benchmark_test.go create mode 100644 common/tls/apple_client_platform_dispatch_test.go create mode 100644 common/tls/apple_client_platform_dispatch_testhelper_darwin.go create mode 100644 common/tls/system_client.go create mode 100644 common/tls/windows_client.go create mode 100644 common/tls/windows_client_stub.go create mode 100644 common/tls/windows_client_test.go diff --git a/adapter/certificate.go b/adapter/certificate.go index 0998e1302a..dfed642dfd 100644 --- a/adapter/certificate.go +++ b/adapter/certificate.go @@ -10,6 +10,7 @@ import ( type CertificateStore interface { LifecycleService Pool() *x509.CertPool + ExclusiveAnchors() bool } func RootPoolFromContext(ctx context.Context) *x509.CertPool { diff --git a/adapter/certificate_darwin.go b/adapter/certificate_darwin.go new file mode 100644 index 0000000000..ddcdb55f77 --- /dev/null +++ b/adapter/certificate_darwin.go @@ -0,0 +1,17 @@ +//go:build darwin && cgo + +package adapter + +import "unsafe" + +type AppleAnchors interface { + Retain() AppleAnchors + Release() + // Ref returns the underlying CFArrayRef, or nil if the anchor set is empty. + Ref() unsafe.Pointer +} + +type AppleCertificateStore interface { + CertificateStore + AppleAnchors() AppleAnchors +} diff --git a/common/certificate/anchors_darwin.h b/common/certificate/anchors_darwin.h new file mode 100644 index 0000000000..f535f5ca83 --- /dev/null +++ b/common/certificate/anchors_darwin.h @@ -0,0 +1,17 @@ +#ifndef BOX_CERTIFICATE_ANCHORS_DARWIN_H +#define BOX_CERTIFICATE_ANCHORS_DARWIN_H + +#include +#include + +// box_certificate_anchors_from_der wraps an array of DER-encoded certificate +// blobs into a retained CFArrayRef of SecCertificateRef, returned as an opaque +// pointer. The caller owns the returned reference and must call +// box_certificate_release_anchors. Returns NULL when no blobs were accepted. +void *box_certificate_anchors_from_der(const uint8_t *const *ders, const size_t *lens, size_t count); + +// box_certificate_release_anchors drops one reference from a CFArray handle +// previously returned by box_certificate_anchors_from_der. No-op on NULL. +void box_certificate_release_anchors(void *anchors); + +#endif diff --git a/common/certificate/anchors_darwin.m b/common/certificate/anchors_darwin.m new file mode 100644 index 0000000000..e9f471c67c --- /dev/null +++ b/common/certificate/anchors_darwin.m @@ -0,0 +1,42 @@ +#import "anchors_darwin.h" + +#import +#import + +void *box_certificate_anchors_from_der(const uint8_t *const *ders, const size_t *lens, size_t count) { + if (count == 0 || ders == NULL || lens == NULL) { + return NULL; + } + CFMutableArrayRef certificates = CFArrayCreateMutable(NULL, (CFIndex)count, &kCFTypeArrayCallBacks); + if (certificates == NULL) { + return NULL; + } + for (size_t index = 0; index < count; index++) { + if (ders[index] == NULL || lens[index] == 0) { + continue; + } + CFDataRef data = CFDataCreate(NULL, ders[index], (CFIndex)lens[index]); + if (data == NULL) { + continue; + } + SecCertificateRef certificate = SecCertificateCreateWithData(NULL, data); + CFRelease(data); + if (certificate == NULL) { + continue; + } + CFArrayAppendValue(certificates, certificate); + CFRelease(certificate); + } + if (CFArrayGetCount(certificates) == 0) { + CFRelease(certificates); + return NULL; + } + return certificates; +} + +void box_certificate_release_anchors(void *anchors) { + if (anchors == NULL) { + return; + } + CFRelease((CFTypeRef)anchors); +} diff --git a/common/certificate/store.go b/common/certificate/store.go index 0e0c25b107..54b2251f5c 100644 --- a/common/certificate/store.go +++ b/common/certificate/store.go @@ -1,6 +1,7 @@ package certificate import ( + "bytes" "context" "crypto/x509" "io/fs" @@ -25,11 +26,11 @@ type Store struct { storeType string systemPool *x509.CertPool currentPool *x509.CertPool - currentPEM []string certificate string certificatePaths []string certificateDirectoryPaths []string watcher *fswatch.Watcher + platform storePlatform } func NewStore(ctx context.Context, logger logger.Logger, options option.CertificateOptions) (*Store, error) { @@ -114,10 +115,18 @@ func (s *Store) Start(stage adapter.StartStage) error { } func (s *Store) Close() error { - if s.watcher != nil { - return s.watcher.Close() + watcher := s.watcher + s.watcher = nil + + var closeErr error + if watcher != nil { + closeErr = watcher.Close() } - return nil + platformErr := s.closePlatform() + if platformErr != nil { + closeErr = platformErr + } + return closeErr } func (s *Store) Pool() *x509.CertPool { @@ -130,37 +139,35 @@ func (s *Store) StoreKind() string { return s.storeType } -func (s *Store) CurrentPEM() []string { - s.access.RLock() - defer s.access.RUnlock() - return append([]string(nil), s.currentPEM...) +func (s *Store) ExclusiveAnchors() bool { + return s.storeType != C.CertificateStoreSystem } func (s *Store) update() error { currentPool, err := s.newBasePool() - var currentPEM []string if err != nil { return err } + pemBuffer := new(bytes.Buffer) switch s.storeType { case C.CertificateStoreMozilla: pemContent := mozillaIncludedPEM() if !currentPool.AppendCertsFromPEM([]byte(pemContent)) { return E.New("invalid Mozilla included certificate PEM") } - currentPEM = append(currentPEM, pemContent) + appendPEMBlock(pemBuffer, string(pemContent)) case C.CertificateStoreChrome: pemContent := chromeIncludedPEM() if !currentPool.AppendCertsFromPEM([]byte(pemContent)) { return E.New("invalid Chrome included certificate PEM") } - currentPEM = append(currentPEM, pemContent) + appendPEMBlock(pemBuffer, string(pemContent)) } if s.certificate != "" { if !currentPool.AppendCertsFromPEM([]byte(s.certificate)) { return E.New("invalid certificate PEM strings") } - currentPEM = append(currentPEM, s.certificate) + appendPEMBlock(pemBuffer, s.certificate) } for _, path := range s.certificatePaths { pemContent, err := os.ReadFile(path) @@ -170,7 +177,7 @@ func (s *Store) update() error { if !currentPool.AppendCertsFromPEM(pemContent) { return E.New("invalid certificate PEM file: ", path) } - currentPEM = append(currentPEM, string(pemContent)) + appendPEMBlock(pemBuffer, string(pemContent)) } var firstErr error for _, directoryPath := range s.certificateDirectoryPaths { @@ -184,7 +191,7 @@ func (s *Store) update() error { for _, directoryEntry := range directoryEntries { pemContent, err := os.ReadFile(filepath.Join(directoryPath, directoryEntry.Name())) if err == nil && currentPool.AppendCertsFromPEM(pemContent) { - currentPEM = append(currentPEM, string(pemContent)) + appendPEMBlock(pemBuffer, string(pemContent)) } } } @@ -194,8 +201,15 @@ func (s *Store) update() error { s.access.Lock() defer s.access.Unlock() s.currentPool = currentPool - s.currentPEM = currentPEM - return nil + return s.updatePlatformLocked(pemBuffer.Bytes()) +} + +func appendPEMBlock(buffer *bytes.Buffer, block string) { + existing := buffer.Bytes() + if len(existing) > 0 && existing[len(existing)-1] != '\n' { + buffer.WriteByte('\n') + } + buffer.WriteString(block) } func (s *Store) newBasePool() (*x509.CertPool, error) { diff --git a/common/certificate/store_darwin.go b/common/certificate/store_darwin.go new file mode 100644 index 0000000000..bbc8d4ba84 --- /dev/null +++ b/common/certificate/store_darwin.go @@ -0,0 +1,167 @@ +//go:build darwin && cgo + +package certificate + +/* +#cgo CFLAGS: -x objective-c -fobjc-arc +#cgo LDFLAGS: -framework Foundation -framework Security + +#include +#include "anchors_darwin.h" +*/ +import "C" + +import ( + "crypto/sha256" + "encoding/pem" + "runtime" + "sync/atomic" + "unsafe" + + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" +) + +var ( + _ adapter.AppleCertificateStore = (*Store)(nil) + _ adapter.AppleAnchors = (*appleAnchors)(nil) +) + +type storePlatform struct { + anchors *appleAnchors + hash [sha256.Size]byte +} + +type appleAnchors struct { + cfArray unsafe.Pointer + refs atomic.Int32 +} + +func newAppleAnchors(pemBytes []byte) (*appleAnchors, error) { + anchors := &appleAnchors{} + anchors.refs.Store(1) + if len(pemBytes) == 0 { + return anchors, nil + } + derBlocks := decodeCertificatePEM(pemBytes) + if len(derBlocks) == 0 { + return nil, E.New("parse certificate PEM") + } + pointerSize := C.size_t(unsafe.Sizeof((*C.uint8_t)(nil))) + lenSize := C.size_t(unsafe.Sizeof(C.size_t(0))) + pointersC := (**C.uint8_t)(C.malloc(pointerSize * C.size_t(len(derBlocks)))) + defer C.free(unsafe.Pointer(pointersC)) + lensC := (*C.size_t)(C.malloc(lenSize * C.size_t(len(derBlocks)))) + defer C.free(unsafe.Pointer(lensC)) + pointersSlice := unsafe.Slice(pointersC, len(derBlocks)) + lensSlice := unsafe.Slice(lensC, len(derBlocks)) + var pinner runtime.Pinner + defer pinner.Unpin() + for index, der := range derBlocks { + pinner.Pin(&der[0]) + pointersSlice[index] = (*C.uint8_t)(unsafe.Pointer(&der[0])) + lensSlice[index] = C.size_t(len(der)) + } + cfArray := C.box_certificate_anchors_from_der(pointersC, lensC, C.size_t(len(derBlocks))) + if cfArray == nil { + return nil, E.New("parse certificate PEM") + } + anchors.cfArray = cfArray + return anchors, nil +} + +// NewAppleAnchors parses the given PEM and returns a ref-counted handle +// wrapping a CFArray of SecCertificateRef. The caller owns the returned +// reference and must call Release when finished. Returns an error when +// pemBytes is non-empty but contains no usable CERTIFICATE blocks. +func NewAppleAnchors(pemBytes []byte) (adapter.AppleAnchors, error) { + return newAppleAnchors(pemBytes) +} + +// AcquireAnchors returns a retained AppleAnchors handle, preferring the +// per-config userAnchors over the process-wide certificate store. Returns +// nil when neither source is available. Callers must Release the handle. +func AcquireAnchors(userAnchors adapter.AppleAnchors, store adapter.CertificateStore) adapter.AppleAnchors { + if userAnchors != nil { + return userAnchors.Retain() + } + if store == nil { + return nil + } + apple, loaded := store.(adapter.AppleCertificateStore) + if !loaded { + return nil + } + return apple.AppleAnchors() +} + +func (a *appleAnchors) Retain() adapter.AppleAnchors { + a.refs.Add(1) + return a +} + +func (a *appleAnchors) Release() { + if a.refs.Add(-1) != 0 { + return + } + if a.cfArray != nil { + C.box_certificate_release_anchors(a.cfArray) + } +} + +func (a *appleAnchors) Ref() unsafe.Pointer { + return a.cfArray +} + +func (s *Store) AppleAnchors() adapter.AppleAnchors { + s.access.RLock() + defer s.access.RUnlock() + if s.platform.anchors == nil { + return nil + } + return s.platform.anchors.Retain() +} + +func (s *Store) updatePlatformLocked(pemBytes []byte) error { + hash := sha256.Sum256(pemBytes) + if s.platform.anchors != nil && s.platform.hash == hash { + return nil + } + newAnchors, err := newAppleAnchors(pemBytes) + if err != nil { + return err + } + old := s.platform.anchors + s.platform.anchors = newAnchors + s.platform.hash = hash + if old != nil { + old.Release() + } + return nil +} + +func (s *Store) closePlatform() error { + s.access.Lock() + defer s.access.Unlock() + if s.platform.anchors != nil { + s.platform.anchors.Release() + s.platform.anchors = nil + } + return nil +} + +func decodeCertificatePEM(pemBytes []byte) [][]byte { + var blocks [][]byte + rest := pemBytes + for { + block, next := pem.Decode(rest) + if block == nil { + break + } + if block.Type == "CERTIFICATE" && len(block.Bytes) > 0 { + blocks = append(blocks, block.Bytes) + } + rest = next + } + return blocks +} diff --git a/common/certificate/store_other.go b/common/certificate/store_other.go new file mode 100644 index 0000000000..c2d68ed213 --- /dev/null +++ b/common/certificate/store_other.go @@ -0,0 +1,13 @@ +//go:build !(darwin && cgo) + +package certificate + +type storePlatform struct{} + +func (s *Store) updatePlatformLocked(_ []byte) error { + return nil +} + +func (s *Store) closePlatform() error { + return nil +} diff --git a/common/httpclient/apple_transport_darwin.go b/common/httpclient/apple_transport_darwin.go index b9174009b0..4619dc58de 100644 --- a/common/httpclient/apple_transport_darwin.go +++ b/common/httpclient/apple_transport_darwin.go @@ -25,6 +25,8 @@ import ( "time" "unsafe" + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/certificate" "github.com/sagernet/sing-box/common/proxybridge" boxTLS "github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/option" @@ -37,6 +39,15 @@ import ( const applePinnedHashSize = sha256.Size +var ( + newAppleUserAnchors = certificate.NewAppleAnchors + newAppleProxyBridge = proxybridge.New + newAppleTransportSession = func(shared *appleTransportShared) (unsafe.Pointer, error) { + session, err := shared.newSession() + return unsafe.Pointer(session), err + } +) + func verifyApplePinnedPublicKeySHA256(flatHashes []byte, leafCertificate []byte) error { if len(flatHashes)%applePinnedHashSize != 0 { return E.New("invalid pinned public key list") @@ -64,8 +75,9 @@ type appleSessionConfig struct { minVersion uint16 maxVersion uint16 insecure bool - anchorPEM string anchorOnly bool + userAnchors adapter.AppleAnchors + store adapter.CertificateStore pinnedPublicKeySHA256s []byte } @@ -89,7 +101,13 @@ func newAppleTransport(ctx context.Context, logger logger.ContextLogger, rawDial if err != nil { return nil, err } - bridge, err := proxybridge.New(ctx, logger, "apple http proxy", rawDialer) + releaseConfig := true + defer func() { + if releaseConfig { + sessionConfig.close() + } + }() + bridge, err := newAppleProxyBridge(ctx, logger, "apple http proxy", rawDialer) if err != nil { return nil, err } @@ -100,11 +118,13 @@ func newAppleTransport(ctx context.Context, logger logger.ContextLogger, rawDial timeFunc: ntp.TimeFuncFromContext(ctx), } shared.refs.Store(1) - session, err := shared.newSession() + sessionRef, err := newAppleTransportSession(shared) if err != nil { bridge.Close() return nil, err } + session := (*C.box_apple_http_session_t)(sessionRef) + releaseConfig = false return &appleTransport{ shared: shared, session: session, @@ -142,7 +162,7 @@ func newAppleSessionConfig(ctx context.Context, options option.HTTPClientOptions if len(tlsOptions.ALPN) > 0 { return appleSessionConfig{}, E.New("tls.alpn is unsupported in Apple HTTP engine") } - validated, err := boxTLS.ValidateAppleTLSOptions(ctx, tlsOptions, "Apple HTTP engine") + validated, err := boxTLS.ValidateSystemTLSOptions(ctx, tlsOptions, "Apple HTTP engine") if err != nil { return appleSessionConfig{}, err } @@ -152,13 +172,23 @@ func newAppleSessionConfig(ctx context.Context, options option.HTTPClientOptions minVersion: validated.MinVersion, maxVersion: validated.MaxVersion, insecure: tlsOptions.Insecure || len(tlsOptions.CertificatePublicKeySHA256) > 0, - anchorPEM: validated.AnchorPEM, - anchorOnly: validated.AnchorOnly, + anchorOnly: validated.Exclusive, + store: validated.Store, + } + if len(validated.UserPEM) > 0 { + userAnchors, anchorsErr := newAppleUserAnchors(validated.UserPEM) + if anchorsErr != nil { + return appleSessionConfig{}, anchorsErr + } + config.userAnchors = userAnchors } if len(tlsOptions.CertificatePublicKeySHA256) > 0 { config.pinnedPublicKeySHA256s = make([]byte, 0, len(tlsOptions.CertificatePublicKeySHA256)*applePinnedHashSize) for _, hashValue := range tlsOptions.CertificatePublicKeySHA256 { if len(hashValue) != applePinnedHashSize { + if config.userAnchors != nil { + config.userAnchors.Release() + } return appleSessionConfig{}, E.New("invalid certificate_public_key_sha256 length: ", len(hashValue)) } config.pinnedPublicKeySHA256s = append(config.pinnedPublicKeySHA256s, hashValue...) @@ -167,12 +197,20 @@ func newAppleSessionConfig(ctx context.Context, options option.HTTPClientOptions return config, nil } +func (c *appleSessionConfig) close() { + if c.userAnchors != nil { + c.userAnchors.Release() + c.userAnchors = nil + } +} + func (s *appleTransportShared) retain() { s.refs.Add(1) } func (s *appleTransportShared) release() error { if s.refs.Add(-1) == 0 { + s.config.close() return s.bridge.Close() } return nil @@ -185,16 +223,17 @@ func (s *appleTransportShared) newSession() (*C.box_apple_http_session_t, error) defer C.free(unsafe.Pointer(cProxyUsername)) cProxyPassword := C.CString(s.bridge.Password()) defer C.free(unsafe.Pointer(cProxyPassword)) - var cAnchorPEM *C.char - if s.config.anchorPEM != "" { - cAnchorPEM = C.CString(s.config.anchorPEM) - defer C.free(unsafe.Pointer(cAnchorPEM)) - } var pinnedPointer *C.uint8_t if len(s.config.pinnedPublicKeySHA256s) > 0 { pinnedPointer = (*C.uint8_t)(C.CBytes(s.config.pinnedPublicKeySHA256s)) defer C.free(unsafe.Pointer(pinnedPointer)) } + anchors := certificate.AcquireAnchors(s.config.userAnchors, s.config.store) + var anchorsRef unsafe.Pointer + if anchors != nil { + anchorsRef = anchors.Ref() + defer anchors.Release() + } cConfig := C.box_apple_http_session_config_t{ proxy_host: cProxyHost, proxy_port: C.int(s.bridge.Port()), @@ -203,8 +242,7 @@ func (s *appleTransportShared) newSession() (*C.box_apple_http_session_t, error) min_tls_version: C.uint16_t(s.config.minVersion), max_tls_version: C.uint16_t(s.config.maxVersion), insecure: C.bool(s.config.insecure), - anchor_pem: cAnchorPEM, - anchor_pem_len: C.size_t(len(s.config.anchorPEM)), + anchors_cf: anchorsRef, anchor_only: C.bool(s.config.anchorOnly), pinned_public_key_sha256: pinnedPointer, pinned_public_key_sha256_len: C.size_t(len(s.config.pinnedPublicKeySHA256s)), diff --git a/common/httpclient/apple_transport_darwin.h b/common/httpclient/apple_transport_darwin.h index 26d6a77bcf..4774a6b4de 100644 --- a/common/httpclient/apple_transport_darwin.h +++ b/common/httpclient/apple_transport_darwin.h @@ -13,8 +13,7 @@ typedef struct box_apple_http_session_config { uint16_t min_tls_version; uint16_t max_tls_version; bool insecure; - const char *anchor_pem; - size_t anchor_pem_len; + void *anchors_cf; bool anchor_only; const uint8_t *pinned_public_key_sha256; size_t pinned_public_key_sha256_len; diff --git a/common/httpclient/apple_transport_darwin.m b/common/httpclient/apple_transport_darwin.m index d7c09350cf..1e72a3e0ab 100644 --- a/common/httpclient/apple_transport_darwin.m +++ b/common/httpclient/apple_transport_darwin.m @@ -36,44 +36,6 @@ static void box_set_error_from_nserror(char **error_out, NSError *error) { box_set_error_string(error_out, error.localizedDescription ?: error.description); } -static NSArray *box_parse_certificates_from_pem(const char *pem, size_t pem_len) { - if (pem == NULL || pem_len == 0) { - return @[]; - } - NSString *content = [[NSString alloc] initWithBytes:pem length:pem_len encoding:NSUTF8StringEncoding]; - if (content == nil) { - return @[]; - } - NSString *beginMarker = @"-----BEGIN CERTIFICATE-----"; - NSString *endMarker = @"-----END CERTIFICATE-----"; - NSMutableArray *certificates = [NSMutableArray array]; - NSUInteger searchFrom = 0; - while (searchFrom < content.length) { - NSRange beginRange = [content rangeOfString:beginMarker options:0 range:NSMakeRange(searchFrom, content.length - searchFrom)]; - if (beginRange.location == NSNotFound) { - break; - } - NSUInteger bodyStart = beginRange.location + beginRange.length; - NSRange endRange = [content rangeOfString:endMarker options:0 range:NSMakeRange(bodyStart, content.length - bodyStart)]; - if (endRange.location == NSNotFound) { - break; - } - NSString *base64Section = [content substringWithRange:NSMakeRange(bodyStart, endRange.location - bodyStart)]; - NSArray *components = [base64Section componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; - NSString *base64Content = [components componentsJoinedByString:@""]; - NSData *der = [[NSData alloc] initWithBase64EncodedString:base64Content options:0]; - if (der != nil) { - SecCertificateRef certificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)der); - if (certificate != NULL) { - [certificates addObject:(__bridge id)certificate]; - CFRelease(certificate); - } - } - searchFrom = endRange.location + endRange.length; - } - return certificates; -} - static bool box_evaluate_trust(SecTrustRef trustRef, NSArray *anchors, bool anchor_only, NSDate *verifyDate) { if (trustRef == NULL) { return false; @@ -249,7 +211,11 @@ @implementation BoxAppleHTTPSessionHandle if (config != NULL) { delegate.insecure = config->insecure; delegate.anchorOnly = config->anchor_only; - delegate.anchors = box_parse_certificates_from_pem(config->anchor_pem, config->anchor_pem_len); + if (config->anchors_cf != NULL) { + delegate.anchors = (__bridge NSArray *)config->anchors_cf; + } else { + delegate.anchors = @[]; + } if (config->pinned_public_key_sha256 != NULL && config->pinned_public_key_sha256_len > 0) { delegate.pinnedPublicKeyHashes = [NSData dataWithBytes:config->pinned_public_key_sha256 length:config->pinned_public_key_sha256_len]; } diff --git a/common/httpclient/apple_transport_darwin_test.go b/common/httpclient/apple_transport_darwin_test.go index 47c7de6dd4..17485cc363 100644 --- a/common/httpclient/apple_transport_darwin_test.go +++ b/common/httpclient/apple_transport_darwin_test.go @@ -19,13 +19,16 @@ import ( "strings" "testing" "time" + "unsafe" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/proxybridge" boxTLS "github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/route" "github.com/sagernet/sing/common/json/badoption" + commonLogger "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" @@ -58,6 +61,23 @@ type appleHTTPTestServer struct { publicKeyHash []byte } +type appleTestAnchors struct { + ref unsafe.Pointer + releases int +} + +func (a *appleTestAnchors) Retain() adapter.AppleAnchors { + return a +} + +func (a *appleTestAnchors) Release() { + a.releases++ +} + +func (a *appleTestAnchors) Ref() unsafe.Pointer { + return a.ref +} + func TestNewAppleSessionConfig(t *testing.T) { serverCertificate, serverCertificatePEM := newAppleHTTPTestCertificate(t, "localhost") serverHash := certificatePublicKeySHA256(t, serverCertificate.Certificate[0]) @@ -103,8 +123,14 @@ func TestNewAppleSessionConfig(t *testing.T) { if !config.anchorOnly { t.Fatal("expected anchor_only") } - if !strings.Contains(config.anchorPEM, "BEGIN CERTIFICATE") { - t.Fatalf("unexpected anchor pem: %q", config.anchorPEM) + if config.userAnchors == nil { + t.Fatal("expected user anchors") + } + if config.userAnchors.Ref() == nil { + t.Fatal("expected non-empty user anchors") + } + if config.store != nil { + t.Fatal("unexpected store reference") } if len(config.pinnedPublicKeySHA256s) != 0 { t.Fatalf("unexpected pinned hashes: %d", len(config.pinnedPublicKeySHA256s)) @@ -137,8 +163,8 @@ func TestNewAppleSessionConfig(t *testing.T) { if !bytes.Equal(config.pinnedPublicKeySHA256s[applePinnedHashSize:], otherHash) { t.Fatal("unexpected second pin") } - if config.anchorPEM != "" { - t.Fatalf("unexpected anchor pem: %q", config.anchorPEM) + if config.userAnchors != nil { + t.Fatal("unexpected user anchors") } if config.anchorOnly { t.Fatal("unexpected anchor_only") @@ -392,6 +418,46 @@ func TestAppleTransportVerifyPublicKeySHA256(t *testing.T) { } } +func TestNewAppleTransportClosesSessionConfigOnBridgeFailure(t *testing.T) { + _, serverCertificatePEM := newAppleHTTPTestCertificate(t, "localhost") + restoreAppleTransportFactories(t) + testAnchors := &appleTestAnchors{ref: unsafe.Pointer(new(int))} + newAppleUserAnchors = func([]byte) (adapter.AppleAnchors, error) { + return testAnchors, nil + } + newAppleProxyBridge = func(context.Context, commonLogger.ContextLogger, string, N.Dialer) (*proxybridge.Bridge, error) { + return nil, errors.New("bridge boom") + } + + _, err := newAppleTransport(newAppleHTTPTestContext(), log.NewNOPFactory().NewLogger("httpclient"), &appleHTTPTestDialer{}, appleTransportAnchorOptions(serverCertificatePEM)) + if err == nil || !strings.Contains(err.Error(), "bridge boom") { + t.Fatalf("unexpected error: %v", err) + } + if testAnchors.releases != 1 { + t.Fatalf("expected 1 anchor release, got %d", testAnchors.releases) + } +} + +func TestNewAppleTransportClosesSessionConfigOnSessionFailure(t *testing.T) { + _, serverCertificatePEM := newAppleHTTPTestCertificate(t, "localhost") + restoreAppleTransportFactories(t) + testAnchors := &appleTestAnchors{ref: unsafe.Pointer(new(int))} + newAppleUserAnchors = func([]byte) (adapter.AppleAnchors, error) { + return testAnchors, nil + } + newAppleTransportSession = func(*appleTransportShared) (unsafe.Pointer, error) { + return nil, errors.New("session boom") + } + + _, err := newAppleTransport(newAppleHTTPTestContext(), log.NewNOPFactory().NewLogger("httpclient"), &appleHTTPTestDialer{}, appleTransportAnchorOptions(serverCertificatePEM)) + if err == nil || !strings.Contains(err.Error(), "session boom") { + t.Fatalf("unexpected error: %v", err) + } + if testAnchors.releases != 1 { + t.Fatalf("expected 1 anchor release, got %d", testAnchors.releases) + } +} + func TestAppleTransportRoundTripHTTPS(t *testing.T) { requests := make(chan appleHTTPObservedRequest, 1) server := startAppleHTTPTestServer(t, func(w http.ResponseWriter, r *http.Request) { @@ -665,7 +731,8 @@ func TestAppleTransportLifecycle(t *testing.T) { assertAppleHTTPSucceeds(t, transport, server.URL("/reset")) innerTransport := transport.(*appleTransport) - if err := innerTransport.Close(); err != nil { + err := innerTransport.Close() + if err != nil { t.Fatal(err) } @@ -722,10 +789,7 @@ func (s *appleHTTPTestServer) URL(path string) string { func newAppleHTTPTestTransport(t *testing.T, server *appleHTTPTestServer, options option.HTTPClientOptions) innerTransport { t.Helper() - ctx := service.ContextWith[adapter.ConnectionManager]( - context.Background(), - route.NewConnectionManager(log.NewNOPFactory().NewLogger("connection")), - ) + ctx := newAppleHTTPTestContext() dialer := &appleHTTPTestDialer{ hostMap: make(map[string]string), } @@ -743,6 +807,39 @@ func newAppleHTTPTestTransport(t *testing.T, server *appleHTTPTestServer, option return transport } +func newAppleHTTPTestContext() context.Context { + return service.ContextWith[adapter.ConnectionManager]( + context.Background(), + route.NewConnectionManager(log.NewNOPFactory().NewLogger("connection")), + ) +} + +func appleTransportAnchorOptions(certificatePEM string) option.HTTPClientOptions { + return option.HTTPClientOptions{ + Version: 2, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "localhost", + MinVersion: "1.2", + Certificate: badoption.Listable[string]{certificatePEM}, + }, + }, + } +} + +func restoreAppleTransportFactories(t *testing.T) { + t.Helper() + oldAnchors := newAppleUserAnchors + oldBridge := newAppleProxyBridge + oldSession := newAppleTransportSession + t.Cleanup(func() { + newAppleUserAnchors = oldAnchors + newAppleProxyBridge = oldBridge + newAppleTransportSession = oldSession + }) +} + func (d *appleHTTPTestDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { host := destination.AddrString() if destination.IsDomain() { diff --git a/common/httpclient/client.go b/common/httpclient/client.go index a6fde9c02d..9dbb8cc1d5 100644 --- a/common/httpclient/client.go +++ b/common/httpclient/client.go @@ -50,7 +50,7 @@ func NewTransport(ctx context.Context, logger logger.ContextLogger, tag string, } managedTransport.epoch.Store(&transportEpoch{transport: inner}) return managedTransport, nil - case C.TLSEngineDefault, "go": + case "", C.TLSEngineGo: cheapRebuild = true default: return nil, E.New("unknown HTTP engine: ", options.Engine) diff --git a/common/schannel/doc.go b/common/schannel/doc.go new file mode 100644 index 0000000000..bdcb2f1a36 --- /dev/null +++ b/common/schannel/doc.go @@ -0,0 +1,5 @@ +// Package schannel wraps the Windows Schannel security provider (SSPI) for +// client-side TLS. The public API is implemented on Windows only; on other +// platforms the package is empty and intended for transitive imports from +// build-tagged callers. +package schannel diff --git a/common/schannel/schannel_windows.go b/common/schannel/schannel_windows.go new file mode 100644 index 0000000000..d912ca2315 --- /dev/null +++ b/common/schannel/schannel_windows.go @@ -0,0 +1,719 @@ +package schannel + +import ( + "bytes" + "crypto/tls" + "encoding/binary" + "os" + "sync" + "syscall" + "unsafe" + + E "github.com/sagernet/sing/common/exceptions" + + "golang.org/x/sys/windows" +) + +const clientCredentialFlags = schCredManualCredValidation | schCredNoDefaultCreds | schUseStrongCrypto + +var versionCheck = sync.OnceValue(func() error { + major, _, build := windows.RtlGetNtVersionNumbers() + build &= 0xffff + if major < 10 || (major == 10 && build < 17763) { + return E.New("Windows TLS engine requires Windows build 17763 or later (Windows 10 version 1809, Windows Server 2019, or newer)") + } + return nil +}) + +// CheckPlatform returns an error when the running Windows version does not +// support the SCH_CREDENTIALS structure used by this package. +func CheckPlatform() error { + return versionCheck() +} + +type clientCredentialKey struct { + disabledProtocols uint32 + flags uint32 +} + +type clientCredential struct { + key clientCredentialKey + once sync.Once + handle secHandle + tlsParams tlsParameters + err error +} + +var clientCredentialCache sync.Map + +func cachedClientCredential(minVersion, maxVersion uint16) (*clientCredential, error) { + key := clientCredentialKey{ + disabledProtocols: disabledProtocolsMask(minVersion, maxVersion), + flags: clientCredentialFlags, + } + actual, _ := clientCredentialCache.LoadOrStore(key, &clientCredential{key: key}) + credential := actual.(*clientCredential) + credential.once.Do(func() { + credential.err = credential.acquire() + }) + if credential.err != nil { + clientCredentialCache.Delete(key) + return nil, credential.err + } + return credential, nil +} + +func (c *clientCredential) acquire() error { + c.tlsParams.grbitDisabledProtocols = c.key.disabledProtocols + sch := schCredentials{ + dwVersion: schCredentialsVersion, + dwFlags: c.key.flags, + cTlsParameters: 1, + pTlsParameters: &c.tlsParams, + } + pkg, err := windows.UTF16PtrFromString(unispNameW) + if err != nil { + return err + } + var expiry windows.Filetime + status := sspiAcquireCredentialsHandle( + nil, + pkg, + secPkgCredOutbound, + nil, + unsafe.Pointer(&sch), + 0, + 0, + &c.handle, + &expiry, + ) + if status != secEOK { + return sspiError("AcquireCredentialsHandle", status) + } + return nil +} + +// ClientContext owns the per-connection Schannel security context and drives +// it through handshake and application-data phases. +type ClientContext struct { + credential *clientCredential + handle secHandle + targetName *uint16 + + // alpnBuffer is the SEC_APPLICATION_PROTOCOLS blob; kept alive for the + // duration of the first handshake call. + alpnBuffer []byte + + firstCall bool + valid bool +} + +// NewClientContext allocates a new client context, reuses the Schannel +// credential handle for the supplied TLS version bounds, and advertises ALPN +// protocols through an SECBUFFER_APPLICATION_PROTOCOLS buffer on the first +// handshake call. +func NewClientContext(minVersion, maxVersion uint16, serverName string, alpn []string) (*ClientContext, error) { + if minVersion != 0 && maxVersion != 0 && minVersion > maxVersion { + return nil, os.ErrInvalid + } + err := CheckPlatform() + if err != nil { + return nil, err + } + targetName, err := windows.UTF16PtrFromString(serverName) + if err != nil { + return nil, err + } + credential, err := cachedClientCredential(minVersion, maxVersion) + if err != nil { + return nil, err + } + c := &ClientContext{ + credential: credential, + targetName: targetName, + firstCall: true, + } + if len(alpn) > 0 { + c.alpnBuffer, err = encodeAlpnBuffer(alpn) + if err != nil { + return nil, err + } + } + return c, nil +} + +// Close releases the per-connection security context. Safe to call multiple +// times. +func (c *ClientContext) Close() { + if c == nil { + return + } + if c.valid { + sspiDeleteSecurityContext(&c.handle) + c.valid = false + c.handle = secHandle{} + } +} + +type StepResult struct { + // Output must be written to the peer verbatim before the next Step call. + // When Done is true, leftover input[Consumed:] is the first application + // ciphertext — not more handshake bytes. + Output []byte + Consumed int + Done bool + Incomplete bool +} + +// Step drives one handshake iteration. Input may be nil on the first call. +// Callers must write Output to the peer, append more peer bytes when +// Incomplete is true, and loop until Done is true. +func (c *ClientContext) Step(input []byte) (StepResult, error) { + var inputDesc *secBufferDesc + var inputBufs [2]secBuffer + if c.firstCall { + if len(c.alpnBuffer) > 0 { + inputBufs[0].bufferType = secbufferApplicationProtocols + inputBufs[0].cbBuffer = uint32(len(c.alpnBuffer)) + inputBufs[0].pvBuffer = &c.alpnBuffer[0] + inputDesc = &secBufferDesc{ + ulVersion: secbufferVersion, + cBuffers: 1, + pBuffers: &inputBufs[0], + } + } + } else { + if len(input) == 0 { + return StepResult{}, E.New("schannel: empty handshake input after first step") + } + inputBufs[0].bufferType = secbufferToken + inputBufs[0].cbBuffer = uint32(len(input)) + inputBufs[0].pvBuffer = &input[0] + inputBufs[1].bufferType = secbufferEmpty + inputDesc = &secBufferDesc{ + ulVersion: secbufferVersion, + cBuffers: 2, + pBuffers: &inputBufs[0], + } + } + + result, terminal, err := c.runInitializeSecurityContext(inputDesc, "InitializeSecurityContext") + if err != nil || terminal { + return result, err + } + + switch { + case c.firstCall: + result.Consumed = 0 + case inputBufs[1].bufferType == secbufferExtra && inputBufs[1].cbBuffer > 0: + consumed, extraErr := consumedFromExtra(&inputBufs[1], len(input)) + if extraErr != nil { + return result, extraErr + } + result.Consumed = consumed + default: + result.Consumed = len(input) + } + + c.firstCall = false + c.alpnBuffer = nil + return result, nil +} + +// StreamSizes must be called after Step returns Done=true. +func (c *ClientContext) StreamSizes() (header, trailer, maxMessage uint32, err error) { + var sizes secPkgContextStreamSizes + status := sspiQueryContextAttributes(&c.handle, secpkgAttrStreamSizes, unsafe.Pointer(&sizes)) + if status != secEOK { + return 0, 0, 0, sspiError("QueryContextAttributes(stream sizes)", status) + } + return sizes.cbHeader, sizes.cbTrailer, sizes.cbMaximumMessage, nil +} + +// Encrypt wraps a plaintext chunk into a TLS record using the supplied +// backing buffer which must have room for header + plaintext + trailer bytes. +// Plaintext is copied into buffer starting at `header` offset before calling +// EncryptMessage. Returns the encrypted record as a slice into buffer. +func (c *ClientContext) Encrypt(header, trailer uint32, plaintext []byte, buffer []byte) ([]byte, error) { + if len(buffer) < int(header)+len(plaintext)+int(trailer) { + return nil, E.New("schannel: encrypt buffer too small") + } + copy(buffer[header:], plaintext) + headerPtr := &buffer[0] + dataPtr := &buffer[header] + trailerPtr := &buffer[int(header)+len(plaintext)] + + bufs := [4]secBuffer{ + {cbBuffer: header, bufferType: secbufferStreamHeader, pvBuffer: headerPtr}, + {cbBuffer: uint32(len(plaintext)), bufferType: secbufferData, pvBuffer: dataPtr}, + {cbBuffer: trailer, bufferType: secbufferStreamTrailer, pvBuffer: trailerPtr}, + {bufferType: secbufferEmpty}, + } + desc := secBufferDesc{ + ulVersion: secbufferVersion, + cBuffers: 4, + pBuffers: &bufs[0], + } + status := sspiEncryptMessage(&c.handle, 0, &desc, 0) + if status != secEOK { + return nil, sspiError("EncryptMessage", status) + } + total := int(bufs[0].cbBuffer + bufs[1].cbBuffer + bufs[2].cbBuffer) + return buffer[:total], nil +} + +type DecryptResult struct { + // Plaintext aliases memory inside the input buffer passed to Decrypt; + // callers must copy before the next Decrypt call reuses that buffer. + Plaintext []byte + // ConsumedTotal is the number of input bytes Schannel consumed, i.e. + // input[ConsumedTotal:] are unprocessed leftover ciphertext. + ConsumedTotal int + // RenegotiateToken aliases the post-handshake token that must be fed back + // through InitializeSecurityContext after SEC_I_RENEGOTIATE. + RenegotiateToken []byte + Incomplete bool + Renegotiate bool + Expired bool +} + +// Decrypt processes a chunk of TLS ciphertext in-place. The returned Plaintext +// aliases memory inside input until the next Decrypt call; callers must copy +// the bytes they want to keep. +func (c *ClientContext) Decrypt(input []byte) (DecryptResult, error) { + var result DecryptResult + if len(input) == 0 { + result.Incomplete = true + return result, nil + } + bufs := [4]secBuffer{ + {cbBuffer: uint32(len(input)), bufferType: secbufferData, pvBuffer: &input[0]}, + {bufferType: secbufferEmpty}, + {bufferType: secbufferEmpty}, + {bufferType: secbufferEmpty}, + } + desc := secBufferDesc{ + ulVersion: secbufferVersion, + cBuffers: 4, + pBuffers: &bufs[0], + } + status := sspiDecryptMessage(&c.handle, &desc, 0, nil) + switch status { + case secEOK: + case secEIncompleteMessage: + result.Incomplete = true + return result, nil + case secIContextExpired: + result.Expired = true + return result, nil + case secIRenegotiate: + result.Renegotiate = true + default: + return result, sspiError("DecryptMessage", status) + } + return parseDecryptResult(input, bufs[:], result.Renegotiate) +} + +// PostHandshake processes a TLS 1.3 post-handshake message +// (NewSessionTicket, KeyUpdate) after DecryptMessage returned +// SEC_I_RENEGOTIATE. Pass the token preserved from Decrypt on the first call; +// pass more peer bytes on subsequent calls when Incomplete. +func (c *ClientContext) PostHandshake(input []byte) (StepResult, error) { + var inputDesc *secBufferDesc + var inputBufs [2]secBuffer + if len(input) > 0 { + inputBufs[0].bufferType = secbufferToken + inputBufs[0].cbBuffer = uint32(len(input)) + inputBufs[0].pvBuffer = &input[0] + inputBufs[1].bufferType = secbufferEmpty + inputDesc = &secBufferDesc{ + ulVersion: secbufferVersion, + cBuffers: 2, + pBuffers: &inputBufs[0], + } + } + + result, terminal, err := c.runInitializeSecurityContext(inputDesc, "InitializeSecurityContext(post-handshake)") + if err != nil || terminal { + return result, err + } + + if len(input) > 0 && inputBufs[1].bufferType == secbufferExtra && inputBufs[1].cbBuffer > 0 { + consumed, extraErr := consumedFromExtra(&inputBufs[1], len(input)) + if extraErr != nil { + return result, extraErr + } + result.Consumed = consumed + } else { + result.Consumed = len(input) + } + return result, nil +} + +func parseDecryptResult(input []byte, bufs []secBuffer, renegotiate bool) (DecryptResult, error) { + var result DecryptResult + var dataBuffer, extraBuffer *secBuffer + for index := range bufs { + switch bufs[index].bufferType { + case secbufferData: + dataBuffer = &bufs[index] + case secbufferExtra: + extraBuffer = &bufs[index] + } + } + if dataBuffer != nil && dataBuffer.cbBuffer > 0 && dataBuffer.pvBuffer != nil { + result.Plaintext = unsafe.Slice(dataBuffer.pvBuffer, int(dataBuffer.cbBuffer)) + } + if extraBuffer != nil && extraBuffer.cbBuffer > 0 { + consumed, err := consumedFromExtra(extraBuffer, len(input)) + if err != nil { + return result, err + } + result.ConsumedTotal = consumed + } else { + result.ConsumedTotal = len(input) + } + if renegotiate { + result.Renegotiate = true + if extraBuffer != nil && extraBuffer.cbBuffer > 0 { + result.RenegotiateToken = input[result.ConsumedTotal:] + } else { + result.RenegotiateToken = input + } + } + return result, nil +} + +// ApplicationProtocol returns the empty string when ALPN was not negotiated. +func (c *ClientContext) ApplicationProtocol() (string, error) { + var info secPkgContextApplicationProtocol + status := sspiQueryContextAttributes(&c.handle, secpkgAttrApplicationProtocol, unsafe.Pointer(&info)) + if status != secEOK { + return "", sspiError("QueryContextAttributes(application protocol)", status) + } + if info.protoNegoStatus != secApplicationProtocolNegotiationStatusSuccess { + return "", nil + } + size := int(info.protocolIDSize) + if size > len(info.protocolID) { + return "", E.New("schannel: invalid ALPN protocol size") + } + return string(info.protocolID[:size]), nil +} + +// ConnectionInfo reports the negotiated TLS version and cipher suite. +// cipherSuite may be zero when the Windows build does not return a +// mappable cipher name. +func (c *ClientContext) ConnectionInfo() (version, cipherSuite uint16, err error) { + var info secPkgContextConnectionInfo + status := sspiQueryContextAttributes(&c.handle, secpkgAttrConnectionInfo, unsafe.Pointer(&info)) + if status != secEOK { + return 0, 0, sspiError("QueryContextAttributes(connection info)", status) + } + version = sspProtocolToTLSVersion(info.dwProtocol) + + var cipherInfo secPkgContextCipherInfo + cipherInfo.dwVersion = 1 + status = sspiQueryContextAttributes(&c.handle, secpkgAttrCipherInfo, unsafe.Pointer(&cipherInfo)) + if status == secEOK { + cipherSuite = cipherSuiteID(windows.UTF16ToString(cipherInfo.szCipherSuite[:])) + } + return version, cipherSuite, nil +} + +func cipherSuiteID(name string) uint16 { + for _, suite := range tls.CipherSuites() { + if suite.Name == name { + return suite.ID + } + } + for _, suite := range tls.InsecureCipherSuites() { + if suite.Name == name { + return suite.ID + } + } + return 0 +} + +// RemoteCertificateChain returns freshly allocated DER bytes ordered +// leaf → intermediates. +func (c *ClientContext) RemoteCertificateChain() ([][]byte, error) { + var leaf *windows.CertContext + status := sspiQueryContextAttributes(&c.handle, secpkgAttrRemoteCertContext, unsafe.Pointer(&leaf)) + if status != secEOK { + return nil, sspiError("QueryContextAttributes(remote cert context)", status) + } + if leaf == nil { + return nil, nil + } + defer windows.CertFreeCertificateContext(leaf) + + chain, err := buildCertChainDER(leaf) + if err != nil { + return [][]byte{certContextDER(leaf)}, nil + } + return chain, nil +} + +const handshakeContextReq = iscReqSequenceDetect | + iscReqReplayDetect | + iscReqConfidentiality | + iscReqAllocateMemory | + iscReqStream | + iscReqUseSuppliedCreds | + iscReqManualCredValidation | + iscReqExtendedError + +// runInitializeSecurityContext returns terminal=true when the result is +// final (error or more-data-needed), signalling that the caller must skip +// extra-buffer post-processing. +func (c *ClientContext) runInitializeSecurityContext(inputDesc *secBufferDesc, opLabel string) (StepResult, bool, error) { + var outputBufs [1]secBuffer + outputBufs[0].bufferType = secbufferToken + outputDesc := secBufferDesc{ + ulVersion: secbufferVersion, + cBuffers: 1, + pBuffers: &outputBufs[0], + } + var ctxIn *secHandle + if c.valid { + ctxIn = &c.handle + } + var contextAttr uint32 + var expiry windows.Filetime + status := sspiInitializeSecurityContext( + &c.credential.handle, + ctxIn, + c.targetName, + handshakeContextReq, + 0, + 0, + inputDesc, + 0, + &c.handle, + &outputDesc, + &contextAttr, + &expiry, + ) + + switch status { + case secEOK, secICompleteNeeded, secICompleteAndContinue, secIContinueNeeded: + c.valid = true + } + if status == secICompleteNeeded || status == secICompleteAndContinue { + completeStatus := sspiCompleteAuthToken(&c.handle, &outputDesc) + if completeStatus != secEOK { + if outputBufs[0].pvBuffer != nil { + sspiFreeContextBuffer(outputBufs[0].pvBuffer) + } + return StepResult{}, true, sspiError("CompleteAuthToken", completeStatus) + } + } + + var result StepResult + if outputBufs[0].cbBuffer > 0 && outputBufs[0].pvBuffer != nil { + result.Output = unsafeSliceCopy(outputBufs[0].pvBuffer, int(outputBufs[0].cbBuffer)) + sspiFreeContextBuffer(outputBufs[0].pvBuffer) + } + + switch status { + case secEOK, secICompleteNeeded: + result.Done = true + return result, false, nil + case secIContinueNeeded, secICompleteAndContinue: + return result, false, nil + case secEIncompleteMessage: + c.valid = true + result.Incomplete = true + return result, true, nil + default: + return result, true, sspiError(opLabel, status) + } +} + +func consumedFromExtra(extraBuf *secBuffer, inputLen int) (int, error) { + extraLen := int(extraBuf.cbBuffer) + if extraLen > inputLen { + return 0, E.New("schannel: SECBUFFER_EXTRA exceeds input length") + } + return inputLen - extraLen, nil +} + +func disabledProtocolsMask(minVersion, maxVersion uint16) uint32 { + allowed := uint32(0) + versions := []struct { + id uint16 + mask uint32 + }{ + {tls.VersionTLS10, spProtTLS10Client}, + {tls.VersionTLS11, spProtTLS11Client}, + {tls.VersionTLS12, spProtTLS12Client}, + {tls.VersionTLS13, spProtTLS13Client}, + } + effectiveMin := minVersion + if effectiveMin == 0 { + effectiveMin = tls.VersionTLS12 + if maxVersion != 0 && maxVersion < tls.VersionTLS12 { + effectiveMin = versions[0].id + } + } + effectiveMax := maxVersion + if effectiveMax == 0 { + effectiveMax = tls.VersionTLS13 + } + for _, v := range versions { + if v.id >= effectiveMin && v.id <= effectiveMax { + allowed |= v.mask + } + } + if allowed == 0 { + return 0 + } + return spProtAllTLSClients &^ allowed +} + +func sspProtocolToTLSVersion(sp uint32) uint16 { + switch { + case sp&spProtTLS13Client != 0: + return tls.VersionTLS13 + case sp&spProtTLS12Client != 0: + return tls.VersionTLS12 + case sp&spProtTLS11Client != 0: + return tls.VersionTLS11 + case sp&spProtTLS10Client != 0: + return tls.VersionTLS10 + } + return 0 +} + +func encodeAlpnBuffer(protocols []string) ([]byte, error) { + var protoList []byte + for _, proto := range protocols { + if len(proto) == 0 || len(proto) > 255 { + return nil, E.New("schannel: invalid ALPN protocol: ", proto) + } + protoList = append(protoList, byte(len(proto))) + protoList = append(protoList, []byte(proto)...) + } + if len(protoList) > 0xFFFF { + return nil, E.New("schannel: ALPN list too long") + } + // Layout: + // uint32 ProtocolListsSize + // uint32 ProtoNegoExt + // uint16 ProtocolListSize + // bytes ProtocolList + inner := 4 + 2 + len(protoList) + buffer := make([]byte, 4+inner) + binary.LittleEndian.PutUint32(buffer[0:4], uint32(inner)) + binary.LittleEndian.PutUint32(buffer[4:8], secApplicationProtocolNegotiationExtALPN) + binary.LittleEndian.PutUint16(buffer[8:10], uint16(len(protoList))) + copy(buffer[10:], protoList) + return buffer, nil +} + +func unsafeSliceCopy(ptr *byte, size int) []byte { + if ptr == nil || size <= 0 { + return nil + } + out := make([]byte, size) + copy(out, unsafe.Slice(ptr, size)) + return out +} + +func certContextDER(ctx *windows.CertContext) []byte { + if ctx == nil || ctx.EncodedCert == nil || ctx.Length == 0 { + return nil + } + out := make([]byte, ctx.Length) + copy(out, unsafe.Slice(ctx.EncodedCert, int(ctx.Length))) + return out +} + +func buildCertChainDER(leaf *windows.CertContext) ([][]byte, error) { + var chainPara windows.CertChainPara + chainPara.Size = uint32(unsafe.Sizeof(chainPara)) + var chainCtx *windows.CertChainContext + err := windows.CertGetCertificateChain(0, leaf, nil, leaf.Store, &chainPara, 0, 0, &chainCtx) + if err != nil { + return nil, err + } + defer windows.CertFreeCertificateChain(chainCtx) + return extractCertChainDER(chainCtx) +} + +func extractCertChainDER(chainCtx *windows.CertChainContext) ([][]byte, error) { + if chainCtx == nil || chainCtx.ChainCount == 0 || chainCtx.Chains == nil { + return nil, E.New("schannel: empty certificate chain") + } + chains := unsafe.Slice(chainCtx.Chains, int(chainCtx.ChainCount)) + chain := chains[0] + if chain == nil || chain.NumElements == 0 || chain.Elements == nil { + return nil, E.New("schannel: empty certificate chain") + } + elements := unsafe.Slice(chain.Elements, int(chain.NumElements)) + if len(elements) > 1 && + chain.TrustStatus.ErrorStatus&windows.CERT_TRUST_IS_PARTIAL_CHAIN == 0 && + isSelfSignedCertContext(elements[len(elements)-1].CertContext) { + elements = elements[:len(elements)-1] + } + derChain := make([][]byte, 0, len(elements)) + for index, element := range elements { + if element == nil || element.CertContext == nil { + return nil, E.New("schannel: missing certificate chain element ", index) + } + der := certContextDER(element.CertContext) + if len(der) == 0 { + return nil, E.New("schannel: empty certificate chain element ", index) + } + derChain = append(derChain, der) + } + return derChain, nil +} + +func isSelfSignedCertContext(ctx *windows.CertContext) bool { + if ctx == nil || ctx.CertInfo == nil { + return false + } + return bytes.Equal( + certNameBlobBytes(ctx.CertInfo.Issuer), + certNameBlobBytes(ctx.CertInfo.Subject), + ) +} + +func certNameBlobBytes(blob windows.CertNameBlob) []byte { + if blob.Size == 0 || blob.Data == nil { + return nil + } + return unsafe.Slice(blob.Data, int(blob.Size)) +} + +func sspiError(where string, status syscall.Errno) error { + return E.New("schannel: ", where, ": ", formatStatus(status)) +} + +var statusNames = map[syscall.Errno]string{ + secEUnsupportedFunc: "SEC_E_UNSUPPORTED_FUNCTION", + secEInternalError: "SEC_E_INTERNAL_ERROR", + secEInvalidToken: "SEC_E_INVALID_TOKEN", + secELogonDenied: "SEC_E_LOGON_DENIED", + secEMessageAltered: "SEC_E_MESSAGE_ALTERED", + secENoAuthenticatingAuthority: "SEC_E_NO_AUTHENTICATING_AUTHORITY", + secEContextExpired: "SEC_E_CONTEXT_EXPIRED", + secEIncompleteMessage: "SEC_E_INCOMPLETE_MESSAGE", + secEIncompleteCreds: "SEC_E_INCOMPLETE_CREDENTIALS", + secEBufferTooSmall: "SEC_E_BUFFER_TOO_SMALL", + secEWrongPrincipal: "SEC_E_WRONG_PRINCIPAL", + secEIllegalMessage: "SEC_E_ILLEGAL_MESSAGE", + secECertUnknown: "SEC_E_CERT_UNKNOWN", + secECertExpired: "SEC_E_CERT_EXPIRED", + secEAlgorithmMismatch: "SEC_E_ALGORITHM_MISMATCH", +} + +func formatStatus(status syscall.Errno) string { + name, loaded := statusNames[status] + if !loaded { + return status.Error() + } + return name + ": " + status.Error() +} diff --git a/common/schannel/schannel_windows_test.go b/common/schannel/schannel_windows_test.go new file mode 100644 index 0000000000..35bcaabb21 --- /dev/null +++ b/common/schannel/schannel_windows_test.go @@ -0,0 +1,188 @@ +//go:build windows + +package schannel + +import ( + "bytes" + "crypto/tls" + "testing" + "unsafe" + + "golang.org/x/sys/windows" +) + +func TestExtractCertChainDERExcludesSelfSignedRoot(t *testing.T) { + leaf := certContextForTest([]byte("leaf"), []byte("intermediate"), []byte("leaf")) + intermediate := certContextForTest([]byte("intermediate"), []byte("root"), []byte("intermediate")) + root := certContextForTest([]byte("root"), []byte("root"), []byte("root")) + + chainCtx := certChainContextForTest(leaf, intermediate, root) + derChain, err := extractCertChainDER(chainCtx) + if err != nil { + t.Fatal(err) + } + if len(derChain) != 2 { + t.Fatalf("expected 2 certificates, got %d", len(derChain)) + } + if !bytes.Equal(derChain[0], []byte("leaf")) { + t.Fatalf("unexpected leaf certificate: %q", string(derChain[0])) + } + if !bytes.Equal(derChain[1], []byte("intermediate")) { + t.Fatalf("unexpected intermediate certificate: %q", string(derChain[1])) + } +} + +func TestExtractCertChainDERKeepsLastIntermediateWithoutRoot(t *testing.T) { + leaf := certContextForTest([]byte("leaf"), []byte("intermediate"), []byte("leaf")) + intermediate := certContextForTest([]byte("intermediate"), []byte("root"), []byte("intermediate")) + + chainCtx := certChainContextForTest(leaf, intermediate) + derChain, err := extractCertChainDER(chainCtx) + if err != nil { + t.Fatal(err) + } + if len(derChain) != 2 { + t.Fatalf("expected 2 certificates, got %d", len(derChain)) + } + if !bytes.Equal(derChain[1], []byte("intermediate")) { + t.Fatalf("unexpected last certificate: %q", string(derChain[1])) + } +} + +func TestDisabledProtocolsMask(t *testing.T) { + testCases := []struct { + name string + minVersion uint16 + maxVersion uint16 + want uint32 + }{ + { + name: "default range", + want: spProtAllTLSClients &^ (spProtTLS12Client | spProtTLS13Client), + }, + { + name: "default minimum with explicit max", + maxVersion: tls.VersionTLS12, + want: spProtAllTLSClients &^ spProtTLS12Client, + }, + { + name: "explicit tls10 range", + minVersion: tls.VersionTLS10, + maxVersion: tls.VersionTLS13, + want: 0, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + got := disabledProtocolsMask(testCase.minVersion, testCase.maxVersion) + if got != testCase.want { + t.Fatalf("disabledProtocolsMask(%#x, %#x) = %#x, want %#x", testCase.minVersion, testCase.maxVersion, got, testCase.want) + } + }) + } +} + +func TestClientCredentialCacheReusesVersionRange(t *testing.T) { + if err := CheckPlatform(); err != nil { + t.Skip(err) + } + first, err := NewClientContext(tls.VersionTLS12, tls.VersionTLS13, "localhost", []string{"h2"}) + if err != nil { + t.Fatal(err) + } + defer first.Close() + second, err := NewClientContext(tls.VersionTLS12, tls.VersionTLS13, "example.com", []string{"http/1.1"}) + if err != nil { + t.Fatal(err) + } + defer second.Close() + if first.credential != second.credential { + t.Fatal("expected same TLS version range to reuse credential") + } + + tls12Only, err := NewClientContext(tls.VersionTLS12, tls.VersionTLS12, "localhost", nil) + if err != nil { + t.Fatal(err) + } + defer tls12Only.Close() + if first.credential == tls12Only.credential { + t.Fatal("expected distinct TLS version range to use a distinct credential") + } + if first.credential.key.disabledProtocols != disabledProtocolsMask(tls.VersionTLS12, tls.VersionTLS13) { + t.Fatalf("unexpected cached disabled protocol mask: %#x", first.credential.key.disabledProtocols) + } +} + +func TestParseDecryptResultKeepsRenegotiateExtraToken(t *testing.T) { + input := []byte("plain-ticket") + result, err := parseDecryptResult(input, []secBuffer{ + {bufferType: secbufferExtra, cbBuffer: 6}, + }, true) + if err != nil { + t.Fatal(err) + } + if !result.Renegotiate { + t.Fatal("expected Renegotiate to be true") + } + if result.ConsumedTotal != len(input)-6 { + t.Fatalf("unexpected consumed total: %d", result.ConsumedTotal) + } + if !bytes.Equal(result.RenegotiateToken, []byte("ticket")) { + t.Fatalf("unexpected renegotiate token: %q", string(result.RenegotiateToken)) + } +} + +func TestParseDecryptResultKeepsRenegotiateWholeBufferWithoutExtra(t *testing.T) { + input := []byte("ticket") + result, err := parseDecryptResult(input, []secBuffer{ + {bufferType: secbufferData, cbBuffer: uint32(len(input)), pvBuffer: &input[0]}, + }, true) + if err != nil { + t.Fatal(err) + } + if !result.Renegotiate { + t.Fatal("expected Renegotiate to be true") + } + if result.ConsumedTotal != len(input) { + t.Fatalf("unexpected consumed total: %d", result.ConsumedTotal) + } + if !bytes.Equal(result.RenegotiateToken, input) { + t.Fatalf("unexpected renegotiate token: %q", string(result.RenegotiateToken)) + } +} + +func certChainContextForTest(certs ...*windows.CertContext) *windows.CertChainContext { + elements := make([]*windows.CertChainElement, 0, len(certs)) + for _, cert := range certs { + elements = append(elements, &windows.CertChainElement{CertContext: cert}) + } + simpleChain := &windows.CertSimpleChain{ + NumElements: uint32(len(elements)), + Elements: &elements[0], + } + chains := []*windows.CertSimpleChain{simpleChain} + return &windows.CertChainContext{ + ChainCount: 1, + Chains: &chains[0], + } +} + +func certContextForTest(der, issuer, subject []byte) *windows.CertContext { + certInfo := &windows.CertInfo{ + Issuer: certNameBlobForTest(issuer), + Subject: certNameBlobForTest(subject), + } + return &windows.CertContext{ + EncodedCert: &der[0], + Length: uint32(len(der)), + CertInfo: certInfo, + } +} + +func certNameBlobForTest(value []byte) windows.CertNameBlob { + return windows.CertNameBlob{ + Size: uint32(len(value)), + Data: (*byte)(unsafe.Pointer(&value[0])), + } +} diff --git a/common/schannel/syscall_windows.go b/common/schannel/syscall_windows.go new file mode 100644 index 0000000000..654869b664 --- /dev/null +++ b/common/schannel/syscall_windows.go @@ -0,0 +1,28 @@ +package schannel + +import ( + "syscall" + "unsafe" +) + +//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go syscall_windows.go + +// secur32.dll — SSPI / Schannel interface + +//sys sspiAcquireCredentialsHandle(principal *uint16, pkgname *uint16, credentialUse uint32, logonID *uint64, authData unsafe.Pointer, getKeyFn uintptr, getKeyArg uintptr, credential *secHandle, expiry *windows.Filetime) (ret syscall.Errno) = secur32.AcquireCredentialsHandleW +//sys sspiFreeCredentialsHandle(credential *secHandle) (ret syscall.Errno) = secur32.FreeCredentialsHandle +//sys sspiInitializeSecurityContext(credential *secHandle, context *secHandle, targetName *uint16, contextReq uint32, reserved1 uint32, targetDataRep uint32, input *secBufferDesc, reserved2 uint32, newContext *secHandle, output *secBufferDesc, contextAttr *uint32, expiry *windows.Filetime) (ret syscall.Errno) = secur32.InitializeSecurityContextW +//sys sspiDeleteSecurityContext(context *secHandle) (ret syscall.Errno) = secur32.DeleteSecurityContext +//sys sspiQueryContextAttributes(context *secHandle, attribute uint32, buffer unsafe.Pointer) (ret syscall.Errno) = secur32.QueryContextAttributesW +//sys sspiEncryptMessage(context *secHandle, qop uint32, message *secBufferDesc, sequenceNumber uint32) (ret syscall.Errno) = secur32.EncryptMessage +//sys sspiDecryptMessage(context *secHandle, message *secBufferDesc, sequenceNumber uint32, qop *uint32) (ret syscall.Errno) = secur32.DecryptMessage +//sys sspiFreeContextBuffer(buffer *byte) (ret syscall.Errno) = secur32.FreeContextBuffer + +// mkwinsyscall does not emit CompleteAuthToken for this package, so bind it manually. +var procCompleteAuthToken = modsecur32.NewProc("CompleteAuthToken") + +func sspiCompleteAuthToken(context *secHandle, token *secBufferDesc) (ret syscall.Errno) { + r0, _, _ := syscall.SyscallN(procCompleteAuthToken.Addr(), uintptr(unsafe.Pointer(context)), uintptr(unsafe.Pointer(token))) + ret = syscall.Errno(r0) + return +} diff --git a/common/schannel/types_windows.go b/common/schannel/types_windows.go new file mode 100644 index 0000000000..5d0bc134a5 --- /dev/null +++ b/common/schannel/types_windows.go @@ -0,0 +1,161 @@ +package schannel + +import ( + "syscall" + + "golang.org/x/sys/windows" +) + +const ( + unispNameW = "Microsoft Unified Security Protocol Provider" + + schCredentialsVersion = 5 + + secPkgCredOutbound = 2 + + iscReqSequenceDetect = 0x00000008 + iscReqReplayDetect = 0x00000004 + iscReqConfidentiality = 0x00000010 + iscReqAllocateMemory = 0x00000100 + iscReqStream = 0x00008000 + iscReqUseSuppliedCreds = 0x00000080 + iscReqManualCredValidation = 0x00080000 + iscReqExtendedError = 0x00004000 + + secbufferEmpty = 0 + secbufferData = 1 + secbufferToken = 2 + secbufferExtra = 5 + secbufferStreamTrailer = 6 + secbufferStreamHeader = 7 + secbufferApplicationProtocols = 18 + secbufferVersion = 0 + + secApplicationProtocolNegotiationExtALPN = 2 + + secApplicationProtocolNegotiationStatusSuccess = 1 + + schCredManualCredValidation = 0x00000008 + schCredNoDefaultCreds = 0x00000010 + schUseStrongCrypto = 0x00400000 + + spProtTLS10Client = 0x00000080 + spProtTLS11Client = 0x00000200 + spProtTLS12Client = 0x00000800 + spProtTLS13Client = 0x00002000 + + spProtAllTLSClients = spProtTLS10Client | spProtTLS11Client | spProtTLS12Client | spProtTLS13Client + + secpkgAttrStreamSizes = 4 + secpkgAttrConnectionInfo = 0x5A + secpkgAttrApplicationProtocol = 0x23 + secpkgAttrCipherInfo = 0x64 + secpkgAttrRemoteCertContext = 0x53 +) + +const ( + secEOK = syscall.Errno(windows.SEC_E_OK) + secICompleteNeeded = syscall.Errno(windows.SEC_I_COMPLETE_NEEDED) + secICompleteAndContinue = syscall.Errno(windows.SEC_I_COMPLETE_AND_CONTINUE) + secIContinueNeeded = syscall.Errno(windows.SEC_I_CONTINUE_NEEDED) + secIContextExpired = syscall.Errno(windows.SEC_I_CONTEXT_EXPIRED) + secIRenegotiate = syscall.Errno(windows.SEC_I_RENEGOTIATE) + secEIncompleteMessage = syscall.Errno(windows.SEC_E_INCOMPLETE_MESSAGE) + secEIncompleteCreds = syscall.Errno(windows.SEC_E_INCOMPLETE_CREDENTIALS) + secEBufferTooSmall = syscall.Errno(windows.SEC_E_BUFFER_TOO_SMALL) + secEMessageAltered = syscall.Errno(windows.SEC_E_MESSAGE_ALTERED) + secEContextExpired = syscall.Errno(windows.SEC_E_CONTEXT_EXPIRED) + secEUnsupportedFunc = syscall.Errno(windows.SEC_E_UNSUPPORTED_FUNCTION) + secEInvalidToken = syscall.Errno(windows.SEC_E_INVALID_TOKEN) + secELogonDenied = syscall.Errno(windows.SEC_E_LOGON_DENIED) + secEIllegalMessage = syscall.Errno(windows.SEC_E_ILLEGAL_MESSAGE) + secEWrongPrincipal = syscall.Errno(windows.SEC_E_WRONG_PRINCIPAL) + secECertUnknown = syscall.Errno(windows.SEC_E_CERT_UNKNOWN) + secECertExpired = syscall.Errno(windows.SEC_E_CERT_EXPIRED) + secEAlgorithmMismatch = syscall.Errno(windows.SEC_E_ALGORITHM_MISMATCH) + secEInternalError = syscall.Errno(windows.SEC_E_INTERNAL_ERROR) + secENoAuthenticatingAuthority = syscall.Errno(windows.SEC_E_NO_AUTHENTICATING_AUTHORITY) +) + +type secHandle struct { + lower uintptr + upper uintptr +} + +type secBuffer struct { + cbBuffer uint32 + bufferType uint32 + pvBuffer *byte +} + +type secBufferDesc struct { + ulVersion uint32 + cBuffers uint32 + pBuffers *secBuffer +} + +type schCredentials struct { + dwVersion uint32 + dwCredFormat uint32 + cCreds uint32 + paCred uintptr + hRootStore windows.Handle + cMappers uint32 + aphMappers uintptr + dwSessionLifespan uint32 + dwFlags uint32 + cTlsParameters uint32 + pTlsParameters *tlsParameters +} + +type tlsParameters struct { + cAlpnIds uint32 + rgstrAlpnIds uintptr + grbitDisabledProtocols uint32 + cDisabledCrypto uint32 + pDisabledCrypto uintptr + dwFlags uint32 +} + +type secPkgContextStreamSizes struct { + cbHeader uint32 + cbTrailer uint32 + cbMaximumMessage uint32 + cBuffers uint32 + cbBlockSize uint32 +} + +type secPkgContextConnectionInfo struct { + dwProtocol uint32 + aiCipher uint32 + dwCipherStrength uint32 + aiHash uint32 + dwHashStrength uint32 + aiExch uint32 + dwExchStrength uint32 +} + +type secPkgContextApplicationProtocol struct { + protoNegoStatus uint32 + protoNegoExt uint32 + protocolIDSize byte + protocolID [255]byte +} + +type secPkgContextCipherInfo struct { + dwVersion uint32 + dwProtocol uint32 + dwCipherSuite uint32 + dwBaseCipherSuite uint32 + szCipherSuite [64]uint16 + szCipher [64]uint16 + dwCipherLen uint32 + dwCipherBlockLen uint32 + szHash [64]uint16 + dwHashLen uint32 + szExchange [64]uint16 + dwMinExchangeLen uint32 + dwMaxExchangeLen uint32 + szCertificate [64]uint16 + dwKeyType uint32 +} diff --git a/common/schannel/zsyscall_windows.go b/common/schannel/zsyscall_windows.go new file mode 100644 index 0000000000..85a7bc8087 --- /dev/null +++ b/common/schannel/zsyscall_windows.go @@ -0,0 +1,99 @@ +// Code generated by 'go generate'; DO NOT EDIT. + +package schannel + +import ( + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var _ unsafe.Pointer + +// Do the interface allocations only once for common +// Errno values. +const ( + errnoERROR_IO_PENDING = 997 +) + +var ( + errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) + errERROR_EINVAL error = syscall.EINVAL +) + +// errnoErr returns common boxed Errno values, to prevent +// allocations at runtime. +func errnoErr(e syscall.Errno) error { + switch e { + case 0: + return errERROR_EINVAL + case errnoERROR_IO_PENDING: + return errERROR_IO_PENDING + } + // TODO: add more here, after collecting data on the common + // error values see on Windows. (perhaps when running + // all.bat?) + return e +} + +var ( + modsecur32 = windows.NewLazySystemDLL("secur32.dll") + + procAcquireCredentialsHandleW = modsecur32.NewProc("AcquireCredentialsHandleW") + procDecryptMessage = modsecur32.NewProc("DecryptMessage") + procDeleteSecurityContext = modsecur32.NewProc("DeleteSecurityContext") + procEncryptMessage = modsecur32.NewProc("EncryptMessage") + procFreeContextBuffer = modsecur32.NewProc("FreeContextBuffer") + procFreeCredentialsHandle = modsecur32.NewProc("FreeCredentialsHandle") + procInitializeSecurityContextW = modsecur32.NewProc("InitializeSecurityContextW") + procQueryContextAttributesW = modsecur32.NewProc("QueryContextAttributesW") +) + +func sspiAcquireCredentialsHandle(principal *uint16, pkgname *uint16, credentialUse uint32, logonID *uint64, authData unsafe.Pointer, getKeyFn uintptr, getKeyArg uintptr, credential *secHandle, expiry *windows.Filetime) (ret syscall.Errno) { + r0, _, _ := syscall.SyscallN(procAcquireCredentialsHandleW.Addr(), uintptr(unsafe.Pointer(principal)), uintptr(unsafe.Pointer(pkgname)), uintptr(credentialUse), uintptr(unsafe.Pointer(logonID)), uintptr(authData), uintptr(getKeyFn), uintptr(getKeyArg), uintptr(unsafe.Pointer(credential)), uintptr(unsafe.Pointer(expiry))) + ret = syscall.Errno(r0) + return +} + +func sspiDecryptMessage(context *secHandle, message *secBufferDesc, sequenceNumber uint32, qop *uint32) (ret syscall.Errno) { + r0, _, _ := syscall.SyscallN(procDecryptMessage.Addr(), uintptr(unsafe.Pointer(context)), uintptr(unsafe.Pointer(message)), uintptr(sequenceNumber), uintptr(unsafe.Pointer(qop))) + ret = syscall.Errno(r0) + return +} + +func sspiDeleteSecurityContext(context *secHandle) (ret syscall.Errno) { + r0, _, _ := syscall.SyscallN(procDeleteSecurityContext.Addr(), uintptr(unsafe.Pointer(context))) + ret = syscall.Errno(r0) + return +} + +func sspiEncryptMessage(context *secHandle, qop uint32, message *secBufferDesc, sequenceNumber uint32) (ret syscall.Errno) { + r0, _, _ := syscall.SyscallN(procEncryptMessage.Addr(), uintptr(unsafe.Pointer(context)), uintptr(qop), uintptr(unsafe.Pointer(message)), uintptr(sequenceNumber)) + ret = syscall.Errno(r0) + return +} + +func sspiFreeContextBuffer(buffer *byte) (ret syscall.Errno) { + r0, _, _ := syscall.SyscallN(procFreeContextBuffer.Addr(), uintptr(unsafe.Pointer(buffer))) + ret = syscall.Errno(r0) + return +} + +func sspiFreeCredentialsHandle(credential *secHandle) (ret syscall.Errno) { + r0, _, _ := syscall.SyscallN(procFreeCredentialsHandle.Addr(), uintptr(unsafe.Pointer(credential))) + ret = syscall.Errno(r0) + return +} + +func sspiInitializeSecurityContext(credential *secHandle, context *secHandle, targetName *uint16, contextReq uint32, reserved1 uint32, targetDataRep uint32, input *secBufferDesc, reserved2 uint32, newContext *secHandle, output *secBufferDesc, contextAttr *uint32, expiry *windows.Filetime) (ret syscall.Errno) { + r0, _, _ := syscall.SyscallN(procInitializeSecurityContextW.Addr(), uintptr(unsafe.Pointer(credential)), uintptr(unsafe.Pointer(context)), uintptr(unsafe.Pointer(targetName)), uintptr(contextReq), uintptr(reserved1), uintptr(targetDataRep), uintptr(unsafe.Pointer(input)), uintptr(reserved2), uintptr(unsafe.Pointer(newContext)), uintptr(unsafe.Pointer(output)), uintptr(unsafe.Pointer(contextAttr)), uintptr(unsafe.Pointer(expiry))) + ret = syscall.Errno(r0) + return +} + +func sspiQueryContextAttributes(context *secHandle, attribute uint32, buffer unsafe.Pointer) (ret syscall.Errno) { + r0, _, _ := syscall.SyscallN(procQueryContextAttributesW.Addr(), uintptr(unsafe.Pointer(context)), uintptr(attribute), uintptr(buffer)) + ret = syscall.Errno(r0) + return +} diff --git a/common/tls/apple_client.go b/common/tls/apple_client.go index 01043fd3d2..441c29b34b 100644 --- a/common/tls/apple_client.go +++ b/common/tls/apple_client.go @@ -4,218 +4,41 @@ package tls import ( "context" - "net" - "os" - "strings" - "time" "github.com/sagernet/sing-box/adapter" - boxConstant "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/common/certificate" "github.com/sagernet/sing-box/option" - E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" - "github.com/sagernet/sing/common/ntp" - "github.com/sagernet/sing/service" ) -type appleCertificateStore interface { - StoreKind() string - CurrentPEM() []string -} +const appleTLSEngineName = "Apple TLS engine" type appleClientConfig struct { - serverName string - nextProtos []string - handshakeTimeout time.Duration - minVersion uint16 - maxVersion uint16 - insecure bool - anchorPEM string - anchorOnly bool - certificatePublicKeySHA256 [][]byte - timeFunc func() time.Time -} - -func (c *appleClientConfig) ServerName() string { - return c.serverName -} - -func (c *appleClientConfig) SetServerName(serverName string) { - c.serverName = serverName -} - -func (c *appleClientConfig) NextProtos() []string { - return c.nextProtos -} - -func (c *appleClientConfig) SetNextProtos(nextProto []string) { - c.nextProtos = append(c.nextProtos[:0], nextProto...) -} - -func (c *appleClientConfig) HandshakeTimeout() time.Duration { - return c.handshakeTimeout -} - -func (c *appleClientConfig) SetHandshakeTimeout(timeout time.Duration) { - c.handshakeTimeout = timeout -} - -func (c *appleClientConfig) STDConfig() (*STDConfig, error) { - return nil, E.New("unsupported usage for Apple TLS engine") -} - -func (c *appleClientConfig) Client(conn net.Conn) (Conn, error) { - return nil, os.ErrInvalid + systemTLSConfig + userPEM []byte } func (c *appleClientConfig) Clone() Config { return &appleClientConfig{ - serverName: c.serverName, - nextProtos: append([]string(nil), c.nextProtos...), - handshakeTimeout: c.handshakeTimeout, - minVersion: c.minVersion, - maxVersion: c.maxVersion, - insecure: c.insecure, - anchorPEM: c.anchorPEM, - anchorOnly: c.anchorOnly, - certificatePublicKeySHA256: append([][]byte(nil), c.certificatePublicKeySHA256...), - timeFunc: c.timeFunc, + systemTLSConfig: c.systemTLSConfig.clone(), + userPEM: append([]byte(nil), c.userPEM...), } } -func newAppleClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) { - validated, err := ValidateAppleTLSOptions(ctx, options, "Apple TLS engine") - if err != nil { - return nil, err - } - - var serverName string - if options.ServerName != "" { - serverName = options.ServerName - } else if serverAddress != "" { - serverName = serverAddress - } - if serverName == "" && !options.Insecure && !allowEmptyServerName { - return nil, errMissingServerName - } - - var handshakeTimeout time.Duration - if options.HandshakeTimeout > 0 { - handshakeTimeout = options.HandshakeTimeout.Build() - } else { - handshakeTimeout = boxConstant.TCPTimeout +func (c *appleClientConfig) resolveAnchors() (adapter.AppleAnchors, error) { + if len(c.userPEM) > 0 { + return certificate.NewAppleAnchors(c.userPEM) } - - return &appleClientConfig{ - serverName: serverName, - nextProtos: append([]string(nil), options.ALPN...), - handshakeTimeout: handshakeTimeout, - minVersion: validated.MinVersion, - maxVersion: validated.MaxVersion, - insecure: options.Insecure || len(options.CertificatePublicKeySHA256) > 0, - anchorPEM: validated.AnchorPEM, - anchorOnly: validated.AnchorOnly, - certificatePublicKeySHA256: append([][]byte(nil), options.CertificatePublicKeySHA256...), - timeFunc: ntp.TimeFuncFromContext(ctx), - }, nil + return certificate.AcquireAnchors(nil, c.store), nil } -type AppleTLSValidated struct { - MinVersion uint16 - MaxVersion uint16 - AnchorPEM string - AnchorOnly bool -} - -func ValidateAppleTLSOptions(ctx context.Context, options option.OutboundTLSOptions, engineName string) (AppleTLSValidated, error) { - if options.Reality != nil && options.Reality.Enabled { - return AppleTLSValidated{}, E.New("reality is unsupported in ", engineName) - } - if options.UTLS != nil && options.UTLS.Enabled { - return AppleTLSValidated{}, E.New("utls is unsupported in ", engineName) - } - if options.ECH != nil && options.ECH.Enabled { - return AppleTLSValidated{}, E.New("ech is unsupported in ", engineName) - } - if options.DisableSNI { - return AppleTLSValidated{}, E.New("disable_sni is unsupported in ", engineName) - } - if len(options.CipherSuites) > 0 { - return AppleTLSValidated{}, E.New("cipher_suites is unsupported in ", engineName) - } - if len(options.CurvePreferences) > 0 { - return AppleTLSValidated{}, E.New("curve_preferences is unsupported in ", engineName) - } - if len(options.ClientCertificate) > 0 || options.ClientCertificatePath != "" || len(options.ClientKey) > 0 || options.ClientKeyPath != "" { - return AppleTLSValidated{}, E.New("client certificate is unsupported in ", engineName) - } - if options.Fragment || options.RecordFragment { - return AppleTLSValidated{}, E.New("tls fragment is unsupported in ", engineName) - } - if options.KernelTx || options.KernelRx { - return AppleTLSValidated{}, E.New("ktls is unsupported in ", engineName) - } - if options.Spoof != "" || options.SpoofMethod != "" { - return AppleTLSValidated{}, E.New("spoof is unsupported in ", engineName) - } - if len(options.CertificatePublicKeySHA256) > 0 && (len(options.Certificate) > 0 || options.CertificatePath != "") { - return AppleTLSValidated{}, E.New("certificate_public_key_sha256 is conflict with certificate or certificate_path") - } - var minVersion uint16 - if options.MinVersion != "" { - var err error - minVersion, err = ParseTLSVersion(options.MinVersion) - if err != nil { - return AppleTLSValidated{}, E.Cause(err, "parse min_version") - } - } - var maxVersion uint16 - if options.MaxVersion != "" { - var err error - maxVersion, err = ParseTLSVersion(options.MaxVersion) - if err != nil { - return AppleTLSValidated{}, E.Cause(err, "parse max_version") - } - } - anchorPEM, anchorOnly, err := AppleAnchorPEM(ctx, options) +func newAppleClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) { + base, validated, err := newSystemTLSConfig(ctx, serverAddress, options, allowEmptyServerName, appleTLSEngineName) if err != nil { - return AppleTLSValidated{}, err + return nil, err } - return AppleTLSValidated{ - MinVersion: minVersion, - MaxVersion: maxVersion, - AnchorPEM: anchorPEM, - AnchorOnly: anchorOnly, + return &appleClientConfig{ + systemTLSConfig: base, + userPEM: append([]byte(nil), validated.UserPEM...), }, nil } - -func AppleAnchorPEM(ctx context.Context, options option.OutboundTLSOptions) (string, bool, error) { - if len(options.Certificate) > 0 { - return strings.Join(options.Certificate, "\n"), true, nil - } - if options.CertificatePath != "" { - content, err := os.ReadFile(options.CertificatePath) - if err != nil { - return "", false, E.Cause(err, "read certificate") - } - return string(content), true, nil - } - - certificateStore := service.FromContext[adapter.CertificateStore](ctx) - if certificateStore == nil { - return "", false, nil - } - store, ok := certificateStore.(appleCertificateStore) - if !ok { - return "", false, nil - } - - switch store.StoreKind() { - case boxConstant.CertificateStoreSystem, "": - return strings.Join(store.CurrentPEM(), "\n"), false, nil - case boxConstant.CertificateStoreMozilla, boxConstant.CertificateStoreChrome, boxConstant.CertificateStoreNone: - return strings.Join(store.CurrentPEM(), "\n"), true, nil - default: - return "", false, E.New("unsupported certificate store for Apple TLS engine: ", store.StoreKind()) - } -} diff --git a/common/tls/apple_client_platform.go b/common/tls/apple_client_platform.go index 9e7d6e73a2..9e2b7b53e0 100644 --- a/common/tls/apple_client_platform.go +++ b/common/tls/apple_client_platform.go @@ -20,24 +20,25 @@ import ( "math" "net" "os" + "runtime/cgo" "strings" "sync" - "syscall" "time" "unsafe" - "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" "golang.org/x/sys/unix" ) func (c *appleClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (Conn, error) { - rawSyscallConn, ok := common.Cast[syscall.Conn](conn) + tcpConn, ok := N.UnwrapReader(conn).(*net.TCPConn) if !ok { return nil, E.New("apple TLS: requires fd-backed TCP connection") } - syscallConn, err := rawSyscallConn.SyscallConn() + syscallConn, err := tcpConn.SyscallConn() if err != nil { return nil, E.Cause(err, "access raw connection") } @@ -61,8 +62,14 @@ func (c *appleClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) alpnPtr := cStringOrNil(alpn) defer cFree(alpnPtr) - anchorPEMPtr := cStringOrNil(c.anchorPEM) - defer cFree(anchorPEMPtr) + anchors, err := c.resolveAnchors() + if err != nil { + return nil, err + } + var anchorsRef unsafe.Pointer + if anchors != nil { + anchorsRef = anchors.Ref() + } var ( hasVerifyTime bool @@ -82,13 +89,15 @@ func (c *appleClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) C.uint16_t(c.minVersion), C.uint16_t(c.maxVersion), C.bool(c.insecure), - anchorPEMPtr, - C.size_t(len(c.anchorPEM)), + anchorsRef, C.bool(c.anchorOnly), C.bool(hasVerifyTime), C.int64_t(verifyTimeUnixMilli), &errorPtr, ) + if anchors != nil { + anchors.Release() + } if client == nil { if errorPtr != nil { defer C.free(unsafe.Pointer(errorPtr)) @@ -138,21 +147,27 @@ func (c *appleClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) }, nil } -const appleTLSHandshakePollInterval = 100 * time.Millisecond +const ( + appleTLSHandshakePollInterval = 100 * time.Millisecond + appleTLSWriteChunkSize = 32 * 1024 +) func waitAppleTLSClientReady(ctx context.Context, client *C.box_apple_tls_client_t) error { for { - if err := ctx.Err(); err != nil { + err := ctx.Err() + if err != nil { C.box_apple_tls_client_cancel(client) return err } waitTimeout := appleTLSHandshakePollInterval - if deadline, loaded := ctx.Deadline(); loaded { + deadline, loaded := ctx.Deadline() + if loaded { remaining := time.Until(deadline) if remaining <= 0 { C.box_apple_tls_client_cancel(client) - if err := ctx.Err(); err != nil { + err = ctx.Err() + if err != nil { return err } return context.DeadlineExceeded @@ -201,6 +216,11 @@ type appleTLSConn struct { writeTimedOut bool } +var ( + _ N.ExtendedConn = (*appleTLSConn)(nil) + _ N.ReadWaitCreator = (*appleTLSConn)(nil) +) + func (c *appleTLSConn) Read(p []byte) (int, error) { c.readAccess.Lock() defer c.readAccess.Unlock() @@ -211,6 +231,29 @@ func (c *appleTLSConn) Read(p []byte) (int, error) { return 0, nil } + return c.readIntoLocked(p) +} + +func (c *appleTLSConn) ReadBuffer(buffer *buf.Buffer) error { + c.readAccess.Lock() + defer c.readAccess.Unlock() + if buffer.IsFull() { + return io.ErrShortBuffer + } + startLen := buffer.Len() + n, err := c.readIntoLocked(buffer.FreeBytes()) + buffer.Truncate(startLen + n) + return err +} + +func (c *appleTLSConn) readIntoLocked(p []byte) (int, error) { + if c.readEOF { + return 0, io.EOF + } + if len(p) == 0 { + return 0, nil + } + timeoutMs, err := c.prepareReadTimeout() if err != nil { return 0, err @@ -256,34 +299,55 @@ func (c *appleTLSConn) Write(p []byte) (int, error) { return 0, nil } - timeoutMs, err := c.prepareWriteTimeout() - if err != nil { - return 0, err - } - client, err := c.acquireClient() if err != nil { return 0, err } defer c.releaseClient() - var errorPtr *C.char - n := C.box_apple_tls_client_write(client, unsafe.Pointer(&p[0]), C.size_t(len(p)), C.int(timeoutMs), &errorPtr) - switch { - case n == -2: - c.markWriteTimedOut() - return 0, os.ErrDeadlineExceeded - case n >= 0: - return int(n), nil + deadline, err := c.prepareWriteDeadline() + if err != nil { + return 0, err } - if errorPtr != nil { - defer C.free(unsafe.Pointer(errorPtr)) - if c.isClosed() { - return 0, net.ErrClosed + var written int + for written < len(p) { + timeoutMs, expired := deadlineTimeoutMs(deadline) + if expired { + C.box_apple_tls_client_cancel(client) + c.markWriteTimedOut() + return written, os.ErrDeadlineExceeded + } + chunkSize := min(len(p)-written, appleTLSWriteChunkSize) + chunk := p[written : written+chunkSize] + var errorPtr *C.char + n := C.box_apple_tls_client_write(client, unsafe.Pointer(&chunk[0]), C.size_t(len(chunk)), C.int(timeoutMs), &errorPtr) + switch { + case n == -2: + c.markWriteTimedOut() + return written, os.ErrDeadlineExceeded + case n >= 0: + written += int(n) + if int(n) != len(chunk) { + return written, io.ErrShortWrite + } + continue } - return 0, E.New(C.GoString(errorPtr)) + return written, c.errorFromPointer(errorPtr) } - return 0, net.ErrClosed + return written, nil +} + +func (c *appleTLSConn) WriteBuffer(buffer *buf.Buffer) error { + defer buffer.Release() + _, err := c.Write(buffer.Bytes()) + return err +} + +func (c *appleTLSConn) CreateReadWaiter() (N.ReadWaiter, bool) { + return &appleTLSReadWaiter{ + conn: c, + results: make(chan *C.box_apple_tls_read_result_t, 1), + }, true } func (c *appleTLSConn) Close() error { @@ -358,18 +422,18 @@ func (c *appleTLSConn) prepareReadTimeout() (int, error) { return timeoutMs, nil } -func (c *appleTLSConn) prepareWriteTimeout() (int, error) { +func (c *appleTLSConn) prepareWriteDeadline() (time.Time, error) { c.deadlineAccess.Lock() defer c.deadlineAccess.Unlock() if c.writeTimedOut { - return 0, os.ErrDeadlineExceeded + return time.Time{}, os.ErrDeadlineExceeded } - timeoutMs, expired := deadlineTimeoutMs(c.writeDeadline) + _, expired := deadlineTimeoutMs(c.writeDeadline) if expired { c.writeTimedOut = true - return 0, os.ErrDeadlineExceeded + return time.Time{}, os.ErrDeadlineExceeded } - return timeoutMs, nil + return c.writeDeadline, nil } func (c *appleTLSConn) markReadTimedOut() { @@ -422,6 +486,138 @@ func (c *appleTLSConn) releaseClient() { c.ioGroup.Done() } +func (c *appleTLSConn) errorFromPointer(errorPtr *C.char) error { + if errorPtr != nil { + defer C.free(unsafe.Pointer(errorPtr)) + if c.isClosed() { + return net.ErrClosed + } + return E.New(C.GoString(errorPtr)) + } + return net.ErrClosed +} + +type appleTLSReadWaiter struct { + conn *appleTLSConn + options N.ReadWaitOptions + results chan *C.box_apple_tls_read_result_t +} + +var _ N.ReadWaiter = (*appleTLSReadWaiter)(nil) + +func (w *appleTLSReadWaiter) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { + w.options = options + if w.results == nil { + w.results = make(chan *C.box_apple_tls_read_result_t, 1) + } + return false +} + +func (w *appleTLSReadWaiter) WaitReadBuffer() (*buf.Buffer, error) { + c := w.conn + c.readAccess.Lock() + defer c.readAccess.Unlock() + if c.readEOF { + return nil, io.EOF + } + maximumLen := readWaitFreeLen(w.options) + if maximumLen <= 0 { + return nil, io.ErrShortBuffer + } + timeoutMs, err := c.prepareReadTimeout() + if err != nil { + return nil, err + } + client, err := c.acquireClient() + if err != nil { + return nil, err + } + defer c.releaseClient() + + handle := cgo.NewHandle(w) + defer handle.Delete() + var errorPtr *C.char + if !bool(C.box_apple_tls_client_read_async(client, C.size_t(maximumLen), C.uintptr_t(handle), &errorPtr)) { + return nil, c.errorFromPointer(errorPtr) + } + + var result *C.box_apple_tls_read_result_t + if timeoutMs >= 0 { + timer := time.NewTimer(time.Duration(timeoutMs) * time.Millisecond) + defer timer.Stop() + select { + case result = <-w.results: + case <-timer.C: + C.box_apple_tls_client_cancel(client) + result = <-w.results + if result != nil { + C.box_apple_tls_read_result_free(result) + } + c.markReadTimedOut() + return nil, os.ErrDeadlineExceeded + } + } else { + result = <-w.results + } + return c.readWaitResultToBuffer(result, w.options) +} + +func (c *appleTLSConn) readWaitResultToBuffer(result *C.box_apple_tls_read_result_t, options N.ReadWaitOptions) (*buf.Buffer, error) { + defer C.box_apple_tls_read_result_free(result) + buffer := options.NewBuffer() + if buffer.IsFull() { + buffer.Release() + return nil, io.ErrShortBuffer + } + startLen := buffer.Len() + var eof C.bool + var errorPtr *C.char + n := C.box_apple_tls_read_result_copy(result, unsafe.Pointer(&buffer.FreeBytes()[0]), C.size_t(buffer.FreeLen()), &eof, &errorPtr) + if n < 0 { + buffer.Release() + return nil, c.errorFromPointer(errorPtr) + } + if bool(eof) { + c.readEOF = true + if n == 0 { + buffer.Release() + return nil, io.EOF + } + } + if n == 0 { + buffer.Release() + return nil, io.ErrNoProgress + } + buffer.Truncate(startLen + int(n)) + options.PostReturn(buffer) + return buffer, nil +} + +func readWaitFreeLen(options N.ReadWaitOptions) int { + if options.IncreaseBuffer { + return 65535 - options.FrontHeadroom - options.RearHeadroom + } + if options.MTU > 0 { + return options.MTU + } + return buf.BufferSize - options.FrontHeadroom - options.RearHeadroom +} + +//export box_apple_tls_read_callback +func box_apple_tls_read_callback(callbackHandle C.uintptr_t, result *C.box_apple_tls_read_result_t) { + handle := cgo.Handle(callbackHandle) + waiter, ok := handle.Value().(*appleTLSReadWaiter) + if !ok { + C.box_apple_tls_read_result_free(result) + return + } + select { + case waiter.results <- result: + default: + C.box_apple_tls_read_result_free(result) + } +} + func (c *appleTLSConn) NetConn() net.Conn { return c.rawConn } diff --git a/common/tls/apple_client_platform_benchmark_test.go b/common/tls/apple_client_platform_benchmark_test.go new file mode 100644 index 0000000000..27fcf1532c --- /dev/null +++ b/common/tls/apple_client_platform_benchmark_test.go @@ -0,0 +1,278 @@ +//go:build darwin && cgo + +package tls + +import ( + "bytes" + stdtls "crypto/tls" + "crypto/x509" + "errors" + "io" + "net" + "testing" + + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/json/badoption" + N "github.com/sagernet/sing/common/network" +) + +const ( + appleTLSBenchmarkReadPayloadSize = 16 * 1024 + appleTLSBenchmarkWritePayloadSize = 48 * 1024 +) + +func BenchmarkAppleClientReadBuffer(b *testing.B) { + payload := bytes.Repeat([]byte{'r'}, appleTLSBenchmarkReadPayloadSize) + start := make(chan struct{}) + clientConn, serverResult := newAppleBenchmarkClientConn(b, func(conn *stdtls.Conn) error { + <-start + for range b.N { + if err := writeBenchmarkPayload(conn, payload); err != nil { + return err + } + } + return nil + }) + defer clientConn.Close() + + extendedConn := clientConn.(N.ExtendedConn) + b.ReportAllocs() + b.SetBytes(int64(len(payload))) + b.ReportMetric(float64(len(payload)), "payload_B") + b.ResetTimer() + close(start) + target := b.N * len(payload) + var received int + for received < target { + buffer := buf.NewSize(len(payload)) + err := extendedConn.ReadBuffer(buffer) + if err != nil { + buffer.Release() + b.Fatal(err) + } + received += buffer.Len() + buffer.Release() + } + b.StopTimer() + if err := <-serverResult; err != nil { + b.Fatal(err) + } +} + +func BenchmarkAppleClientReadWaiter(b *testing.B) { + payload := bytes.Repeat([]byte{'w'}, appleTLSBenchmarkReadPayloadSize) + start := make(chan struct{}) + clientConn, serverResult := newAppleBenchmarkClientConn(b, func(conn *stdtls.Conn) error { + <-start + for range b.N { + if err := writeBenchmarkPayload(conn, payload); err != nil { + return err + } + } + return nil + }) + defer clientConn.Close() + + readWaiter, ok := clientConn.(N.ReadWaitCreator).CreateReadWaiter() + if !ok { + b.Fatal("expected read waiter") + } + readWaiter.InitializeReadWaiter(N.ReadWaitOptions{ + MTU: appleTLSBenchmarkReadPayloadSize, + }) + b.ReportAllocs() + b.SetBytes(int64(len(payload))) + b.ReportMetric(float64(len(payload)), "payload_B") + b.ResetTimer() + close(start) + target := b.N * len(payload) + var received int + for received < target { + buffer, err := readWaiter.WaitReadBuffer() + if err != nil { + if errors.Is(err, io.ErrNoProgress) { + continue + } + b.Fatal(err) + } + received += buffer.Len() + buffer.Release() + } + b.StopTimer() + if err := <-serverResult; err != nil { + b.Fatal(err) + } +} + +func BenchmarkAppleClientWriteBuffer(b *testing.B) { + payload := bytes.Repeat([]byte{'x'}, appleTLSBenchmarkWritePayloadSize) + start := make(chan struct{}) + clientConn, serverResult := newAppleBenchmarkClientConn(b, func(conn *stdtls.Conn) error { + <-start + _, err := io.CopyN(io.Discard, conn, int64(b.N*len(payload))) + return err + }) + defer clientConn.Close() + + extendedConn := clientConn.(N.ExtendedConn) + b.ReportAllocs() + b.SetBytes(int64(len(payload))) + b.ReportMetric(float64(len(payload)), "payload_B") + b.ReportMetric(float64(appleTLSWriteChunkSize), "write_chunk_B") + b.ResetTimer() + close(start) + for range b.N { + buffer := buf.NewSize(len(payload)) + _, err := buffer.Write(payload) + if err != nil { + buffer.Release() + b.Fatal(err) + } + err = extendedConn.WriteBuffer(buffer) + if err != nil { + b.Fatal(err) + } + } + b.StopTimer() + if err := <-serverResult; err != nil { + b.Fatal(err) + } +} + +func BenchmarkStdlibClientReadBuffer(b *testing.B) { + payload := bytes.Repeat([]byte{'r'}, appleTLSBenchmarkReadPayloadSize) + start := make(chan struct{}) + clientConn, serverResult := newStdlibBenchmarkClientConn(b, func(conn *stdtls.Conn) error { + <-start + for range b.N { + if err := writeBenchmarkPayload(conn, payload); err != nil { + return err + } + } + return nil + }) + defer clientConn.Close() + + b.ReportAllocs() + b.SetBytes(int64(len(payload))) + b.ReportMetric(float64(len(payload)), "payload_B") + b.ResetTimer() + close(start) + target := b.N * len(payload) + var received int + for received < target { + buffer := buf.NewSize(len(payload)) + n, err := clientConn.Read(buffer.FreeBytes()) + if n > 0 { + buffer.Truncate(buffer.Len() + n) + } + received += buffer.Len() + buffer.Release() + if err != nil && received < target { + b.Fatal(err) + } + } + b.StopTimer() + if err := <-serverResult; err != nil { + b.Fatal(err) + } +} + +func BenchmarkStdlibClientWriteBuffer(b *testing.B) { + payload := bytes.Repeat([]byte{'x'}, appleTLSBenchmarkWritePayloadSize) + start := make(chan struct{}) + clientConn, serverResult := newStdlibBenchmarkClientConn(b, func(conn *stdtls.Conn) error { + <-start + _, err := io.CopyN(io.Discard, conn, int64(b.N*len(payload))) + return err + }) + defer clientConn.Close() + + b.ReportAllocs() + b.SetBytes(int64(len(payload))) + b.ReportMetric(float64(len(payload)), "payload_B") + b.ResetTimer() + close(start) + for range b.N { + buffer := buf.NewSize(len(payload)) + _, err := buffer.Write(payload) + if err != nil { + buffer.Release() + b.Fatal(err) + } + _, err = clientConn.Write(buffer.Bytes()) + buffer.Release() + if err != nil { + b.Fatal(err) + } + } + b.StopTimer() + if err := <-serverResult; err != nil { + b.Fatal(err) + } +} + +func newAppleBenchmarkClientConn(b *testing.B, handler func(*stdtls.Conn) error) (Conn, <-chan error) { + b.Helper() + + serverCertificate, serverCertificatePEM := newAppleTestCertificate(b, "localhost") + serverResult, serverAddress := startAppleTLSIOTestServer(b, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: stdtls.VersionTLS12, + MaxVersion: stdtls.VersionTLS12, + }, handler) + + clientConn, err := newAppleTestClientConn(b, serverAddress, option.OutboundTLSOptions{ + Enabled: true, + Engine: "apple", + ServerName: "localhost", + MinVersion: "1.2", + MaxVersion: "1.2", + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }) + if err != nil { + b.Fatal(err) + } + return clientConn, serverResult +} + +func newStdlibBenchmarkClientConn(b *testing.B, handler func(*stdtls.Conn) error) (*stdtls.Conn, <-chan error) { + b.Helper() + + serverCertificate, serverCertificatePEM := newAppleTestCertificate(b, "localhost") + serverResult, serverAddress := startAppleTLSIOTestServer(b, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: stdtls.VersionTLS12, + MaxVersion: stdtls.VersionTLS12, + }, handler) + + roots := x509.NewCertPool() + if !roots.AppendCertsFromPEM([]byte(serverCertificatePEM)) { + b.Fatal("parse benchmark certificate") + } + dialer := &net.Dialer{ + Timeout: appleTLSTestTimeout, + } + clientConn, err := stdtls.DialWithDialer(dialer, "tcp", serverAddress, &stdtls.Config{ + ServerName: "localhost", + RootCAs: roots, + MinVersion: stdtls.VersionTLS12, + MaxVersion: stdtls.VersionTLS12, + }) + if err != nil { + b.Fatal(err) + } + return clientConn, serverResult +} + +func writeBenchmarkPayload(writer io.Writer, payload []byte) error { + for len(payload) > 0 { + n, err := writer.Write(payload) + if err != nil { + return err + } + payload = payload[n:] + } + return nil +} diff --git a/common/tls/apple_client_platform_darwin.h b/common/tls/apple_client_platform_darwin.h index 9d765835fc..43b3e031d8 100644 --- a/common/tls/apple_client_platform_darwin.h +++ b/common/tls/apple_client_platform_darwin.h @@ -4,6 +4,7 @@ #include typedef struct box_apple_tls_client box_apple_tls_client_t; +typedef struct box_apple_tls_read_result box_apple_tls_read_result_t; typedef struct box_apple_tls_state { uint16_t version; @@ -22,8 +23,7 @@ box_apple_tls_client_t *box_apple_tls_client_create( uint16_t min_version, uint16_t max_version, bool insecure, - const char *anchor_pem, - size_t anchor_pem_len, + void *anchors_cf, bool anchor_only, bool has_verify_time, int64_t verify_time_unix_millis, @@ -35,5 +35,9 @@ void box_apple_tls_client_cancel(box_apple_tls_client_t *client); void box_apple_tls_client_free(box_apple_tls_client_t *client); ssize_t box_apple_tls_client_read(box_apple_tls_client_t *client, void *buffer, size_t buffer_len, int timeout_msec, bool *eof_out, char **error_out); ssize_t box_apple_tls_client_write(box_apple_tls_client_t *client, const void *buffer, size_t buffer_len, int timeout_msec, char **error_out); +bool box_apple_tls_client_read_async(box_apple_tls_client_t *client, size_t maximum_len, uintptr_t callback_handle, char **error_out); +ssize_t box_apple_tls_read_result_copy(box_apple_tls_read_result_t *result, void *buffer, size_t buffer_len, bool *eof_out, char **error_out); +void box_apple_tls_read_result_free(box_apple_tls_read_result_t *result); bool box_apple_tls_client_copy_state(box_apple_tls_client_t *client, box_apple_tls_state_t *state, char **error_out); void box_apple_tls_state_free(box_apple_tls_state_t *state); +ssize_t box_apple_tls_copy_dispatch_data_for_test(const void *first, size_t first_len, const void *second, size_t second_len, void *buffer, size_t buffer_len, char **error_out); diff --git a/common/tls/apple_client_platform_darwin.m b/common/tls/apple_client_platform_darwin.m index d03f9fff93..f4d0eb6793 100644 --- a/common/tls/apple_client_platform_darwin.m +++ b/common/tls/apple_client_platform_darwin.m @@ -21,6 +21,7 @@ void *connection; void *queue; void *ready_semaphore; + void *anchors; atomic_int ref_count; atomic_bool ready; atomic_bool ready_done; @@ -28,6 +29,14 @@ box_apple_tls_state_t state; } box_apple_tls_client_t; +struct box_apple_tls_read_result { + void *content; + bool eof; + char *error; +}; + +extern void box_apple_tls_read_callback(uintptr_t callback_handle, box_apple_tls_read_result_t *result); + static nw_connection_t box_apple_tls_connection(box_apple_tls_client_t *client) { if (client == NULL || client->connection == NULL) { return nil; @@ -49,6 +58,20 @@ static dispatch_semaphore_t box_apple_tls_ready_semaphore(box_apple_tls_client_t return (__bridge dispatch_semaphore_t)client->ready_semaphore; } +static NSArray *box_apple_tls_client_anchors(box_apple_tls_client_t *client) { + if (client == NULL || client->anchors == NULL) { + return nil; + } + return (__bridge NSArray *)client->anchors; +} + +static dispatch_data_t box_apple_tls_read_result_content(box_apple_tls_read_result_t *result) { + if (result == NULL || result->content == NULL) { + return nil; + } + return (__bridge dispatch_data_t)result->content; +} + static void box_apple_tls_state_reset(box_apple_tls_state_t *state) { if (state == NULL) { return; @@ -62,6 +85,9 @@ static void box_apple_tls_state_reset(box_apple_tls_state_t *state) { static void box_apple_tls_client_destroy(box_apple_tls_client_t *client) { free(client->ready_error); box_apple_tls_state_reset(&client->state); + if (client->anchors != NULL) { + CFRelease((CFTypeRef)client->anchors); + } if (client->ready_semaphore != NULL) { CFBridgingRelease(client->ready_semaphore); } @@ -113,6 +139,51 @@ static void box_set_error_from_nw_error(char **error_out, nw_error_t error) { CFRelease(cfError); } +static ssize_t box_apple_tls_dispatch_data_copy(dispatch_data_t content, void *buffer, size_t buffer_len, char **error_out) { + if (content == nil) { + return 0; + } + size_t content_size = dispatch_data_get_size(content); + if (content_size == 0) { + return 0; + } + if (buffer == NULL) { + box_set_error_message(error_out, "apple TLS: read buffer unavailable"); + return -1; + } + __block size_t copied = 0; + __block bool overflow = false; + bool complete = dispatch_data_apply(content, ^bool(dispatch_data_t region, size_t offset, const void *region_buffer, size_t region_size) { + (void)region; + (void)offset; + if (region_size == 0) { + return true; + } + if (region_buffer == NULL || region_size > buffer_len - copied) { + overflow = true; + return false; + } + memcpy((uint8_t *)buffer + copied, region_buffer, region_size); + copied += region_size; + return true; + }); + if (!complete || overflow) { + box_set_error_message(error_out, "apple TLS: read buffer too small"); + return -1; + } + return (ssize_t)copied; +} + +ssize_t box_apple_tls_copy_dispatch_data_for_test(const void *first, size_t first_len, const void *second, size_t second_len, void *buffer, size_t buffer_len, char **error_out) { + @autoreleasepool { + dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0); + dispatch_data_t first_data = first_len > 0 ? dispatch_data_create(first, first_len, queue, DISPATCH_DATA_DESTRUCTOR_DEFAULT) : dispatch_data_empty; + dispatch_data_t second_data = second_len > 0 ? dispatch_data_create(second, second_len, queue, DISPATCH_DATA_DESTRUCTOR_DEFAULT) : dispatch_data_empty; + dispatch_data_t content = dispatch_data_create_concat(first_data, second_data); + return box_apple_tls_dispatch_data_copy(content, buffer, buffer_len, error_out); + } +} + static char *box_apple_tls_metadata_copy_negotiated_protocol(sec_protocol_metadata_t metadata) { static box_sec_protocol_metadata_string_accessor_f copy_fn; static box_sec_protocol_metadata_string_accessor_f get_fn; @@ -170,44 +241,6 @@ static void box_set_error_from_nw_error(char **error_out, nw_error_t error) { return lines; } -static NSArray *box_parse_certificates_from_pem(const char *pem, size_t pem_len) { - if (pem == NULL || pem_len == 0) { - return @[]; - } - NSString *content = [[NSString alloc] initWithBytes:pem length:pem_len encoding:NSUTF8StringEncoding]; - if (content == nil) { - return @[]; - } - NSString *beginMarker = @"-----BEGIN CERTIFICATE-----"; - NSString *endMarker = @"-----END CERTIFICATE-----"; - NSMutableArray *certificates = [NSMutableArray array]; - NSUInteger searchFrom = 0; - while (searchFrom < content.length) { - NSRange beginRange = [content rangeOfString:beginMarker options:0 range:NSMakeRange(searchFrom, content.length - searchFrom)]; - if (beginRange.location == NSNotFound) { - break; - } - NSUInteger bodyStart = beginRange.location + beginRange.length; - NSRange endRange = [content rangeOfString:endMarker options:0 range:NSMakeRange(bodyStart, content.length - bodyStart)]; - if (endRange.location == NSNotFound) { - break; - } - NSString *base64Section = [content substringWithRange:NSMakeRange(bodyStart, endRange.location - bodyStart)]; - NSArray *components = [base64Section componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; - NSString *base64Content = [components componentsJoinedByString:@""]; - NSData *der = [[NSData alloc] initWithBase64EncodedString:base64Content options:0]; - if (der != nil) { - SecCertificateRef certificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)der); - if (certificate != NULL) { - [certificates addObject:(__bridge id)certificate]; - CFRelease(certificate); - } - } - searchFrom = endRange.location + endRange.length; - } - return certificates; -} - static bool box_evaluate_trust(sec_trust_t trust, NSArray *anchors, bool anchor_only, NSDate *verify_date) { bool result = false; SecTrustRef trustRef = sec_trust_copy_ref(trust); @@ -328,8 +361,7 @@ static void box_apple_tls_state_load(sec_protocol_metadata_t sec_metadata, box_a uint16_t min_version, uint16_t max_version, bool insecure, - const char *anchor_pem, - size_t anchor_pem_len, + void *anchors_cf, bool anchor_only, bool has_verify_time, int64_t verify_time_unix_millis, @@ -346,9 +378,11 @@ static void box_apple_tls_state_load(sec_protocol_metadata_t sec_metadata, box_a atomic_init(&client->ref_count, 1); atomic_init(&client->ready, false); atomic_init(&client->ready_done, false); + if (anchors_cf != NULL) { + client->anchors = (void *)CFRetain(anchors_cf); + } NSArray *alpnList = box_split_lines(alpn, alpn_len); - NSArray *anchors = box_parse_certificates_from_pem(anchor_pem, anchor_pem_len); NSDate *verifyDate = nil; if (has_verify_time) { verifyDate = [NSDate dateWithTimeIntervalSince1970:(NSTimeInterval)verify_time_unix_millis / 1000.0]; @@ -372,13 +406,16 @@ static void box_apple_tls_state_load(sec_protocol_metadata_t sec_metadata, box_a if (client->state.version == 0) { box_apple_tls_state_load(metadata, &client->state); } - complete(insecure || box_evaluate_trust(trust, anchors, anchor_only, verifyDate)); + complete(insecure || box_evaluate_trust(trust, box_apple_tls_client_anchors(client), anchor_only, verifyDate)); }, box_apple_tls_client_queue(client)); }, NW_PARAMETERS_DEFAULT_CONFIGURATION); nw_connection_t connection = box_apple_tls_create_connection(connected_socket, parameters); if (connection == NULL) { close(connected_socket); + if (client->anchors != NULL) { + CFRelease((CFTypeRef)client->anchors); + } if (client->ready_semaphore != NULL) { CFBridgingRelease(client->ready_semaphore); } @@ -485,128 +522,202 @@ void box_apple_tls_client_free(box_apple_tls_client_t *client) { } ssize_t box_apple_tls_client_read(box_apple_tls_client_t *client, void *buffer, size_t buffer_len, int timeout_msec, bool *eof_out, char **error_out) { - nw_connection_t connection = box_apple_tls_connection(client); - if (connection == nil) { - box_set_error_message(error_out, "apple TLS: invalid client"); - return -1; - } - - dispatch_semaphore_t read_semaphore = dispatch_semaphore_create(0); - __block NSData *content_data = nil; - __block bool read_eof = false; - __block char *local_error = NULL; - - nw_connection_receive(connection, 1, (uint32_t)buffer_len, ^(dispatch_data_t content, nw_content_context_t context, bool is_complete, nw_error_t error) { - if (content != NULL) { - const void *mapped = NULL; - size_t mapped_len = 0; - dispatch_data_t mapped_data = dispatch_data_create_map(content, &mapped, &mapped_len); - if (mapped != NULL && mapped_len > 0) { - content_data = [NSData dataWithBytes:mapped length:mapped_len]; + @autoreleasepool { + nw_connection_t connection = box_apple_tls_connection(client); + if (connection == nil) { + box_set_error_message(error_out, "apple TLS: invalid client"); + return -1; + } + + dispatch_semaphore_t read_semaphore = dispatch_semaphore_create(0); + __block size_t content_len = 0; + __block bool read_eof = false; + __block char *local_error = NULL; + + nw_connection_receive(connection, 1, (uint32_t)buffer_len, ^(dispatch_data_t content, nw_content_context_t context, bool is_complete, nw_error_t error) { + @autoreleasepool { + if (content != NULL) { + ssize_t copied = box_apple_tls_dispatch_data_copy(content, buffer, buffer_len, &local_error); + if (copied >= 0) { + content_len = (size_t)copied; + } + } + if (error != NULL && content_len == 0 && local_error == NULL) { + box_set_error_from_nw_error(&local_error, error); + } + if (is_complete && (context == NULL || nw_content_context_get_is_final(context))) { + read_eof = true; + } + dispatch_semaphore_signal(read_semaphore); } - (void)mapped_data; - } - if (error != NULL && content_data.length == 0) { - box_set_error_from_nw_error(&local_error, error); + }); + + dispatch_time_t wait_deadline = DISPATCH_TIME_FOREVER; + if (timeout_msec >= 0) { + wait_deadline = dispatch_time(DISPATCH_TIME_NOW, (int64_t)timeout_msec * NSEC_PER_MSEC); } - if (is_complete && (context == NULL || nw_content_context_get_is_final(context))) { - read_eof = true; + long wait_result = dispatch_semaphore_wait(read_semaphore, wait_deadline); + if (wait_result != 0) { + nw_connection_cancel(connection); + dispatch_semaphore_wait(read_semaphore, DISPATCH_TIME_FOREVER); + if (local_error != NULL) { + free(local_error); + local_error = NULL; + } + return -2; } - dispatch_semaphore_signal(read_semaphore); - }); - - dispatch_time_t wait_deadline = DISPATCH_TIME_FOREVER; - if (timeout_msec >= 0) { - wait_deadline = dispatch_time(DISPATCH_TIME_NOW, (int64_t)timeout_msec * NSEC_PER_MSEC); - } - long wait_result = dispatch_semaphore_wait(read_semaphore, wait_deadline); - if (wait_result != 0) { - nw_connection_cancel(connection); - dispatch_semaphore_wait(read_semaphore, DISPATCH_TIME_FOREVER); if (local_error != NULL) { - free(local_error); - local_error = NULL; + if (error_out != NULL) { + *error_out = local_error; + } else { + free(local_error); + } + return -1; } - return -2; - } - if (local_error != NULL) { - if (error_out != NULL) { - *error_out = local_error; - } else { - free(local_error); + if (eof_out != NULL) { + *eof_out = read_eof; } - return -1; + return (ssize_t)content_len; } - if (eof_out != NULL) { - *eof_out = read_eof; - } - if (content_data == nil || content_data.length == 0) { - return 0; - } - memcpy(buffer, content_data.bytes, content_data.length); - return (ssize_t)content_data.length; } ssize_t box_apple_tls_client_write(box_apple_tls_client_t *client, const void *buffer, size_t buffer_len, int timeout_msec, char **error_out) { - nw_connection_t connection = box_apple_tls_connection(client); - if (connection == nil) { - box_set_error_message(error_out, "apple TLS: invalid client"); - return -1; - } - if (buffer_len == 0) { - return 0; - } + @autoreleasepool { + nw_connection_t connection = box_apple_tls_connection(client); + if (connection == nil) { + box_set_error_message(error_out, "apple TLS: invalid client"); + return -1; + } + if (buffer_len == 0) { + return 0; + } + + void *content_copy = malloc(buffer_len); + if (content_copy == NULL) { + box_set_error_message(error_out, "apple TLS: out of memory"); + return -1; + } + dispatch_queue_t queue = box_apple_tls_client_queue(client); + if (queue == nil) { + free(content_copy); + box_set_error_message(error_out, "apple TLS: invalid client"); + return -1; + } + memcpy(content_copy, buffer, buffer_len); + dispatch_data_t content = dispatch_data_create(content_copy, buffer_len, queue, ^{ + free(content_copy); + }); + + dispatch_semaphore_t write_semaphore = dispatch_semaphore_create(0); + __block char *local_error = NULL; + + nw_connection_send(connection, content, NW_CONNECTION_DEFAULT_STREAM_CONTEXT, false, ^(nw_error_t error) { + @autoreleasepool { + if (error != NULL) { + box_set_error_from_nw_error(&local_error, error); + } + dispatch_semaphore_signal(write_semaphore); + } + }); - void *content_copy = malloc(buffer_len); - dispatch_queue_t queue = box_apple_tls_client_queue(client); - if (content_copy == NULL) { - free(content_copy); - box_set_error_message(error_out, "apple TLS: out of memory"); - return -1; - } - if (queue == nil) { - free(content_copy); - box_set_error_message(error_out, "apple TLS: invalid client"); - return -1; + dispatch_time_t wait_deadline = DISPATCH_TIME_FOREVER; + if (timeout_msec >= 0) { + wait_deadline = dispatch_time(DISPATCH_TIME_NOW, (int64_t)timeout_msec * NSEC_PER_MSEC); + } + long wait_result = dispatch_semaphore_wait(write_semaphore, wait_deadline); + if (wait_result != 0) { + nw_connection_cancel(connection); + dispatch_semaphore_wait(write_semaphore, DISPATCH_TIME_FOREVER); + if (local_error != NULL) { + free(local_error); + local_error = NULL; + } + return -2; + } + if (local_error != NULL) { + if (error_out != NULL) { + *error_out = local_error; + } else { + free(local_error); + } + return -1; + } + return (ssize_t)buffer_len; } - memcpy(content_copy, buffer, buffer_len); - dispatch_data_t content = dispatch_data_create(content_copy, buffer_len, queue, ^{ - free(content_copy); - }); +} - dispatch_semaphore_t write_semaphore = dispatch_semaphore_create(0); - __block char *local_error = NULL; +bool box_apple_tls_client_read_async(box_apple_tls_client_t *client, size_t maximum_len, uintptr_t callback_handle, char **error_out) { + @autoreleasepool { + nw_connection_t connection = box_apple_tls_connection(client); + if (connection == nil) { + box_set_error_message(error_out, "apple TLS: invalid client"); + return false; + } + if (maximum_len == 0) { + box_set_error_message(error_out, "apple TLS: empty read buffer"); + return false; + } + uint32_t receive_len = maximum_len > UINT32_MAX ? UINT32_MAX : (uint32_t)maximum_len; + nw_connection_receive(connection, 1, receive_len, ^(dispatch_data_t content, nw_content_context_t context, bool is_complete, nw_error_t error) { + @autoreleasepool { + box_apple_tls_read_result_t *result = calloc(1, sizeof(box_apple_tls_read_result_t)); + if (result == NULL) { + box_apple_tls_read_callback(callback_handle, NULL); + return; + } + size_t content_size = content != NULL ? dispatch_data_get_size(content) : 0; + if (content_size > 0) { + result->content = (__bridge_retained void *)content; + } + if (error != NULL && content_size == 0) { + box_set_error_from_nw_error(&result->error, error); + } + if (is_complete && (context == NULL || nw_content_context_get_is_final(context))) { + result->eof = true; + } + box_apple_tls_read_callback(callback_handle, result); + } + }); + return true; + } +} - nw_connection_send(connection, content, NW_CONNECTION_DEFAULT_STREAM_CONTEXT, false, ^(nw_error_t error) { - if (error != NULL) { - box_set_error_from_nw_error(&local_error, error); +ssize_t box_apple_tls_read_result_copy(box_apple_tls_read_result_t *result, void *buffer, size_t buffer_len, bool *eof_out, char **error_out) { + @autoreleasepool { + if (result == NULL) { + box_set_error_message(error_out, "apple TLS: read result unavailable"); + return -1; + } + if (result->error != NULL) { + if (error_out != NULL) { + *error_out = result->error; + result->error = NULL; + } else { + free(result->error); + result->error = NULL; + } + return -1; } - dispatch_semaphore_signal(write_semaphore); - }); - - dispatch_time_t wait_deadline = DISPATCH_TIME_FOREVER; - if (timeout_msec >= 0) { - wait_deadline = dispatch_time(DISPATCH_TIME_NOW, (int64_t)timeout_msec * NSEC_PER_MSEC); - } - long wait_result = dispatch_semaphore_wait(write_semaphore, wait_deadline); - if (wait_result != 0) { - nw_connection_cancel(connection); - dispatch_semaphore_wait(write_semaphore, DISPATCH_TIME_FOREVER); - if (local_error != NULL) { - free(local_error); - local_error = NULL; + if (eof_out != NULL) { + *eof_out = result->eof; } - return -2; - } - if (local_error != NULL) { - if (error_out != NULL) { - *error_out = local_error; - } else { - free(local_error); + dispatch_data_t content = box_apple_tls_read_result_content(result); + if (content == nil) { + return 0; } - return -1; + return box_apple_tls_dispatch_data_copy(content, buffer, buffer_len, error_out); + } +} + +void box_apple_tls_read_result_free(box_apple_tls_read_result_t *result) { + if (result == NULL) { + return; + } + free(result->error); + if (result->content != NULL) { + CFBridgingRelease(result->content); } - return (ssize_t)buffer_len; + free(result); } bool box_apple_tls_client_copy_state(box_apple_tls_client_t *client, box_apple_tls_state_t *state, char **error_out) { diff --git a/common/tls/apple_client_platform_dispatch_test.go b/common/tls/apple_client_platform_dispatch_test.go new file mode 100644 index 0000000000..65261a925e --- /dev/null +++ b/common/tls/apple_client_platform_dispatch_test.go @@ -0,0 +1,50 @@ +//go:build darwin && cgo + +package tls + +import ( + "bytes" + "strings" + "testing" +) + +func TestAppleTLSDispatchDataCopySegments(t *testing.T) { + first := []byte("hello ") + second := []byte("world") + + buffer := make([]byte, len(first)+len(second)) + n, errorMessage := appleTLSCopyDispatchDataForTest(first, second, buffer) + if n < 0 { + t.Fatalf("copy failed: %s", errorMessage) + } + if int(n) != len(buffer) { + t.Fatalf("copied %d bytes, want %d", n, len(buffer)) + } + if !bytes.Equal(buffer, []byte("hello world")) { + t.Fatalf("unexpected copy result: %q", string(buffer)) + } +} + +func TestAppleTLSDispatchDataCopyRejectsSmallBuffer(t *testing.T) { + first := []byte("hello") + second := []byte("world") + + buffer := make([]byte, len(first)+len(second)-1) + n, errorMessage := appleTLSCopyDispatchDataForTest(first, second, buffer) + if n != -1 { + t.Fatalf("copied %d bytes, want error", n) + } + if !strings.Contains(errorMessage, "read buffer too small") { + t.Fatalf("unexpected error: %q", errorMessage) + } +} + +func TestAppleTLSDispatchDataCopyEmpty(t *testing.T) { + n, errorMessage := appleTLSCopyDispatchDataForTest(nil, nil, nil) + if n != 0 { + t.Fatalf("copied %d bytes, want 0", n) + } + if errorMessage != "" { + t.Fatalf("unexpected error: %q", errorMessage) + } +} diff --git a/common/tls/apple_client_platform_dispatch_testhelper_darwin.go b/common/tls/apple_client_platform_dispatch_testhelper_darwin.go new file mode 100644 index 0000000000..0d80d429d9 --- /dev/null +++ b/common/tls/apple_client_platform_dispatch_testhelper_darwin.go @@ -0,0 +1,44 @@ +//go:build darwin && cgo + +package tls + +/* +#include +#include "apple_client_platform_darwin.h" +*/ +import "C" + +import "unsafe" + +func appleTLSCopyDispatchDataForTest(first, second []byte, buffer []byte) (int, string) { + var firstPtr unsafe.Pointer + if len(first) > 0 { + firstPtr = C.CBytes(first) + defer C.free(firstPtr) + } + var secondPtr unsafe.Pointer + if len(second) > 0 { + secondPtr = C.CBytes(second) + defer C.free(secondPtr) + } + var bufferPtr unsafe.Pointer + if len(buffer) > 0 { + bufferPtr = unsafe.Pointer(&buffer[0]) + } + var errPtr *C.char + n := C.box_apple_tls_copy_dispatch_data_for_test( + firstPtr, + C.size_t(len(first)), + secondPtr, + C.size_t(len(second)), + bufferPtr, + C.size_t(len(buffer)), + &errPtr, + ) + if errPtr == nil { + return int(n), "" + } + errorMessage := C.GoString(errPtr) + C.free(unsafe.Pointer(errPtr)) + return int(n), errorMessage +} diff --git a/common/tls/apple_client_platform_test.go b/common/tls/apple_client_platform_test.go index 6c915f68ca..18d040fb83 100644 --- a/common/tls/apple_client_platform_test.go +++ b/common/tls/apple_client_platform_test.go @@ -3,17 +3,21 @@ package tls import ( + "bytes" "context" stdtls "crypto/tls" "errors" + "io" "net" "os" "testing" "time" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/json/badoption" "github.com/sagernet/sing/common/logger" + N "github.com/sagernet/sing/common/network" ) const appleTLSTestTimeout = 5 * time.Second @@ -28,6 +32,11 @@ type appleTLSServerResult struct { err error } +var ( + _ N.ExtendedConn = (*appleTLSConn)(nil) + _ N.ReadWaitCreator = (*appleTLSConn)(nil) +) + func TestAppleClientHandshakeAppliesALPNAndVersion(t *testing.T) { serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost") for index := 0; index < appleTLSSuccessHandshakeLoops; index++ { @@ -75,6 +84,29 @@ func TestAppleClientHandshakeAppliesALPNAndVersion(t *testing.T) { } } +func TestAppleClientHandshakeRejectsOpaqueConn(t *testing.T) { + clientConfig, err := NewClientWithOptions(ClientOptions{ + Context: context.Background(), + Logger: logger.NOP(), + Options: option.OutboundTLSOptions{ + Enabled: true, + Engine: "apple", + ServerName: "localhost", + Insecure: true, + }, + }) + if err != nil { + t.Fatal(err) + } + clientConn, serverConn := net.Pipe() + defer clientConn.Close() + defer serverConn.Close() + _, err = ClientHandshake(context.Background(), clientConn, clientConfig) + if err == nil { + t.Fatal("expected handshake to reject non-TCP connection") + } +} + func TestAppleClientHandshakeRejectsVersionMismatch(t *testing.T) { serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost") serverResult, serverAddress := startAppleTLSTestServer(t, &stdtls.Config{ @@ -209,6 +241,237 @@ func TestAppleClientHandshakeRecoversAfterFailure(t *testing.T) { } } +func TestAppleClientConfigCloneWithInlineCertificate(t *testing.T) { + serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost") + clientConfig, err := NewClientWithOptions(ClientOptions{ + Context: context.Background(), + Logger: logger.NOP(), + Options: option.OutboundTLSOptions{ + Enabled: true, + Engine: "apple", + ServerName: "localhost", + MinVersion: "1.2", + MaxVersion: "1.2", + ALPN: badoption.Listable[string]{"h2", "http/1.1"}, + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }, + }) + if err != nil { + t.Fatal(err) + } + + clone := clientConfig.Clone() + clone.SetServerName("other") + clone.SetNextProtos([]string{"http/1.1"}) + if clientConfig.ServerName() == "other" { + t.Fatal("Clone shares server name with original") + } + nextProtos := clientConfig.NextProtos() + if len(nextProtos) != 2 || nextProtos[0] != "h2" || nextProtos[1] != "http/1.1" { + t.Fatalf("Clone shares ALPN slice with original: %v", nextProtos) + } + + for index := 0; index < appleTLSFailureRecoveryLoops; index++ { + serverResult, serverAddress := startAppleTLSTestServer(t, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: stdtls.VersionTLS12, + MaxVersion: stdtls.VersionTLS12, + NextProtos: []string{"h2"}, + }) + + handshakeConfig := clientConfig.Clone() + handshakeConfig.SetNextProtos([]string{"h2"}) + clientConn, err := dialAppleTestClientConn(t, serverAddress, handshakeConfig) + if err != nil { + t.Fatalf("iteration %d: %v", index, err) + } + + clientState := clientConn.ConnectionState() + if clientState.NegotiatedProtocol != "h2" { + _ = clientConn.Close() + t.Fatalf("iteration %d: unexpected negotiated protocol: %q", index, clientState.NegotiatedProtocol) + } + _ = clientConn.Close() + + result := <-serverResult + if result.err != nil { + t.Fatalf("iteration %d: %v", index, result.err) + } + } +} + +func TestAppleClientReadBuffer(t *testing.T) { + serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost") + payload := []byte("apple tls read buffer payload") + serverResult, serverAddress := startAppleTLSIOTestServer(t, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: stdtls.VersionTLS12, + MaxVersion: stdtls.VersionTLS12, + }, func(conn *stdtls.Conn) error { + _, err := conn.Write(payload) + return err + }) + + clientConn, err := newAppleTestClientConn(t, serverAddress, option.OutboundTLSOptions{ + Enabled: true, + Engine: "apple", + ServerName: "localhost", + MinVersion: "1.2", + MaxVersion: "1.2", + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }) + if err != nil { + t.Fatal(err) + } + defer clientConn.Close() + + extendedConn := clientConn.(N.ExtendedConn) + const ( + frontHeadroom = 17 + rearHeadroom = 19 + ) + buffer := buf.NewSize(frontHeadroom + len(payload) + rearHeadroom) + defer buffer.Release() + buffer.Resize(frontHeadroom, 0) + buffer.Reserve(rearHeadroom) + err = extendedConn.ReadBuffer(buffer) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(buffer.Bytes(), payload) { + t.Fatalf("unexpected payload: %q", buffer.Bytes()) + } + if buffer.Start() != frontHeadroom { + t.Fatalf("unexpected front headroom: %d", buffer.Start()) + } + if buffer.FreeLen() != 0 { + t.Fatalf("unexpected reserved free length before PostReturn: %d", buffer.FreeLen()) + } + buffer.OverCap(rearHeadroom) + if buffer.FreeLen() != rearHeadroom { + t.Fatalf("unexpected rear headroom after PostReturn: %d", buffer.FreeLen()) + } + + if err = <-serverResult; err != nil { + t.Fatal(err) + } +} + +func TestAppleClientWriteBuffer(t *testing.T) { + serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost") + payload := bytes.Repeat([]byte("apple-write-buffer-"), 3000) + serverResult, serverAddress := startAppleTLSIOTestServer(t, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: stdtls.VersionTLS12, + MaxVersion: stdtls.VersionTLS12, + }, func(conn *stdtls.Conn) error { + received := make([]byte, len(payload)) + _, err := io.ReadFull(conn, received) + if err != nil { + return err + } + if !bytes.Equal(received, payload) { + return errors.New("payload mismatch") + } + return nil + }) + + clientConn, err := newAppleTestClientConn(t, serverAddress, option.OutboundTLSOptions{ + Enabled: true, + Engine: "apple", + ServerName: "localhost", + MinVersion: "1.2", + MaxVersion: "1.2", + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }) + if err != nil { + t.Fatal(err) + } + defer clientConn.Close() + + extendedConn := clientConn.(N.ExtendedConn) + buffer := buf.NewSize(len(payload)) + _, err = buffer.Write(payload) + if err != nil { + t.Fatal(err) + } + err = extendedConn.WriteBuffer(buffer) + if err != nil { + t.Fatal(err) + } + if buffer.RawCap() != 0 { + t.Fatalf("buffer was not released: raw cap %d", buffer.RawCap()) + } + if err = <-serverResult; err != nil { + t.Fatal(err) + } +} + +func TestAppleClientCreateReadWaiter(t *testing.T) { + serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost") + payload := []byte("apple tls read waiter payload") + serverResult, serverAddress := startAppleTLSIOTestServer(t, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: stdtls.VersionTLS12, + MaxVersion: stdtls.VersionTLS12, + }, func(conn *stdtls.Conn) error { + _, err := conn.Write(payload) + return err + }) + + clientConn, err := newAppleTestClientConn(t, serverAddress, option.OutboundTLSOptions{ + Enabled: true, + Engine: "apple", + ServerName: "localhost", + MinVersion: "1.2", + MaxVersion: "1.2", + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }) + if err != nil { + t.Fatal(err) + } + defer clientConn.Close() + + readWaitCreator := clientConn.(N.ReadWaitCreator) + readWaiter, ok := readWaitCreator.CreateReadWaiter() + if !ok { + t.Fatal("expected read waiter") + } + const ( + frontHeadroom = 11 + rearHeadroom = 13 + ) + needCopy := readWaiter.InitializeReadWaiter(N.ReadWaitOptions{ + FrontHeadroom: frontHeadroom, + RearHeadroom: rearHeadroom, + MTU: len(payload), + }) + if needCopy { + t.Fatal("expected owned read waiter buffer") + } + buffer, err := readWaiter.WaitReadBuffer() + if err != nil { + t.Fatal(err) + } + defer buffer.Release() + if !bytes.Equal(buffer.Bytes(), payload) { + t.Fatalf("unexpected payload: %q", buffer.Bytes()) + } + if buffer.Start() != frontHeadroom { + t.Fatalf("unexpected front headroom: %d", buffer.Start()) + } + if buffer.FreeLen() != rearHeadroom { + t.Fatalf("unexpected rear headroom: %d", buffer.FreeLen()) + } + if buffer.Cap() != buffer.RawCap() { + t.Fatalf("capacity was not restored: cap=%d raw=%d", buffer.Cap(), buffer.RawCap()) + } + + if err = <-serverResult; err != nil { + t.Fatal(err) + } +} + func TestAppleClientReadDeadline(t *testing.T) { serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost") serverDone, serverAddress := startAppleTLSSilentServer(t, &stdtls.Config{ @@ -359,7 +622,52 @@ func startAppleTLSSilentServer(t *testing.T, tlsConfig *stdtls.Config) (chan<- s return done, listener.Addr().String() } -func newAppleTestCertificate(t *testing.T, serverName string) (stdtls.Certificate, string) { +func startAppleTLSIOTestServer(t testing.TB, tlsConfig *stdtls.Config, handler func(*stdtls.Conn) error) (<-chan error, string) { + t.Helper() + + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + listener.Close() + }) + + if tcpListener, isTCP := listener.(*net.TCPListener); isTCP { + err = tcpListener.SetDeadline(time.Now().Add(appleTLSTestTimeout)) + if err != nil { + t.Fatal(err) + } + } + + result := make(chan error, 1) + go func() { + conn, err := listener.Accept() + if err != nil { + result <- err + return + } + defer conn.Close() + + err = conn.SetDeadline(time.Now().Add(appleTLSTestTimeout)) + if err != nil { + result <- err + return + } + + tlsConn := stdtls.Server(conn, tlsConfig) + defer tlsConn.Close() + err = tlsConn.Handshake() + if err != nil { + result <- err + return + } + result <- handler(tlsConn) + }() + return result, listener.Addr().String() +} + +func newAppleTestCertificate(t testing.TB, serverName string) (stdtls.Certificate, string) { t.Helper() privateKeyPEM, certificatePEM, err := GenerateCertificate(nil, nil, time.Now, serverName, time.Now().Add(time.Hour)) @@ -423,14 +731,11 @@ func startAppleTLSTestServer(t *testing.T, tlsConfig *stdtls.Config) (<-chan app return result, listener.Addr().String() } -func newAppleTestClientConn(t *testing.T, serverAddress string, options option.OutboundTLSOptions) (Conn, error) { +func newAppleTestClientConn(t testing.TB, serverAddress string, options option.OutboundTLSOptions) (Conn, error) { t.Helper() - ctx, cancel := context.WithTimeout(context.Background(), appleTLSTestTimeout) - t.Cleanup(cancel) - clientConfig, err := NewClientWithOptions(ClientOptions{ - Context: ctx, + Context: context.Background(), Logger: logger.NOP(), ServerAddress: "", Options: options, @@ -438,6 +743,14 @@ func newAppleTestClientConn(t *testing.T, serverAddress string, options option.O if err != nil { return nil, err } + return dialAppleTestClientConn(t, serverAddress, clientConfig) +} + +func dialAppleTestClientConn(t testing.TB, serverAddress string, clientConfig Config) (Conn, error) { + t.Helper() + + ctx, cancel := context.WithTimeout(context.Background(), appleTLSTestTimeout) + t.Cleanup(cancel) conn, err := net.DialTimeout("tcp", serverAddress, appleTLSTestTimeout) if err != nil { diff --git a/common/tls/client.go b/common/tls/client.go index b5b975bf23..5134384197 100644 --- a/common/tls/client.go +++ b/common/tls/client.go @@ -98,9 +98,11 @@ func NewClientWithOptions(options ClientOptions) (Config, error) { options.Logger.Warn("enabling kTLS RX will definitely reduce performance, please checkout https://sing-box.sagernet.org/configuration/shared/tls/#kernel_rx") } switch options.Options.Engine { - case C.TLSEngineDefault, "go": + case "", C.TLSEngineGo: case C.TLSEngineApple: return newAppleClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName) + case C.TLSEngineWindows: + return newWindowsClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName) default: return nil, E.New("unknown tls engine: ", options.Options.Engine) } diff --git a/common/tls/std_client.go b/common/tls/std_client.go index 031a256f7d..6531039604 100644 --- a/common/tls/std_client.go +++ b/common/tls/std_client.go @@ -271,19 +271,7 @@ func verifyConnection(rootCAs *x509.CertPool, timeFunc func() time.Time, serverN if serverName == "" { return errMissingServerName } - verifyOptions := x509.VerifyOptions{ - Roots: rootCAs, - DNSName: serverName, - Intermediates: x509.NewCertPool(), - } - for _, cert := range state.PeerCertificates[1:] { - verifyOptions.Intermediates.AddCert(cert) - } - if timeFunc != nil { - verifyOptions.CurrentTime = timeFunc() - } - _, err := state.PeerCertificates[0].Verify(verifyOptions) - return err + return verifySystemTLSPeer(rootCAs, serverName, timeFunc, state.PeerCertificates) } } diff --git a/common/tls/system_client.go b/common/tls/system_client.go new file mode 100644 index 0000000000..356030ece0 --- /dev/null +++ b/common/tls/system_client.go @@ -0,0 +1,218 @@ +package tls + +import ( + "context" + "crypto/x509" + "net" + "os" + "strings" + "time" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/ntp" + "github.com/sagernet/sing/service" +) + +type systemTLSConfig struct { + serverName string + nextProtos []string + handshakeTimeout time.Duration + minVersion uint16 + maxVersion uint16 + insecure bool + anchorOnly bool + certificatePublicKeySHA256 [][]byte + timeFunc func() time.Time + store adapter.CertificateStore +} + +func (c *systemTLSConfig) ServerName() string { + return c.serverName +} + +func (c *systemTLSConfig) SetServerName(serverName string) { + c.serverName = serverName +} + +func (c *systemTLSConfig) NextProtos() []string { + return c.nextProtos +} + +func (c *systemTLSConfig) SetNextProtos(nextProto []string) { + c.nextProtos = append([]string(nil), nextProto...) +} + +func (c *systemTLSConfig) HandshakeTimeout() time.Duration { + return c.handshakeTimeout +} + +func (c *systemTLSConfig) SetHandshakeTimeout(timeout time.Duration) { + c.handshakeTimeout = timeout +} + +func (c *systemTLSConfig) STDConfig() (*STDConfig, error) { + return nil, E.New("STDConfig is unsupported for the system TLS engine") +} + +func (c *systemTLSConfig) Client(conn net.Conn) (Conn, error) { + return nil, os.ErrInvalid +} + +func (c *systemTLSConfig) clone() systemTLSConfig { + return systemTLSConfig{ + serverName: c.serverName, + nextProtos: append([]string(nil), c.nextProtos...), + handshakeTimeout: c.handshakeTimeout, + minVersion: c.minVersion, + maxVersion: c.maxVersion, + insecure: c.insecure, + anchorOnly: c.anchorOnly, + certificatePublicKeySHA256: append([][]byte(nil), c.certificatePublicKeySHA256...), + timeFunc: c.timeFunc, + store: c.store, + } +} + +type SystemTLSValidated struct { + MinVersion uint16 + MaxVersion uint16 + UserPEM []byte + Exclusive bool + Store adapter.CertificateStore +} + +func ValidateSystemTLSOptions(ctx context.Context, options option.OutboundTLSOptions, engineName string) (SystemTLSValidated, error) { + if options.Reality != nil && options.Reality.Enabled { + return SystemTLSValidated{}, E.New("reality is unsupported in ", engineName) + } + if options.UTLS != nil && options.UTLS.Enabled { + return SystemTLSValidated{}, E.New("utls is unsupported in ", engineName) + } + if options.ECH != nil && options.ECH.Enabled { + return SystemTLSValidated{}, E.New("ech is unsupported in ", engineName) + } + if options.DisableSNI { + return SystemTLSValidated{}, E.New("disable_sni is unsupported in ", engineName) + } + if len(options.CipherSuites) > 0 { + return SystemTLSValidated{}, E.New("cipher_suites is unsupported in ", engineName) + } + if len(options.CurvePreferences) > 0 { + return SystemTLSValidated{}, E.New("curve_preferences is unsupported in ", engineName) + } + if len(options.ClientCertificate) > 0 || options.ClientCertificatePath != "" || len(options.ClientKey) > 0 || options.ClientKeyPath != "" { + return SystemTLSValidated{}, E.New("client certificate is unsupported in ", engineName) + } + if options.Fragment || options.RecordFragment { + return SystemTLSValidated{}, E.New("tls fragment is unsupported in ", engineName) + } + if options.KernelTx || options.KernelRx { + return SystemTLSValidated{}, E.New("ktls is unsupported in ", engineName) + } + if options.Spoof != "" || options.SpoofMethod != "" { + return SystemTLSValidated{}, E.New("spoof is unsupported in ", engineName) + } + if len(options.CertificatePublicKeySHA256) > 0 && (len(options.Certificate) > 0 || options.CertificatePath != "") { + return SystemTLSValidated{}, E.New("certificate_public_key_sha256 is conflict with certificate or certificate_path") + } + var minVersion uint16 + if options.MinVersion != "" { + parsed, err := ParseTLSVersion(options.MinVersion) + if err != nil { + return SystemTLSValidated{}, E.Cause(err, "parse min_version") + } + minVersion = parsed + } + var maxVersion uint16 + if options.MaxVersion != "" { + parsed, err := ParseTLSVersion(options.MaxVersion) + if err != nil { + return SystemTLSValidated{}, E.Cause(err, "parse max_version") + } + maxVersion = parsed + } + userPEM, exclusive, store, err := resolveSystemAnchors(ctx, options) + if err != nil { + return SystemTLSValidated{}, err + } + return SystemTLSValidated{ + MinVersion: minVersion, + MaxVersion: maxVersion, + UserPEM: userPEM, + Exclusive: exclusive, + Store: store, + }, nil +} + +func resolveSystemAnchors(ctx context.Context, options option.OutboundTLSOptions) ([]byte, bool, adapter.CertificateStore, error) { + if len(options.Certificate) > 0 { + return []byte(strings.Join(options.Certificate, "\n")), true, nil, nil + } + if options.CertificatePath != "" { + content, err := os.ReadFile(options.CertificatePath) + if err != nil { + return nil, false, nil, E.Cause(err, "read certificate") + } + return content, true, nil, nil + } + store := service.FromContext[adapter.CertificateStore](ctx) + if store == nil { + return nil, false, nil, nil + } + return nil, store.ExclusiveAnchors(), store, nil +} + +func newSystemTLSConfig(ctx context.Context, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool, engineName string) (systemTLSConfig, SystemTLSValidated, error) { + validated, err := ValidateSystemTLSOptions(ctx, options, engineName) + if err != nil { + return systemTLSConfig{}, SystemTLSValidated{}, err + } + var serverName string + if options.ServerName != "" { + serverName = options.ServerName + } else if serverAddress != "" { + serverName = serverAddress + } + if serverName == "" && !options.Insecure && !allowEmptyServerName { + return systemTLSConfig{}, SystemTLSValidated{}, errMissingServerName + } + handshakeTimeout := C.TCPTimeout + if options.HandshakeTimeout > 0 { + handshakeTimeout = options.HandshakeTimeout.Build() + } + return systemTLSConfig{ + serverName: serverName, + nextProtos: append([]string(nil), options.ALPN...), + handshakeTimeout: handshakeTimeout, + minVersion: validated.MinVersion, + maxVersion: validated.MaxVersion, + insecure: options.Insecure || len(options.CertificatePublicKeySHA256) > 0, + anchorOnly: validated.Exclusive, + certificatePublicKeySHA256: append([][]byte(nil), options.CertificatePublicKeySHA256...), + timeFunc: ntp.TimeFuncFromContext(ctx), + store: validated.Store, + }, validated, nil +} + +func verifySystemTLSPeer(roots *x509.CertPool, serverName string, timeFunc func() time.Time, peerCertificates []*x509.Certificate) error { + if len(peerCertificates) == 0 { + return E.New("no peer certificates") + } + intermediates := x509.NewCertPool() + for _, cert := range peerCertificates[1:] { + intermediates.AddCert(cert) + } + verifyOptions := x509.VerifyOptions{ + Roots: roots, + Intermediates: intermediates, + DNSName: serverName, + } + if timeFunc != nil { + verifyOptions.CurrentTime = timeFunc() + } + _, err := peerCertificates[0].Verify(verifyOptions) + return err +} diff --git a/common/tls/windows_client.go b/common/tls/windows_client.go new file mode 100644 index 0000000000..9a865c0aa1 --- /dev/null +++ b/common/tls/windows_client.go @@ -0,0 +1,848 @@ +//go:build windows + +package tls + +import ( + "context" + "crypto/tls" + "crypto/x509" + "errors" + "io" + "net" + "os" + "sync" + "sync/atomic" + "time" + + "github.com/sagernet/sing-box/common/schannel" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + N "github.com/sagernet/sing/common/network" +) + +const ( + windowsTLSEngineName = "Windows TLS engine" + handshakeReadChunkSize = 8192 + readScratchSize = 16 * 1024 + readWaitCiphertextChunkSize = 4096 +) + +type windowsClientConfig struct { + systemTLSConfig + userRoots *x509.CertPool +} + +func (c *windowsClientConfig) Clone() Config { + return &windowsClientConfig{ + systemTLSConfig: c.systemTLSConfig.clone(), + userRoots: c.userRoots, + } +} + +func newWindowsClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) { + err := schannel.CheckPlatform() + if err != nil { + return nil, err + } + base, validated, err := newSystemTLSConfig(ctx, serverAddress, options, allowEmptyServerName, windowsTLSEngineName) + if err != nil { + return nil, err + } + var userRoots *x509.CertPool + if len(validated.UserPEM) > 0 { + userRoots = x509.NewCertPool() + if !userRoots.AppendCertsFromPEM(validated.UserPEM) { + return nil, E.New("parse certificate PEM") + } + } + return &windowsClientConfig{ + systemTLSConfig: base, + userRoots: userRoots, + }, nil +} + +func (c *windowsClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (Conn, error) { + deadline, hasDeadline := ctx.Deadline() + if hasDeadline { + deadlineErr := conn.SetDeadline(deadline) + if deadlineErr != nil { + return nil, E.Cause(deadlineErr, "set handshake deadline") + } + defer conn.SetDeadline(time.Time{}) + } + + client, err := schannel.NewClientContext(c.minVersion, c.maxVersion, c.serverName, c.nextProtos) + if err != nil { + return nil, err + } + + handshakeOK := false + defer func() { + if !handshakeOK { + client.Close() + } + }() + + stopCancel := installHandshakeCancel(ctx, conn) + defer stopCancel() + + scratch := make([]byte, handshakeReadChunkSize) + leftover, err := driveHandshake(ctx, conn, client, scratch) + if err != nil { + return nil, err + } + state, rawCerts, err := buildConnectionState(c.serverName, client) + if err != nil { + return nil, err + } + err = c.verifyPeerCertificates(state.PeerCertificates) + if err != nil { + return nil, err + } + if len(c.certificatePublicKeySHA256) > 0 { + err = VerifyPublicKeySHA256(c.certificatePublicKeySHA256, rawCerts) + if err != nil { + return nil, err + } + } + header, trailer, maxMessage, err := client.StreamSizes() + if err != nil { + return nil, err + } + + handshakeOK = true + tlsConn := &windowsTLSConn{ + rawConn: conn, + client: client, + state: state, + header: header, + trailer: trailer, + maxMessage: maxMessage, + cipher: leftover, + } + return tlsConn, nil +} + +func driveHandshake(ctx context.Context, conn net.Conn, client *schannel.ClientContext, scratch []byte) ([]byte, error) { + readMore := func() ([]byte, error) { + more, err := readTLSRaw(conn, scratch, true) + if err != nil { + return nil, handshakeIOError(ctx, err, "read handshake") + } + return more, nil + } + writeOut := func(data []byte) error { + _, err := conn.Write(data) + if err != nil { + return handshakeIOError(ctx, err, "write handshake") + } + return nil + } + leftover, err := driveSteps(nil, client.Step, readMore, writeOut) + if err != nil { + return nil, E.Cause(err, "tls handshake") + } + return leftover, nil +} + +func driveSteps( + initial []byte, + step func([]byte) (schannel.StepResult, error), + readMore func() ([]byte, error), + writeOut func([]byte) error, +) ([]byte, error) { + buffer := initial + for { + result, stepErr := step(buffer) + if stepErr != nil { + return nil, stepErr + } + if len(result.Output) > 0 { + writeErr := writeOut(result.Output) + if writeErr != nil { + return nil, writeErr + } + } + if result.Incomplete { + // readMore reuses scratch storage, so keep the buffered handshake + // bytes in stable memory before the next read overwrites them. + buffer = append([]byte(nil), buffer...) + more, readErr := readMore() + if readErr != nil { + return nil, readErr + } + buffer = append(buffer, more...) + continue + } + if result.Consumed > len(buffer) { + return nil, E.New("schannel: Consumed > input length") + } + buffer = buffer[result.Consumed:] + if result.Done { + return buffer, nil + } + if len(buffer) == 0 { + more, readErr := readMore() + if readErr != nil { + return nil, readErr + } + buffer = append(buffer, more...) + } + } +} + +// installHandshakeCancel unblocks an in-flight read/write by forcing an +// immediate deadline on conn when ctx is cancelled. The returned cleanup +// waits for a racing cancel to finish and clears the forced deadline. +func installHandshakeCancel(ctx context.Context, conn net.Conn) func() { + var fired atomic.Bool + done := make(chan struct{}) + stop := context.AfterFunc(ctx, func() { + defer close(done) + fired.Store(true) + _ = conn.SetDeadline(time.Now()) + }) + return func() { + if stop() { + return + } + <-done + if fired.Load() { + _ = conn.SetDeadline(time.Time{}) + } + } +} + +func handshakeIOError(ctx context.Context, err error, message string) error { + ctxErr := ctx.Err() + if ctxErr != nil && isTimeoutError(err) { + return ctxErr + } + return E.Cause(err, message) +} + +func readTLSRaw(conn net.Conn, scratch []byte, requireMore bool) ([]byte, error) { + n, err := conn.Read(scratch) + if n > 0 { + return scratch[:n], nil + } + if err != nil { + if requireMore && errors.Is(err, io.EOF) { + return nil, io.ErrUnexpectedEOF + } + return nil, err + } + return nil, io.ErrUnexpectedEOF +} + +func isTimeoutError(err error) bool { + if errors.Is(err, os.ErrDeadlineExceeded) { + return true + } + var netErr net.Error + return errors.As(err, &netErr) && netErr.Timeout() +} + +func buildConnectionState(serverName string, client *schannel.ClientContext) (tls.ConnectionState, [][]byte, error) { + version, cipherSuite, err := client.ConnectionInfo() + if err != nil { + return tls.ConnectionState{}, nil, err + } + alpn, err := client.ApplicationProtocol() + if err != nil { + return tls.ConnectionState{}, nil, err + } + rawCerts, err := client.RemoteCertificateChain() + if err != nil { + return tls.ConnectionState{}, nil, err + } + peerCertificates := make([]*x509.Certificate, 0, len(rawCerts)) + for index, der := range rawCerts { + cert, parseErr := x509.ParseCertificate(der) + if parseErr != nil { + return tls.ConnectionState{}, nil, E.Cause(parseErr, "parse peer certificate ", index) + } + peerCertificates = append(peerCertificates, cert) + } + return tls.ConnectionState{ + Version: version, + HandshakeComplete: true, + CipherSuite: cipherSuite, + NegotiatedProtocol: alpn, + ServerName: serverName, + PeerCertificates: peerCertificates, + }, rawCerts, nil +} + +func (c *windowsClientConfig) verifyPeerCertificates(peerCertificates []*x509.Certificate) error { + if c.insecure { + return nil + } + var roots *x509.CertPool + switch { + case c.userRoots != nil: + roots = c.userRoots + case c.store != nil: + roots = c.store.Pool() + } + return verifySystemTLSPeer(roots, c.serverName, c.timeFunc, peerCertificates) +} + +type windowsTLSConn struct { + rawConn net.Conn + client *schannel.ClientContext + state tls.ConnectionState + header uint32 + trailer uint32 + maxMessage uint32 + + readAccess sync.Mutex + writeAccess sync.Mutex + contextAccess sync.RWMutex + + writeState sync.Mutex + writeStateOnce sync.Once + writeReady *sync.Cond + postHandshake bool + writeActive bool + + cipher []byte + plain []byte + readScratch []byte + writeScratch []byte + readEOF bool + + deadlineAccess sync.Mutex + readDeadline time.Time + writeDeadline time.Time + closed atomic.Bool +} + +var ( + _ N.ExtendedConn = (*windowsTLSConn)(nil) + _ N.ReadWaitCreator = (*windowsTLSConn)(nil) +) + +type ( + windowsTLSAppendCipherFunc func(requireMore bool) error + windowsTLSReadRawFunc func(requireMore bool) ([]byte, error) +) + +func (c *windowsTLSConn) Read(p []byte) (int, error) { + c.readAccess.Lock() + defer c.readAccess.Unlock() + if len(p) == 0 { + return 0, nil + } + if c.isClosed() { + return 0, net.ErrClosed + } + return c.readIntoLocked(p, c.appendRaw, c.readRaw) +} + +func (c *windowsTLSConn) ReadBuffer(buffer *buf.Buffer) error { + c.readAccess.Lock() + defer c.readAccess.Unlock() + if buffer.IsFull() { + return io.ErrShortBuffer + } + if c.isClosed() { + return net.ErrClosed + } + startLen := buffer.Len() + n, err := c.readIntoLocked(buffer.FreeBytes(), c.appendRaw, c.readRaw) + buffer.Truncate(startLen + n) + return err +} + +func (c *windowsTLSConn) readIntoLocked(p []byte, appendCipher windowsTLSAppendCipherFunc, readRaw windowsTLSReadRawFunc) (int, error) { + plaintext, err := c.readPlaintextLocked(appendCipher, readRaw) + if err != nil { + return 0, err + } + n := copy(p, plaintext) + if n < len(plaintext) { + c.plain = append([]byte(nil), plaintext[n:]...) + } + return n, nil +} + +func (c *windowsTLSConn) readPlaintextLocked(appendCipher windowsTLSAppendCipherFunc, readRaw windowsTLSReadRawFunc) ([]byte, error) { + if len(c.plain) > 0 { + plaintext := c.plain + c.plain = nil + return plaintext, nil + } + if c.readEOF { + return nil, io.EOF + } + + cleanup, err := c.applyReadDeadline() + if err != nil { + return nil, err + } + defer cleanup() + + for { + if len(c.cipher) > 0 { + result, decryptErr := c.decrypt(c.cipher) + if decryptErr != nil { + return nil, decryptErr + } + if result.Expired { + c.readEOF = true + return nil, io.EOF + } + if !result.Incomplete { + plaintext := result.Plaintext + if result.Renegotiate && len(plaintext) > 0 { + plaintext = append([]byte(nil), plaintext...) + } + nextCipher := c.cipher[result.ConsumedTotal:] + if len(result.RenegotiateToken) > 0 { + nextCipher = result.RenegotiateToken + } + c.cipher = nextCipher + if len(c.cipher) == 0 { + c.cipher = nil + } + if result.Renegotiate { + postErr := c.drivePostHandshake(readRaw) + if postErr != nil { + return nil, postErr + } + } + if len(plaintext) > 0 { + return plaintext, nil + } + continue + } + } + err = appendCipher(len(c.cipher) > 0) + if err != nil { + return nil, err + } + } +} + +func (c *windowsTLSConn) drivePostHandshake(readRaw windowsTLSReadRawFunc) error { + initial := c.cipher + c.cipher = nil + err := c.beginPostHandshakeWrite() + if err != nil { + return err + } + defer c.finishPostHandshakeWrite() + c.contextAccess.Lock() + if c.client == nil { + c.contextAccess.Unlock() + return net.ErrClosed + } + writeFailed := false + readMore := func() ([]byte, error) { + more, err := readRaw(true) + if err != nil { + return nil, E.Cause(err, "tls post-handshake read") + } + return more, nil + } + writeOut := func(data []byte) error { + err := c.writePostHandshakeReplyLocked(data) + if err != nil { + writeFailed = true + return E.Cause(err, "tls post-handshake write") + } + return nil + } + leftover, err := driveSteps(initial, c.client.PostHandshake, readMore, writeOut) + c.contextAccess.Unlock() + if err != nil { + if writeFailed { + _ = c.Close() + } + return E.Cause(err, "tls post-handshake") + } + if len(leftover) > 0 { + c.cipher = leftover + } + return nil +} + +func (c *windowsTLSConn) writePostHandshakeReplyLocked(data []byte) error { + c.deadlineAccess.Lock() + deadline := c.readDeadline + c.deadlineAccess.Unlock() + cleanup, err := c.applyDeadline(deadline, c.rawConn.SetWriteDeadline) + if err != nil { + return err + } + defer cleanup() + _, err = c.rawConn.Write(data) + return err +} + +func (c *windowsTLSConn) decrypt(input []byte) (schannel.DecryptResult, error) { + c.contextAccess.RLock() + defer c.contextAccess.RUnlock() + if c.client == nil { + return schannel.DecryptResult{}, net.ErrClosed + } + return c.client.Decrypt(input) +} + +func (c *windowsTLSConn) encrypt(plaintext []byte) ([]byte, error) { + c.contextAccess.RLock() + defer c.contextAccess.RUnlock() + if c.client == nil { + return nil, net.ErrClosed + } + if c.writeScratch == nil { + c.writeScratch = make([]byte, int(c.header)+int(c.maxMessage)+int(c.trailer)) + } + return c.client.Encrypt(c.header, c.trailer, plaintext, c.writeScratch) +} + +func (c *windowsTLSConn) readRaw(requireMore bool) ([]byte, error) { + if c.readScratch == nil { + c.readScratch = make([]byte, readScratchSize) + } + return readTLSRaw(c.rawConn, c.readScratch, requireMore) +} + +func (c *windowsTLSConn) appendRaw(requireMore bool) error { + more, err := c.readRaw(requireMore) + if err != nil { + return err + } + c.cipher = append(c.cipher, more...) + return nil +} + +func (c *windowsTLSConn) Write(p []byte) (int, error) { + err := c.beginWrite() + if err != nil { + return 0, err + } + defer c.finishWrite() + if len(p) == 0 { + return 0, nil + } + if c.isClosed() { + return 0, net.ErrClosed + } + + cleanup, err := c.applyWriteDeadline() + if err != nil { + return 0, err + } + defer cleanup() + + total := 0 + chunkSize := int(c.maxMessage) + for len(p) > 0 { + chunk := p + if len(chunk) > chunkSize { + chunk = chunk[:chunkSize] + } + encrypted, encryptErr := c.encrypt(chunk) + if encryptErr != nil { + if errors.Is(encryptErr, net.ErrClosed) { + return total, net.ErrClosed + } + return total, E.Cause(encryptErr, "tls encrypt") + } + _, writeErr := c.rawConn.Write(encrypted) + if writeErr != nil { + _ = c.Close() + return total, E.Cause(writeErr, "tls write") + } + total += len(chunk) + p = p[len(chunk):] + } + return total, nil +} + +func (c *windowsTLSConn) WriteBuffer(buffer *buf.Buffer) error { + defer buffer.Release() + _, err := c.Write(buffer.Bytes()) + return err +} + +func (c *windowsTLSConn) CreateReadWaiter() (N.ReadWaiter, bool) { + rawWaiter, ok := bufio.CreateReadWaiter(c.rawConn) + if !ok { + return nil, false + } + return &windowsTLSReadWaiter{ + conn: c, + rawWaiter: rawWaiter, + }, true +} + +func (c *windowsTLSConn) Close() error { + if !c.closed.CompareAndSwap(false, true) { + return nil + } + ready := c.writeCondition() + c.writeState.Lock() + ready.Broadcast() + c.writeState.Unlock() + closeErr := c.rawConn.Close() + c.contextAccess.Lock() + if c.client != nil { + c.client.Close() + c.client = nil + } + c.contextAccess.Unlock() + return closeErr +} + +func (c *windowsTLSConn) LocalAddr() net.Addr { + return c.rawConn.LocalAddr() +} + +func (c *windowsTLSConn) RemoteAddr() net.Addr { + return c.rawConn.RemoteAddr() +} + +func (c *windowsTLSConn) SetDeadline(t time.Time) error { + c.deadlineAccess.Lock() + defer c.deadlineAccess.Unlock() + err := c.rawConn.SetDeadline(t) + if err != nil { + return err + } + c.readDeadline = t + c.writeDeadline = t + return nil +} + +func (c *windowsTLSConn) SetReadDeadline(t time.Time) error { + c.deadlineAccess.Lock() + defer c.deadlineAccess.Unlock() + err := c.rawConn.SetReadDeadline(t) + if err != nil { + return err + } + c.readDeadline = t + return nil +} + +func (c *windowsTLSConn) SetWriteDeadline(t time.Time) error { + c.deadlineAccess.Lock() + defer c.deadlineAccess.Unlock() + err := c.rawConn.SetWriteDeadline(t) + if err != nil { + return err + } + c.writeDeadline = t + return nil +} + +func (c *windowsTLSConn) NetConn() net.Conn { + return c.rawConn +} + +func (c *windowsTLSConn) HandshakeContext(ctx context.Context) error { + return nil +} + +func (c *windowsTLSConn) ConnectionState() ConnectionState { + return c.state +} + +func (c *windowsTLSConn) applyReadDeadline() (func(), error) { + c.deadlineAccess.Lock() + deadline := c.readDeadline + c.deadlineAccess.Unlock() + return c.applyDeadline(deadline, c.rawConn.SetReadDeadline) +} + +func (c *windowsTLSConn) applyWriteDeadline() (func(), error) { + c.deadlineAccess.Lock() + deadline := c.writeDeadline + c.deadlineAccess.Unlock() + return c.applyDeadline(deadline, c.rawConn.SetWriteDeadline) +} + +func (c *windowsTLSConn) applyDeadline(deadline time.Time, set func(time.Time) error) (func(), error) { + if deadline.IsZero() { + return func() {}, nil + } + if !deadline.After(time.Now()) { + return nil, os.ErrDeadlineExceeded + } + err := set(deadline) + if err != nil { + return nil, err + } + return func() { _ = set(time.Time{}) }, nil +} + +func (c *windowsTLSConn) beginWrite() error { + ready := c.writeCondition() + c.writeState.Lock() + for c.postHandshake || c.writeActive { + if c.closed.Load() { + c.writeState.Unlock() + return net.ErrClosed + } + ready.Wait() + } + c.writeActive = true + c.writeState.Unlock() + + c.writeAccess.Lock() + if c.closed.Load() { + c.writeAccess.Unlock() + c.writeState.Lock() + c.writeActive = false + ready.Broadcast() + c.writeState.Unlock() + return net.ErrClosed + } + return nil +} + +func (c *windowsTLSConn) finishWrite() { + c.writeAccess.Unlock() + ready := c.writeCondition() + c.writeState.Lock() + c.writeActive = false + ready.Broadcast() + c.writeState.Unlock() +} + +func (c *windowsTLSConn) beginPostHandshakeWrite() error { + ready := c.writeCondition() + c.writeState.Lock() + c.postHandshake = true + for c.writeActive { + if c.closed.Load() { + c.postHandshake = false + ready.Broadcast() + c.writeState.Unlock() + return net.ErrClosed + } + ready.Wait() + } + c.writeActive = true + c.writeState.Unlock() + + c.writeAccess.Lock() + if c.closed.Load() { + c.writeAccess.Unlock() + c.writeState.Lock() + c.writeActive = false + c.postHandshake = false + ready.Broadcast() + c.writeState.Unlock() + return net.ErrClosed + } + return nil +} + +func (c *windowsTLSConn) finishPostHandshakeWrite() { + c.writeAccess.Unlock() + ready := c.writeCondition() + c.writeState.Lock() + c.writeActive = false + c.postHandshake = false + ready.Broadcast() + c.writeState.Unlock() +} + +func (c *windowsTLSConn) writeCondition() *sync.Cond { + c.writeStateOnce.Do(func() { + c.writeReady = sync.NewCond(&c.writeState) + }) + return c.writeReady +} + +func (c *windowsTLSConn) isClosed() bool { + return c.closed.Load() +} + +type windowsTLSReadWaiter struct { + conn *windowsTLSConn + rawWaiter N.ReadWaiter + options N.ReadWaitOptions +} + +var _ N.ReadWaiter = (*windowsTLSReadWaiter)(nil) + +func (w *windowsTLSReadWaiter) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { + w.options = options + w.rawWaiter.InitializeReadWaiter(N.ReadWaitOptions{ + MTU: readWaitCiphertextChunkSize, + }) + return false +} + +func (w *windowsTLSReadWaiter) WaitReadBuffer() (*buf.Buffer, error) { + c := w.conn + c.readAccess.Lock() + defer c.readAccess.Unlock() + if c.isClosed() { + return nil, net.ErrClosed + } + plaintext, err := c.readPlaintextLocked(w.appendRaw, w.readRaw) + if err != nil { + return nil, err + } + buffer := w.options.NewBuffer() + n, writeErr := buffer.Write(plaintext) + if writeErr != nil { + buffer.Release() + return nil, writeErr + } + if n == 0 { + buffer.Release() + return nil, io.ErrShortBuffer + } + if n < len(plaintext) { + c.plain = append([]byte(nil), plaintext[n:]...) + } + w.options.PostReturn(buffer) + return buffer, nil +} + +func (w *windowsTLSReadWaiter) appendRaw(requireMore bool) error { + rawBuffer, err := w.readRawBuffer(requireMore) + if err != nil { + return err + } + w.conn.cipher = append(w.conn.cipher, rawBuffer.Bytes()...) + rawBuffer.Release() + return nil +} + +func (w *windowsTLSReadWaiter) readRaw(requireMore bool) ([]byte, error) { + rawBuffer, err := w.readRawBuffer(requireMore) + if err != nil { + return nil, err + } + data := append([]byte(nil), rawBuffer.Bytes()...) + rawBuffer.Release() + return data, nil +} + +func (w *windowsTLSReadWaiter) readRawBuffer(requireMore bool) (*buf.Buffer, error) { + rawBuffer, err := w.rawWaiter.WaitReadBuffer() + if err != nil { + if requireMore && errors.Is(err, io.EOF) { + return nil, io.ErrUnexpectedEOF + } + return nil, err + } + if rawBuffer == nil || rawBuffer.Len() == 0 { + if rawBuffer != nil { + rawBuffer.Release() + } + return nil, io.ErrUnexpectedEOF + } + return rawBuffer, nil +} diff --git a/common/tls/windows_client_stub.go b/common/tls/windows_client_stub.go new file mode 100644 index 0000000000..7ad506a725 --- /dev/null +++ b/common/tls/windows_client_stub.go @@ -0,0 +1,15 @@ +//go:build !windows + +package tls + +import ( + "context" + + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" +) + +func newWindowsClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) { + return nil, E.New("Windows TLS engine is not available on non-Windows platforms") +} diff --git a/common/tls/windows_client_test.go b/common/tls/windows_client_test.go new file mode 100644 index 0000000000..13c46522ab --- /dev/null +++ b/common/tls/windows_client_test.go @@ -0,0 +1,2505 @@ +//go:build windows + +package tls + +import ( + "bytes" + "context" + "crypto/sha256" + stdtls "crypto/tls" + "crypto/x509" + "errors" + "io" + "net" + "os" + "strings" + "sync" + "testing" + "time" + + "github.com/sagernet/sing-box/common/schannel" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + "github.com/sagernet/sing/common/json/badoption" + "github.com/sagernet/sing/common/logger" + N "github.com/sagernet/sing/common/network" +) + +const windowsTLSTestTimeout = 5 * time.Second + +var ( + _ N.ExtendedConn = (*windowsTLSConn)(nil) + _ N.ReadWaitCreator = (*windowsTLSConn)(nil) +) + +func newTestWindowsTLSConn(rawConn net.Conn) *windowsTLSConn { + return &windowsTLSConn{rawConn: rawConn} +} + +// writePostHandshakeReply wraps writePostHandshakeReplyLocked with the +// writeAccess locking and auto-close behavior that drivePostHandshake +// composes from beginPostHandshakeWrite/finishPostHandshakeWrite plus the +// writeFailed → Close branch. +// Kept here as a test seam. +func (c *windowsTLSConn) writePostHandshakeReply(data []byte) error { + c.writeAccess.Lock() + defer c.writeAccess.Unlock() + err := c.writePostHandshakeReplyLocked(data) + if err != nil { + _ = c.Close() + } + return err +} + +type windowsTLSServerResult struct { + state stdtls.ConnectionState + err error +} + +type windowsTestDeadlineConn struct { + access sync.Mutex + readCalled chan struct{} + writeCalled chan struct{} + readCalledOnce sync.Once + writeCalledOnce sync.Once + readDeadline time.Time + writeDeadline time.Time + readDeadlines []time.Time + writeDeadlines []time.Time +} + +type windowsTestWriteGateConn struct { + writeCalled chan struct{} + releaseWrite chan struct{} +} + +type windowsTestIOConn struct { + access sync.Mutex + readErr error + writeErr error + writeN int + writeCalls int + closed bool +} + +func (c *windowsTestDeadlineConn) Read(_ []byte) (int, error) { + if c.readCalled != nil { + c.readCalledOnce.Do(func() { + close(c.readCalled) + }) + } + for { + c.access.Lock() + deadline := c.readDeadline + c.access.Unlock() + if deadline.IsZero() { + time.Sleep(5 * time.Millisecond) + continue + } + if !deadline.After(time.Now()) { + return 0, os.ErrDeadlineExceeded + } + time.Sleep(time.Until(deadline)) + return 0, os.ErrDeadlineExceeded + } +} + +func (c *windowsTestDeadlineConn) Write(_ []byte) (int, error) { + if c.writeCalled != nil { + c.writeCalledOnce.Do(func() { + close(c.writeCalled) + }) + } + for { + c.access.Lock() + deadline := c.writeDeadline + c.access.Unlock() + if deadline.IsZero() { + time.Sleep(5 * time.Millisecond) + continue + } + if !deadline.After(time.Now()) { + return 0, os.ErrDeadlineExceeded + } + time.Sleep(time.Until(deadline)) + return 0, os.ErrDeadlineExceeded + } +} + +func (c *windowsTestDeadlineConn) Close() error { + return nil +} + +func (c *windowsTestDeadlineConn) LocalAddr() net.Addr { + return windowsTestAddr("local") +} + +func (c *windowsTestDeadlineConn) RemoteAddr() net.Addr { + return windowsTestAddr("remote") +} + +func (c *windowsTestDeadlineConn) SetDeadline(t time.Time) error { + c.access.Lock() + c.readDeadline = t + c.writeDeadline = t + c.readDeadlines = append(c.readDeadlines, t) + c.writeDeadlines = append(c.writeDeadlines, t) + c.access.Unlock() + return nil +} + +func (c *windowsTestDeadlineConn) SetReadDeadline(t time.Time) error { + c.access.Lock() + c.readDeadline = t + c.readDeadlines = append(c.readDeadlines, t) + c.access.Unlock() + return nil +} + +func (c *windowsTestDeadlineConn) SetWriteDeadline(t time.Time) error { + c.access.Lock() + c.writeDeadline = t + c.writeDeadlines = append(c.writeDeadlines, t) + c.access.Unlock() + return nil +} + +func (c *windowsTestDeadlineConn) recordedWriteDeadlines() []time.Time { + c.access.Lock() + defer c.access.Unlock() + return append([]time.Time(nil), c.writeDeadlines...) +} + +func (c *windowsTestDeadlineConn) recordedReadDeadlines() []time.Time { + c.access.Lock() + defer c.access.Unlock() + return append([]time.Time(nil), c.readDeadlines...) +} + +func (c *windowsTestWriteGateConn) Read(_ []byte) (int, error) { + return 0, io.EOF +} + +func (c *windowsTestWriteGateConn) Write(p []byte) (int, error) { + close(c.writeCalled) + <-c.releaseWrite + return len(p), nil +} + +func (c *windowsTestWriteGateConn) Close() error { + return nil +} + +func (c *windowsTestWriteGateConn) LocalAddr() net.Addr { + return windowsTestAddr("local") +} + +func (c *windowsTestWriteGateConn) RemoteAddr() net.Addr { + return windowsTestAddr("remote") +} + +func (c *windowsTestWriteGateConn) SetDeadline(time.Time) error { + return nil +} + +func (c *windowsTestWriteGateConn) SetReadDeadline(time.Time) error { + return nil +} + +func (c *windowsTestWriteGateConn) SetWriteDeadline(time.Time) error { + return nil +} + +func (c *windowsTestIOConn) Read(_ []byte) (int, error) { + return 0, c.readErr +} + +func (c *windowsTestIOConn) Write(p []byte) (int, error) { + c.access.Lock() + defer c.access.Unlock() + c.writeCalls++ + if c.writeErr == nil { + return len(p), nil + } + n := c.writeN + if n <= 0 || n > len(p) { + n = 0 + } + return n, c.writeErr +} + +func (c *windowsTestIOConn) Close() error { + c.access.Lock() + c.closed = true + c.access.Unlock() + return nil +} + +func (c *windowsTestIOConn) LocalAddr() net.Addr { + return windowsTestAddr("local") +} + +func (c *windowsTestIOConn) RemoteAddr() net.Addr { + return windowsTestAddr("remote") +} + +func (c *windowsTestIOConn) SetDeadline(time.Time) error { + return nil +} + +func (c *windowsTestIOConn) SetReadDeadline(time.Time) error { + return nil +} + +func (c *windowsTestIOConn) SetWriteDeadline(time.Time) error { + return nil +} + +func (c *windowsTestIOConn) isClosed() bool { + c.access.Lock() + defer c.access.Unlock() + return c.closed +} + +func (c *windowsTestIOConn) totalWriteCalls() int { + c.access.Lock() + defer c.access.Unlock() + return c.writeCalls +} + +type windowsTestAddr string + +func (a windowsTestAddr) Network() string { + return "test" +} + +func (a windowsTestAddr) String() string { + return string(a) +} + +type windowsOpaqueConn struct { + net.Conn +} + +func TestWindowsClientHandshakeTLS12(t *testing.T) { + serverCertificate, serverCertificatePEM := newWindowsTestCertificate(t, "localhost") + serverResult, serverAddress := startWindowsTLSTestServer(t, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: stdtls.VersionTLS12, + MaxVersion: stdtls.VersionTLS12, + NextProtos: []string{"h2"}, + }) + + clientConn, err := newWindowsTestClientConn(t, serverAddress, option.OutboundTLSOptions{ + Enabled: true, + Engine: C.TLSEngineWindows, + ServerName: "localhost", + MinVersion: "1.2", + MaxVersion: "1.2", + ALPN: badoption.Listable[string]{"h2"}, + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }) + if err != nil { + t.Fatal(err) + } + defer clientConn.Close() + + clientState := clientConn.ConnectionState() + if clientState.Version != stdtls.VersionTLS12 { + t.Fatalf("unexpected negotiated version: %x", clientState.Version) + } + if clientState.NegotiatedProtocol != "h2" { + t.Fatalf("unexpected negotiated protocol: %q", clientState.NegotiatedProtocol) + } + if !clientState.HandshakeComplete { + t.Fatal("HandshakeComplete is false") + } + if len(clientState.PeerCertificates) == 0 { + t.Fatal("no peer certificates") + } + + result := <-serverResult + if result.err != nil { + t.Fatal(result.err) + } + if result.state.Version != stdtls.VersionTLS12 { + t.Fatalf("server negotiated unexpected version: %x", result.state.Version) + } + if result.state.NegotiatedProtocol != "h2" { + t.Fatalf("server negotiated unexpected protocol: %q", result.state.NegotiatedProtocol) + } +} + +func TestWindowsClientHandshakeWrappedConn(t *testing.T) { + serverCertificate, serverCertificatePEM := newWindowsTestCertificate(t, "localhost") + serverResult, serverAddress := startWindowsTLSTestServer(t, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: stdtls.VersionTLS12, + MaxVersion: stdtls.VersionTLS12, + }) + + ctx, cancel := context.WithTimeout(context.Background(), windowsTLSTestTimeout) + t.Cleanup(cancel) + + clientConfig, err := NewClientWithOptions(ClientOptions{ + Context: ctx, + Logger: logger.NOP(), + Options: option.OutboundTLSOptions{ + Enabled: true, + Engine: C.TLSEngineWindows, + ServerName: "localhost", + MinVersion: "1.2", + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }, + }) + if err != nil { + t.Fatal(err) + } + rawConn, err := net.DialTimeout(N.NetworkTCP, serverAddress, windowsTLSTestTimeout) + if err != nil { + t.Fatal(err) + } + tlsConn, err := ClientHandshake(ctx, windowsOpaqueConn{Conn: rawConn}, clientConfig) + if err != nil { + rawConn.Close() + t.Fatal(err) + } + _ = tlsConn.Close() + + result := <-serverResult + if result.err != nil { + t.Fatal(result.err) + } + if result.state.Version != stdtls.VersionTLS12 { + t.Fatalf("server negotiated unexpected version: %x", result.state.Version) + } +} + +func TestWindowsClientHandshakeTLS13(t *testing.T) { + serverCertificate, serverCertificatePEM := newWindowsTestCertificate(t, "localhost") + serverResult, serverAddress := startWindowsTLSTestServer(t, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: stdtls.VersionTLS13, + MaxVersion: stdtls.VersionTLS13, + NextProtos: []string{"h2"}, + }) + + clientConn, err := newWindowsTestClientConn(t, serverAddress, option.OutboundTLSOptions{ + Enabled: true, + Engine: C.TLSEngineWindows, + ServerName: "localhost", + MinVersion: "1.3", + ALPN: badoption.Listable[string]{"h2"}, + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }) + if err != nil { + t.Fatal(err) + } + defer clientConn.Close() + + clientState := clientConn.ConnectionState() + if clientState.Version != stdtls.VersionTLS13 { + t.Fatalf("expected TLS 1.3, got %x", clientState.Version) + } + if clientState.NegotiatedProtocol != "h2" { + t.Fatalf("expected negotiated protocol h2, got %q", clientState.NegotiatedProtocol) + } + + result := <-serverResult + if result.err != nil { + t.Fatal(result.err) + } + if result.state.Version != stdtls.VersionTLS13 { + t.Fatalf("server negotiated unexpected version: %x", result.state.Version) + } +} + +func TestWindowsClientHandshakeALPNNoOverlap(t *testing.T) { + serverCertificate, serverCertificatePEM := newWindowsTestCertificate(t, "localhost") + _, serverAddress := startWindowsTLSTestServer(t, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: stdtls.VersionTLS12, + MaxVersion: stdtls.VersionTLS12, + NextProtos: []string{"http/1.1"}, + }) + + clientConn, err := newWindowsTestClientConn(t, serverAddress, option.OutboundTLSOptions{ + Enabled: true, + Engine: C.TLSEngineWindows, + ServerName: "localhost", + MinVersion: "1.2", + MaxVersion: "1.2", + ALPN: badoption.Listable[string]{"h2"}, + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }) + // Go's TLS server returns a TLS alert when the client advertises ALPN but + // the server has no overlap. The handshake fails. + if err == nil { + _ = clientConn.Close() + t.Fatal("expected handshake to fail with no ALPN overlap") + } +} + +func TestWindowsClientHandshakeMultipleALPN(t *testing.T) { + serverCertificate, serverCertificatePEM := newWindowsTestCertificate(t, "localhost") + _, serverAddress := startWindowsTLSTestServer(t, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: stdtls.VersionTLS12, + NextProtos: []string{"h2", "http/1.1"}, + }) + + clientConn, err := newWindowsTestClientConn(t, serverAddress, option.OutboundTLSOptions{ + Enabled: true, + Engine: C.TLSEngineWindows, + ServerName: "localhost", + MinVersion: "1.2", + ALPN: badoption.Listable[string]{"spdy/3", "h2"}, + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }) + if err != nil { + t.Fatal(err) + } + defer clientConn.Close() + + // Schannel follows the standard selection: first protocol offered by + // the client that the server also supports. Here: spdy/3 is not in the + // server list but h2 is, so h2 wins. + if got := clientConn.ConnectionState().NegotiatedProtocol; got != "h2" { + t.Fatalf("expected h2, got %q", got) + } +} + +func TestWindowsClientHandshakeRejectsVersionMismatch(t *testing.T) { + serverCertificate, serverCertificatePEM := newWindowsTestCertificate(t, "localhost") + serverResult, serverAddress := startWindowsTLSTestServer(t, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: stdtls.VersionTLS13, + MaxVersion: stdtls.VersionTLS13, + }) + + clientConn, err := newWindowsTestClientConn(t, serverAddress, option.OutboundTLSOptions{ + Enabled: true, + Engine: C.TLSEngineWindows, + ServerName: "localhost", + MaxVersion: "1.2", + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }) + if err == nil { + clientConn.Close() + t.Fatal("expected version mismatch handshake to fail") + } + + result := <-serverResult + if result.err == nil { + t.Fatal("expected server handshake to fail on version mismatch") + } +} + +func TestWindowsClientHandshakeRejectsServerNameMismatch(t *testing.T) { + serverCertificate, serverCertificatePEM := newWindowsTestCertificate(t, "localhost") + _, serverAddress := startWindowsTLSTestServer(t, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + }) + + clientConn, err := newWindowsTestClientConn(t, serverAddress, option.OutboundTLSOptions{ + Enabled: true, + Engine: C.TLSEngineWindows, + ServerName: "example.com", + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }) + if err == nil { + clientConn.Close() + t.Fatal("expected server name mismatch handshake to fail") + } +} + +func TestWindowsClientHandshakeRejectsUntrustedCA(t *testing.T) { + serverCertificate, _ := newWindowsTestCertificate(t, "localhost") + _, serverAddress := startWindowsTLSTestServer(t, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + }) + + clientConn, err := newWindowsTestClientConn(t, serverAddress, option.OutboundTLSOptions{ + Enabled: true, + Engine: C.TLSEngineWindows, + ServerName: "localhost", + }) + if err == nil { + clientConn.Close() + t.Fatal("expected untrusted CA handshake to fail") + } +} + +func TestWindowsClientHandshakeInsecureSkipsValidation(t *testing.T) { + serverCertificate, _ := newWindowsTestCertificate(t, "localhost") + _, serverAddress := startWindowsTLSTestServer(t, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + }) + + // Server name mismatch but insecure=true → handshake succeeds. + clientConn, err := newWindowsTestClientConn(t, serverAddress, option.OutboundTLSOptions{ + Enabled: true, + Engine: C.TLSEngineWindows, + ServerName: "example.com", + Insecure: true, + }) + if err != nil { + t.Fatal(err) + } + defer clientConn.Close() + + if !clientConn.ConnectionState().HandshakeComplete { + t.Fatal("expected handshake to complete with insecure=true") + } +} + +func TestWindowsClientHandshakeHonorsPublicKeyPinSuccess(t *testing.T) { + serverCertificate, _ := newWindowsTestCertificate(t, "localhost") + pin := publicKeyPin(t, serverCertificate.Leaf) + _, serverAddress := startWindowsTLSTestServer(t, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + }) + + clientConn, err := newWindowsTestClientConn(t, serverAddress, option.OutboundTLSOptions{ + Enabled: true, + Engine: C.TLSEngineWindows, + ServerName: "localhost", + CertificatePublicKeySHA256: [][]byte{pin}, + }) + if err != nil { + t.Fatal(err) + } + defer clientConn.Close() +} + +func TestWindowsClientHandshakeHonorsPublicKeyPinFailure(t *testing.T) { + serverCertificate, _ := newWindowsTestCertificate(t, "localhost") + wrongPin := sha256.Sum256([]byte("not the public key")) + _, serverAddress := startWindowsTLSTestServer(t, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + }) + + clientConn, err := newWindowsTestClientConn(t, serverAddress, option.OutboundTLSOptions{ + Enabled: true, + Engine: C.TLSEngineWindows, + ServerName: "localhost", + CertificatePublicKeySHA256: [][]byte{wrongPin[:]}, + }) + if err == nil { + clientConn.Close() + t.Fatal("expected public-key pin mismatch to fail") + } +} + +func TestWindowsClientHandshakeContextCancellation(t *testing.T) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { listener.Close() }) + + clientHelloRead := make(chan struct{}, 1) + serverDone := make(chan struct{}) + defer close(serverDone) + go func() { + c, acceptErr := listener.Accept() + if acceptErr != nil { + return + } + defer c.Close() + buffer := make([]byte, 8192) + n, readErr := c.Read(buffer) + if n > 0 { + clientHelloRead <- struct{}{} + } + if readErr != nil { + return + } + <-serverDone + }() + + _, serverCertificatePEM := newWindowsTestCertificate(t, "localhost") + + ctx, cancel := context.WithCancel(context.Background()) + clientConfig, err := NewClientWithOptions(ClientOptions{ + Context: ctx, + Logger: logger.NOP(), + Options: option.OutboundTLSOptions{ + Enabled: true, + Engine: C.TLSEngineWindows, + ServerName: "localhost", + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }, + }) + if err != nil { + t.Fatal(err) + } + + conn, err := net.DialTimeout("tcp", listener.Addr().String(), windowsTLSTestTimeout) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + + handshakeDone := make(chan error, 1) + go func() { + _, err := ClientHandshake(ctx, conn, clientConfig) + handshakeDone <- err + }() + + select { + case <-clientHelloRead: + case <-time.After(2 * time.Second): + t.Fatal("server did not receive the client hello") + } + + cancel() + + select { + case err := <-handshakeDone: + if !errors.Is(err, context.Canceled) { + t.Fatalf("expected context.Canceled, got %v", err) + } + case <-time.After(2 * time.Second): + t.Fatal("handshake did not return after cancellation") + } +} + +func TestWindowsClientHandshakeTimeout(t *testing.T) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { listener.Close() }) + + // Accept but never respond. + go func() { + c, acceptErr := listener.Accept() + if acceptErr != nil { + return + } + defer c.Close() + time.Sleep(3 * time.Second) + }() + + _, serverCertificatePEM := newWindowsTestCertificate(t, "localhost") + + ctx, cancel := context.WithTimeout(context.Background(), windowsTLSTestTimeout) + defer cancel() + + clientConfig, err := NewClientWithOptions(ClientOptions{ + Context: ctx, + Logger: logger.NOP(), + Options: option.OutboundTLSOptions{ + Enabled: true, + Engine: C.TLSEngineWindows, + ServerName: "localhost", + HandshakeTimeout: badoption.Duration(300 * time.Millisecond), + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }, + }) + if err != nil { + t.Fatal(err) + } + + conn, err := net.DialTimeout("tcp", listener.Addr().String(), windowsTLSTestTimeout) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + + start := time.Now() + _, err = ClientHandshake(ctx, conn, clientConfig) + elapsed := time.Since(start) + if err == nil { + t.Fatal("expected handshake to time out") + } + if elapsed > 2*time.Second { + t.Fatalf("handshake took %v, expected ~300ms timeout", elapsed) + } +} + +func TestWindowsClientRoundtrip(t *testing.T) { + clientConn, serverDone := startWindowsEchoServer(t, stdtls.VersionTLS12) + defer clientConn.Close() + + _, err := clientConn.Write([]byte("ping")) + if err != nil { + t.Fatalf("write: %v", err) + } + reply := make([]byte, 4) + _, err = io.ReadFull(clientConn, reply) + if err != nil { + t.Fatalf("read: %v", err) + } + if string(reply) != "ping" { + t.Fatalf("unexpected reply: %q", string(reply)) + } + + clientConn.Close() + <-serverDone +} + +func TestWindowsClientRoundtripTLS13(t *testing.T) { + clientConn, serverDone := startWindowsEchoServer(t, stdtls.VersionTLS13) + defer clientConn.Close() + + payload := []byte("hello tls 1.3") + _, err := clientConn.Write(payload) + if err != nil { + t.Fatalf("write: %v", err) + } + reply := make([]byte, len(payload)) + _, err = io.ReadFull(clientConn, reply) + if err != nil { + t.Fatalf("read: %v", err) + } + if !bytes.Equal(payload, reply) { + t.Fatalf("unexpected reply: %q", string(reply)) + } + + clientConn.Close() + <-serverDone +} + +func TestWindowsClientReadBuffer(t *testing.T) { + payload := []byte("windows tls read buffer payload") + clientConn, serverErr := startWindowsPayloadServer(t, stdtls.VersionTLS12, payload) + defer clientConn.Close() + + const ( + frontHeadroom = 8 + rearHeadroom = 8 + ) + buffer := buf.NewSize(len(payload) + frontHeadroom + rearHeadroom) + defer buffer.Release() + buffer.Resize(frontHeadroom, 0) + buffer.Reserve(rearHeadroom) + + err := clientConn.ReadBuffer(buffer) + if err != nil { + t.Fatalf("ReadBuffer: %v", err) + } + if buffer.Start() != frontHeadroom { + t.Fatalf("expected front headroom %d, got %d", frontHeadroom, buffer.Start()) + } + if !bytes.Equal(buffer.Bytes(), payload) { + t.Fatalf("unexpected payload: %q", string(buffer.Bytes())) + } + if err = <-serverErr; err != nil { + t.Fatal(err) + } +} + +func TestWindowsClientWriteBuffer(t *testing.T) { + clientConn, serverDone := startWindowsEchoEngineServer(t, stdtls.VersionTLS12) + defer clientConn.Close() + + payload := []byte("windows tls write buffer payload") + buffer := buf.NewSize(len(payload)) + _, err := buffer.Write(payload) + if err != nil { + t.Fatal(err) + } + + err = clientConn.WriteBuffer(buffer) + if err != nil { + t.Fatalf("WriteBuffer: %v", err) + } + if buffer.RawCap() != 0 { + t.Fatalf("expected WriteBuffer to release buffer, raw cap %d", buffer.RawCap()) + } + + reply := make([]byte, len(payload)) + _, err = io.ReadFull(clientConn, reply) + if err != nil { + t.Fatalf("read echo: %v", err) + } + if !bytes.Equal(reply, payload) { + t.Fatalf("unexpected echo: %q", string(reply)) + } + + clientConn.Close() + <-serverDone +} + +func TestWindowsClientCreateReadWaiter(t *testing.T) { + payload := []byte("windows tls read waiter payload") + clientConn, serverErr := startWindowsPayloadServer(t, stdtls.VersionTLS12, payload) + defer clientConn.Close() + + readWaiter, created := bufio.CreateReadWaiter(clientConn) + if !created { + t.Fatal("expected read waiter") + } + readWaiter.InitializeReadWaiter(N.ReadWaitOptions{ + FrontHeadroom: 7, + RearHeadroom: 5, + MTU: len(payload), + }) + + buffer, err := readWaiter.WaitReadBuffer() + if err != nil { + t.Fatalf("WaitReadBuffer: %v", err) + } + defer buffer.Release() + if buffer.Start() != 7 { + t.Fatalf("expected front headroom 7, got %d", buffer.Start()) + } + if buffer.FreeLen() < 5 { + t.Fatalf("expected rear headroom at least 5, got %d", buffer.FreeLen()) + } + if !bytes.Equal(buffer.Bytes(), payload) { + t.Fatalf("unexpected payload: %q", string(buffer.Bytes())) + } + if err = <-serverErr; err != nil { + t.Fatal(err) + } +} + +func TestWindowsClientCreateReadWaiterFallback(t *testing.T) { + tlsConn := newTestWindowsTLSConn(&windowsTestIOConn{}) + _, created := tlsConn.CreateReadWaiter() + if created { + t.Fatal("expected read waiter fallback") + } +} + +func TestWindowsClientTLS13PostHandshakeConcurrentWrite(t *testing.T) { + serverCertificate, serverCertificatePEM := newWindowsTestCertificate(t, "localhost") + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { listener.Close() }) + + const payloadSize = 4 << 20 + const prefixSize = 32 << 10 + reply := []byte("tls13 post-handshake reply") + + serverErr := make(chan error, 1) + prefixRead := make(chan struct{}) + go func() { + conn, acceptErr := listener.Accept() + if acceptErr != nil { + serverErr <- acceptErr + return + } + defer conn.Close() + + tlsConn := stdtls.Server(conn, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: stdtls.VersionTLS13, + MaxVersion: stdtls.VersionTLS13, + }) + defer tlsConn.Close() + + err := tlsConn.SetDeadline(time.Now().Add(2 * windowsTLSTestTimeout)) + if err != nil { + serverErr <- err + return + } + err = tlsConn.Handshake() + if err != nil { + serverErr <- err + return + } + prefix := make([]byte, prefixSize) + _, err = io.ReadFull(tlsConn, prefix) + if err != nil { + serverErr <- err + return + } + close(prefixRead) + _, err = tlsConn.Write(reply) + if err != nil { + serverErr <- err + return + } + _, err = io.Copy(io.Discard, io.LimitReader(tlsConn, int64(payloadSize-prefixSize))) + if err != nil { + serverErr <- err + return + } + serverErr <- nil + }() + + clientConn, err := newWindowsTestClientConn(t, listener.Addr().String(), option.OutboundTLSOptions{ + Enabled: true, + Engine: C.TLSEngineWindows, + ServerName: "localhost", + MinVersion: "1.3", + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }) + if err != nil { + t.Fatal(err) + } + defer clientConn.Close() + + payload := make([]byte, payloadSize) + for index := range payload { + payload[index] = byte(index % 251) + } + writeDone := make(chan error, 1) + go func() { + _, err := clientConn.Write(payload) + writeDone <- err + }() + + select { + case <-prefixRead: + case <-time.After(2 * time.Second): + t.Fatal("server did not observe the client write") + } + + replyBuffer := make([]byte, len(reply)) + _, err = io.ReadFull(clientConn, replyBuffer) + if err != nil { + t.Fatalf("read: %v", err) + } + if !bytes.Equal(reply, replyBuffer) { + t.Fatalf("unexpected reply: %q", string(replyBuffer)) + } + + writeErr := <-writeDone + if writeErr != nil { + t.Fatalf("write: %v", writeErr) + } + serverErrValue := <-serverErr + if serverErrValue != nil { + t.Fatal(serverErrValue) + } +} + +func TestWindowsClientLargeMessage(t *testing.T) { + clientConn, serverDone := startWindowsEchoServer(t, stdtls.VersionTLS12) + defer clientConn.Close() + + // 1 MiB exercises multiple TLS records and the chunking logic. + // Writes must run concurrently with reads to avoid TCP-buffer deadlock. + payload := make([]byte, 1<<20) + for index := range payload { + payload[index] = byte(index % 251) + } + writeErr := make(chan error, 1) + go func() { + _, err := clientConn.Write(payload) + writeErr <- err + }() + + reply := make([]byte, len(payload)) + _, err := io.ReadFull(clientConn, reply) + if err != nil { + t.Fatalf("read: %v", err) + } + writeResult := <-writeErr + if writeResult != nil { + t.Fatalf("write: %v", writeResult) + } + if !bytes.Equal(payload, reply) { + t.Fatal("payload mismatch after round-trip") + } + + clientConn.Close() + <-serverDone +} + +func TestWindowsClientFullDuplexLargePayload(t *testing.T) { + serverCertificate, serverCertificatePEM := newWindowsTestCertificate(t, "localhost") + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { listener.Close() }) + + const payloadSize = 2 << 20 + clientPayload := make([]byte, payloadSize) + serverPayload := make([]byte, payloadSize) + for index := range clientPayload { + clientPayload[index] = byte(index % 251) + serverPayload[index] = byte((index + 97) % 251) + } + + serverErr := make(chan error, 1) + go func() { + conn, acceptErr := listener.Accept() + if acceptErr != nil { + serverErr <- acceptErr + return + } + defer conn.Close() + + tlsConn := stdtls.Server(conn, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: stdtls.VersionTLS12, + MaxVersion: stdtls.VersionTLS12, + }) + defer tlsConn.Close() + err := tlsConn.SetDeadline(time.Now().Add(2 * windowsTLSTestTimeout)) + if err != nil { + serverErr <- err + return + } + err = tlsConn.Handshake() + if err != nil { + serverErr <- err + return + } + + readDone := make(chan error, 1) + writeDone := make(chan error, 1) + go func() { + received := make([]byte, len(clientPayload)) + _, readErr := io.ReadFull(tlsConn, received) + if readErr == nil && !bytes.Equal(received, clientPayload) { + readErr = errors.New("client payload mismatch") + } + readDone <- readErr + }() + go func() { + _, writeErr := tlsConn.Write(serverPayload) + writeDone <- writeErr + }() + if readErr := <-readDone; readErr != nil { + serverErr <- readErr + return + } + serverErr <- <-writeDone + }() + + clientConn, err := newWindowsTestEngineConn(t, listener.Addr().String(), option.OutboundTLSOptions{ + Enabled: true, + Engine: C.TLSEngineWindows, + ServerName: "localhost", + MinVersion: "1.2", + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }) + if err != nil { + t.Fatal(err) + } + defer clientConn.Close() + + writeDone := make(chan error, 1) + go func() { + n, writeErr := clientConn.Write(clientPayload) + if writeErr == nil && n != len(clientPayload) { + writeErr = io.ErrShortWrite + } + writeDone <- writeErr + }() + + reply := make([]byte, len(serverPayload)) + _, err = io.ReadFull(clientConn, reply) + if err != nil { + t.Fatalf("read: %v", err) + } + if !bytes.Equal(reply, serverPayload) { + t.Fatal("server payload mismatch") + } + if writeErr := <-writeDone; writeErr != nil { + t.Fatalf("write: %v", writeErr) + } + if err = <-serverErr; err != nil { + t.Fatal(err) + } +} + +func TestWindowsClientMultipleRoundtrips(t *testing.T) { + clientConn, serverDone := startWindowsEchoServer(t, stdtls.VersionTLS12) + defer clientConn.Close() + + for i := 0; i < 100; i++ { + payload := []byte("msg" + string(rune('A'+(i%26)))) + _, err := clientConn.Write(payload) + if err != nil { + t.Fatalf("write %d: %v", i, err) + } + reply := make([]byte, len(payload)) + _, err = io.ReadFull(clientConn, reply) + if err != nil { + t.Fatalf("read %d: %v", i, err) + } + if !bytes.Equal(payload, reply) { + t.Fatalf("iteration %d: expected %q got %q", i, payload, reply) + } + } + + clientConn.Close() + <-serverDone +} + +func TestWindowsClientConcurrentReadWrite(t *testing.T) { + clientConn, serverDone := startWindowsEchoServer(t, stdtls.VersionTLS12) + defer clientConn.Close() + + const messageCount = 200 + const messageSize = 64 + payloads := make([][]byte, messageCount) + for i := range payloads { + buffer := make([]byte, messageSize) + for j := range buffer { + buffer[j] = byte(i + j) + } + payloads[i] = buffer + } + + readErr := make(chan error, 1) + readBack := make(chan []byte, messageCount) + go func() { + for i := 0; i < messageCount; i++ { + reply := make([]byte, messageSize) + _, err := io.ReadFull(clientConn, reply) + if err != nil { + readErr <- err + return + } + readBack <- reply + } + readErr <- nil + }() + + for i, payload := range payloads { + _, err := clientConn.Write(payload) + if err != nil { + t.Fatalf("write %d: %v", i, err) + } + } + + readResult := <-readErr + if readResult != nil { + t.Fatal(readResult) + } + for i := 0; i < messageCount; i++ { + got := <-readBack + if !bytes.Equal(payloads[i], got) { + t.Fatalf("iteration %d: payload mismatch", i) + } + } + + clientConn.Close() + <-serverDone +} + +func TestWindowsClientServerCloseReturnsEOF(t *testing.T) { + serverCertificate, serverCertificatePEM := newWindowsTestCertificate(t, "localhost") + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { listener.Close() }) + + done := make(chan struct{}) + go func() { + defer close(done) + conn, acceptErr := listener.Accept() + if acceptErr != nil { + return + } + defer conn.Close() + tlsConn := stdtls.Server(conn, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: stdtls.VersionTLS12, + MaxVersion: stdtls.VersionTLS12, + }) + _ = tlsConn.Handshake() + // Send close_notify then exit. + _ = tlsConn.Close() + }() + + clientConn, err := newWindowsTestClientConn(t, listener.Addr().String(), option.OutboundTLSOptions{ + Enabled: true, + Engine: C.TLSEngineWindows, + ServerName: "localhost", + MinVersion: "1.2", + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }) + if err != nil { + t.Fatal(err) + } + defer clientConn.Close() + + buffer := make([]byte, 16) + _, err = clientConn.Read(buffer) + if !errors.Is(err, io.EOF) { + t.Fatalf("expected io.EOF, got %v", err) + } + <-done +} + +func TestWindowsClientCloseUnblocksRead(t *testing.T) { + serverCertificate, serverCertificatePEM := newWindowsTestCertificate(t, "localhost") + _, serverAddress := startWindowsTLSSilentServer(t, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: stdtls.VersionTLS12, + }) + + clientConn, err := newWindowsTestClientConn(t, serverAddress, option.OutboundTLSOptions{ + Enabled: true, + Engine: C.TLSEngineWindows, + ServerName: "localhost", + MinVersion: "1.2", + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }) + if err != nil { + t.Fatal(err) + } + defer clientConn.Close() + + readDone := make(chan error, 1) + go func() { + buffer := make([]byte, 16) + _, err := clientConn.Read(buffer) + readDone <- err + }() + + time.Sleep(100 * time.Millisecond) + clientConn.Close() + + select { + case err := <-readDone: + if err == nil { + t.Fatal("expected Read to return an error after Close") + } + case <-time.After(2 * time.Second): + t.Fatal("Read did not return within 2s after Close") + } +} + +func TestWindowsClientReadAfterCloseReturnsError(t *testing.T) { + clientConn, serverDone := startWindowsEchoServer(t, stdtls.VersionTLS12) + clientConn.Close() + <-serverDone + + buffer := make([]byte, 16) + _, err := clientConn.Read(buffer) + if err == nil { + t.Fatal("expected Read after Close to return error") + } +} + +func TestWindowsClientReadAfterCloseDoesNotServeBufferedPlaintext(t *testing.T) { + serverCertificate, serverCertificatePEM := newWindowsTestCertificate(t, "localhost") + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { listener.Close() }) + + serverDone := make(chan struct{}) + serverErr := make(chan error, 1) + payload := bytes.Repeat([]byte("buffered plaintext "), 32) + go func() { + conn, acceptErr := listener.Accept() + if acceptErr != nil { + serverErr <- acceptErr + return + } + defer conn.Close() + + tlsConn := stdtls.Server(conn, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: stdtls.VersionTLS12, + MaxVersion: stdtls.VersionTLS12, + }) + defer tlsConn.Close() + + err := tlsConn.SetDeadline(time.Now().Add(windowsTLSTestTimeout)) + if err != nil { + serverErr <- err + return + } + err = tlsConn.Handshake() + if err != nil { + serverErr <- err + return + } + _, err = tlsConn.Write(payload) + if err != nil { + serverErr <- err + return + } + <-serverDone + serverErr <- nil + }() + + clientConn, err := newWindowsTestClientConn(t, listener.Addr().String(), option.OutboundTLSOptions{ + Enabled: true, + Engine: C.TLSEngineWindows, + ServerName: "localhost", + MinVersion: "1.2", + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }) + if err != nil { + t.Fatal(err) + } + + buffer := make([]byte, 8) + n, err := clientConn.Read(buffer) + if err != nil { + t.Fatalf("first read: %v", err) + } + if n != len(buffer) { + t.Fatalf("expected first read to fill the buffer, got %d", n) + } + + clientConn.Close() + close(serverDone) + + _, err = clientConn.Read(make([]byte, len(payload))) + if !errors.Is(err, net.ErrClosed) { + t.Fatalf("expected net.ErrClosed, got %v", err) + } + serverErrValue := <-serverErr + if serverErrValue != nil { + t.Fatal(serverErrValue) + } +} + +func TestWindowsClientWriteAfterCloseReturnsError(t *testing.T) { + clientConn, serverDone := startWindowsEchoServer(t, stdtls.VersionTLS12) + clientConn.Close() + <-serverDone + + _, err := clientConn.Write([]byte("after close")) + if err == nil { + t.Fatal("expected Write after Close to return error") + } +} + +func TestWindowsClientReadDeadline(t *testing.T) { + serverCertificate, serverCertificatePEM := newWindowsTestCertificate(t, "localhost") + serverDone, serverAddress := startWindowsTLSSilentServer(t, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: stdtls.VersionTLS12, + MaxVersion: stdtls.VersionTLS12, + }) + + clientConn, err := newWindowsTestClientConn(t, serverAddress, option.OutboundTLSOptions{ + Enabled: true, + Engine: C.TLSEngineWindows, + ServerName: "localhost", + MinVersion: "1.2", + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }) + if err != nil { + t.Fatal(err) + } + defer clientConn.Close() + defer close(serverDone) + + err = clientConn.SetReadDeadline(time.Now().Add(200 * time.Millisecond)) + if err != nil { + t.Fatalf("SetReadDeadline: %v", err) + } + + readDone := make(chan error, 1) + buffer := make([]byte, 64) + go func() { + _, readErr := clientConn.Read(buffer) + readDone <- readErr + }() + + select { + case readErr := <-readDone: + if !errors.Is(readErr, os.ErrDeadlineExceeded) { + t.Fatalf("expected os.ErrDeadlineExceeded, got %v", readErr) + } + case <-time.After(2 * time.Second): + t.Fatal("Read did not return within 2s after deadline") + } +} + +func TestWindowsClientSetReadDeadlinePreExpired(t *testing.T) { + serverCertificate, serverCertificatePEM := newWindowsTestCertificate(t, "localhost") + serverDone, serverAddress := startWindowsTLSSilentServer(t, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: stdtls.VersionTLS12, + MaxVersion: stdtls.VersionTLS12, + }) + + clientConn, err := newWindowsTestClientConn(t, serverAddress, option.OutboundTLSOptions{ + Enabled: true, + Engine: C.TLSEngineWindows, + ServerName: "localhost", + MinVersion: "1.2", + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }) + if err != nil { + t.Fatal(err) + } + defer clientConn.Close() + defer close(serverDone) + + err = clientConn.SetReadDeadline(time.Now().Add(-time.Second)) + if err != nil { + t.Fatalf("SetReadDeadline past: %v", err) + } + + buffer := make([]byte, 16) + _, err = clientConn.Read(buffer) + if !errors.Is(err, os.ErrDeadlineExceeded) { + t.Fatalf("expected os.ErrDeadlineExceeded, got %v", err) + } + + // Clearing the deadline must restore normal blocking behaviour. + err = clientConn.SetReadDeadline(time.Time{}) + if err != nil { + t.Fatalf("SetReadDeadline zero: %v", err) + } + err = clientConn.SetReadDeadline(time.Now().Add(200 * time.Millisecond)) + if err != nil { + t.Fatalf("SetReadDeadline future: %v", err) + } + start := time.Now() + _, err = clientConn.Read(buffer) + elapsed := time.Since(start) + if !errors.Is(err, os.ErrDeadlineExceeded) { + t.Fatalf("expected os.ErrDeadlineExceeded after re-arm, got %v", err) + } + if elapsed < 150*time.Millisecond { + t.Fatalf("Read returned too fast (%v), pre-expired flag leaked", elapsed) + } +} + +func TestWindowsClientSetDeadlinePropagatesToRawConn(t *testing.T) { + rawConn := &windowsTestDeadlineConn{} + tlsConn := newTestWindowsTLSConn(rawConn) + + deadline := time.Now().Add(time.Second) + err := tlsConn.SetDeadline(deadline) + if err != nil { + t.Fatalf("SetDeadline: %v", err) + } + + readDeadlines := rawConn.recordedReadDeadlines() + if len(readDeadlines) != 1 { + t.Fatalf("expected 1 read deadline update, got %d", len(readDeadlines)) + } + if !readDeadlines[0].Equal(deadline) { + t.Fatalf("expected read deadline %v, got %v", deadline, readDeadlines[0]) + } + + writeDeadlines := rawConn.recordedWriteDeadlines() + if len(writeDeadlines) != 1 { + t.Fatalf("expected 1 write deadline update, got %d", len(writeDeadlines)) + } + if !writeDeadlines[0].Equal(deadline) { + t.Fatalf("expected write deadline %v, got %v", deadline, writeDeadlines[0]) + } +} + +func TestWindowsClientSetReadDeadlineCancelsBlockedRead(t *testing.T) { + rawConn := &windowsTestDeadlineConn{ + readCalled: make(chan struct{}), + } + tlsConn := newTestWindowsTLSConn(rawConn) + tlsConn.readScratch = make([]byte, 16) + + readErrCh := make(chan error, 1) + go func() { + _, err := tlsConn.Read(make([]byte, 1)) + readErrCh <- err + }() + + select { + case <-rawConn.readCalled: + case <-time.After(time.Second): + t.Fatal("Read did not reach the raw connection") + } + + deadline := time.Now().Add(150 * time.Millisecond) + err := tlsConn.SetReadDeadline(deadline) + if err != nil { + t.Fatalf("SetReadDeadline: %v", err) + } + + select { + case err = <-readErrCh: + if !errors.Is(err, os.ErrDeadlineExceeded) { + t.Fatalf("expected os.ErrDeadlineExceeded, got %v", err) + } + case <-time.After(2 * time.Second): + t.Fatal("Read did not return after SetReadDeadline") + } + + readDeadlines := rawConn.recordedReadDeadlines() + if len(readDeadlines) != 1 { + t.Fatalf("expected 1 read deadline update, got %d", len(readDeadlines)) + } + if !readDeadlines[0].Equal(deadline) { + t.Fatalf("expected read deadline %v, got %v", deadline, readDeadlines[0]) + } +} + +func TestWindowsClientSetWriteDeadlineCancelsBlockedWrite(t *testing.T) { + serverCertificate, serverCertificatePEM := newWindowsTestCertificate(t, "localhost") + serverDone, serverAddress := startWindowsTLSSilentServer(t, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: stdtls.VersionTLS12, + MaxVersion: stdtls.VersionTLS12, + }) + defer close(serverDone) + + tlsConn, err := newWindowsTestEngineConn(t, serverAddress, option.OutboundTLSOptions{ + Enabled: true, + Engine: C.TLSEngineWindows, + ServerName: "localhost", + MinVersion: "1.2", + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }) + if err != nil { + t.Fatal(err) + } + + originalRawConn := tlsConn.rawConn + rawConn := &windowsTestDeadlineConn{ + writeCalled: make(chan struct{}), + } + tlsConn.rawConn = rawConn + t.Cleanup(func() { + _ = originalRawConn.Close() + _ = tlsConn.Close() + }) + + writeErrCh := make(chan error, 1) + go func() { + _, err := tlsConn.Write([]byte("ping")) + writeErrCh <- err + }() + + select { + case <-rawConn.writeCalled: + case <-time.After(time.Second): + t.Fatal("Write did not reach the raw connection") + } + + deadline := time.Now().Add(150 * time.Millisecond) + err = tlsConn.SetWriteDeadline(deadline) + if err != nil { + t.Fatalf("SetWriteDeadline: %v", err) + } + + select { + case err = <-writeErrCh: + if !errors.Is(err, os.ErrDeadlineExceeded) { + t.Fatalf("expected os.ErrDeadlineExceeded, got %v", err) + } + case <-time.After(2 * time.Second): + t.Fatal("Write did not return after SetWriteDeadline") + } + + writeDeadlines := rawConn.recordedWriteDeadlines() + if len(writeDeadlines) != 1 { + t.Fatalf("expected 1 write deadline update, got %d", len(writeDeadlines)) + } + if !writeDeadlines[0].Equal(deadline) { + t.Fatalf("expected write deadline %v, got %v", deadline, writeDeadlines[0]) + } +} + +func TestWindowsClientPostHandshakeReplyUsesReadDeadline(t *testing.T) { + rawConn := &windowsTestDeadlineConn{} + tlsConn := newTestWindowsTLSConn(rawConn) + + readDeadline := time.Now().Add(150 * time.Millisecond) + err := tlsConn.SetReadDeadline(readDeadline) + if err != nil { + t.Fatalf("SetReadDeadline: %v", err) + } + + start := time.Now() + err = tlsConn.writePostHandshakeReply([]byte("reply")) + elapsed := time.Since(start) + if !errors.Is(err, os.ErrDeadlineExceeded) { + t.Fatalf("expected os.ErrDeadlineExceeded, got %v", err) + } + if elapsed < 100*time.Millisecond { + t.Fatalf("post-handshake write returned too fast: %v", elapsed) + } + + deadlines := rawConn.recordedWriteDeadlines() + if len(deadlines) != 2 { + t.Fatalf("expected 2 write deadline updates, got %d", len(deadlines)) + } + if !deadlines[0].Equal(readDeadline) { + t.Fatalf("expected first write deadline %v, got %v", readDeadline, deadlines[0]) + } + if !deadlines[1].IsZero() { + t.Fatalf("expected write deadline cleanup, got %v", deadlines[1]) + } +} + +func TestWindowsClientPostHandshakeReplyPreExpiredReadDeadline(t *testing.T) { + rawConn := &windowsTestDeadlineConn{} + tlsConn := newTestWindowsTLSConn(rawConn) + + err := tlsConn.SetReadDeadline(time.Now().Add(-time.Second)) + if err != nil { + t.Fatalf("SetReadDeadline: %v", err) + } + + start := time.Now() + err = tlsConn.writePostHandshakeReply([]byte("reply")) + elapsed := time.Since(start) + if !errors.Is(err, os.ErrDeadlineExceeded) { + t.Fatalf("expected os.ErrDeadlineExceeded, got %v", err) + } + if elapsed > 50*time.Millisecond { + t.Fatalf("pre-expired post-handshake write returned too slowly: %v", elapsed) + } + + deadlines := rawConn.recordedWriteDeadlines() + if len(deadlines) != 0 { + t.Fatalf("expected no write deadline update for pre-expired read deadline, got %d", len(deadlines)) + } +} + +func TestDriveStepsPreservesBufferedHandshakeBytes(t *testing.T) { + scratch := make([]byte, 8) + copy(scratch, "abc") + + readCalls := 0 + stepCalls := 0 + leftover, err := driveSteps( + scratch[:3], + func(input []byte) (schannel.StepResult, error) { + stepCalls++ + switch stepCalls { + case 1: + if string(input) != "abc" { + t.Fatalf("first step input = %q, want %q", input, "abc") + } + return schannel.StepResult{Incomplete: true}, nil + case 2: + if string(input) != "abcdef" { + t.Fatalf("second step input = %q, want %q", input, "abcdef") + } + return schannel.StepResult{Consumed: len(input), Done: true}, nil + default: + t.Fatalf("unexpected step call %d", stepCalls) + return schannel.StepResult{}, nil + } + }, + func() ([]byte, error) { + readCalls++ + copy(scratch, "def") + return scratch[:3], nil + }, + func([]byte) error { return nil }, + ) + if err != nil { + t.Fatal(err) + } + if readCalls != 1 { + t.Fatalf("readMore called %d times, want 1", readCalls) + } + if len(leftover) != 0 { + t.Fatalf("leftover = %q, want empty", leftover) + } +} + +func TestWindowsTLSRawReadEOFAtRecordBoundary(t *testing.T) { + rawConn := &windowsTestIOConn{readErr: io.EOF} + _, err := readTLSRaw(rawConn, make([]byte, 16), false) + if !errors.Is(err, io.EOF) { + t.Fatalf("expected io.EOF, got %v", err) + } +} + +func TestWindowsTLSRawReadEOFWithPendingRecord(t *testing.T) { + rawConn := &windowsTestIOConn{readErr: io.EOF} + _, err := readTLSRaw(rawConn, make([]byte, 16), true) + if !errors.Is(err, io.ErrUnexpectedEOF) { + t.Fatalf("expected io.ErrUnexpectedEOF, got %v", err) + } +} + +func TestWindowsClientPostHandshakeReplyWaitsForWriteAccess(t *testing.T) { + rawConn := &windowsTestWriteGateConn{ + writeCalled: make(chan struct{}), + releaseWrite: make(chan struct{}), + } + tlsConn := newTestWindowsTLSConn(rawConn) + + tlsConn.writeAccess.Lock() + errCh := make(chan error, 1) + go func() { + errCh <- tlsConn.writePostHandshakeReply([]byte("reply")) + }() + + select { + case <-rawConn.writeCalled: + t.Fatal("post-handshake write bypassed writeAccess") + case <-time.After(100 * time.Millisecond): + } + + tlsConn.writeAccess.Unlock() + + select { + case <-rawConn.writeCalled: + case <-time.After(time.Second): + t.Fatal("post-handshake write did not resume after writeAccess release") + } + + close(rawConn.releaseWrite) + err := <-errCh + if err != nil { + t.Fatal(err) + } +} + +func TestWindowsClientPostHandshakeWritePreemptsNewWrite(t *testing.T) { + tlsConn := newTestWindowsTLSConn(&windowsTestIOConn{}) + err := tlsConn.beginWrite() + if err != nil { + t.Fatal(err) + } + + postHandshakeReady := make(chan error, 1) + go func() { + postHandshakeReady <- tlsConn.beginPostHandshakeWrite() + }() + + deadline := time.After(time.Second) + for { + tlsConn.writeState.Lock() + pending := tlsConn.postHandshake + tlsConn.writeState.Unlock() + if pending { + break + } + select { + case <-deadline: + t.Fatal("post-handshake write did not become pending") + default: + time.Sleep(time.Millisecond) + } + } + + writeReady := make(chan error, 1) + go func() { + writeReady <- tlsConn.beginWrite() + }() + + tlsConn.finishWrite() + + select { + case err = <-postHandshakeReady: + if err != nil { + t.Fatal(err) + } + case err = <-writeReady: + t.Fatalf("new write preempted post-handshake write: %v", err) + case <-time.After(time.Second): + t.Fatal("post-handshake write did not resume") + } + + select { + case err = <-writeReady: + t.Fatalf("new write acquired before post-handshake finished: %v", err) + case <-time.After(100 * time.Millisecond): + } + + tlsConn.finishPostHandshakeWrite() + select { + case err = <-writeReady: + if err != nil { + t.Fatal(err) + } + case <-time.After(time.Second): + t.Fatal("new write did not resume after post-handshake write") + } + tlsConn.finishWrite() +} + +func TestWindowsClientPostHandshakeReplyErrorClosesConn(t *testing.T) { + rawConn := &windowsTestIOConn{ + writeErr: os.ErrDeadlineExceeded, + writeN: 1, + } + tlsConn := newTestWindowsTLSConn(rawConn) + + err := tlsConn.writePostHandshakeReply([]byte("reply")) + if !errors.Is(err, os.ErrDeadlineExceeded) { + t.Fatalf("expected os.ErrDeadlineExceeded, got %v", err) + } + if !rawConn.isClosed() { + t.Fatal("expected raw conn to be closed") + } + if !tlsConn.isClosed() { + t.Fatal("expected tls conn to be closed") + } +} + +func TestWindowsClientWriteErrorClosesConn(t *testing.T) { + serverCertificate, serverCertificatePEM := newWindowsTestCertificate(t, "localhost") + serverDone, serverAddress := startWindowsTLSSilentServer(t, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: stdtls.VersionTLS12, + MaxVersion: stdtls.VersionTLS12, + }) + defer close(serverDone) + + tlsConn, err := newWindowsTestEngineConn(t, serverAddress, option.OutboundTLSOptions{ + Enabled: true, + Engine: C.TLSEngineWindows, + ServerName: "localhost", + MinVersion: "1.2", + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }) + if err != nil { + t.Fatal(err) + } + + originalRawConn := tlsConn.rawConn + rawConn := &windowsTestIOConn{ + writeErr: os.ErrDeadlineExceeded, + writeN: 1, + } + tlsConn.rawConn = rawConn + t.Cleanup(func() { + _ = originalRawConn.Close() + _ = tlsConn.Close() + }) + + _, err = tlsConn.Write([]byte("ping")) + if !errors.Is(err, os.ErrDeadlineExceeded) { + t.Fatalf("expected os.ErrDeadlineExceeded, got %v", err) + } + if !rawConn.isClosed() { + t.Fatal("expected raw conn to be closed") + } + if !tlsConn.isClosed() { + t.Fatal("expected tls conn to be closed") + } + + _, err = tlsConn.Write([]byte("again")) + if !errors.Is(err, net.ErrClosed) { + t.Fatalf("expected net.ErrClosed on second write, got %v", err) + } + if rawConn.totalWriteCalls() != 1 { + t.Fatalf("expected exactly 1 raw write, got %d", rawConn.totalWriteCalls()) + } + + _, err = tlsConn.Read(make([]byte, 1)) + if !errors.Is(err, net.ErrClosed) { + t.Fatalf("expected net.ErrClosed on read after write failure, got %v", err) + } +} + +func TestWindowsClientConnectionStateFields(t *testing.T) { + serverCertificate, serverCertificatePEM := newWindowsTestCertificate(t, "localhost") + _, serverAddress := startWindowsTLSTestServer(t, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: stdtls.VersionTLS12, + NextProtos: []string{"h2"}, + }) + + clientConn, err := newWindowsTestClientConn(t, serverAddress, option.OutboundTLSOptions{ + Enabled: true, + Engine: C.TLSEngineWindows, + ServerName: "localhost", + MinVersion: "1.2", + ALPN: badoption.Listable[string]{"h2"}, + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }) + if err != nil { + t.Fatal(err) + } + defer clientConn.Close() + + state := clientConn.ConnectionState() + if state.ServerName != "localhost" { + t.Errorf("ServerName: expected localhost, got %q", state.ServerName) + } + if state.NegotiatedProtocol != "h2" { + t.Errorf("NegotiatedProtocol: expected h2, got %q", state.NegotiatedProtocol) + } + if !state.HandshakeComplete { + t.Error("HandshakeComplete: expected true") + } + if state.Version < stdtls.VersionTLS12 || state.Version > stdtls.VersionTLS13 { + t.Errorf("Version: expected TLS 1.2–1.3, got %x", state.Version) + } + if len(state.PeerCertificates) == 0 { + t.Fatal("PeerCertificates: expected at least one certificate") + } + // CipherSuite may be 0 when the Schannel name does not map to a Go + // constant; just ensure it's consistent with the protocol. + if state.Version == stdtls.VersionTLS13 && state.CipherSuite != 0 { + switch state.CipherSuite { + case stdtls.TLS_AES_128_GCM_SHA256, stdtls.TLS_AES_256_GCM_SHA384, stdtls.TLS_CHACHA20_POLY1305_SHA256: + default: + t.Errorf("unexpected TLS 1.3 cipher suite: %x", state.CipherSuite) + } + } +} + +func TestWindowsClientNetConnReturnsUnderlying(t *testing.T) { + clientConn, serverDone := startWindowsEchoServer(t, stdtls.VersionTLS12) + defer func() { <-serverDone }() + defer clientConn.Close() + + underlying := clientConn.NetConn() + if _, isTCP := underlying.(*net.TCPConn); !isTCP { + t.Fatalf("NetConn returned %T, expected *net.TCPConn", underlying) + } +} + +func TestNewWindowsClientMissingServerName(t *testing.T) { + _, err := NewClientWithOptions(ClientOptions{ + Context: context.Background(), + Logger: logger.NOP(), + Options: option.OutboundTLSOptions{ + Enabled: true, + Engine: C.TLSEngineWindows, + }, + }) + if err == nil { + t.Fatal("expected missing server_name error") + } +} + +func TestNewWindowsClientInsecureAllowsMissingServerName(t *testing.T) { + _, err := NewClientWithOptions(ClientOptions{ + Context: context.Background(), + Logger: logger.NOP(), + Options: option.OutboundTLSOptions{ + Enabled: true, + Engine: C.TLSEngineWindows, + Insecure: true, + }, + }) + if err != nil { + t.Fatal(err) + } +} + +func TestWindowsClientConfigSTDConfigReturnsError(t *testing.T) { + config, err := NewClientWithOptions(ClientOptions{ + Context: context.Background(), + Logger: logger.NOP(), + Options: option.OutboundTLSOptions{ + Enabled: true, + Engine: C.TLSEngineWindows, + ServerName: "localhost", + }, + }) + if err != nil { + t.Fatal(err) + } + _, err = config.STDConfig() + if err == nil { + t.Fatal("expected STDConfig() to return error for Windows engine") + } + if !strings.Contains(err.Error(), "system TLS engine") { + t.Fatalf("expected error to name the engine, got %q", err.Error()) + } +} + +func TestWindowsClientConfigClientReturnsErrInvalid(t *testing.T) { + config, err := NewClientWithOptions(ClientOptions{ + Context: context.Background(), + Logger: logger.NOP(), + Options: option.OutboundTLSOptions{ + Enabled: true, + Engine: C.TLSEngineWindows, + ServerName: "localhost", + }, + }) + if err != nil { + t.Fatal(err) + } + _, err = config.Client(nil) + if !errors.Is(err, os.ErrInvalid) { + t.Fatalf("expected os.ErrInvalid, got %v", err) + } +} + +func TestWindowsClientConfigClone(t *testing.T) { + config, err := NewClientWithOptions(ClientOptions{ + Context: context.Background(), + Logger: logger.NOP(), + Options: option.OutboundTLSOptions{ + Enabled: true, + Engine: C.TLSEngineWindows, + ServerName: "localhost", + ALPN: badoption.Listable[string]{"h2", "http/1.1"}, + }, + }) + if err != nil { + t.Fatal(err) + } + + clone := config.Clone() + + // Mutating the clone must not affect the original. + clone.SetServerName("other") + clone.SetNextProtos([]string{"h3"}) + if config.ServerName() == "other" { + t.Error("Clone shares server name with original") + } + if len(config.NextProtos()) != 2 { + t.Error("Clone shares ALPN slice with original") + } +} + +func TestValidateWindowsTLSOptionsRejections(t *testing.T) { + cases := []struct { + name string + options option.OutboundTLSOptions + needle string + }{ + {"reality", option.OutboundTLSOptions{ + Enabled: true, Engine: C.TLSEngineWindows, ServerName: "x", + Reality: &option.OutboundRealityOptions{Enabled: true, ShortID: "abc"}, + }, "reality"}, + {"utls", option.OutboundTLSOptions{ + Enabled: true, Engine: C.TLSEngineWindows, ServerName: "x", + UTLS: &option.OutboundUTLSOptions{Enabled: true}, + }, "utls"}, + {"ech", option.OutboundTLSOptions{ + Enabled: true, Engine: C.TLSEngineWindows, ServerName: "x", + ECH: &option.OutboundECHOptions{Enabled: true}, + }, "ech"}, + {"disable_sni", option.OutboundTLSOptions{ + Enabled: true, Engine: C.TLSEngineWindows, ServerName: "x", + DisableSNI: true, + }, "disable_sni"}, + {"cipher_suites", option.OutboundTLSOptions{ + Enabled: true, Engine: C.TLSEngineWindows, ServerName: "x", + CipherSuites: []string{"TLS_AES_128_GCM_SHA256"}, + }, "cipher_suites"}, + {"curve_preferences", option.OutboundTLSOptions{ + Enabled: true, Engine: C.TLSEngineWindows, ServerName: "x", + CurvePreferences: []option.CurvePreference{option.CurvePreference(29)}, + }, "curve_preferences"}, + {"client_certificate", option.OutboundTLSOptions{ + Enabled: true, Engine: C.TLSEngineWindows, ServerName: "x", + ClientCertificate: badoption.Listable[string]{"pem"}, + }, "client certificate"}, + {"fragment", option.OutboundTLSOptions{ + Enabled: true, Engine: C.TLSEngineWindows, ServerName: "x", + Fragment: true, + }, "tls fragment"}, + {"record_fragment", option.OutboundTLSOptions{ + Enabled: true, Engine: C.TLSEngineWindows, ServerName: "x", + RecordFragment: true, + }, "tls fragment"}, + {"kernel_tx", option.OutboundTLSOptions{ + Enabled: true, Engine: C.TLSEngineWindows, ServerName: "x", + KernelTx: true, + }, "ktls"}, + {"kernel_rx", option.OutboundTLSOptions{ + Enabled: true, Engine: C.TLSEngineWindows, ServerName: "x", + KernelRx: true, + }, "ktls"}, + {"spoof", option.OutboundTLSOptions{ + Enabled: true, Engine: C.TLSEngineWindows, ServerName: "x", + Spoof: "decoy.example", + }, "spoof"}, + {"pin_and_cert_conflict", option.OutboundTLSOptions{ + Enabled: true, Engine: C.TLSEngineWindows, ServerName: "x", + Certificate: badoption.Listable[string]{"-----BEGIN CERTIFICATE-----"}, + CertificatePublicKeySHA256: [][]byte{make([]byte, 32)}, + }, "certificate_public_key_sha256"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + _, err := NewClientWithOptions(ClientOptions{ + Context: context.Background(), + Logger: logger.NOP(), + Options: tc.options, + }) + if err == nil { + t.Fatalf("expected error containing %q, got nil", tc.needle) + } + if !strings.Contains(strings.ToLower(err.Error()), strings.ToLower(tc.needle)) { + t.Fatalf("expected error to contain %q, got %q", tc.needle, err.Error()) + } + }) + } +} + +func startWindowsTLSSilentServer(t *testing.T, tlsConfig *stdtls.Config) (chan<- struct{}, string) { + t.Helper() + + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { listener.Close() }) + + if tcpListener, isTCP := listener.(*net.TCPListener); isTCP { + err = tcpListener.SetDeadline(time.Now().Add(windowsTLSTestTimeout)) + if err != nil { + t.Fatal(err) + } + } + + done := make(chan struct{}) + go func() { + conn, acceptErr := listener.Accept() + if acceptErr != nil { + return + } + defer conn.Close() + deadlineErr := conn.SetDeadline(time.Now().Add(windowsTLSTestTimeout)) + if deadlineErr != nil { + return + } + tlsConn := stdtls.Server(conn, tlsConfig) + defer tlsConn.Close() + handshakeErr := tlsConn.Handshake() + if handshakeErr != nil { + return + } + handshakeErr = conn.SetDeadline(time.Time{}) + if handshakeErr != nil { + return + } + <-done + }() + return done, listener.Addr().String() +} + +// sharedWindowsTestCertificate caches a localhost certificate so the RSA key +// generation runs once per test binary instead of once per test. +var sharedWindowsTestCertificate = sync.OnceValues(func() (stdtls.Certificate, string) { + return generateWindowsTestCertificate("localhost") +}) + +func newWindowsTestCertificate(t *testing.T, serverName string) (stdtls.Certificate, string) { + t.Helper() + if serverName == "localhost" { + return sharedWindowsTestCertificate() + } + return generateWindowsTestCertificate(serverName) +} + +func generateWindowsTestCertificate(serverName string) (stdtls.Certificate, string) { + privateKeyPEM, certificatePEM, err := GenerateCertificate(nil, nil, time.Now, serverName, time.Now().Add(time.Hour)) + if err != nil { + panic(err) + } + certificate, err := stdtls.X509KeyPair(certificatePEM, privateKeyPEM) + if err != nil { + panic(err) + } + leaf, err := x509.ParseCertificate(certificate.Certificate[0]) + if err != nil { + panic(err) + } + certificate.Leaf = leaf + return certificate, string(certificatePEM) +} + +func publicKeyPin(t *testing.T, cert *x509.Certificate) []byte { + t.Helper() + pub, err := x509.MarshalPKIXPublicKey(cert.PublicKey) + if err != nil { + t.Fatal(err) + } + sum := sha256.Sum256(pub) + return sum[:] +} + +func startWindowsTLSTestServer(t *testing.T, tlsConfig *stdtls.Config) (<-chan windowsTLSServerResult, string) { + t.Helper() + + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { listener.Close() }) + + if tcpListener, isTCP := listener.(*net.TCPListener); isTCP { + err = tcpListener.SetDeadline(time.Now().Add(windowsTLSTestTimeout)) + if err != nil { + t.Fatal(err) + } + } + + result := make(chan windowsTLSServerResult, 1) + go func() { + defer close(result) + + conn, err := listener.Accept() + if err != nil { + result <- windowsTLSServerResult{err: err} + return + } + defer conn.Close() + + err = conn.SetDeadline(time.Now().Add(windowsTLSTestTimeout)) + if err != nil { + result <- windowsTLSServerResult{err: err} + return + } + + tlsConn := stdtls.Server(conn, tlsConfig) + defer tlsConn.Close() + + err = tlsConn.Handshake() + if err != nil { + result <- windowsTLSServerResult{err: err} + return + } + + result <- windowsTLSServerResult{state: tlsConn.ConnectionState()} + }() + + return result, listener.Addr().String() +} + +func newWindowsTestClientConn(t *testing.T, serverAddress string, options option.OutboundTLSOptions) (Conn, error) { + t.Helper() + + ctx, cancel := context.WithTimeout(context.Background(), windowsTLSTestTimeout) + t.Cleanup(cancel) + + clientConfig, err := NewClientWithOptions(ClientOptions{ + Context: ctx, + Logger: logger.NOP(), + ServerAddress: "", + Options: options, + }) + if err != nil { + return nil, err + } + + conn, err := net.DialTimeout("tcp", serverAddress, windowsTLSTestTimeout) + if err != nil { + return nil, err + } + + tlsConn, err := ClientHandshake(ctx, conn, clientConfig) + if err != nil { + conn.Close() + return nil, err + } + return tlsConn, nil +} + +func newWindowsTestEngineConn(t *testing.T, serverAddress string, options option.OutboundTLSOptions) (*windowsTLSConn, error) { + t.Helper() + + ctx, cancel := context.WithTimeout(context.Background(), windowsTLSTestTimeout) + t.Cleanup(cancel) + + clientConfig, err := NewClientWithOptions(ClientOptions{ + Context: ctx, + Logger: logger.NOP(), + ServerAddress: "", + Options: options, + }) + if err != nil { + return nil, err + } + + engineConfig, ok := clientConfig.(*windowsClientConfig) + if !ok { + return nil, errors.New("unexpected windows config type") + } + + conn, err := net.DialTimeout("tcp", serverAddress, windowsTLSTestTimeout) + if err != nil { + return nil, err + } + + tlsConn, err := engineConfig.ClientHandshake(ctx, conn) + if err != nil { + conn.Close() + return nil, err + } + + engineConn, ok := tlsConn.(*windowsTLSConn) + if !ok { + tlsConn.Close() + return nil, errors.New("unexpected windows conn type") + } + return engineConn, nil +} + +func startWindowsPayloadServer(t *testing.T, minVersion uint16, payload []byte) (*windowsTLSConn, <-chan error) { + t.Helper() + + serverCertificate, serverCertificatePEM := newWindowsTestCertificate(t, "localhost") + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { listener.Close() }) + + serverErr := make(chan error, 1) + go func() { + conn, acceptErr := listener.Accept() + if acceptErr != nil { + serverErr <- acceptErr + return + } + defer conn.Close() + + tlsConn := stdtls.Server(conn, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: minVersion, + MaxVersion: minVersion, + }) + defer tlsConn.Close() + + handshakeErr := tlsConn.Handshake() + if handshakeErr != nil { + serverErr <- handshakeErr + return + } + _, writeErr := tlsConn.Write(payload) + serverErr <- writeErr + }() + + version := "1.2" + if minVersion == stdtls.VersionTLS13 { + version = "1.3" + } + clientConn, err := newWindowsTestEngineConn(t, listener.Addr().String(), option.OutboundTLSOptions{ + Enabled: true, + Engine: C.TLSEngineWindows, + ServerName: "localhost", + MinVersion: version, + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }) + if err != nil { + t.Fatal(err) + } + return clientConn, serverErr +} + +// startWindowsEchoServer brings up a TLS echo server with a self-signed cert +// and dials an engine client against it. The returned channel closes after +// the server goroutine exits. +func startWindowsEchoServer(t *testing.T, minVersion uint16) (Conn, <-chan struct{}) { + t.Helper() + + serverCertificate, serverCertificatePEM := newWindowsTestCertificate(t, "localhost") + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { listener.Close() }) + + done := make(chan struct{}) + go func() { + defer close(done) + conn, acceptErr := listener.Accept() + if acceptErr != nil { + return + } + defer conn.Close() + + tlsConn := stdtls.Server(conn, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: minVersion, + MaxVersion: minVersion, + }) + defer tlsConn.Close() + + handshakeErr := tlsConn.Handshake() + if handshakeErr != nil { + return + } + + buffer := make([]byte, 32*1024) + for { + n, readErr := tlsConn.Read(buffer) + if n > 0 { + _, writeErr := tlsConn.Write(buffer[:n]) + if writeErr != nil { + return + } + } + if readErr != nil { + return + } + } + }() + + version := "1.2" + if minVersion == stdtls.VersionTLS13 { + version = "1.3" + } + clientConn, err := newWindowsTestClientConn(t, listener.Addr().String(), option.OutboundTLSOptions{ + Enabled: true, + Engine: C.TLSEngineWindows, + ServerName: "localhost", + MinVersion: version, + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }) + if err != nil { + t.Fatal(err) + } + return clientConn, done +} + +func startWindowsEchoEngineServer(t *testing.T, minVersion uint16) (*windowsTLSConn, <-chan struct{}) { + t.Helper() + + serverCertificate, serverCertificatePEM := newWindowsTestCertificate(t, "localhost") + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { listener.Close() }) + + done := make(chan struct{}) + go func() { + defer close(done) + conn, acceptErr := listener.Accept() + if acceptErr != nil { + return + } + defer conn.Close() + + tlsConn := stdtls.Server(conn, &stdtls.Config{ + Certificates: []stdtls.Certificate{serverCertificate}, + MinVersion: minVersion, + MaxVersion: minVersion, + }) + defer tlsConn.Close() + + handshakeErr := tlsConn.Handshake() + if handshakeErr != nil { + return + } + + buffer := make([]byte, 32*1024) + for { + n, readErr := tlsConn.Read(buffer) + if n > 0 { + _, writeErr := tlsConn.Write(buffer[:n]) + if writeErr != nil { + return + } + } + if readErr != nil { + return + } + } + }() + + version := "1.2" + if minVersion == stdtls.VersionTLS13 { + version = "1.3" + } + clientConn, err := newWindowsTestEngineConn(t, listener.Addr().String(), option.OutboundTLSOptions{ + Enabled: true, + Engine: C.TLSEngineWindows, + ServerName: "localhost", + MinVersion: version, + Certificate: badoption.Listable[string]{serverCertificatePEM}, + }) + if err != nil { + t.Fatal(err) + } + return clientConn, done +} diff --git a/constant/tls.go b/constant/tls.go index c81740a492..fdd813b758 100644 --- a/constant/tls.go +++ b/constant/tls.go @@ -4,5 +4,7 @@ const ACMETLS1Protocol = "acme-tls/1" const ( TLSEngineDefault = "" + TLSEngineGo = "go" TLSEngineApple = "apple" + TLSEngineWindows = "windows" ) diff --git a/docs/configuration/shared/tls.md b/docs/configuration/shared/tls.md index f7e510b384..3b74e890ae 100644 --- a/docs/configuration/shared/tls.md +++ b/docs/configuration/shared/tls.md @@ -8,6 +8,7 @@ icon: material/new-box :material-plus: [handshake_timeout](#handshake_timeout) :material-plus: [spoof](#spoof) :material-plus: [spoof_method](#spoof_method) + :material-plus: [engine](#engine) :material-delete-clock: [acme](#acme-fields) !!! quote "Changes in sing-box 1.13.0" @@ -195,6 +196,8 @@ Enable TLS. #### engine +!!! question "Since sing-box 1.14.0" + ==Client only== TLS engine to use. @@ -203,15 +206,40 @@ Values: * `go` (default) * `apple` +* `windows` -`apple` uses Network.framework, only available on Apple platforms and only supports **direct** TCP TLS client connections. +Supported fields: -!!! warning "" +* `server_name` +* `insecure` +* `alpn` +* `min_version` +* `max_version` +* `certificate` / `certificate_path` +* `certificate_public_key_sha256` +* `handshake_timeout` + +Unsupported fields: + +* `disable_sni` +* `cipher_suites` +* `curve_preferences` +* `client_certificate` / `client_certificate_path` / `client_key` / `client_key_path` +* `fragment` / `record_fragment` +* `kernel_tx` / `kernel_rx` +* `ech` +* `utls` +* `reality` + +!!! note "" + + `windows` uses Schannel via SSPI. Only available on Windows build 17763 or later (Windows 10 version 1809, Windows Server 2019, or newer). + +!!! note "" + + TLS 1.3 is only negotiated on Windows 11 or Windows Server 2022 and newer. On older Windows versions, Schannel caps the connection at TLS 1.2 even when `max_version` is `1.3`. - Experimental only: due to the high memory overhead of both CGO and Network.framework, - do not use in hot paths on iOS and tvOS. - If you want to circumvent TLS fingerprint-based proxy censorship, - use [NaiveProxy](/configuration/outbound/naive/) instead. +The default version range is TLS 1.2 to TLS 1.3, matching the `go` engine. Supported fields: diff --git a/docs/configuration/shared/tls.zh.md b/docs/configuration/shared/tls.zh.md index 7ae851ed71..3cc411da45 100644 --- a/docs/configuration/shared/tls.zh.md +++ b/docs/configuration/shared/tls.zh.md @@ -8,6 +8,7 @@ icon: material/new-box :material-plus: [handshake_timeout](#handshake_timeout) :material-plus: [spoof](#spoof) :material-plus: [spoof_method](#spoof_method) + :material-plus: [engine](#engine) :material-delete-clock: [acme](#acme-字段) !!! quote "sing-box 1.13.0 中的更改" @@ -195,6 +196,8 @@ TLS 版本值: #### engine +!!! question "自 sing-box 1.14.0 起" + ==仅客户端== 要使用的 TLS 引擎。 @@ -203,14 +206,40 @@ TLS 版本值: * `go`(默认) * `apple` +* `windows` -`apple` 使用 Network.framework,仅在 Apple 平台可用,且仅支持 **直接** TCP TLS 客户端连接。 +支持的字段: -!!! warning "" +* `server_name` +* `insecure` +* `alpn` +* `min_version` +* `max_version` +* `certificate` / `certificate_path` +* `certificate_public_key_sha256` +* `handshake_timeout` + +不支持的字段: + +* `disable_sni` +* `cipher_suites` +* `curve_preferences` +* `client_certificate` / `client_certificate_path` / `client_key` / `client_key_path` +* `fragment` / `record_fragment` +* `kernel_tx` / `kernel_rx` +* `ech` +* `utls` +* `reality` + +!!! note "" + + `windows` 通过 SSPI 使用 Schannel,仅在 Windows build 17763 及以上可用,包括 Windows 10 版本 1809、Windows Server 2019 及后续版本。 + +!!! note "" + + TLS 1.3 仅在 Windows 11 或 Windows Server 2022 及后续版本上协商。在更早的 Windows 版本上,即使 `max_version` 设为 `1.3`,Schannel 也会把连接上限固定在 TLS 1.2。 - 仅供实验用途:由于 CGO 和 Network.framework 占用的内存都很多, - 不应在 iOS 和 tvOS 的热路径中使用。 - 如果您想规避基于 TLS 指纹的代理审查,应使用 [NaiveProxy](/zh/configuration/outbound/naive/)。 +默认版本范围为 TLS 1.2 到 TLS 1.3,与 `go` 引擎一致。证书验证在 Go 侧基于 Schannel 返回的证书链执行,默认使用系统证书存储。当设置了 `certificate` 或 `certificate_path` 时,这些根证书会替代系统存储。 支持的字段: From d9c9edd6f88b2f1775a5afc44306a44d83ccd36d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 24 Apr 2026 10:31:06 +0800 Subject: [PATCH 56/93] tun: Add compatibility with docker bridge --- go.mod | 2 +- go.sum | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 0e76fa5f73..5693a8b728 100644 --- a/go.mod +++ b/go.mod @@ -44,7 +44,7 @@ require ( github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 - github.com/sagernet/sing-tun v0.8.10-0.20260420012056-73f8bbda86a6 + github.com/sagernet/sing-tun v0.8.10-0.20260424013140-ab5c89505846 github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 github.com/sagernet/smux v1.5.50-sing-box-mod.1 github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7 diff --git a/go.sum b/go.sum index 8ec6f38a6e..acc16a0003 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,6 @@ github.com/anthropics/anthropic-sdk-go v1.26.0 h1:oUTzFaUpAevfuELAP1sjL6CQJ9HHAf github.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q= github.com/anytls/sing-anytls v0.0.11 h1:w8e9Uj1oP3m4zxkyZDewPk0EcQbvVxb7Nn+rapEx4fc= github.com/anytls/sing-anytls v0.0.11/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8= -github.com/caddyserver/certmagic v0.25.2 h1:D7xcS7ggX/WEY54x0czj7ioTkmDWKIgxtIi2OcQclUc= -github.com/caddyserver/certmagic v0.25.2/go.mod h1:llW/CvsNmza8S6hmsuggsZeiX+uS27dkqY27wDIuBWg= github.com/caddyserver/certmagic v0.25.3-0.20260421143802-60d9d8b415d6 h1:LYSB6VgWzKtNrcxElw3c97BP40Oc7bizKxA9K1Vi/5k= github.com/caddyserver/certmagic v0.25.3-0.20260421143802-60d9d8b415d6/go.mod h1:llW/CvsNmza8S6hmsuggsZeiX+uS27dkqY27wDIuBWg= github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE= @@ -258,8 +256,8 @@ github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnq github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA= -github.com/sagernet/sing-tun v0.8.10-0.20260420012056-73f8bbda86a6 h1:44lj7uQQES94KGjTEInxmj+b3C9aVfYT4yv5Jf/nL1s= -github.com/sagernet/sing-tun v0.8.10-0.20260420012056-73f8bbda86a6/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ= +github.com/sagernet/sing-tun v0.8.10-0.20260424013140-ab5c89505846 h1:X7Y507oQkoeWaSQfYzRk1CN35GFiRqVuwtc3Z+WDcfY= +github.com/sagernet/sing-tun v0.8.10-0.20260424013140-ab5c89505846/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY= github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478= From fd1624e426607ca7f669faff7197ead5bc1598f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 28 Apr 2026 08:10:05 +0800 Subject: [PATCH 57/93] Preserve comments between formatting --- cmd/sing-box/cmd_format.go | 2 ++ go.mod | 2 +- go.sum | 4 ++-- option/options.go | 13 +++++++++++++ 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/cmd/sing-box/cmd_format.go b/cmd/sing-box/cmd_format.go index ab59c9ae5d..a8ecb6ab72 100644 --- a/cmd/sing-box/cmd_format.go +++ b/cmd/sing-box/cmd_format.go @@ -38,10 +38,12 @@ func format() error { return err } for _, optionsEntry := range optionsList { + comments := optionsEntry.options.Comments() optionsEntry.options, err = badjson.Omitempty(globalCtx, optionsEntry.options) if err != nil { return err } + optionsEntry.options.SetComments(comments) buffer := new(bytes.Buffer) encoder := json.NewEncoder(buffer) encoder.SetIndent("", " ") diff --git a/go.mod b/go.mod index 5693a8b728..8b6730cb34 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,7 @@ require ( github.com/sagernet/gomobile v0.1.12 github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 - github.com/sagernet/sing v0.8.10-0.20260424005254-7b2d7ac5204c + github.com/sagernet/sing v0.8.10-0.20260427232324-f758c54b7409 github.com/sagernet/sing-cloudflared v0.1.0 github.com/sagernet/sing-mux v0.3.4 github.com/sagernet/sing-quic v0.6.2-0.20260412143638-8f65b6be7cd6 diff --git a/go.sum b/go.sum index acc16a0003..faafae0166 100644 --- a/go.sum +++ b/go.sum @@ -242,8 +242,8 @@ github.com/sagernet/nftables v0.3.0-mod.2 h1:ck2KMU02OxL1eDFgGaWYglMDpoOZ7OHzxje github.com/sagernet/nftables v0.3.0-mod.2/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4= -github.com/sagernet/sing v0.8.10-0.20260424005254-7b2d7ac5204c h1:hSVSiYyv3x0wNn38mnlOwoTwod+vW4XE251KG/uaA4U= -github.com/sagernet/sing v0.8.10-0.20260424005254-7b2d7ac5204c/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing v0.8.10-0.20260427232324-f758c54b7409 h1:TjqWXRWjiZi98SGX0Rmdl/XF7066+q3Idf+xjqJFF1U= +github.com/sagernet/sing v0.8.10-0.20260427232324-f758c54b7409/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA= github.com/sagernet/sing-cloudflared v0.1.0 h1:to+2fcCx8zu4X/DirRw9Ihc+FrEZ7oEyIqeCoJiwIpw= github.com/sagernet/sing-cloudflared v0.1.0/go.mod h1:bH2NKX+NpDTY1Zkxfboxw6MXB/ZywaNLmrDJYgKMJ2Y= github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s= diff --git a/option/options.go b/option/options.go index 1b4685bac8..4e87852ac3 100644 --- a/option/options.go +++ b/option/options.go @@ -11,6 +11,7 @@ import ( type _Options struct { RawMessage json.RawMessage `json:"-"` + CommentsSet *json.CommentSet `json:"-"` Schema string `json:"$schema,omitempty"` Log *LogOptions `json:"log,omitempty"` DNS *DNSOptions `json:"dns,omitempty"` @@ -28,6 +29,10 @@ type _Options struct { type Options _Options +func (o Options) MarshalJSONContext(ctx context.Context) ([]byte, error) { + return json.MarshalContext(ctx, (_Options)(o)) +} + func (o *Options) UnmarshalJSONContext(ctx context.Context, content []byte) error { decoder := json.NewDecoderContext(ctx, bytes.NewReader(content)) decoder.DisallowUnknownFields() @@ -39,6 +44,14 @@ func (o *Options) UnmarshalJSONContext(ctx context.Context, content []byte) erro return checkOptions(o) } +func (o Options) Comments() *json.CommentSet { + return o.CommentsSet +} + +func (o *Options) SetComments(comments *json.CommentSet) { + o.CommentsSet = comments +} + type LogOptions struct { Disabled bool `json:"disabled,omitempty"` Level string `json:"level,omitempty"` From b418ee1a98afbefca99593432e3390c183cd7e07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 28 Apr 2026 08:18:08 +0800 Subject: [PATCH 58/93] Improve oom-killer --- dns/router.go | 3 +++ experimental/clashapi/server.go | 4 ++++ .../clashapi/trafficontrol/manager.go | 7 ++++--- service/oomkiller/badcleanup.go | 19 +++++++++++++++++++ service/oomkiller/badcleanup_stub.go | 6 ++++++ service/oomkiller/service_darwin.go | 2 -- service/oomkiller/timer.go | 14 +++++++++----- 7 files changed, 45 insertions(+), 10 deletions(-) create mode 100644 service/oomkiller/badcleanup.go create mode 100644 service/oomkiller/badcleanup_stub.go diff --git a/dns/router.go b/dns/router.go index adde3bac0c..c0d681abdb 100644 --- a/dns/router.go +++ b/dns/router.go @@ -856,6 +856,9 @@ func (r *Router) ClearCache() { if r.platformInterface != nil { r.platformInterface.ClearDNSCache() } + if r.dnsReverseMapping != nil { + r.dnsReverseMapping.Purge() + } } func (r *Router) LookupReverseMapping(ip netip.Addr) (string, bool) { diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index ec40a95fc2..20cea0bf9e 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -21,6 +21,7 @@ import ( "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/cleanup" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" N "github.com/sagernet/sing/common/network" @@ -51,6 +52,7 @@ type Server struct { trafficManager *trafficontrol.Manager urlTestHistory adapter.URLTestHistoryStorage logDebug bool + cleaner *cleanup.Cleaner mode string modeList []string @@ -82,6 +84,7 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op externalController: options.ExternalController != "", externalUIDownloadURL: options.ExternalUIDownloadURL, externalUIDownloadDetour: options.ExternalUIDownloadDetour, + cleaner: cleanup.Add(trafficManager.Clear), } s.urlTestHistory = service.FromContext[adapter.URLTestHistoryStorage](ctx) if s.urlTestHistory == nil { @@ -193,6 +196,7 @@ func (s *Server) Close() error { common.PtrOrNil(s.httpServer), s.trafficManager, s.urlTestHistory, + common.PtrOrNil(s.cleaner), ) } diff --git a/experimental/clashapi/trafficontrol/manager.go b/experimental/clashapi/trafficontrol/manager.go index 6763436d84..6bd77def10 100644 --- a/experimental/clashapi/trafficontrol/manager.go +++ b/experimental/clashapi/trafficontrol/manager.go @@ -159,9 +159,10 @@ func (m *Manager) Snapshot() *Snapshot { } } -func (m *Manager) ResetStatistic() { - m.uploadTotal.Store(0) - m.downloadTotal.Store(0) +func (m *Manager) Clear() { + m.closedConnectionsAccess.Lock() + defer m.closedConnectionsAccess.Unlock() + m.closedConnections.Init() } type Snapshot struct { diff --git a/service/oomkiller/badcleanup.go b/service/oomkiller/badcleanup.go new file mode 100644 index 0000000000..4ba4b0cef8 --- /dev/null +++ b/service/oomkiller/badcleanup.go @@ -0,0 +1,19 @@ +//go:build badlinkname + +package oomkiller + +import ( + "sync" + _ "unsafe" +) + +//go:linkname jsonFieldCache json.fieldCache +var jsonFieldCache sync.Map + +//go:linkname contextJSONFieldCache github.com/sagernet/sing/common/json/internal/contextjson.fieldCache +var contextJSONFieldCache sync.Map + +func badCleanup() { + jsonFieldCache.Clear() + contextJSONFieldCache.Clear() +} diff --git a/service/oomkiller/badcleanup_stub.go b/service/oomkiller/badcleanup_stub.go new file mode 100644 index 0000000000..d734eb9f16 --- /dev/null +++ b/service/oomkiller/badcleanup_stub.go @@ -0,0 +1,6 @@ +//go:build !badlinkname + +package oomkiller + +func badCleanup() { +} diff --git a/service/oomkiller/service_darwin.go b/service/oomkiller/service_darwin.go index a40daea10e..ad3164b831 100644 --- a/service/oomkiller/service_darwin.go +++ b/service/oomkiller/service_darwin.go @@ -33,7 +33,6 @@ static void stopMemoryPressureMonitor() { import "C" import ( - runtimeDebug "runtime/debug" "sync" "github.com/sagernet/sing-box/adapter" @@ -90,7 +89,6 @@ func (s *Service) Close() error { //export goMemoryPressureCallback func goMemoryPressureCallback(status C.ulong) { - runtimeDebug.FreeOSMemory() globalAccess.Lock() services := make([]*Service, len(globalServices)) copy(services, globalServices) diff --git a/service/oomkiller/timer.go b/service/oomkiller/timer.go index a5bef3a710..f2070ce3f0 100644 --- a/service/oomkiller/timer.go +++ b/service/oomkiller/timer.go @@ -105,6 +105,7 @@ type adaptiveTimer struct { limitThresholds pressureThresholds access sync.Mutex + cleanupTriggered bool timer *time.Timer state pressureState currentInterval time.Duration @@ -161,10 +162,6 @@ func (t *adaptiveTimer) stop() { } func (t *adaptiveTimer) poll() { - if t.timerConfig.policyMode == policyModeNetworkExtension { - runtimeDebug.FreeOSMemory() - } - var triggered bool var rateTriggered bool sample := readMemorySample(t.policyMode) @@ -174,6 +171,12 @@ func (t *adaptiveTimer) poll() { t.access.Unlock() return } + if t.timerConfig.policyMode == policyModeNetworkExtension { + if t.cleanupTriggered { + runtimeDebug.FreeOSMemory() + t.cleanupTriggered = true + } + } if t.pendingPressureBaseline { t.pressureBaseline = sample t.pressureBaselineTime = time.Now() @@ -205,10 +208,10 @@ func (t *adaptiveTimer) poll() { } } t.access.Unlock() - if !triggered { return } + t.cleanupTriggered = false t.onTriggered(sample.usage) if rateTriggered { if t.killerDisabled { @@ -225,6 +228,7 @@ func (t *adaptiveTimer) poll() { t.router.ResetNetwork() } } + badCleanup() runtimeDebug.FreeOSMemory() } From 78f502d016f0649e7327ce24e4c0f8b099470175 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 28 Apr 2026 08:20:29 +0800 Subject: [PATCH 59/93] tun: Add read waiter support for gVisor conn --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8b6730cb34..94e49c8c1f 100644 --- a/go.mod +++ b/go.mod @@ -44,7 +44,7 @@ require ( github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 - github.com/sagernet/sing-tun v0.8.10-0.20260424013140-ab5c89505846 + github.com/sagernet/sing-tun v0.8.10-0.20260427231103-812b89ea042d github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 github.com/sagernet/smux v1.5.50-sing-box-mod.1 github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7 diff --git a/go.sum b/go.sum index faafae0166..9efd5bb815 100644 --- a/go.sum +++ b/go.sum @@ -256,8 +256,8 @@ github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnq github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA= -github.com/sagernet/sing-tun v0.8.10-0.20260424013140-ab5c89505846 h1:X7Y507oQkoeWaSQfYzRk1CN35GFiRqVuwtc3Z+WDcfY= -github.com/sagernet/sing-tun v0.8.10-0.20260424013140-ab5c89505846/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ= +github.com/sagernet/sing-tun v0.8.10-0.20260427231103-812b89ea042d h1:rcFzy3rMpx9M/Zel+YLd2iNGHl0ElH7T8Pl7Y6oxPOQ= +github.com/sagernet/sing-tun v0.8.10-0.20260427231103-812b89ea042d/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY= github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478= From 610aa9432d63497a88d3ed1ddd921c0115d5901c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 28 Apr 2026 09:01:24 +0800 Subject: [PATCH 60/93] ssh: Add cipher, MAC, and key exchange configuration --- docs/configuration/outbound/ssh.md | 27 +++++++++++++++++++++++++++ docs/configuration/outbound/ssh.zh.md | 27 +++++++++++++++++++++++++++ option/ssh.go | 3 +++ protocol/ssh/outbound.go | 15 +++++++++++++++ 4 files changed, 72 insertions(+) diff --git a/docs/configuration/outbound/ssh.md b/docs/configuration/outbound/ssh.md index 45ec72b2b5..8ef49b9cac 100644 --- a/docs/configuration/outbound/ssh.md +++ b/docs/configuration/outbound/ssh.md @@ -1,3 +1,9 @@ +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [cipher](#cipher) + :material-plus: [mac](#mac) + :material-plus: [kex_algorithm](#kex_algorithm) + ### Structure ```json @@ -17,6 +23,9 @@ ], "host_key_algorithms": [], "client_version": "SSH-2.0-OpenSSH_7.4p1", + "cipher": [], + "mac": [], + "kex_algorithm": [], ... // Dial Fields } @@ -66,6 +75,24 @@ Host key algorithms. Client version. Random version will be used if empty. +#### cipher + +!!! question "Since sing-box 1.14.0" + +Allowed ciphers. Default values are used if empty. + +#### mac + +!!! question "Since sing-box 1.14.0" + +Allowed MAC algorithms. Default values are used if empty. + +#### kex_algorithm + +!!! question "Since sing-box 1.14.0" + +Allowed key exchange algorithms. Default values are used if empty. + ### Dial Fields See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/outbound/ssh.zh.md b/docs/configuration/outbound/ssh.zh.md index e538e64aa8..cb2c0345e4 100644 --- a/docs/configuration/outbound/ssh.zh.md +++ b/docs/configuration/outbound/ssh.zh.md @@ -1,3 +1,9 @@ +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [cipher](#cipher) + :material-plus: [mac](#mac) + :material-plus: [kex_algorithm](#kex_algorithm) + ### 结构 ```json @@ -17,6 +23,9 @@ ], "host_key_algorithms": [], "client_version": "SSH-2.0-OpenSSH_7.4p1", + "cipher": [], + "mac": [], + "kex_algorithm": [], ... // 拨号字段 } @@ -66,6 +75,24 @@ SSH 用户, 默认使用 root。 客户端版本,默认使用随机值。 +#### cipher + +!!! question "自 sing-box 1.14.0 起" + +允许的加密算法。留空使用默认值。 + +#### mac + +!!! question "自 sing-box 1.14.0 起" + +允许的 MAC 算法。留空使用默认值。 + +#### kex_algorithm + +!!! question "自 sing-box 1.14.0 起" + +允许的密钥交换算法。留空使用默认值。 + ### 拨号字段 参阅 [拨号字段](/zh/configuration/shared/dial/)。 diff --git a/option/ssh.go b/option/ssh.go index 1c6ca6bb96..8ead4f670e 100644 --- a/option/ssh.go +++ b/option/ssh.go @@ -13,4 +13,7 @@ type SSHOutboundOptions struct { HostKey badoption.Listable[string] `json:"host_key,omitempty"` HostKeyAlgorithms badoption.Listable[string] `json:"host_key_algorithms,omitempty"` ClientVersion string `json:"client_version,omitempty"` + Cipher badoption.Listable[string] `json:"cipher,omitempty"` + MAC badoption.Listable[string] `json:"mac,omitempty"` + KexAlgorithm badoption.Listable[string] `json:"kex_algorithm,omitempty"` } diff --git a/protocol/ssh/outbound.go b/protocol/ssh/outbound.go index b2f9680747..380275ca8b 100644 --- a/protocol/ssh/outbound.go +++ b/protocol/ssh/outbound.go @@ -42,6 +42,9 @@ type Outbound struct { user string hostKey []ssh.PublicKey hostKeyAlgorithms []string + cipher []string + mac []string + kexAlgorithm []string clientVersion string authMethod []ssh.AuthMethod clientAccess sync.Mutex @@ -62,6 +65,9 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL serverAddr: options.ServerOptions.Build(), user: options.User, hostKeyAlgorithms: options.HostKeyAlgorithms, + cipher: options.Cipher, + mac: options.MAC, + kexAlgorithm: options.KexAlgorithm, clientVersion: options.ClientVersion, } if outbound.serverAddr.Port == 0 { @@ -155,6 +161,15 @@ func (s *Outbound) connect() (*ssh.Client, error) { return E.New("host key mismatch, server send ", key.Type(), " ", base64.StdEncoding.EncodeToString(serverKey)) }, } + if len(s.cipher) > 0 { + config.Ciphers = s.cipher + } + if len(s.mac) > 0 { + config.MACs = s.mac + } + if len(s.kexAlgorithm) > 0 { + config.KeyExchanges = s.kexAlgorithm + } clientConn, chans, reqs, err := ssh.NewClientConn(conn, s.serverAddr.Addr.String(), config) if err != nil { conn.Close() From 60a5f874e2ad41c7861d2a83b962be59546c07ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 28 Apr 2026 09:05:41 +0800 Subject: [PATCH 61/93] Cleanup compatible code for legacy Go --- log/id.go | 6 ------ protocol/dns/handle.go | 4 ++-- protocol/tailscale/tun_device_unix.go | 3 +-- transport/v2raygrpc/client.go | 3 +-- transport/wireguard/client_bind.go | 2 +- transport/wireguard/device_system.go | 2 +- transport/wireguard/device_system_stack.go | 3 +-- 7 files changed, 7 insertions(+), 16 deletions(-) diff --git a/log/id.go b/log/id.go index 7cac29d2a4..e23ed3d5e8 100644 --- a/log/id.go +++ b/log/id.go @@ -4,14 +4,8 @@ import ( "context" "math/rand" "time" - - "github.com/sagernet/sing/common/random" ) -func init() { - random.InitializeSeed() -} - type idKey struct{} type ID struct { diff --git a/protocol/dns/handle.go b/protocol/dns/handle.go index e132350996..d7d89ca8a9 100644 --- a/protocol/dns/handle.go +++ b/protocol/dns/handle.go @@ -82,7 +82,7 @@ func NewDNSPacketConnection(ctx context.Context, router adapter.DNSRouter, conn } break } - fastClose, cancel := common.ContextWithCancelCause(ctx) + fastClose, cancel := context.WithCancelCause(ctx) timeout := canceler.New(fastClose, cancel, C.DNSTimeout) var group task.Group group.Append0(func(_ context.Context) error { @@ -150,7 +150,7 @@ func NewDNSPacketConnection(ctx context.Context, router adapter.DNSRouter, conn } func newDNSPacketConnection(ctx context.Context, router adapter.DNSRouter, conn N.PacketConn, readWaiter N.PacketReadWaiter, readCounters []N.CountFunc, cached []*N.PacketBuffer, metadata adapter.InboundContext) error { - fastClose, cancel := common.ContextWithCancelCause(ctx) + fastClose, cancel := context.WithCancelCause(ctx) timeout := canceler.New(fastClose, cancel, C.DNSTimeout) var group task.Group group.Append0(func(_ context.Context) error { diff --git a/protocol/tailscale/tun_device_unix.go b/protocol/tailscale/tun_device_unix.go index a8d237abf3..6df4e1044a 100644 --- a/protocol/tailscale/tun_device_unix.go +++ b/protocol/tailscale/tun_device_unix.go @@ -11,7 +11,6 @@ import ( "sync/atomic" singTun "github.com/sagernet/sing-tun" - "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/logger" wgTun "github.com/sagernet/wireguard-go/tun" ) @@ -92,7 +91,7 @@ func (a *tunDeviceAdapter) Write(bufs [][]byte, offset int) (count int, err erro for _, packet := range bufs { a.debugPacket("write", packet[offset:]) if singTun.PacketOffset > 0 { - common.ClearArray(packet[offset-singTun.PacketOffset : offset]) + clear(packet[offset-singTun.PacketOffset : offset]) singTun.PacketFillHeader(packet[offset-singTun.PacketOffset:], singTun.PacketIPVersion(packet[offset:])) } _, err = a.tun.Write(packet[offset-singTun.PacketOffset:]) diff --git a/transport/v2raygrpc/client.go b/transport/v2raygrpc/client.go index 5af53856fd..a915e1eb34 100644 --- a/transport/v2raygrpc/client.go +++ b/transport/v2raygrpc/client.go @@ -10,7 +10,6 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing/common" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" @@ -100,7 +99,7 @@ func (c *Client) DialContext(ctx context.Context) (net.Conn, error) { return nil, err } client := NewGunServiceClient(clientConn).(GunServiceCustomNameClient) - ctx, cancel := common.ContextWithCancelCause(ctx) + ctx, cancel := context.WithCancelCause(ctx) stream, err := client.TunCustomName(ctx, c.serviceName) if err != nil { cancel(err) diff --git a/transport/wireguard/client_bind.go b/transport/wireguard/client_bind.go index 54b7be86cc..e0e8b645a3 100644 --- a/transport/wireguard/client_bind.go +++ b/transport/wireguard/client_bind.go @@ -136,7 +136,7 @@ func (c *ClientBind) receive(packets [][]byte, sizes []int, eps []conn.Endpoint) sizes[0] = n if n > 3 { b := packets[0] - common.ClearArray(b[1:4]) + clear(b[1:4]) } eps[0] = remoteEndpoint(M.SocksaddrFromNet(addr).Unwrap().AddrPort()) count = 1 diff --git a/transport/wireguard/device_system.go b/transport/wireguard/device_system.go index dcf2959b63..4e5d8da0f3 100644 --- a/transport/wireguard/device_system.go +++ b/transport/wireguard/device_system.go @@ -147,7 +147,7 @@ func (w *systemDevice) Write(bufs [][]byte, offset int) (count int, err error) { } else { for _, packet := range bufs { if tun.PacketOffset > 0 { - common.ClearArray(packet[offset-tun.PacketOffset : offset]) + clear(packet[offset-tun.PacketOffset : offset]) tun.PacketFillHeader(packet[offset-tun.PacketOffset:], tun.PacketIPVersion(packet[offset:])) } _, err = w.device.Write(packet[offset-tun.PacketOffset:]) diff --git a/transport/wireguard/device_system_stack.go b/transport/wireguard/device_system_stack.go index 94fd6f4f97..d3b47222aa 100644 --- a/transport/wireguard/device_system_stack.go +++ b/transport/wireguard/device_system_stack.go @@ -20,7 +20,6 @@ import ( "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun/ping" - "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" "github.com/sagernet/wireguard-go/device" @@ -110,7 +109,7 @@ func (w *systemStackDevice) Write(bufs [][]byte, offset int) (count int, err err for _, packet := range bufs { if !w.writeStack(packet[offset:]) { if tun.PacketOffset > 0 { - common.ClearArray(packet[offset-tun.PacketOffset : offset]) + clear(packet[offset-tun.PacketOffset : offset]) tun.PacketFillHeader(packet[offset-tun.PacketOffset:], tun.PacketIPVersion(packet[offset:])) } _, err = w.device.Write(packet[offset-tun.PacketOffset:]) From 1bad8d87eb5390a3cf526ddcfdb8b6e6629d73f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 28 Apr 2026 14:29:52 +0800 Subject: [PATCH 62/93] dns: Add timeout configuration --- adapter/dns.go | 2 ++ common/dialer/dialer.go | 1 + dns/client.go | 11 +++++++---- dns/router.go | 10 ++++++++++ docs/configuration/dns/index.md | 14 +++++++++++++- docs/configuration/dns/index.zh.md | 14 +++++++++++++- docs/configuration/dns/rule_action.md | 22 +++++++++++++++++++++- docs/configuration/dns/rule_action.zh.md | 22 +++++++++++++++++++++- docs/configuration/route/rule_action.md | 12 +++++++++++- docs/configuration/route/rule_action.zh.md | 12 +++++++++++- docs/configuration/shared/dial.md | 4 ++++ docs/configuration/shared/dial.zh.md | 4 ++++ option/dns.go | 1 + option/outbound.go | 2 ++ option/rule_action.go | 3 +++ route/network.go | 1 + route/route.go | 1 + route/rule/rule_action.go | 15 +++++++++++++++ 18 files changed, 141 insertions(+), 10 deletions(-) diff --git a/adapter/dns.go b/adapter/dns.go index 7545f16633..eeaf12a44c 100644 --- a/adapter/dns.go +++ b/adapter/dns.go @@ -38,6 +38,7 @@ type DNSQueryOptions struct { DisableCache bool DisableOptimisticCache bool RewriteTTL *uint32 + Timeout time.Duration ClientSubnet netip.Prefix } @@ -56,6 +57,7 @@ func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptio DisableCache: options.DisableCache, DisableOptimisticCache: options.DisableOptimisticCache, RewriteTTL: options.RewriteTTL, + Timeout: time.Duration(options.Timeout), ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}), }, nil } diff --git a/common/dialer/dialer.go b/common/dialer/dialer.go index f78aa9f3d9..f0e8043093 100644 --- a/common/dialer/dialer.go +++ b/common/dialer/dialer.go @@ -97,6 +97,7 @@ func NewWithOptions(options Options) (N.Dialer, error) { dnsQueryOptions = adapter.DNSQueryOptions{ Transport: transport, Strategy: strategy, + Timeout: time.Duration(dialOptions.DomainResolver.Timeout), DisableCache: dialOptions.DomainResolver.DisableCache, DisableOptimisticCache: dialOptions.DomainResolver.DisableOptimisticCache, RewriteTTL: dialOptions.DomainResolver.RewriteTTL, diff --git a/dns/client.go b/dns/client.go index 318ee2322f..72f264a609 100644 --- a/dns/client.go +++ b/dns/client.go @@ -222,7 +222,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m return nil, ErrResponseRejectedCached } } - response, err := c.exchangeToTransport(ctx, transport, message) + response, err := c.exchangeToTransport(ctx, transport, message, options.Timeout) if err != nil { return nil, err } @@ -497,7 +497,7 @@ func (c *Client) backgroundRefreshDNS(transport adapter.DNSTransport, question d go func() { defer c.backgroundRefresh.Delete(key) ctx := contextWithTransportTag(c.ctx, transport.Tag()) - response, err := c.exchangeToTransport(ctx, transport, message) + response, err := c.exchangeToTransport(ctx, transport, message, options.Timeout) if err != nil { if c.logger != nil { c.logger.DebugContext(ctx, "optimistic refresh failed for ", FqdnToDomain(question.Name), ": ", err) @@ -552,8 +552,11 @@ func stripDNSPadding(response *dns.Msg) { } } -func (c *Client) exchangeToTransport(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg) (*dns.Msg, error) { - ctx, cancel := context.WithTimeout(ctx, c.timeout) +func (c *Client) exchangeToTransport(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, timeout time.Duration) (*dns.Msg, error) { + if timeout == 0 { + timeout = c.timeout + } + ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() response, err := transport.Exchange(ctx, message) if err == nil { diff --git a/dns/router.go b/dns/router.go index c0d681abdb..ceb9ea2cdb 100644 --- a/dns/router.go +++ b/dns/router.go @@ -80,6 +80,7 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOp } router.client = NewClient(ClientOptions{ Context: ctx, + Timeout: time.Duration(options.DNSClientOptions.Timeout), DisableCache: options.DNSClientOptions.DisableCache, DisableExpire: options.DNSClientOptions.DisableExpire, OptimisticTimeout: optimisticTimeout, @@ -314,6 +315,9 @@ func (r *Router) matchDNS(ctx context.Context, rules []adapter.DNSRule, allowFak if action.RewriteTTL != nil { options.RewriteTTL = action.RewriteTTL } + if action.Timeout > 0 { + options.Timeout = action.Timeout + } if action.ClientSubnet.IsValid() { options.ClientSubnet = action.ClientSubnet } @@ -328,6 +332,9 @@ func (r *Router) matchDNS(ctx context.Context, rules []adapter.DNSRule, allowFak if action.RewriteTTL != nil { options.RewriteTTL = action.RewriteTTL } + if action.Timeout > 0 { + options.Timeout = action.Timeout + } if action.ClientSubnet.IsValid() { options.ClientSubnet = action.ClientSubnet } @@ -355,6 +362,9 @@ func (r *Router) applyDNSRouteOptions(options *adapter.DNSQueryOptions, routeOpt if routeOptions.RewriteTTL != nil { options.RewriteTTL = routeOptions.RewriteTTL } + if routeOptions.Timeout > 0 { + options.Timeout = routeOptions.Timeout + } if routeOptions.ClientSubnet.IsValid() { options.ClientSubnet = routeOptions.ClientSubnet } diff --git a/docs/configuration/dns/index.md b/docs/configuration/dns/index.md index b78a49e7ac..19a183ac25 100644 --- a/docs/configuration/dns/index.md +++ b/docs/configuration/dns/index.md @@ -5,7 +5,8 @@ icon: material/alert-decagram !!! quote "Changes in sing-box 1.14.0" :material-delete-clock: [independent_cache](#independent_cache) - :material-plus: [optimistic](#optimistic) + :material-plus: [optimistic](#optimistic) + :material-plus: [timeout](#timeout) !!! quote "Changes in sing-box 1.12.0" @@ -31,6 +32,7 @@ icon: material/alert-decagram "independent_cache": false, "cache_capacity": 0, "optimistic": false, // or {} + "timeout": "", "reverse_mapping": false, "client_subnet": "", "fakeip": {} @@ -115,6 +117,16 @@ The maximum time an expired cache entry can be served optimistically. `3d` is used by default. +#### timeout + +!!! question "Since sing-box 1.14.0" + +Default timeout for each DNS query. + +`10s` is used by default. + +Can be overridden by `rules.[].timeout` (DNS rule action) or `domain_resolver.timeout`. + #### reverse_mapping Stores a reverse mapping of IP addresses after responding to a DNS query in order to provide domain names when routing. diff --git a/docs/configuration/dns/index.zh.md b/docs/configuration/dns/index.zh.md index ae06b8ab6d..e5c2213953 100644 --- a/docs/configuration/dns/index.zh.md +++ b/docs/configuration/dns/index.zh.md @@ -5,7 +5,8 @@ icon: material/alert-decagram !!! quote "sing-box 1.14.0 中的更改" :material-delete-clock: [independent_cache](#independent_cache) - :material-plus: [optimistic](#optimistic) + :material-plus: [optimistic](#optimistic) + :material-plus: [timeout](#timeout) !!! quote "sing-box 1.12.0 中的更改" @@ -31,6 +32,7 @@ icon: material/alert-decagram "independent_cache": false, "cache_capacity": 0, "optimistic": false, // or {} + "timeout": "", "reverse_mapping": false, "client_subnet": "", "fakeip": {} @@ -114,6 +116,16 @@ LRU 缓存容量。 默认使用 `3d`。 +#### timeout + +!!! question "自 sing-box 1.14.0 起" + +每次 DNS 查询的默认超时时间。 + +默认使用 `10s`。 + +可被 `rules.[].timeout`(DNS 规则动作)或 `domain_resolver.timeout` 覆盖。 + #### reverse_mapping 在响应 DNS 查询后存储 IP 地址的反向映射以为路由目的提供域名。 diff --git a/docs/configuration/dns/rule_action.md b/docs/configuration/dns/rule_action.md index e5a99be3c8..3555d6ede3 100644 --- a/docs/configuration/dns/rule_action.md +++ b/docs/configuration/dns/rule_action.md @@ -7,7 +7,8 @@ icon: material/new-box :material-delete-clock: [strategy](#strategy) :material-plus: [evaluate](#evaluate) :material-plus: [respond](#respond) - :material-plus: [disable_optimistic_cache](#disable_optimistic_cache) + :material-plus: [disable_optimistic_cache](#disable_optimistic_cache) + :material-plus: [timeout](#timeout) !!! quote "Changes in sing-box 1.12.0" @@ -26,6 +27,7 @@ icon: material/new-box "disable_cache": false, "disable_optimistic_cache": false, "rewrite_ttl": null, + "timeout": "", "client_subnet": null } ``` @@ -64,6 +66,14 @@ Disable optimistic DNS caching in this query. Rewrite TTL in DNS responses. +#### timeout + +!!! question "Since sing-box 1.14.0" + +Override the DNS query timeout for matched queries. + +Will override `dns.timeout`. + #### client_subnet Append a `edns0-subnet` OPT extra record with the specified IP prefix to every query by default. @@ -83,6 +93,7 @@ Will override `dns.client_subnet`. "disable_cache": false, "disable_optimistic_cache": false, "rewrite_ttl": null, + "timeout": "", "client_subnet": null } ``` @@ -116,6 +127,14 @@ Disable optimistic DNS caching in this query. Rewrite TTL in DNS responses. +#### timeout + +!!! question "Since sing-box 1.14.0" + +Override the DNS query timeout for matched queries. + +Will override `dns.timeout`. + #### client_subnet Append a `edns0-subnet` OPT extra record with the specified IP prefix to every query by default. @@ -148,6 +167,7 @@ Only allowed after a preceding top-level `evaluate` rule. If the action is reach "disable_cache": false, "disable_optimistic_cache": false, "rewrite_ttl": null, + "timeout": "", "client_subnet": null } ``` diff --git a/docs/configuration/dns/rule_action.zh.md b/docs/configuration/dns/rule_action.zh.md index 24179977f0..756051dd9b 100644 --- a/docs/configuration/dns/rule_action.zh.md +++ b/docs/configuration/dns/rule_action.zh.md @@ -7,7 +7,8 @@ icon: material/new-box :material-delete-clock: [strategy](#strategy) :material-plus: [evaluate](#evaluate) :material-plus: [respond](#respond) - :material-plus: [disable_optimistic_cache](#disable_optimistic_cache) + :material-plus: [disable_optimistic_cache](#disable_optimistic_cache) + :material-plus: [timeout](#timeout) !!! quote "sing-box 1.12.0 中的更改" @@ -26,6 +27,7 @@ icon: material/new-box "disable_cache": false, "disable_optimistic_cache": false, "rewrite_ttl": null, + "timeout": "", "client_subnet": null } ``` @@ -64,6 +66,14 @@ icon: material/new-box 重写 DNS 回应中的 TTL。 +#### timeout + +!!! question "自 sing-box 1.14.0 起" + +覆盖匹配查询的 DNS 查询超时时间。 + +将覆盖 `dns.timeout`。 + #### client_subnet 默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。 @@ -83,6 +93,7 @@ icon: material/new-box "disable_cache": false, "disable_optimistic_cache": false, "rewrite_ttl": null, + "timeout": "", "client_subnet": null } ``` @@ -114,6 +125,14 @@ icon: material/new-box 重写 DNS 回应中的 TTL。 +#### timeout + +!!! question "自 sing-box 1.14.0 起" + +覆盖匹配查询的 DNS 查询超时时间。 + +将覆盖 `dns.timeout`。 + #### client_subnet 默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。 @@ -146,6 +165,7 @@ icon: material/new-box "disable_cache": false, "disable_optimistic_cache": false, "rewrite_ttl": null, + "timeout": "", "client_subnet": null } ``` diff --git a/docs/configuration/route/rule_action.md b/docs/configuration/route/rule_action.md index 1ba690398d..f4dfad3840 100644 --- a/docs/configuration/route/rule_action.md +++ b/docs/configuration/route/rule_action.md @@ -9,7 +9,8 @@ icon: material/new-box !!! quote "Changes in sing-box 1.14.0" - :material-plus: [resolve.disable_optimistic_cache](#disable_optimistic_cache) + :material-plus: [resolve.disable_optimistic_cache](#disable_optimistic_cache) + :material-plus: [resolve.timeout](#timeout) !!! quote "Changes in sing-box 1.12.0" @@ -285,6 +286,7 @@ Timeout for sniffing. "disable_cache": false, "disable_optimistic_cache": false, "rewrite_ttl": null, + "timeout": "", "client_subnet": null } ``` @@ -319,6 +321,14 @@ Disable optimistic DNS caching in this query. Rewrite TTL in DNS responses. +#### timeout + +!!! question "Since sing-box 1.14.0" + +Override the DNS query timeout for this lookup. + +Will override `dns.timeout`. + #### client_subnet !!! question "Since sing-box 1.12.0" diff --git a/docs/configuration/route/rule_action.zh.md b/docs/configuration/route/rule_action.zh.md index f2ae41862b..f832a30d7e 100644 --- a/docs/configuration/route/rule_action.zh.md +++ b/docs/configuration/route/rule_action.zh.md @@ -9,7 +9,8 @@ icon: material/new-box !!! quote "sing-box 1.14.0 中的更改" - :material-plus: [resolve.disable_optimistic_cache](#disable_optimistic_cache) + :material-plus: [resolve.disable_optimistic_cache](#disable_optimistic_cache) + :material-plus: [resolve.timeout](#timeout) !!! quote "sing-box 1.12.0 中的更改" @@ -277,6 +278,7 @@ UDP 连接超时时间。 "disable_cache": false, "disable_optimistic_cache": false, "rewrite_ttl": null, + "timeout": "", "client_subnet": null } ``` @@ -311,6 +313,14 @@ DNS 解析策略,可用值有:`prefer_ipv4`、`prefer_ipv6`、`ipv4_only`、 重写 DNS 回应中的 TTL。 +#### timeout + +!!! question "自 sing-box 1.14.0 起" + +覆盖此查询的 DNS 查询超时时间。 + +将覆盖 `dns.timeout`。 + #### client_subnet !!! question "自 sing-box 1.12.0 起" diff --git a/docs/configuration/shared/dial.md b/docs/configuration/shared/dial.md index 306952fc4a..2ef2fbdb6e 100644 --- a/docs/configuration/shared/dial.md +++ b/docs/configuration/shared/dial.md @@ -2,6 +2,10 @@ icon: material/new-box --- +!!! quote "Changes in sing-box 1.14.0" + + :material-alert: [domain_resolver](#domain_resolver) + !!! quote "Changes in sing-box 1.13.0" :material-plus: [disable_tcp_keep_alive](#disable_tcp_keep_alive) diff --git a/docs/configuration/shared/dial.zh.md b/docs/configuration/shared/dial.zh.md index daf7f8e03c..24388fecfd 100644 --- a/docs/configuration/shared/dial.zh.md +++ b/docs/configuration/shared/dial.zh.md @@ -2,6 +2,10 @@ icon: material/new-box --- +!!! quote "sing-box 1.14.0 中的更改" + + :material-alert: [domain_resolver](#domain_resolver) + !!! quote "sing-box 1.13.0 中的更改" :material-plus: [disable_tcp_keep_alive](#disable_tcp_keep_alive) diff --git a/option/dns.go b/option/dns.go index c09b3d5f32..c0d131a309 100644 --- a/option/dns.go +++ b/option/dns.go @@ -48,6 +48,7 @@ func (o *DNSOptions) UnmarshalJSONContext(ctx context.Context, content []byte) e type DNSClientOptions struct { Strategy DomainStrategy `json:"strategy,omitempty"` + Timeout badoption.Duration `json:"timeout,omitempty"` DisableCache bool `json:"disable_cache,omitempty"` DisableExpire bool `json:"disable_expire,omitempty"` IndependentCache bool `json:"independent_cache,omitempty"` diff --git a/option/outbound.go b/option/outbound.go index d8fcb82214..aa32db8148 100644 --- a/option/outbound.go +++ b/option/outbound.go @@ -94,6 +94,7 @@ type DialerOptions struct { type _DomainResolveOptions struct { Server string `json:"server"` + Timeout badoption.Duration `json:"timeout,omitempty"` Strategy DomainStrategy `json:"strategy,omitempty"` DisableCache bool `json:"disable_cache,omitempty"` DisableOptimisticCache bool `json:"disable_optimistic_cache,omitempty"` @@ -107,6 +108,7 @@ func (o DomainResolveOptions) MarshalJSON() ([]byte, error) { if o.Server == "" { return []byte("{}"), nil } else if o.Strategy == DomainStrategy(C.DomainStrategyAsIS) && + o.Timeout == 0 && !o.DisableCache && !o.DisableOptimisticCache && o.RewriteTTL == nil && diff --git a/option/rule_action.go b/option/rule_action.go index c369cfeb36..303f77d6cc 100644 --- a/option/rule_action.go +++ b/option/rule_action.go @@ -202,6 +202,7 @@ func (r *RouteOptionsActionOptions) UnmarshalJSON(data []byte) error { type DNSRouteActionOptions struct { Server string `json:"server,omitempty"` + Timeout badoption.Duration `json:"timeout,omitempty"` Strategy DomainStrategy `json:"strategy,omitempty"` DisableCache bool `json:"disable_cache,omitempty"` DisableOptimisticCache bool `json:"disable_optimistic_cache,omitempty"` @@ -211,6 +212,7 @@ type DNSRouteActionOptions struct { type _DNSRouteOptionsActionOptions struct { Strategy DomainStrategy `json:"strategy,omitempty"` + Timeout badoption.Duration `json:"timeout,omitempty"` DisableCache bool `json:"disable_cache,omitempty"` DisableOptimisticCache bool `json:"disable_optimistic_cache,omitempty"` RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` @@ -324,6 +326,7 @@ type RouteActionSniff struct { type RouteActionResolve struct { Server string `json:"server,omitempty"` + Timeout badoption.Duration `json:"timeout,omitempty"` Strategy DomainStrategy `json:"strategy,omitempty"` DisableCache bool `json:"disable_cache,omitempty"` DisableOptimisticCache bool `json:"disable_optimistic_cache,omitempty"` diff --git a/route/network.go b/route/network.go index 858ea3b24f..6598ead48d 100644 --- a/route/network.go +++ b/route/network.go @@ -79,6 +79,7 @@ func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, options DomainResolver: defaultDomainResolver.Server, DomainResolveOptions: adapter.DNSQueryOptions{ Strategy: C.DomainStrategy(defaultDomainResolver.Strategy), + Timeout: time.Duration(defaultDomainResolver.Timeout), DisableCache: defaultDomainResolver.DisableCache, DisableOptimisticCache: defaultDomainResolver.DisableOptimisticCache, RewriteTTL: defaultDomainResolver.RewriteTTL, diff --git a/route/route.go b/route/route.go index 0d5e1669a6..1809971ed4 100644 --- a/route/route.go +++ b/route/route.go @@ -791,6 +791,7 @@ func (r *Router) actionResolve(ctx context.Context, metadata *adapter.InboundCon DisableCache: action.DisableCache, DisableOptimisticCache: action.DisableOptimisticCache, RewriteTTL: action.RewriteTTL, + Timeout: action.Timeout, ClientSubnet: action.ClientSubnet, }) if err != nil { diff --git a/route/rule/rule_action.go b/route/rule/rule_action.go index ea239b68cb..2187d80d06 100644 --- a/route/rule/rule_action.go +++ b/route/rule/rule_action.go @@ -108,6 +108,7 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti case C.RuleActionTypeResolve: return &RuleActionResolve{ Server: action.ResolveOptions.Server, + Timeout: time.Duration(action.ResolveOptions.Timeout), Strategy: C.DomainStrategy(action.ResolveOptions.Strategy), DisableCache: action.ResolveOptions.DisableCache, DisableOptimisticCache: action.ResolveOptions.DisableOptimisticCache, @@ -128,6 +129,7 @@ func NewDNSRuleAction(logger logger.ContextLogger, action option.DNSRuleAction) Server: action.RouteOptions.Server, RuleActionDNSRouteOptions: RuleActionDNSRouteOptions{ Strategy: C.DomainStrategy(action.RouteOptions.Strategy), + Timeout: time.Duration(action.RouteOptions.Timeout), DisableCache: action.RouteOptions.DisableCache, DisableOptimisticCache: action.RouteOptions.DisableOptimisticCache, RewriteTTL: action.RouteOptions.RewriteTTL, @@ -139,6 +141,7 @@ func NewDNSRuleAction(logger logger.ContextLogger, action option.DNSRuleAction) Server: action.RouteOptions.Server, RuleActionDNSRouteOptions: RuleActionDNSRouteOptions{ Strategy: C.DomainStrategy(action.RouteOptions.Strategy), + Timeout: time.Duration(action.RouteOptions.Timeout), DisableCache: action.RouteOptions.DisableCache, DisableOptimisticCache: action.RouteOptions.DisableOptimisticCache, RewriteTTL: action.RouteOptions.RewriteTTL, @@ -150,6 +153,7 @@ func NewDNSRuleAction(logger logger.ContextLogger, action option.DNSRuleAction) case C.RuleActionTypeRouteOptions: return &RuleActionDNSRouteOptions{ Strategy: C.DomainStrategy(action.RouteOptionsOptions.Strategy), + Timeout: time.Duration(action.RouteOptionsOptions.Timeout), DisableCache: action.RouteOptionsOptions.DisableCache, DisableOptimisticCache: action.RouteOptionsOptions.DisableOptimisticCache, RewriteTTL: action.RouteOptionsOptions.RewriteTTL, @@ -320,6 +324,9 @@ func formatDNSRouteAction(action string, server string, options RuleActionDNSRou if options.RewriteTTL != nil { descriptions = append(descriptions, F.ToString("rewrite-ttl=", *options.RewriteTTL)) } + if options.Timeout > 0 { + descriptions = append(descriptions, F.ToString("timeout=", options.Timeout.String())) + } if options.ClientSubnet.IsValid() { descriptions = append(descriptions, F.ToString("client-subnet=", options.ClientSubnet)) } @@ -328,6 +335,7 @@ func formatDNSRouteAction(action string, server string, options RuleActionDNSRou type RuleActionDNSRouteOptions struct { Strategy C.DomainStrategy + Timeout time.Duration DisableCache bool DisableOptimisticCache bool RewriteTTL *uint32 @@ -349,6 +357,9 @@ func (r *RuleActionDNSRouteOptions) String() string { if r.RewriteTTL != nil { descriptions = append(descriptions, F.ToString("rewrite-ttl=", *r.RewriteTTL)) } + if r.Timeout > 0 { + descriptions = append(descriptions, F.ToString("timeout=", r.Timeout.String())) + } if r.ClientSubnet.IsValid() { descriptions = append(descriptions, F.ToString("client-subnet=", r.ClientSubnet)) } @@ -522,6 +533,7 @@ func (r *RuleActionSniff) String() string { type RuleActionResolve struct { Server string + Timeout time.Duration Strategy C.DomainStrategy DisableCache bool DisableOptimisticCache bool @@ -550,6 +562,9 @@ func (r *RuleActionResolve) String() string { if r.RewriteTTL != nil { options = append(options, F.ToString("rewrite_ttl=", *r.RewriteTTL)) } + if r.Timeout > 0 { + options = append(options, F.ToString("timeout=", r.Timeout.String())) + } if r.ClientSubnet.IsValid() { options = append(options, F.ToString("client_subnet=", r.ClientSubnet)) } From abedea4e57919d7c624db80a7e840c2036fe1ed3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 7 Mar 2026 16:40:34 +0800 Subject: [PATCH 63/93] Bump version --- docs/changelog.md | 291 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 290 insertions(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index 8cd5296675..e51e6ed94b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,41 +2,277 @@ icon: material/alert-decagram --- +#### 1.14.0-alpha.19 + +* Preserve comments between formatting +* Add cipher, MAC, and key exchange algorithm options for SSH outbound **1** +* Add DNS query timeout options **2** +** Fixes and improvements + +**1**: + +See [SSH](/configuration/outbound/ssh/#cipher). + +**2**: + +Adds [`dns.timeout`](/configuration/dns/#timeout), with per-query +overrides via [DNS rule action](/configuration/dns/rule_action/#timeout) +and [`resolve` route rule action](/configuration/route/rule_action/#timeout), +and a `timeout` field on +[`domain_resolver`](/configuration/shared/dial/#domain_resolver). + +#### 1.14.0-alpha.18 + +* Add Windows TLS engine **1** +* Fixes and improvements + +**1**: + +The new `windows` value for outbound TLS +[`engine`](/configuration/shared/tls/#engine) routes the TLS handshake +through Schannel via SSPI. Only available on Windows build 17763 or +later (Windows 10 version 1809, Windows Server 2019, or newer); TLS 1.3 +is only negotiated on Windows 11 or Windows Server 2022 and newer. + #### 1.13.11 * Fix process searcher failure introduced in 1.13.9 * Fixes and improvements +#### 1.14.0-alpha.16 + +* Add ACME profile support for IP address certificates **1** +* Fixes and improvements + +**1**: + +See [ACME Certificate Provider](/configuration/shared/certificate-provider/acme/#profile). + #### 1.13.10 * Fix process searcher failure introduced in 1.13.9 +#### 1.14.0-alpha.15 + +* Add search domain support for Tailscale DNS **1** +* Fixes and improvements + +**1**: + +See [Tailscale DNS Server](/configuration/dns/server/tailscale/#accept_search_domain). + #### 1.13.9 * Fixes and improvements +#### 1.14.0-alpha.13 + +* Unify HTTP client **1** +* Add Apple HTTP and TLS engines **2** +* Unify HTTP/2 and QUIC parameters **3** +* Add TLS spoof **4** +* Fixes and improvements + +**1**: + +The new top-level [`http_clients`](/configuration/shared/http-client/) +option defines reusable HTTP clients (engine, version, dialer, TLS, +HTTP/2 and QUIC parameters). Components that make outbound HTTP requests +— remote rule-sets, ACME and Cloudflare Origin CA certificate providers, +DERP `verify_client_url`, and the Tailscale `control_http_client` — now +accept an inline HTTP client object or the tag of an `http_clients` +entry, replacing the dial and TLS fields previously inlined in each +component. When the field is omitted, ACME, Cloudflare Origin CA, DERP +and Tailscale dial direct (their existing default). + +Remote rule-sets are the only HTTP-using component whose default for an +omitted `http_client` has historically resolved to the default outbound, +not to direct, and a typical configuration contains many of them. To +avoid repeating the same `http_client` block in every rule-set, +[`route.default_http_client`](/configuration/route/#default_http_client) +selects a default rule-set client by tag and is the only field that +consults it. If `default_http_client` is empty and `http_clients` is +non-empty, the first entry is used automatically. The legacy fallback +(use the default outbound when `http_clients` is empty altogether) is +preserved with a deprecation warning and will be removed in sing-box +1.16.0, together with the legacy `download_detour` remote rule-set +option and the legacy dialer fields on Tailscale endpoints. + +**2**: + +A new `apple` engine is available on Apple platforms in two independent +places: + +* [HTTP client `engine`](/configuration/shared/http-client/#engine) — + routes HTTP requests through `NSURLSession`. +* Outbound TLS [`engine`](/configuration/shared/tls/#engine) — routes + the TLS handshake through `Network.framework` for direct TCP TLS + client connections. + +The default remains `go`. Both engines come with additional CGO and +framework memory overhead and platform restrictions documented on each +field. + +**3**: + +[HTTP/2](/configuration/shared/http2/) and +[QUIC](/configuration/shared/quic/) parameters +(`idle_timeout`, `keep_alive_period`, `stream_receive_window`, +`connection_receive_window`, `max_concurrent_streams`, +`initial_packet_size`, `disable_path_mtu_discovery`) are now shared +across QUIC-based outbounds +([Hysteria](/configuration/outbound/hysteria/), +[Hysteria2](/configuration/outbound/hysteria2/), +[TUIC](/configuration/outbound/tuic/)) and HTTP clients running HTTP/2 +or HTTP/3. + +This deprecates the Hysteria v1 tuning fields `recv_window_conn`, +`recv_window`, `recv_window_client`, `max_conn_client` and +`disable_mtu_discovery`; they will be removed in sing-box 1.16.0. + +**4**: + +Added outbound TLS [`spoof`](/configuration/shared/tls/#spoof) and +[`spoof_method`](/configuration/shared/tls/#spoof_method) fields. When +enabled, a forged ClientHello carrying a whitelisted SNI is sent before +the real handshake to fool SNI-filtering middleboxes. Requires +`CAP_NET_RAW` + `CAP_NET_ADMIN` or root on Linux and macOS, and +Administrator privileges on Windows (ARM64 is not supported). IP-literal +server names are rejected. + +#### 1.14.0-alpha.12 + +* Fix fake-ip DNS server should return SUCCESS when address type is not configured +* Fixes and improvements + #### 1.13.8 * Update naiveproxy to v147.0.7727.49-1 * Fix fake-ip DNS server should return SUCCESS when address type is not configured * Fixes and improvements -#### 1.13.7 +#### 1.14.0-alpha.11 +* Add optimistic DNS cache **1** +* Update NaiveProxy to 147.0.7727.49 * Fixes and improvements +**1**: + +Optimistic DNS cache returns an expired cached response immediately while +refreshing it in the background, reducing tail latency for repeated +queries. Enabled via [`optimistic`](/configuration/dns/#optimistic) +in DNS options, and can be persisted across restarts with the new +[`store_dns`](/configuration/experimental/cache-file/#store_dns) cache +file option. A per-query +[`disable_optimistic_cache`](/configuration/dns/rule_action/#disable_optimistic_cache) +field is also available on DNS rule actions and the `resolve` route rule +action. + +This deprecates the `independent_cache` DNS option (the DNS cache now +always keys by transport) and the `store_rdrc` cache file option +(replaced by `store_dns`); both will be removed in sing-box 1.16.0. +See [Migration](/migration/#migrate-independent-dns-cache). + +#### 1.14.0-alpha.10 + +* Add `evaluate` DNS rule action and Response Match Fields **1** +* `ip_version` and `query_type` now also take effect on internal DNS lookups **2** +* Add `package_name_regex` route, DNS and headless rule item **3** +* Add cloudflared inbound **4** +* Fixes and improvements + +**1**: + +Response Match Fields +([`response_rcode`](/configuration/dns/rule/#response_rcode), +[`response_answer`](/configuration/dns/rule/#response_answer), +[`response_ns`](/configuration/dns/rule/#response_ns), +and [`response_extra`](/configuration/dns/rule/#response_extra)) +match the evaluated DNS response. They are gated by the new +[`match_response`](/configuration/dns/rule/#match_response) field and +populated by a preceding +[`evaluate`](/configuration/dns/rule_action/#evaluate) DNS rule action; +the evaluated response can also be returned directly by a +[`respond`](/configuration/dns/rule_action/#respond) action. + +This deprecates the Legacy Address Filter Fields (`ip_cidr`, +`ip_is_private` without `match_response`) in DNS rules, the Legacy +`strategy` DNS rule action option, and the Legacy +`rule_set_ip_cidr_accept_empty` DNS rule item; all three will be removed +in sing-box 1.16.0. +See [Migration](/migration/#migrate-address-filter-fields-to-response-matching). + +**2**: + +`ip_version` and `query_type` in DNS rules, together with `query_type` in +referenced rule-sets, now take effect on every DNS rule evaluation, +including matches from internal domain resolutions that do not target a +specific DNS server (for example a `resolve` route rule action without +`server` set). In earlier versions they were silently ignored in that +path. Combining these fields with any of the legacy DNS fields deprecated +in **1** in the same DNS configuration is no longer supported and is +rejected at startup. +See [Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules). + +**3**: + +See [Route Rule](/configuration/route/rule/#package_name_regex), +[DNS Rule](/configuration/dns/rule/#package_name_regex) and +[Headless Rule](/configuration/rule-set/headless-rule/#package_name_regex). + +**4**: + +See [Cloudflared](/configuration/inbound/cloudflared/). + +#### 1.13.7 + +* Fixes and improvement + #### 1.13.6 * Fixes and improvements +#### 1.14.0-alpha.8 + +* Add BBR profile and hop interval randomization for Hysteria2 **1** +* Fixes and improvements + +**1**: + +See [Hysteria2 Inbound](/configuration/inbound/hysteria2/#bbr_profile) and [Hysteria2 Outbound](/configuration/outbound/hysteria2/#bbr_profile). + #### 1.13.5 * Fixes and improvements +#### 1.14.0-alpha.7 + +* Fixes and improvements + #### 1.13.4 * Fixes and improvements +#### 1.14.0-alpha.4 + +* Refactor ACME support to certificate provider system **1** +* Add Cloudflare Origin CA certificate provider **2** +* Add Tailscale certificate provider **3** +* Fixes and improvements + +**1**: + +See [Certificate Provider](/configuration/shared/certificate-provider/) and [Migration](/migration/#migrate-inline-acme-to-certificate-provider). + +**2**: + +See [Cloudflare Origin CA](/configuration/shared/certificate-provider/cloudflare-origin-ca). + +**3**: + +See [Tailscale](/configuration/shared/certificate-provider/tailscale). + #### 1.13.3 * Add OpenWrt and Alpine APK packages to release **1** @@ -61,6 +297,59 @@ from [SagerNet/go](https://github.com/SagerNet/go). See [OCM](/configuration/service/ocm). +#### 1.12.24 + +* Fixes and improvements + +#### 1.14.0-alpha.2 + +* Add OpenWrt and Alpine APK packages to release **1** +* Backport to macOS 10.13 High Sierra **2** +* OCM service: Add WebSocket support for Responses API **3** +* Fixes and improvements + +**1**: + +Alpine APK files use `linux` in the filename to distinguish from OpenWrt APKs which use the `openwrt` prefix: + +- OpenWrt: `sing-box_{version}_openwrt_{architecture}.apk` +- Alpine: `sing-box_{version}_linux_{architecture}.apk` + +**2**: + +Legacy macOS binaries (with `-legacy-macos-10.13` suffix) now support +macOS 10.13 High Sierra, built using Go 1.25 with patches +from [SagerNet/go](https://github.com/SagerNet/go). + +**3**: + +See [OCM](/configuration/service/ocm). + +#### 1.14.0-alpha.1 + +* Add `source_mac_address` and `source_hostname` rule items **1** +* Add `include_mac_address` and `exclude_mac_address` TUN options **2** +* Update NaiveProxy to 145.0.7632.159 **3** +* Fixes and improvements + +**1**: + +New rule items for matching LAN devices by MAC address and hostname via neighbor resolution. +Supported on Linux, macOS, or in graphical clients on Android and macOS. + +See [Route Rule](/configuration/route/rule/#source_mac_address), [DNS Rule](/configuration/dns/rule/#source_mac_address) and [Neighbor Resolution](/configuration/shared/neighbor/). + +**2**: + +Limit or exclude devices from TUN routing by MAC address. +Only supported on Linux with `auto_route` and `auto_redirect` enabled. + +See [TUN](/configuration/inbound/tun/#include_mac_address). + +**3**: + +This is not an official update from NaiveProxy. Instead, it's a Chromium codebase update maintained by Project S. + #### 1.13.2 * Fixes and improvements From ddc20355cf1343fded2d8900f0c4e5a6e04ae75a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 28 Apr 2026 18:34:46 +0800 Subject: [PATCH 64/93] sing: Fix contentjson crash --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 94e49c8c1f..11440de956 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,7 @@ require ( github.com/sagernet/gomobile v0.1.12 github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 - github.com/sagernet/sing v0.8.10-0.20260427232324-f758c54b7409 + github.com/sagernet/sing v0.8.10-0.20260428084616-2bc976d03e39 github.com/sagernet/sing-cloudflared v0.1.0 github.com/sagernet/sing-mux v0.3.4 github.com/sagernet/sing-quic v0.6.2-0.20260412143638-8f65b6be7cd6 diff --git a/go.sum b/go.sum index 9efd5bb815..e5bf275cb8 100644 --- a/go.sum +++ b/go.sum @@ -242,8 +242,8 @@ github.com/sagernet/nftables v0.3.0-mod.2 h1:ck2KMU02OxL1eDFgGaWYglMDpoOZ7OHzxje github.com/sagernet/nftables v0.3.0-mod.2/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4= -github.com/sagernet/sing v0.8.10-0.20260427232324-f758c54b7409 h1:TjqWXRWjiZi98SGX0Rmdl/XF7066+q3Idf+xjqJFF1U= -github.com/sagernet/sing v0.8.10-0.20260427232324-f758c54b7409/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA= +github.com/sagernet/sing v0.8.10-0.20260428084616-2bc976d03e39 h1:zUBwI+3KFPZ4UE5RNfjXWFFlldPtVy/XW63n2T28JiM= +github.com/sagernet/sing v0.8.10-0.20260428084616-2bc976d03e39/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA= github.com/sagernet/sing-cloudflared v0.1.0 h1:to+2fcCx8zu4X/DirRw9Ihc+FrEZ7oEyIqeCoJiwIpw= github.com/sagernet/sing-cloudflared v0.1.0/go.mod h1:bH2NKX+NpDTY1Zkxfboxw6MXB/ZywaNLmrDJYgKMJ2Y= github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s= From 2a533b0daaa2571d0f6cf18cb8b982c64563ac04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 28 Apr 2026 18:35:00 +0800 Subject: [PATCH 65/93] Fix tailscale start dependencies --- protocol/tailscale/endpoint.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protocol/tailscale/endpoint.go b/protocol/tailscale/endpoint.go index 3717a9c865..f265a470d1 100644 --- a/protocol/tailscale/endpoint.go +++ b/protocol/tailscale/endpoint.go @@ -246,7 +246,7 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL HTTPClient: controlHTTPClient, } return &Endpoint{ - Adapter: endpoint.NewAdapter(C.TypeTailscale, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, nil), + Adapter: endpoint.NewAdapterWithDialerOptions(C.TypeTailscale, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, controlHTTPClientOptions.DialerOptions), ctx: ctx, router: router, logger: logger, From e171852b19d9cf3db38d1c95f849392a79eac7d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 29 Apr 2026 22:26:10 +0800 Subject: [PATCH 66/93] dns: Add neighbor-based hostname resolution to local server --- adapter/neighbor.go | 1 + dns/transport/local/local.go | 34 +++++++++--- dns/transport/local/local_darwin.go | 65 +++++++++++++---------- dns/transport/local/local_darwin_cgo.go | 6 ++- dns/transport/local/local_neighbor.go | 57 ++++++++++++++++++++ docs/configuration/dns/server/local.md | 20 ++++++- docs/configuration/dns/server/local.zh.md | 16 ++++++ docs/configuration/shared/neighbor.md | 4 +- docs/configuration/shared/neighbor.zh.md | 2 +- option/dns.go | 3 +- route/neighbor_resolver_darwin.go | 6 +++ route/neighbor_resolver_hostname.go | 56 +++++++++++++++++++ route/neighbor_resolver_linux.go | 6 +++ route/neighbor_resolver_platform.go | 6 +++ route/router.go | 63 +++++++++++----------- route/rule_conds.go | 16 ++++++ 16 files changed, 290 insertions(+), 71 deletions(-) create mode 100644 dns/transport/local/local_neighbor.go create mode 100644 route/neighbor_resolver_hostname.go diff --git a/adapter/neighbor.go b/adapter/neighbor.go index d917db5b7a..3115b2935d 100644 --- a/adapter/neighbor.go +++ b/adapter/neighbor.go @@ -14,6 +14,7 @@ type NeighborEntry struct { type NeighborResolver interface { LookupMAC(address netip.Addr) (net.HardwareAddr, bool) LookupHostname(address netip.Addr) (string, bool) + LookupAddresses(hostname string) []netip.Addr Start() error Close() error } diff --git a/dns/transport/local/local.go b/dns/transport/local/local.go index 55933510ad..7cff674d2b 100644 --- a/dns/transport/local/local.go +++ b/dns/transport/local/local.go @@ -14,6 +14,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" mDNS "github.com/miekg/dns" ) @@ -26,12 +27,14 @@ var _ adapter.DNSTransport = (*Transport)(nil) type Transport struct { dns.TransportAdapter - ctx context.Context - logger logger.ContextLogger - hosts *hosts.File - dialer N.Dialer - preferGo bool - resolved ResolvedResolver + ctx context.Context + logger logger.ContextLogger + hosts *hosts.File + dialer N.Dialer + preferGo bool + resolved ResolvedResolver + neighborResolver adapter.NeighborResolver + neighborSuffixes []string } func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.LocalDNSServerOptions) (adapter.DNSTransport, error) { @@ -39,13 +42,17 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt if err != nil { return nil, err } - + suffixes, err := buildNeighborMatchers(options.NeighborDomain) + if err != nil { + return nil, err + } return &Transport{ TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options), ctx: ctx, logger: logger, dialer: transportDialer, preferGo: options.PreferGo, + neighborSuffixes: suffixes, }, nil } @@ -71,6 +78,11 @@ func (t *Transport) Start(stage adapter.StartStage) error { } } } + case adapter.StartStateStart: + router := service.FromContext[adapter.Router](t.ctx) + if router != nil { + t.neighborResolver = router.NeighborResolver() + } } return nil } @@ -87,6 +99,10 @@ func (t *Transport) Reset() { func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { if t.resolved != nil { + response := t.lookupNeighbor(message) + if response != nil { + return response, nil + } return t.resolved.Exchange(ctx, message) } question := message.Question[0] @@ -96,5 +112,9 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil } } + response := t.lookupNeighbor(message) + if response != nil { + return response, nil + } return t.exchange(ctx, message, question.Name) } diff --git a/dns/transport/local/local_darwin.go b/dns/transport/local/local_darwin.go index 75fdfd9abb..85cc16ed0a 100644 --- a/dns/transport/local/local_darwin.go +++ b/dns/transport/local/local_darwin.go @@ -28,12 +28,14 @@ var _ adapter.DNSTransport = (*Transport)(nil) type Transport struct { dns.TransportAdapter - ctx context.Context - logger logger.ContextLogger - hosts *hosts.File - dialer N.Dialer - fallback bool - dhcpTransport dhcpTransport + ctx context.Context + logger logger.ContextLogger + hosts *hosts.File + dialer N.Dialer + fallback bool + dhcpTransport dhcpTransport + neighborResolver adapter.NeighborResolver + neighborSuffixes []string } type dhcpTransport interface { @@ -47,39 +49,48 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt if err != nil { return nil, err } + suffixes, err := buildNeighborMatchers(options.NeighborDomain) + if err != nil { + return nil, err + } return &Transport{ TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options), ctx: ctx, logger: logger, dialer: transportDialer, + neighborSuffixes: suffixes, }, nil } func (t *Transport) Start(stage adapter.StartStage) error { - if stage != adapter.StartStateStart { - return nil - } - defaultHosts, err := hosts.NewDefault() - if err != nil { - t.logger.Warn(err) - } else { - t.hosts = defaultHosts - } - inboundManager := service.FromContext[adapter.InboundManager](t.ctx) - for _, inbound := range inboundManager.Inbounds() { - if inbound.Type() == C.TypeTun { - t.fallback = true - break + switch stage { + case adapter.StartStateStart: + defaultHosts, err := hosts.NewDefault() + if err != nil { + t.logger.Warn(err) + } else { + t.hosts = defaultHosts } - } - if t.fallback { - t.dhcpTransport = newDHCPTransport(t.TransportAdapter, log.ContextWithOverrideLevel(t.ctx, log.LevelDebug), t.dialer, t.logger) - if t.dhcpTransport != nil { - err := t.dhcpTransport.Start(stage) - if err != nil { - return err + inboundManager := service.FromContext[adapter.InboundManager](t.ctx) + for _, inbound := range inboundManager.Inbounds() { + if inbound.Type() == C.TypeTun { + t.fallback = true + break } } + if t.fallback { + t.dhcpTransport = newDHCPTransport(t.TransportAdapter, log.ContextWithOverrideLevel(t.ctx, log.LevelDebug), t.dialer, t.logger) + if t.dhcpTransport != nil { + err = t.dhcpTransport.Start(stage) + if err != nil { + return err + } + } + } + router := service.FromContext[adapter.Router](t.ctx) + if router != nil { + t.neighborResolver = router.NeighborResolver() + } } return nil } diff --git a/dns/transport/local/local_darwin_cgo.go b/dns/transport/local/local_darwin_cgo.go index 6468a31f1a..bfea8dd64c 100644 --- a/dns/transport/local/local_darwin_cgo.go +++ b/dns/transport/local/local_darwin_cgo.go @@ -86,7 +86,11 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, return dns.FixedResponse(message.Id, question, addresses, boxC.DefaultDNSTTL), nil } } - if t.fallback && t.dhcpTransport != nil { + response := t.lookupNeighbor(message) + if response != nil { + return response, nil + } + if t.dhcpTransport != nil { dhcpServers := t.dhcpTransport.Fetch() if len(dhcpServers) > 0 { return t.dhcpTransport.Exchange0(ctx, message, dhcpServers) diff --git a/dns/transport/local/local_neighbor.go b/dns/transport/local/local_neighbor.go new file mode 100644 index 0000000000..e48ba8a851 --- /dev/null +++ b/dns/transport/local/local_neighbor.go @@ -0,0 +1,57 @@ +package local + +import ( + "strings" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + E "github.com/sagernet/sing/common/exceptions" + + mDNS "github.com/miekg/dns" +) + +func buildNeighborMatchers(domains []string) ([]string, error) { + if len(domains) == 0 { + return nil, nil + } + var suffixes []string + for _, domain := range domains { + if !strings.HasPrefix(domain, ".") { + return nil, E.New("neighbor_domain entry must start with '.': ", domain) + } + suffixes = append(suffixes, mDNS.CanonicalName(domain)) + } + return suffixes, nil +} + +func (t *Transport) lookupNeighbor(message *mDNS.Msg) *mDNS.Msg { + if t.neighborResolver == nil { + return nil + } + question := message.Question[0] + if question.Qtype != mDNS.TypeA && question.Qtype != mDNS.TypeAAAA { + return nil + } + host := extractNeighborHost(mDNS.CanonicalName(question.Name), t.neighborSuffixes) + if host == "" { + return nil + } + addresses := t.neighborResolver.LookupAddresses(host) + if len(addresses) == 0 { + return nil + } + return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL) +} + +func extractNeighborHost(canonical string, suffixes []string) string { + for _, suffix := range suffixes { + if !strings.HasSuffix(canonical, suffix) || len(canonical) <= len(suffix) { + continue + } + host := canonical[:len(canonical)-len(suffix)] + if !strings.ContainsRune(host, '.') { + return host + } + } + return "" +} diff --git a/docs/configuration/dns/server/local.md b/docs/configuration/dns/server/local.md index aa7f095a32..39c22b065c 100644 --- a/docs/configuration/dns/server/local.md +++ b/docs/configuration/dns/server/local.md @@ -2,6 +2,10 @@ icon: material/new-box --- +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [neighbor_domain](#neighbor_domain) + !!! quote "Changes in sing-box 1.13.0" :material-plus: [prefer_go](#prefer_go) @@ -19,7 +23,8 @@ icon: material/new-box { "type": "local", "tag": "", - "prefer_go": false + "prefer_go": false, + "neighbor_domain": [] // Dial Fields } @@ -56,6 +61,19 @@ On devices running Android versions lower than 10, this interface can only resol 2. On macOS, `local` will try DHCP first in Network Extension, since DHCP respects DIal Fields, it will not be disabled by `prefer_go`. +#### neighbor_domain + +!!! question "Since sing-box 1.14.0" + +A list of domain suffixes for which A/AAAA queries are answered from the +[neighbor resolver](/configuration/shared/neighbor/) instead of the upstream. + +Each entry must start with `.`. Only queries whose host part (the portion +before the suffix) contains no dots are matched; `.` matches any +single-label name such as `nas`. + +Example: `[".", ".lan"]`. + ### Dial Fields See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/dns/server/local.zh.md b/docs/configuration/dns/server/local.zh.md index 50ac05acd9..c19efc1e15 100644 --- a/docs/configuration/dns/server/local.zh.md +++ b/docs/configuration/dns/server/local.zh.md @@ -2,6 +2,10 @@ icon: material/new-box --- +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [neighbor_domain](#neighbor_domain) + !!! quote "sing-box 1.13.0 中的更改" :material-plus: [prefer_go](#prefer_go) @@ -20,6 +24,7 @@ icon: material/new-box "type": "local", "tag": "", "prefer_go": false, + "neighbor_domain": [], // 拨号字段 } @@ -56,6 +61,17 @@ icon: material/new-box 2. 在 macOS 上,`local` 会在 Network Extension 中首先尝试 DHCP,由于 DHCP 遵循拨号字段, 它不会被 `prefer_go` 禁用。 +#### neighbor_domain + +!!! question "自 sing-box 1.14.0 起" + +用于从[邻居解析器](/zh/configuration/shared/neighbor/)而非上游回答 A/AAAA 查询的域后缀列表。 + +每一项必须以 `.` 开头。仅匹配后缀之前的主机名部分不包含点的查询; +`.` 匹配任意单标签名称,例如 `nas`。 + +示例:`[".", ".lan"]`。 + ### 拨号字段 参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。 \ No newline at end of file diff --git a/docs/configuration/shared/neighbor.md b/docs/configuration/shared/neighbor.md index c67d995ebe..a0d5f36a7d 100644 --- a/docs/configuration/shared/neighbor.md +++ b/docs/configuration/shared/neighbor.md @@ -8,7 +8,9 @@ Match LAN devices by MAC address and hostname using [`source_mac_address`](/configuration/route/rule/#source_mac_address) and [`source_hostname`](/configuration/route/rule/#source_hostname) rule items. -Neighbor resolution is automatically enabled when these rule items exist. +Neighbor resolution is automatically enabled when these rule items exist +or when a [local DNS server](/configuration/dns/server/local/) sets +[neighbor_domain](/configuration/dns/server/local/#neighbor_domain). Use [`route.find_neighbor`](/configuration/route/#find_neighbor) to force enable it for logging without rules. ## Linux diff --git a/docs/configuration/shared/neighbor.zh.md b/docs/configuration/shared/neighbor.zh.md index 96297fcb57..7f4065ecf4 100644 --- a/docs/configuration/shared/neighbor.zh.md +++ b/docs/configuration/shared/neighbor.zh.md @@ -8,7 +8,7 @@ icon: material/lan [`source_mac_address`](/configuration/route/rule/#source_mac_address) 和 [`source_hostname`](/configuration/route/rule/#source_hostname) 规则项匹配局域网设备的 MAC 地址和主机名。 -当这些规则项存在时,邻居解析自动启用。 +当这些规则项存在,或 [local DNS 服务器](/zh/configuration/dns/server/local/) 设置了 [neighbor_domain](/zh/configuration/dns/server/local/#neighbor_domain) 时,邻居解析自动启用。 使用 [`route.find_neighbor`](/configuration/route/#find_neighbor) 可在没有规则时强制启用以输出日志。 ## Linux diff --git a/option/dns.go b/option/dns.go index c0d131a309..53a078bb3b 100644 --- a/option/dns.go +++ b/option/dns.go @@ -154,7 +154,8 @@ type RawLocalDNSServerOptions struct { type LocalDNSServerOptions struct { RawLocalDNSServerOptions - PreferGo bool `json:"prefer_go,omitempty"` + PreferGo bool `json:"prefer_go,omitempty"` + NeighborDomain badoption.Listable[string] `json:"neighbor_domain,omitempty"` } type RemoteDNSServerOptions struct { diff --git a/route/neighbor_resolver_darwin.go b/route/neighbor_resolver_darwin.go index a8884ae628..cc24ec73ec 100644 --- a/route/neighbor_resolver_darwin.go +++ b/route/neighbor_resolver_darwin.go @@ -110,6 +110,12 @@ func (r *neighborResolver) LookupMAC(address netip.Addr) (net.HardwareAddr, bool return nil, false } +func (r *neighborResolver) LookupAddresses(hostname string) []netip.Addr { + r.access.RLock() + defer r.access.RUnlock() + return lookupAddressesByHostname(hostname, r.ipToHostname, r.macToHostname, r.neighborIPToMAC, r.leaseIPToMAC) +} + func (r *neighborResolver) LookupHostname(address netip.Addr) (string, bool) { r.access.RLock() defer r.access.RUnlock() diff --git a/route/neighbor_resolver_hostname.go b/route/neighbor_resolver_hostname.go new file mode 100644 index 0000000000..d030bd1cd3 --- /dev/null +++ b/route/neighbor_resolver_hostname.go @@ -0,0 +1,56 @@ +package route + +import ( + "net" + "net/netip" + "strings" + + "github.com/sagernet/sing-box/dns" +) + +func lookupAddressesByHostname( + hostname string, + ipToHostname map[netip.Addr]string, + macToHostname map[string]string, + ipToMACTables ...map[netip.Addr]net.HardwareAddr, +) []netip.Addr { + hostname = dns.FqdnToDomain(hostname) + if hostname == "" { + return nil + } + resultSet := make(map[netip.Addr]struct{}) + var result []netip.Addr + addAddress := func(address netip.Addr) { + if isScopedIPv6Address(address) { + return + } + if _, exists := resultSet[address]; exists { + return + } + resultSet[address] = struct{}{} + result = append(result, address) + } + for address, entryHostname := range ipToHostname { + if strings.EqualFold(entryHostname, hostname) { + addAddress(address) + } + } + for mac, entryHostname := range macToHostname { + if !strings.EqualFold(entryHostname, hostname) { + continue + } + for _, table := range ipToMACTables { + for address, entryMAC := range table { + if entryMAC.String() == mac { + addAddress(address) + } + } + } + } + return result +} + +func isScopedIPv6Address(address netip.Addr) bool { + // DNS AAAA records cannot carry an interface zone. + return address.Is6() && (address.IsLinkLocalUnicast() || address.Zone() != "") +} diff --git a/route/neighbor_resolver_linux.go b/route/neighbor_resolver_linux.go index b7991b4c89..5c6cdcb722 100644 --- a/route/neighbor_resolver_linux.go +++ b/route/neighbor_resolver_linux.go @@ -114,6 +114,12 @@ func (r *neighborResolver) LookupMAC(address netip.Addr) (net.HardwareAddr, bool return nil, false } +func (r *neighborResolver) LookupAddresses(hostname string) []netip.Addr { + r.access.RLock() + defer r.access.RUnlock() + return lookupAddressesByHostname(hostname, r.ipToHostname, r.macToHostname, r.neighborIPToMAC, r.leaseIPToMAC) +} + func (r *neighborResolver) LookupHostname(address netip.Addr) (string, bool) { r.access.RLock() defer r.access.RUnlock() diff --git a/route/neighbor_resolver_platform.go b/route/neighbor_resolver_platform.go index ddb9a99592..a9930c0e43 100644 --- a/route/neighbor_resolver_platform.go +++ b/route/neighbor_resolver_platform.go @@ -46,6 +46,12 @@ func (r *platformNeighborResolver) LookupMAC(address netip.Addr) (net.HardwareAd return extractMACFromEUI64(address) } +func (r *platformNeighborResolver) LookupAddresses(hostname string) []netip.Addr { + r.access.RLock() + defer r.access.RUnlock() + return lookupAddressesByHostname(hostname, r.ipToHostname, r.macToHostname, r.ipToMAC) +} + func (r *platformNeighborResolver) LookupHostname(address netip.Addr) (string, bool) { r.access.RLock() defer r.access.RUnlock() diff --git a/route/router.go b/route/router.go index 72f549c382..2d50c22e4b 100644 --- a/route/router.go +++ b/route/router.go @@ -63,7 +63,7 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.Route rules: make([]adapter.Rule, 0, len(options.Rules)), ruleSetMap: make(map[string]adapter.RuleSet), needFindProcess: hasRule(options.Rules, isProcessRule) || hasDNSRule(dnsOptions.Rules, isProcessDNSRule) || options.FindProcess, - needFindNeighbor: hasRule(options.Rules, isNeighborRule) || hasDNSRule(dnsOptions.Rules, isNeighborDNSRule) || options.FindNeighbor, + needFindNeighbor: hasRule(options.Rules, isNeighborRule) || hasDNSRule(dnsOptions.Rules, isNeighborDNSRule) || hasLocalNeighborDNSServer(dnsOptions.Servers) || options.FindNeighbor, leaseFiles: options.DHCPLeaseFiles, pauseManager: service.FromContext[pause.Manager](ctx), platformInterface: service.FromContext[adapter.PlatformInterface](ctx), @@ -99,6 +99,36 @@ func (r *Router) Initialize(rules []option.Rule, ruleSets []option.RuleSet) erro func (r *Router) Start(stage adapter.StartStage) error { monitor := taskmonitor.New(r.logger, C.StartTimeout) switch stage { + case adapter.StartStateInitialize: + if r.needFindNeighbor { + if r.platformInterface != nil && r.platformInterface.UsePlatformNeighborResolver() { + monitor.Start("initialize neighbor resolver") + resolver := newPlatformNeighborResolver(r.logger, r.platformInterface) + err := resolver.Start() + monitor.Finish() + if err != nil { + r.logger.Error(E.Cause(err, "start neighbor resolver")) + } else { + r.neighborResolver = resolver + } + } else { + monitor.Start("initialize neighbor resolver") + resolver, err := newNeighborResolver(r.logger, r.leaseFiles) + monitor.Finish() + if err != nil { + if err != os.ErrInvalid { + r.logger.Error(E.Cause(err, "create neighbor resolver")) + } + } else { + err = resolver.Start() + if err != nil { + r.logger.Error(E.Cause(err, "start neighbor resolver")) + } else { + r.neighborResolver = resolver + } + } + } + } case adapter.StartStateStart: var startContext *adapter.HTTPStartContext if len(r.ruleSets) > 0 { @@ -128,7 +158,6 @@ func (r *Router) Start(stage adapter.StartStage) error { } r.network.Initialize(r.ruleSets) needFindProcess := r.needFindProcess - needFindNeighbor := r.needFindNeighbor for _, ruleSet := range r.ruleSets { metadata := ruleSet.Metadata() if metadata.ContainsProcessRule { @@ -163,36 +192,6 @@ func (r *Router) Start(stage adapter.StartStage) error { processCache.SetLifetime(200 * time.Millisecond) r.processCache = processCache } - r.needFindNeighbor = needFindNeighbor - if needFindNeighbor { - if r.platformInterface != nil && r.platformInterface.UsePlatformNeighborResolver() { - monitor.Start("initialize neighbor resolver") - resolver := newPlatformNeighborResolver(r.logger, r.platformInterface) - err := resolver.Start() - monitor.Finish() - if err != nil { - r.logger.Error(E.Cause(err, "start neighbor resolver")) - } else { - r.neighborResolver = resolver - } - } else { - monitor.Start("initialize neighbor resolver") - resolver, err := newNeighborResolver(r.logger, r.leaseFiles) - monitor.Finish() - if err != nil { - if err != os.ErrInvalid { - r.logger.Error(E.Cause(err, "create neighbor resolver")) - } - } else { - err = resolver.Start() - if err != nil { - r.logger.Error(E.Cause(err, "start neighbor resolver")) - } else { - r.neighborResolver = resolver - } - } - } - } case adapter.StartStatePostStart: for i, rule := range r.rules { monitor.Start("initialize rule[", i, "]") diff --git a/route/rule_conds.go b/route/rule_conds.go index 2c62902949..716a19b091 100644 --- a/route/rule_conds.go +++ b/route/rule_conds.go @@ -60,3 +60,19 @@ func isWIFIRule(rule option.DefaultRule) bool { func isWIFIDNSRule(rule option.DefaultDNSRule) bool { return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0 } + +func hasLocalNeighborDNSServer(servers []option.DNSServerOptions) bool { + for _, server := range servers { + if server.Type != C.DNSTypeLocal { + continue + } + localOptions, isLocal := server.Options.(*option.LocalDNSServerOptions) + if !isLocal { + continue + } + if len(localOptions.NeighborDomain) > 0 { + return true + } + } + return false +} From fdec2fe051137c31888b8ac1500153f9909179b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 29 Apr 2026 22:48:02 +0800 Subject: [PATCH 67/93] dns: Add preferred_by rule item --- adapter/dns.go | 5 ++ dns/transport/hosts/hosts.go | 17 +++++- dns/transport/local/local.go | 14 ++++- dns/transport/local/local_darwin.go | 14 ++++- dns/transport/local/local_neighbor.go | 11 ++++ docs/configuration/dns/rule.md | 17 ++++++ docs/configuration/dns/rule.zh.md | 17 ++++++ option/rule_dns.go | 1 + protocol/tailscale/dns_transport.go | 16 +++++ route/rule/rule_dns.go | 5 ++ route/rule/rule_item_preferred_by_dns.go | 74 ++++++++++++++++++++++++ service/resolved/transport.go | 21 ++++++- 12 files changed, 208 insertions(+), 4 deletions(-) create mode 100644 route/rule/rule_item_preferred_by_dns.go diff --git a/adapter/dns.go b/adapter/dns.go index eeaf12a44c..afee5aa8a1 100644 --- a/adapter/dns.go +++ b/adapter/dns.go @@ -86,6 +86,11 @@ type DNSTransport interface { Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error) } +type DNSTransportWithPreferredDomain interface { + DNSTransport + PreferredDomain(domain string) bool +} + type DNSTransportRegistry interface { option.DNSTransportOptionsRegistry CreateDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) (DNSTransport, error) diff --git a/dns/transport/hosts/hosts.go b/dns/transport/hosts/hosts.go index aeb8781799..074c07c95b 100644 --- a/dns/transport/hosts/hosts.go +++ b/dns/transport/hosts/hosts.go @@ -19,7 +19,10 @@ func RegisterTransport(registry *dns.TransportRegistry) { dns.RegisterTransport[option.HostsDNSServerOptions](registry, C.DNSTypeHosts, NewTransport) } -var _ adapter.DNSTransport = (*Transport)(nil) +var ( + _ adapter.DNSTransport = (*Transport)(nil) + _ adapter.DNSTransportWithPreferredDomain = (*Transport)(nil) +) type Transport struct { dns.TransportAdapter @@ -66,6 +69,18 @@ func (t *Transport) Close() error { func (t *Transport) Reset() { } +func (t *Transport) PreferredDomain(domain string) bool { + if _, loaded := t.predefined[domain]; loaded { + return true + } + for _, file := range t.files { + if len(file.Lookup(domain)) > 0 { + return true + } + } + return false +} + func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { question := message.Question[0] domain := mDNS.CanonicalName(question.Name) diff --git a/dns/transport/local/local.go b/dns/transport/local/local.go index 7cff674d2b..d13de3fa4c 100644 --- a/dns/transport/local/local.go +++ b/dns/transport/local/local.go @@ -23,7 +23,10 @@ func RegisterTransport(registry *dns.TransportRegistry) { dns.RegisterTransport[option.LocalDNSServerOptions](registry, C.DNSTypeLocal, NewTransport) } -var _ adapter.DNSTransport = (*Transport)(nil) +var ( + _ adapter.DNSTransport = (*Transport)(nil) + _ adapter.DNSTransportWithPreferredDomain = (*Transport)(nil) +) type Transport struct { dns.TransportAdapter @@ -97,6 +100,15 @@ func (t *Transport) Close() error { func (t *Transport) Reset() { } +func (t *Transport) PreferredDomain(domain string) bool { + if t.hosts != nil && t.resolved == nil { + if len(t.hosts.Lookup(dns.FqdnToDomain(domain))) > 0 { + return true + } + } + return t.hasNeighborHost(domain) +} + func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { if t.resolved != nil { response := t.lookupNeighbor(message) diff --git a/dns/transport/local/local_darwin.go b/dns/transport/local/local_darwin.go index 85cc16ed0a..d38a0bf7be 100644 --- a/dns/transport/local/local_darwin.go +++ b/dns/transport/local/local_darwin.go @@ -24,7 +24,10 @@ func RegisterTransport(registry *dns.TransportRegistry) { dns.RegisterTransport[option.LocalDNSServerOptions](registry, C.DNSTypeLocal, NewTransport) } -var _ adapter.DNSTransport = (*Transport)(nil) +var ( + _ adapter.DNSTransport = (*Transport)(nil) + _ adapter.DNSTransportWithPreferredDomain = (*Transport)(nil) +) type Transport struct { dns.TransportAdapter @@ -106,3 +109,12 @@ func (t *Transport) Reset() { t.dhcpTransport.Reset() } } + +func (t *Transport) PreferredDomain(domain string) bool { + if t.hosts != nil { + if len(t.hosts.Lookup(dns.FqdnToDomain(domain))) > 0 { + return true + } + } + return t.hasNeighborHost(domain) +} diff --git a/dns/transport/local/local_neighbor.go b/dns/transport/local/local_neighbor.go index e48ba8a851..3dd394a8b6 100644 --- a/dns/transport/local/local_neighbor.go +++ b/dns/transport/local/local_neighbor.go @@ -43,6 +43,17 @@ func (t *Transport) lookupNeighbor(message *mDNS.Msg) *mDNS.Msg { return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL) } +func (t *Transport) hasNeighborHost(domain string) bool { + if t.neighborResolver == nil { + return false + } + host := extractNeighborHost(domain, t.neighborSuffixes) + if host == "" { + return false + } + return len(t.neighborResolver.LookupAddresses(host)) > 0 +} + func extractNeighborHost(canonical string, suffixes []string) string { for _, suffix := range suffixes { if !strings.HasSuffix(canonical, suffix) || len(canonical) <= len(suffix) { diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index b0785b7783..b8783d60ee 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -6,6 +6,7 @@ icon: material/alert-decagram :material-plus: [source_mac_address](#source_mac_address) :material-plus: [source_hostname](#source_hostname) + :material-plus: [preferred_by](#preferred_by) :material-plus: [match_response](#match_response) :material-delete-clock: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty) :material-plus: [response_rcode](#response_rcode) @@ -166,6 +167,10 @@ icon: material/alert-decagram "source_hostname": [ "my-device" ], + "preferred_by": [ + "local", + "ts-dns" + ], "wifi_ssid": [ "My WIFI" ], @@ -496,6 +501,18 @@ Match source device MAC address. Match source device hostname from DHCP leases. +#### preferred_by + +!!! question "Since sing-box 1.14.0" + +Match specified DNS servers' preferred domains. + +| Type | Match | +|-------------|-----------------------------------------------------| +| `hosts` | Match predefined entries and entries in hosts files | +| `local` | Match hosts entries and neighbor-resolved hosts | +| `tailscale` | Match MagicDNS hosts and DNS route suffixes | + #### wifi_ssid !!! quote "" diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index cc0a3037e0..2c2de7d033 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -6,6 +6,7 @@ icon: material/alert-decagram :material-plus: [source_mac_address](#source_mac_address) :material-plus: [source_hostname](#source_hostname) + :material-plus: [preferred_by](#preferred_by) :material-plus: [match_response](#match_response) :material-delete-clock: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty) :material-plus: [response_rcode](#response_rcode) @@ -166,6 +167,10 @@ icon: material/alert-decagram "source_hostname": [ "my-device" ], + "preferred_by": [ + "local", + "ts-dns" + ], "wifi_ssid": [ "My WIFI" ], @@ -488,6 +493,18 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`. 匹配源设备从 DHCP 租约获取的主机名。 +#### preferred_by + +!!! question "自 sing-box 1.14.0 起" + +匹配指定 DNS 服务器的首选域名。 + +| 类型 | 匹配 | +|-------------|--------------------------| +| `hosts` | 匹配预定义条目和 hosts 文件中的条目 | +| `local` | 匹配 hosts 中的条目和邻居解析得到的主机名 | +| `tailscale` | 匹配 MagicDNS 主机和 DNS 路由后缀 | + #### wifi_ssid !!! quote "" diff --git a/option/rule_dns.go b/option/rule_dns.go index 74058a6544..ab1ddb24a2 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -103,6 +103,7 @@ type RawDefaultDNSRule struct { DefaultInterfaceAddress badoption.Listable[*badoption.Prefixable] `json:"default_interface_address,omitempty"` SourceMACAddress badoption.Listable[string] `json:"source_mac_address,omitempty"` SourceHostname badoption.Listable[string] `json:"source_hostname,omitempty"` + PreferredBy badoption.Listable[string] `json:"preferred_by,omitempty"` RuleSet badoption.Listable[string] `json:"rule_set,omitempty"` RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"` MatchResponse bool `json:"match_response,omitempty"` diff --git a/protocol/tailscale/dns_transport.go b/protocol/tailscale/dns_transport.go index adfe388bfd..9b2263712c 100644 --- a/protocol/tailscale/dns_transport.go +++ b/protocol/tailscale/dns_transport.go @@ -255,6 +255,22 @@ func (t *DNSTransport) Raw() bool { return true } +func (t *DNSTransport) PreferredDomain(domain string) bool { + t.access.RLock() + hosts := t.hosts + routes := t.routes + t.access.RUnlock() + if _, loaded := hosts[domain]; loaded { + return true + } + for suffix := range routes { + if strings.HasSuffix(domain, suffix) { + return true + } + } + return false +} + func (t *DNSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { if len(message.Question) != 1 { return nil, os.ErrInvalid diff --git a/route/rule/rule_dns.go b/route/rule/rule_dns.go index 646f987edf..edc0e06cc0 100644 --- a/route/rule/rule_dns.go +++ b/route/rule/rule_dns.go @@ -329,6 +329,11 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.PreferredBy) > 0 { + item := NewPreferredByDNSItem(ctx, options.PreferredBy) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } if options.RuleSetIPCIDRAcceptEmpty { //nolint:staticcheck if legacyDNSMode { deprecated.Report(ctx, deprecated.OptionRuleSetIPCIDRAcceptEmpty) diff --git a/route/rule/rule_item_preferred_by_dns.go b/route/rule/rule_item_preferred_by_dns.go new file mode 100644 index 0000000000..d00d780c62 --- /dev/null +++ b/route/rule/rule_item_preferred_by_dns.go @@ -0,0 +1,74 @@ +package rule + +import ( + "context" + "strings" + + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/service" + + mDNS "github.com/miekg/dns" +) + +var _ RuleItem = (*PreferredByDNSItem)(nil) + +type PreferredByDNSItem struct { + ctx context.Context + transportTags []string + transports []adapter.DNSTransportWithPreferredDomain +} + +func NewPreferredByDNSItem(ctx context.Context, transportTags []string) *PreferredByDNSItem { + return &PreferredByDNSItem{ + ctx: ctx, + transportTags: transportTags, + } +} + +func (r *PreferredByDNSItem) Start() error { + transportManager := service.FromContext[adapter.DNSTransportManager](r.ctx) + for _, transportTag := range r.transportTags { + rawTransport, loaded := transportManager.Transport(transportTag) + if !loaded { + return E.New("DNS server not found: ", transportTag) + } + transportWithPreferredDomain, withPreferredDomain := rawTransport.(adapter.DNSTransportWithPreferredDomain) + if !withPreferredDomain { + return E.New("DNS server type does not support preferred_by: ", rawTransport.Type()) + } + r.transports = append(r.transports, transportWithPreferredDomain) + } + return nil +} + +func (r *PreferredByDNSItem) Match(metadata *adapter.InboundContext) bool { + var domainHost string + if metadata.Domain != "" { + domainHost = metadata.Domain + } else { + domainHost = metadata.Destination.Fqdn + } + if domainHost == "" { + return false + } + canonical := mDNS.CanonicalName(domainHost) + for _, transport := range r.transports { + if transport.PreferredDomain(canonical) { + return true + } + } + return false +} + +func (r *PreferredByDNSItem) String() string { + description := "preferred_by=" + pLen := len(r.transportTags) + if pLen == 1 { + description += F.ToString(r.transportTags[0]) + } else { + description += "[" + strings.Join(F.MapToString(r.transportTags), " ") + "]" + } + return description +} diff --git a/service/resolved/transport.go b/service/resolved/transport.go index ac20663ae0..e217904de3 100644 --- a/service/resolved/transport.go +++ b/service/resolved/transport.go @@ -32,7 +32,10 @@ func RegisterTransport(registry *dns.TransportRegistry) { dns.RegisterTransport[option.ResolvedDNSServerOptions](registry, C.TypeResolved, NewTransport) } -var _ adapter.DNSTransport = (*Transport)(nil) +var ( + _ adapter.DNSTransport = (*Transport)(nil) + _ adapter.DNSTransportWithPreferredDomain = (*Transport)(nil) +) type Transport struct { dns.TransportAdapter @@ -191,6 +194,22 @@ func (t *Transport) deleteTransport(link *TransportLink) { delete(t.linkServers, link) } +func (t *Transport) PreferredDomain(domain string) bool { + t.service.linkAccess.RLock() + defer t.service.linkAccess.RUnlock() + for _, link := range t.service.links { + for _, linkDomain := range link.domain { + if linkDomain.Domain == "." { + continue + } + if strings.HasSuffix(domain, linkDomain.Domain) { + return true + } + } + } + return false +} + func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { question := message.Question[0] var selectedLink *TransportLink From 98b21227fad469576c3c6d17bb69e45067b66691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 30 Apr 2026 19:42:30 +0800 Subject: [PATCH 68/93] dns: Add mDNS server --- constant/dns.go | 1 + dns/transport/local/local.go | 98 +++- dns/transport/local/local_darwin.go | 120 ----- dns/transport/local/local_darwin_cgo.go | 19 +- .../{local_darwin_dhcp.go => local_dhcp.go} | 2 +- ...local_darwin_nodhcp.go => local_nodhcp.go} | 2 +- dns/transport/local/local_other.go | 14 + dns/transport/local/local_shared.go | 2 - dns/transport/mdns/mdns.go | 456 ++++++++++++++++++ docs/configuration/dns/rule.md | 11 +- docs/configuration/dns/rule.zh.md | 11 +- docs/configuration/dns/server/index.md | 5 + docs/configuration/dns/server/index.zh.md | 5 + docs/configuration/dns/server/mdns.md | 42 ++ docs/configuration/dns/server/mdns.zh.md | 42 ++ include/registry.go | 2 + mkdocs.yml | 1 + option/dns.go | 5 + 18 files changed, 662 insertions(+), 176 deletions(-) delete mode 100644 dns/transport/local/local_darwin.go rename dns/transport/local/{local_darwin_dhcp.go => local_dhcp.go} (93%) rename dns/transport/local/{local_darwin_nodhcp.go => local_nodhcp.go} (90%) create mode 100644 dns/transport/local/local_other.go create mode 100644 dns/transport/mdns/mdns.go create mode 100644 docs/configuration/dns/server/mdns.md create mode 100644 docs/configuration/dns/server/mdns.zh.md diff --git a/constant/dns.go b/constant/dns.go index c7cd0d0374..d39ed82c8b 100644 --- a/constant/dns.go +++ b/constant/dns.go @@ -26,6 +26,7 @@ const ( DNSTypeHosts = "hosts" DNSTypeFakeIP = "fakeip" DNSTypeDHCP = "dhcp" + DNSTypeMDNS = "mdns" DNSTypeTailscale = "tailscale" ) diff --git a/dns/transport/local/local.go b/dns/transport/local/local.go index d13de3fa4c..34897e128e 100644 --- a/dns/transport/local/local.go +++ b/dns/transport/local/local.go @@ -1,5 +1,3 @@ -//go:build !darwin - package local import ( @@ -9,10 +7,13 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/dns/transport/hosts" + "github.com/sagernet/sing-box/dns/transport/mdns" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" + "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" @@ -35,11 +36,20 @@ type Transport struct { hosts *hosts.File dialer N.Dialer preferGo bool + fallback bool resolved ResolvedResolver + mdnsTransport adapter.DNSTransport + dhcpTransport dhcpTransport neighborResolver adapter.NeighborResolver neighborSuffixes []string } +type dhcpTransport interface { + adapter.DNSTransport + Fetch() []M.Socksaddr + Exchange0(ctx context.Context, message *mDNS.Msg, servers []M.Socksaddr) (*mDNS.Msg, error) +} + func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.LocalDNSServerOptions) (adapter.DNSTransport, error) { transportDialer, err := dns.NewLocalDialer(ctx, options) if err != nil { @@ -68,55 +78,77 @@ func (t *Transport) Start(stage adapter.StartStage) error { } else { t.hosts = defaultHosts } - if !t.preferGo { - if isSystemdResolvedManaged() { - resolvedResolver, err := NewResolvedResolver(t.ctx, t.logger) + if !t.preferGo && isSystemdResolvedManaged() { + resolvedResolver, err := NewResolvedResolver(t.ctx, t.logger) + if err == nil { + err = resolvedResolver.Start() if err == nil { - err = resolvedResolver.Start() - if err == nil { - t.resolved = resolvedResolver - } else { - t.logger.Warn(E.Cause(err, "initialize resolved resolver")) - } + t.resolved = resolvedResolver + } else { + t.logger.Warn(E.Cause(err, "initialize resolved resolver")) } } } case adapter.StartStateStart: + if C.IsDarwin { + inboundManager := service.FromContext[adapter.InboundManager](t.ctx) + for _, inbound := range inboundManager.Inbounds() { + if inbound.Type() == C.TypeTun { + t.fallback = true + break + } + } + if t.fallback { + t.dhcpTransport = newDHCPTransport(t.TransportAdapter, log.ContextWithOverrideLevel(t.ctx, log.LevelDebug), t.dialer, t.logger) + } + } else { + t.mdnsTransport = mdns.NewRawTransport(t.TransportAdapter, t.ctx, t.logger) + } router := service.FromContext[adapter.Router](t.ctx) if router != nil { t.neighborResolver = router.NeighborResolver() } + fallthrough + default: + if t.dhcpTransport != nil { + err := t.dhcpTransport.Start(stage) + if err != nil { + return err + } + } + if t.mdnsTransport != nil { + err := t.mdnsTransport.Start(stage) + if err != nil { + return err + } + } } return nil } func (t *Transport) Close() error { - if t.resolved != nil { - return t.resolved.Close() - } - return nil + return common.Close(t.resolved, t.dhcpTransport, t.mdnsTransport) } func (t *Transport) Reset() { + if t.dhcpTransport != nil { + t.dhcpTransport.Reset() + } + if t.mdnsTransport != nil { + t.mdnsTransport.Reset() + } } func (t *Transport) PreferredDomain(domain string) bool { - if t.hosts != nil && t.resolved == nil { + if t.hosts != nil { if len(t.hosts.Lookup(dns.FqdnToDomain(domain))) > 0 { return true } } - return t.hasNeighborHost(domain) + return t.hasNeighborHost(domain) || mdns.IsLocalDomain(domain) } func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { - if t.resolved != nil { - response := t.lookupNeighbor(message) - if response != nil { - return response, nil - } - return t.resolved.Exchange(ctx, message) - } question := message.Question[0] if t.hosts != nil && (question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA) { addresses := t.hosts.Lookup(dns.FqdnToDomain(question.Name)) @@ -128,5 +160,23 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, if response != nil { return response, nil } + if mdns.IsLocalDomain(question.Name) { + if C.IsDarwin { + return t.systemExchange(ctx, message) + } + return t.mdnsTransport.Exchange(ctx, message) + } + if t.resolved != nil { + return t.resolved.Exchange(ctx, message) + } + if t.dhcpTransport != nil { + servers := t.dhcpTransport.Fetch() + if len(servers) > 0 { + return t.dhcpTransport.Exchange0(ctx, message, servers) + } + } + if t.fallback { + return t.systemExchange(ctx, message) + } return t.exchange(ctx, message, question.Name) } diff --git a/dns/transport/local/local_darwin.go b/dns/transport/local/local_darwin.go deleted file mode 100644 index d38a0bf7be..0000000000 --- a/dns/transport/local/local_darwin.go +++ /dev/null @@ -1,120 +0,0 @@ -//go:build darwin - -package local - -import ( - "context" - - "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/hosts" - "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing/common" - "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 RegisterTransport(registry *dns.TransportRegistry) { - dns.RegisterTransport[option.LocalDNSServerOptions](registry, C.DNSTypeLocal, NewTransport) -} - -var ( - _ adapter.DNSTransport = (*Transport)(nil) - _ adapter.DNSTransportWithPreferredDomain = (*Transport)(nil) -) - -type Transport struct { - dns.TransportAdapter - ctx context.Context - logger logger.ContextLogger - hosts *hosts.File - dialer N.Dialer - fallback bool - dhcpTransport dhcpTransport - neighborResolver adapter.NeighborResolver - neighborSuffixes []string -} - -type dhcpTransport interface { - adapter.DNSTransport - Fetch() []M.Socksaddr - Exchange0(ctx context.Context, message *mDNS.Msg, servers []M.Socksaddr) (*mDNS.Msg, error) -} - -func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.LocalDNSServerOptions) (adapter.DNSTransport, error) { - transportDialer, err := dns.NewLocalDialer(ctx, options) - if err != nil { - return nil, err - } - suffixes, err := buildNeighborMatchers(options.NeighborDomain) - if err != nil { - return nil, err - } - return &Transport{ - TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options), - ctx: ctx, - logger: logger, - dialer: transportDialer, - neighborSuffixes: suffixes, - }, nil -} - -func (t *Transport) Start(stage adapter.StartStage) error { - switch stage { - case adapter.StartStateStart: - defaultHosts, err := hosts.NewDefault() - if err != nil { - t.logger.Warn(err) - } else { - t.hosts = defaultHosts - } - inboundManager := service.FromContext[adapter.InboundManager](t.ctx) - for _, inbound := range inboundManager.Inbounds() { - if inbound.Type() == C.TypeTun { - t.fallback = true - break - } - } - if t.fallback { - t.dhcpTransport = newDHCPTransport(t.TransportAdapter, log.ContextWithOverrideLevel(t.ctx, log.LevelDebug), t.dialer, t.logger) - if t.dhcpTransport != nil { - err = t.dhcpTransport.Start(stage) - if err != nil { - return err - } - } - } - router := service.FromContext[adapter.Router](t.ctx) - if router != nil { - t.neighborResolver = router.NeighborResolver() - } - } - return nil -} - -func (t *Transport) Close() error { - return common.Close( - t.dhcpTransport, - ) -} - -func (t *Transport) Reset() { - if t.dhcpTransport != nil { - t.dhcpTransport.Reset() - } -} - -func (t *Transport) PreferredDomain(domain string) bool { - if t.hosts != nil { - if len(t.hosts.Lookup(dns.FqdnToDomain(domain))) > 0 { - return true - } - } - return t.hasNeighborHost(domain) -} diff --git a/dns/transport/local/local_darwin_cgo.go b/dns/transport/local/local_darwin_cgo.go index bfea8dd64c..de2ec56a4b 100644 --- a/dns/transport/local/local_darwin_cgo.go +++ b/dns/transport/local/local_darwin_cgo.go @@ -31,7 +31,6 @@ import ( "errors" "unsafe" - boxC "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/dns" E "github.com/sagernet/sing/common/exceptions" @@ -78,24 +77,8 @@ func darwinResolverHErrno(name string, hErrno int) error { } } -func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { +func (t *Transport) systemExchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { question := message.Question[0] - if t.hosts != nil && (question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA) { - addresses := t.hosts.Lookup(dns.FqdnToDomain(question.Name)) - if len(addresses) > 0 { - return dns.FixedResponse(message.Id, question, addresses, boxC.DefaultDNSTTL), nil - } - } - response := t.lookupNeighbor(message) - if response != nil { - return response, nil - } - if t.dhcpTransport != nil { - dhcpServers := t.dhcpTransport.Fetch() - if len(dhcpServers) > 0 { - return t.dhcpTransport.Exchange0(ctx, message, dhcpServers) - } - } type resolvResult struct { response *mDNS.Msg err error diff --git a/dns/transport/local/local_darwin_dhcp.go b/dns/transport/local/local_dhcp.go similarity index 93% rename from dns/transport/local/local_darwin_dhcp.go rename to dns/transport/local/local_dhcp.go index b228b76a48..bf77ed252d 100644 --- a/dns/transport/local/local_darwin_dhcp.go +++ b/dns/transport/local/local_dhcp.go @@ -1,4 +1,4 @@ -//go:build darwin && with_dhcp +//go:build with_dhcp package local diff --git a/dns/transport/local/local_darwin_nodhcp.go b/dns/transport/local/local_nodhcp.go similarity index 90% rename from dns/transport/local/local_darwin_nodhcp.go rename to dns/transport/local/local_nodhcp.go index 5ce84690a4..7893d416f6 100644 --- a/dns/transport/local/local_darwin_nodhcp.go +++ b/dns/transport/local/local_nodhcp.go @@ -1,4 +1,4 @@ -//go:build darwin && !with_dhcp +//go:build !with_dhcp package local diff --git a/dns/transport/local/local_other.go b/dns/transport/local/local_other.go new file mode 100644 index 0000000000..9bb3d7777a --- /dev/null +++ b/dns/transport/local/local_other.go @@ -0,0 +1,14 @@ +//go:build !darwin + +package local + +import ( + "context" + "os" + + mDNS "github.com/miekg/dns" +) + +func (t *Transport) systemExchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { + return nil, os.ErrInvalid +} diff --git a/dns/transport/local/local_shared.go b/dns/transport/local/local_shared.go index 64a23a9fcb..7763545841 100644 --- a/dns/transport/local/local_shared.go +++ b/dns/transport/local/local_shared.go @@ -1,5 +1,3 @@ -//go:build !darwin - package local import ( diff --git a/dns/transport/mdns/mdns.go b/dns/transport/mdns/mdns.go new file mode 100644 index 0000000000..2db3390d53 --- /dev/null +++ b/dns/transport/mdns/mdns.go @@ -0,0 +1,456 @@ +package mdns + +import ( + "context" + "net" + "net/netip" + "slices" + "strings" + "time" + + "github.com/sagernet/sing-box/adapter" + 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" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/control" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json/badoption" + "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/common/task" + "github.com/sagernet/sing/service" + + mDNS "github.com/miekg/dns" + "golang.org/x/net/ipv4" + "golang.org/x/net/ipv6" +) + +const ( + mdnsPort = 5353 + mdnsClassTopBit = 1 << 15 + mdnsTimeout = time.Second +) + +var ( + mdnsGroupIPv4 = net.IPv4(224, 0, 0, 251) + mdnsGroupIPv6 = net.ParseIP("ff02::fb") + mdnsLocalZones = []string{ + "local.", + "254.169.in-addr.arpa.", + "8.e.f.ip6.arpa.", + "9.e.f.ip6.arpa.", + "a.e.f.ip6.arpa.", + "b.e.f.ip6.arpa.", + } +) + +func IsLocalDomain(name string) bool { + canonical := mDNS.CanonicalName(name) + return common.Any(mdnsLocalZones, func(zone string) bool { + return canonical == zone || strings.HasSuffix(canonical, "."+zone) + }) +} + +func RegisterTransport(registry *dns.TransportRegistry) { + dns.RegisterTransport[option.MDNSDNSServerOptions](registry, C.DNSTypeMDNS, NewTransport) +} + +var ( + _ adapter.DNSTransport = (*Transport)(nil) + _ adapter.DNSTransportWithPreferredDomain = (*Transport)(nil) +) + +type Transport struct { + dns.TransportAdapter + ctx context.Context + logger logger.ContextLogger + networkManager adapter.NetworkManager + interfaceNames badoption.Listable[string] +} + +func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.MDNSDNSServerOptions) (adapter.DNSTransport, error) { + return &Transport{ + TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeMDNS, tag, options.LocalDNSServerOptions), + ctx: ctx, + logger: logger, + networkManager: service.FromContext[adapter.NetworkManager](ctx), + interfaceNames: options.Interface, + }, nil +} + +func NewRawTransport(transportAdapter dns.TransportAdapter, ctx context.Context, logger log.ContextLogger) *Transport { + return &Transport{ + TransportAdapter: transportAdapter, + ctx: ctx, + logger: logger, + networkManager: service.FromContext[adapter.NetworkManager](ctx), + } +} + +func (t *Transport) Start(stage adapter.StartStage) error { + return nil +} + +func (t *Transport) Close() error { + return nil +} + +func (t *Transport) Reset() { +} + +func (t *Transport) PreferredDomain(domain string) bool { + return IsLocalDomain(domain) +} + +func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { + targets, err := t.queryTargets() + if err != nil { + return nil, E.Cause(err, "mdns: prepare interfaces") + } + request := makeQueryMessage(message) + rawMessage, err := request.Pack() + if err != nil { + return nil, E.Cause(err, "mdns: pack request") + } + deadline, loaded := ctx.Deadline() + if !loaded || deadline.IsZero() { + deadline = time.Now().Add(mdnsTimeout) + } + exchangeCtx, cancel := context.WithDeadline(ctx, deadline) + defer cancel() + results := make(chan exchangeResult, len(targets)) + var group task.Group + for _, target := range targets { + group.Append0(func(ctx context.Context) error { + response, err := t.exchangeTarget(ctx, target, rawMessage, message.Question[0], deadline) + if err != nil || response != nil { + results <- exchangeResult{ + response: response, + err: err, + } + } + return nil + }) + } + groupErr := group.Run(exchangeCtx) + close(results) + response := newResponse(message) + seenRecords := make(map[string]bool) + var lastErr error + for result := range results { + if result.err != nil { + lastErr = result.err + t.logger.TraceContext(ctx, result.err) + continue + } + mergeResponse(response, result.response, seenRecords) + } + if len(response.Answer) > 0 || len(response.Ns) > 0 || len(response.Extra) > 0 { + return response, nil + } + if lastErr != nil { + return nil, lastErr + } + if groupErr != nil && ctx.Err() != nil { + return nil, groupErr + } + return nil, E.New("mdns: query timeout") +} + +type exchangeResult struct { + response *mDNS.Msg + err error +} + +type queryTarget struct { + iface control.Interface + family string +} + +func (t *Transport) exchangeTarget(ctx context.Context, target queryTarget, rawMessage []byte, question mDNS.Question, deadline time.Time) (*mDNS.Msg, error) { + packetConn, destination, err := t.listenPacket(ctx, target) + if err != nil { + return nil, err + } + defer packetConn.Close() + + _, err = packetConn.WriteTo(rawMessage, destination) + if err != nil { + return nil, E.Cause(err, "mdns: write request on ", target.iface.Name, " ", target.family) + } + err = packetConn.SetReadDeadline(deadline) + if err != nil { + return nil, E.Cause(err, "mdns: set deadline on ", target.iface.Name, " ", target.family) + } + response := newResponseFromQuestion(question) + seenRecords := make(map[string]bool) + buffer := buf.Get(buf.UDPBufferSize) + defer buf.Put(buffer) + for { + n, source, readErr := packetConn.ReadFrom(buffer) + if readErr != nil { + if E.IsTimeout(readErr) { + if len(response.Answer) > 0 || len(response.Ns) > 0 || len(response.Extra) > 0 { + return response, nil + } + return nil, nil + } + return nil, E.Cause(readErr, "mdns: read response on ", target.iface.Name, " ", target.family) + } + if !validSource(source, target) { + continue + } + var candidate mDNS.Msg + err = candidate.Unpack(buffer[:n]) + if err != nil { + t.logger.TraceContext(ctx, "mdns: unpack response: ", err) + continue + } + if !validResponse(&candidate, question) { + continue + } + normalizeResponse(&candidate, question) + mergeResponse(response, &candidate, seenRecords) + } +} + +func (t *Transport) listenPacket(ctx context.Context, target queryTarget) (net.PacketConn, net.Addr, error) { + var listenConfig net.ListenConfig + listenConfig.Control = control.Append(listenConfig.Control, control.BindToInterface(t.networkManager.InterfaceFinder(), target.iface.Name, target.iface.Index)) + netInterface := target.iface.NetInterface() + switch target.family { + case "udp4": + packetConn, err := listenConfig.ListenPacket(ctx, "udp4", "0.0.0.0:0") + if err != nil { + return nil, nil, E.Cause(err, "mdns: listen on ", target.iface.Name, " udp4") + } + ipv4Conn := ipv4.NewPacketConn(packetConn) + err = ipv4Conn.SetMulticastInterface(&netInterface) + if err != nil { + packetConn.Close() + return nil, nil, E.Cause(err, "mdns: set multicast interface on ", target.iface.Name, " udp4") + } + _ = ipv4Conn.SetMulticastTTL(255) + return packetConn, &net.UDPAddr{IP: mdnsGroupIPv4, Port: mdnsPort}, nil + case "udp6": + packetConn, err := listenConfig.ListenPacket(ctx, "udp6", "[::]:0") + if err != nil { + return nil, nil, E.Cause(err, "mdns: listen on ", target.iface.Name, " udp6") + } + ipv6Conn := ipv6.NewPacketConn(packetConn) + err = ipv6Conn.SetMulticastInterface(&netInterface) + if err != nil { + packetConn.Close() + return nil, nil, E.Cause(err, "mdns: set multicast interface on ", target.iface.Name, " udp6") + } + _ = ipv6Conn.SetMulticastHopLimit(255) + return packetConn, &net.UDPAddr{IP: mdnsGroupIPv6, Port: mdnsPort, Zone: target.iface.Name}, nil + default: + return nil, nil, E.New("mdns: unknown network: ", target.family) + } +} + +func (t *Transport) queryTargets() ([]queryTarget, error) { + interfaces, err := t.fetchInterfaces() + if err != nil { + return nil, err + } + var targets []queryTarget + for _, iface := range interfaces { + supports4, supports6 := interfaceFamilies(iface) + if supports4 { + targets = append(targets, queryTarget{ + iface: iface, + family: "udp4", + }) + } + if supports6 { + targets = append(targets, queryTarget{ + iface: iface, + family: "udp6", + }) + } + } + if len(targets) == 0 { + return nil, E.New("missing usable mDNS interfaces") + } + return targets, nil +} + +func (t *Transport) fetchInterfaces() ([]control.Interface, error) { + finder := t.networkManager.InterfaceFinder() + var interfaces []control.Interface + if len(t.interfaceNames) > 0 { + for _, interfaceName := range t.interfaceNames { + iface, err := finder.ByName(interfaceName) + if err != nil { + t.logger.Warn("mdns: interface ", interfaceName, " not found") + continue + } + if !isUsableInterface(*iface) { + t.logger.Warn("mdns: interface ", interfaceName, " is not usable") + continue + } + interfaces = append(interfaces, *iface) + } + } else { + interfaces = common.Filter(finder.Interfaces(), isUsableInterface) + } + if len(interfaces) == 0 { + return nil, E.New("mdns: missing usable interface") + } + return interfaces, nil +} + +func isUsableInterface(iface control.Interface) bool { + return iface.Flags&net.FlagUp != 0 && + iface.Flags&net.FlagMulticast != 0 && + iface.Flags&net.FlagLoopback == 0 +} + +func interfaceFamilies(iface control.Interface) (supports4, supports6 bool) { + for _, prefix := range iface.Addresses { + addr := prefix.Addr() + if addr.IsLoopback() { + continue + } + if addr.Is4() { + supports4 = true + } else if addr.Is6() && !addr.Is4In6() { + supports6 = true + } + if supports4 && supports6 { + return + } + } + return +} + +func makeQueryMessage(message *mDNS.Msg) *mDNS.Msg { + request := &mDNS.Msg{ + Question: slices.Clone(message.Question), + } + for i := range request.Question { + stripQuestionClass(&request.Question[i]) + } + return request +} + +func newResponse(message *mDNS.Msg) *mDNS.Msg { + response := newResponseFromQuestion(message.Question[0]) + response.Id = message.Id + return response +} + +func newResponseFromQuestion(question mDNS.Question) *mDNS.Msg { + stripQuestionClass(&question) + return &mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + Response: true, + Authoritative: true, + Rcode: mDNS.RcodeSuccess, + }, + Question: []mDNS.Question{question}, + } +} + +func validSource(source net.Addr, target queryTarget) bool { + sourceUDP, isUDP := source.(*net.UDPAddr) + if !isUDP || sourceUDP.Port != mdnsPort { + return false + } + sourceAddr, loaded := netip.AddrFromSlice(sourceUDP.IP) + if !loaded { + return false + } + sourceAddr = sourceAddr.Unmap() + if (target.family == "udp4" && !sourceAddr.Is4()) || (target.family == "udp6" && !sourceAddr.Is6()) { + return false + } + for _, prefix := range target.iface.Addresses { + if prefix.Contains(sourceAddr) { + return true + } + } + return false +} + +func validResponse(response *mDNS.Msg, question mDNS.Question) bool { + if !response.Response || + response.Opcode != mDNS.OpcodeQuery || + response.Rcode != mDNS.RcodeSuccess { + return false + } + for _, responseQuestion := range response.Question { + if questionMatches(responseQuestion, question) { + return true + } + } + return responseHasMatchingRecord(response, question) +} + +func responseHasMatchingRecord(response *mDNS.Msg, question mDNS.Question) bool { + for _, recordList := range [][]mDNS.RR{response.Answer, response.Ns, response.Extra} { + for _, record := range recordList { + if recordMatchesQuestion(record, question) { + return true + } + } + } + return false +} + +func questionMatches(left mDNS.Question, right mDNS.Question) bool { + stripQuestionClass(&left) + stripQuestionClass(&right) + return left.Qtype == right.Qtype && + left.Qclass == right.Qclass && + strings.EqualFold(left.Name, right.Name) +} + +func recordMatchesQuestion(record mDNS.RR, question mDNS.Question) bool { + header := record.Header() + return strings.EqualFold(header.Name, question.Name) && + (question.Qtype == mDNS.TypeANY || + header.Rrtype == question.Qtype || + header.Rrtype == mDNS.TypeCNAME) +} + +func normalizeResponse(response *mDNS.Msg, question mDNS.Question) { + response.Id = 0 + response.Question = []mDNS.Question{question} + for i := range response.Question { + stripQuestionClass(&response.Question[i]) + } + for _, recordList := range [][]mDNS.RR{response.Answer, response.Ns, response.Extra} { + for _, record := range recordList { + stripRecordClass(record) + } + } +} + +func mergeResponse(destination *mDNS.Msg, source *mDNS.Msg, seenRecords map[string]bool) { + appendRecords := func(destinationRecords *[]mDNS.RR, sourceRecords []mDNS.RR) { + for _, record := range sourceRecords { + key := record.String() + if seenRecords[key] { + continue + } + seenRecords[key] = true + *destinationRecords = append(*destinationRecords, record) + } + } + appendRecords(&destination.Answer, source.Answer) + appendRecords(&destination.Ns, source.Ns) + appendRecords(&destination.Extra, source.Extra) +} + +func stripQuestionClass(question *mDNS.Question) { + question.Qclass &^= mdnsClassTopBit +} + +func stripRecordClass(record mDNS.RR) { + record.Header().Class &^= mdnsClassTopBit +} diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index b8783d60ee..897bb09b88 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -507,11 +507,12 @@ 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 and neighbor-resolved hosts | -| `tailscale` | Match MagicDNS hosts and DNS route suffixes | +| 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 | #### wifi_ssid diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index 2c2de7d033..55e2298059 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -499,11 +499,12 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`. 匹配指定 DNS 服务器的首选域名。 -| 类型 | 匹配 | -|-------------|--------------------------| -| `hosts` | 匹配预定义条目和 hosts 文件中的条目 | -| `local` | 匹配 hosts 中的条目和邻居解析得到的主机名 | -| `tailscale` | 匹配 MagicDNS 主机和 DNS 路由后缀 | +| 类型 | 匹配 | +|-------------|-------------------------------------------------------------| +| `hosts` | 匹配预定义条目和 hosts 文件中的条目 | +| `local` | 匹配 hosts 中的条目、邻居解析得到的主机名以及 mDNS 本地域名 | +| `mdns` | 匹配 mDNS 本地域名(`*.local.` 以及 IPv4/IPv6 链路本地反向区域) | +| `tailscale` | 匹配 MagicDNS 主机和 DNS 路由后缀 | #### wifi_ssid diff --git a/docs/configuration/dns/server/index.md b/docs/configuration/dns/server/index.md index b610cf5b02..bcb0586175 100644 --- a/docs/configuration/dns/server/index.md +++ b/docs/configuration/dns/server/index.md @@ -2,6 +2,10 @@ icon: material/alert-decagram --- +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [mdns](./mdns/) + !!! quote "Changes in sing-box 1.12.0" :material-plus: [type](#type) @@ -39,6 +43,7 @@ The type of the DNS server. | `https` | [HTTPS](./https/) | | `h3` | [HTTP/3](./http3/) | | `dhcp` | [DHCP](./dhcp/) | +| `mdns` | [mDNS](./mdns/) | | `fakeip` | [Fake IP](./fakeip/) | | `tailscale` | [Tailscale](./tailscale/) | | `resolved` | [Resolved](./resolved/) | diff --git a/docs/configuration/dns/server/index.zh.md b/docs/configuration/dns/server/index.zh.md index d1a4dc3c40..54dd97e7f9 100644 --- a/docs/configuration/dns/server/index.zh.md +++ b/docs/configuration/dns/server/index.zh.md @@ -2,6 +2,10 @@ icon: material/alert-decagram --- +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [mdns](./mdns/) + !!! quote "sing-box 1.12.0 中的更改" :material-plus: [type](#type) @@ -39,6 +43,7 @@ DNS 服务器的类型。 | `https` | [HTTPS](./https/) | | `h3` | [HTTP/3](./http3/) | | `dhcp` | [DHCP](./dhcp/) | +| `mdns` | [mDNS](./mdns/) | | `fakeip` | [Fake IP](./fakeip/) | | `tailscale` | [Tailscale](./tailscale/) | | `resolved` | [Resolved](./resolved/) | diff --git a/docs/configuration/dns/server/mdns.md b/docs/configuration/dns/server/mdns.md new file mode 100644 index 0000000000..d42e071522 --- /dev/null +++ b/docs/configuration/dns/server/mdns.md @@ -0,0 +1,42 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.14.0" + +# mDNS + +### Structure + +```json +{ + "dns": { + "servers": [ + { + "type": "mdns", + "tag": "", + + "interface": [], + + // Dial Fields + } + ] + } +} +``` + +!!! info "" + + You usually do not need an explicit `mdns` server in addition to a [Local](./local/) server: the local server already routes queries for `*.local.` and IPv4/IPv6 link-local reverse zones via mDNS on non-Apple platforms and via the system resolver on Apple platforms. Add an explicit `mdns` server only when you want to reference it from [`preferred_by`](../rule/#preferred_by) or use it standalone. + +### Fields + +#### interface + +List of network interface names to send mDNS queries on. + +When empty, all interfaces that are up, multicast-capable, and non-loopback are used. + +### Dial Fields + +See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/dns/server/mdns.zh.md b/docs/configuration/dns/server/mdns.zh.md new file mode 100644 index 0000000000..3af3763e5f --- /dev/null +++ b/docs/configuration/dns/server/mdns.zh.md @@ -0,0 +1,42 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.14.0 起" + +# mDNS + +### 结构 + +```json +{ + "dns": { + "servers": [ + { + "type": "mdns", + "tag": "", + + "interface": [], + + // 拨号字段 + } + ] + } +} +``` + +!!! info "" + + 通常不需要在 [Local](./local/) 服务器之外再添加显式的 `mdns` 服务器:本地服务器已经会在非 Apple 平台通过 mDNS、在 Apple 平台通过系统解析器来回答 `*.local.` 与 IPv4/IPv6 链路本地反向区域的查询。仅当需要从 [`preferred_by`](../rule/#preferred_by) 引用,或独立使用时,才需要显式添加 `mdns` 服务器。 + +### 字段 + +#### interface + +用于发送 mDNS 查询的网络接口名称列表。 + +留空时,将使用所有处于 up 状态、支持多播且非环回的接口。 + +### 拨号字段 + +参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。 diff --git a/include/registry.go b/include/registry.go index 5a1a2f973a..91d66bbd5b 100644 --- a/include/registry.go +++ b/include/registry.go @@ -16,6 +16,7 @@ import ( "github.com/sagernet/sing-box/dns/transport/fakeip" "github.com/sagernet/sing-box/dns/transport/hosts" "github.com/sagernet/sing-box/dns/transport/local" + "github.com/sagernet/sing-box/dns/transport/mdns" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/protocol/anytls" @@ -118,6 +119,7 @@ func DNSTransportRegistry() *dns.TransportRegistry { transport.RegisterHTTPS(registry) hosts.RegisterTransport(registry) local.RegisterTransport(registry) + mdns.RegisterTransport(registry) fakeip.RegisterTransport(registry) resolved.RegisterTransport(registry) diff --git a/mkdocs.yml b/mkdocs.yml index 8a583f15ba..4280979e0e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -93,6 +93,7 @@ nav: - HTTPS: configuration/dns/server/https.md - HTTP3: configuration/dns/server/http3.md - DHCP: configuration/dns/server/dhcp.md + - mDNS: configuration/dns/server/mdns.md - FakeIP: configuration/dns/server/fakeip.md - Tailscale: configuration/dns/server/tailscale.md - Resolved: configuration/dns/server/resolved.md diff --git a/option/dns.go b/option/dns.go index 53a078bb3b..6d3970d884 100644 --- a/option/dns.go +++ b/option/dns.go @@ -184,3 +184,8 @@ type DHCPDNSServerOptions struct { LocalDNSServerOptions Interface string `json:"interface,omitempty"` } + +type MDNSDNSServerOptions struct { + LocalDNSServerOptions + Interface badoption.Listable[string] `json:"interface,omitempty"` +} From 85889fc701743717b500e2ccb3ad65998e4f8401 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 30 Apr 2026 22:18:17 +0800 Subject: [PATCH 69/93] cronet: Remove additional QUIC certificate check --- .github/CRONET_GO_VERSION | 2 +- go.mod | 62 +++++++++---------- go.sum | 124 +++++++++++++++++++------------------- 3 files changed, 94 insertions(+), 94 deletions(-) diff --git a/.github/CRONET_GO_VERSION b/.github/CRONET_GO_VERSION index f8f1198fba..bad6f2267b 100644 --- a/.github/CRONET_GO_VERSION +++ b/.github/CRONET_GO_VERSION @@ -1 +1 @@ -e4926ba205fae5351e3d3eeafff7e7029654424a +6a30864fae5a9a6bff2e12a605cf6da942bc6f0a diff --git a/go.mod b/go.mod index 11440de956..15afa043f1 100644 --- a/go.mod +++ b/go.mod @@ -31,8 +31,8 @@ require ( github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1 github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a github.com/sagernet/cors v1.2.1 - github.com/sagernet/cronet-go v0.0.0-20260413093659-e4926ba205fa - github.com/sagernet/cronet-go/all v0.0.0-20260413093659-e4926ba205fa + github.com/sagernet/cronet-go v0.0.0-20260430111606-b19b50ccea0c + github.com/sagernet/cronet-go/all v0.0.0-20260430111606-b19b50ccea0c github.com/sagernet/fswatch v0.1.2 github.com/sagernet/gomobile v0.1.12 github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 @@ -109,35 +109,35 @@ require ( github.com/prometheus-community/pro-bing v0.4.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/safchain/ethtool v0.3.0 // indirect - github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260413092954-cd09eb3e271b // indirect - github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260413092954-cd09eb3e271b // indirect - github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260413092954-cd09eb3e271b // indirect - github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260413092954-cd09eb3e271b // indirect - github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260413092954-cd09eb3e271b // indirect - github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260413092954-cd09eb3e271b // indirect - github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260413092954-cd09eb3e271b // indirect - github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260413092954-cd09eb3e271b // indirect - github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260413092954-cd09eb3e271b // indirect - github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260413092954-cd09eb3e271b // indirect - github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260413092954-cd09eb3e271b // indirect - github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260413092954-cd09eb3e271b // indirect - github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260413092954-cd09eb3e271b // indirect - github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260413092954-cd09eb3e271b // indirect - github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260413092954-cd09eb3e271b // indirect - github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260413092954-cd09eb3e271b // indirect - github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260413092954-cd09eb3e271b // indirect - github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260413092954-cd09eb3e271b // indirect - github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260413092954-cd09eb3e271b // indirect - github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260413092954-cd09eb3e271b // indirect - github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260413092954-cd09eb3e271b // indirect - github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260413092954-cd09eb3e271b // indirect - github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260413092954-cd09eb3e271b // indirect - github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260413092954-cd09eb3e271b // indirect - github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260413092954-cd09eb3e271b // indirect - github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260413092954-cd09eb3e271b // indirect - github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260413092954-cd09eb3e271b // indirect - github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260413092954-cd09eb3e271b // indirect - github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260430110913-f15849bb7a59 // indirect + github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260430110913-f15849bb7a59 // indirect + github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260430110913-f15849bb7a59 // indirect + github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260430110913-f15849bb7a59 // indirect + github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260430110913-f15849bb7a59 // indirect + github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260430110913-f15849bb7a59 // indirect + github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260430110913-f15849bb7a59 // indirect + github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260430110913-f15849bb7a59 // indirect + github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260430110913-f15849bb7a59 // indirect + github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260430110913-f15849bb7a59 // indirect + github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260430110913-f15849bb7a59 // indirect + github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260430110913-f15849bb7a59 // indirect + github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260430110913-f15849bb7a59 // indirect + github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260430110913-f15849bb7a59 // indirect + github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260430110913-f15849bb7a59 // indirect + github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260430110913-f15849bb7a59 // indirect + github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260430110913-f15849bb7a59 // indirect + github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260430110913-f15849bb7a59 // indirect + github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260430110913-f15849bb7a59 // indirect + github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260430110913-f15849bb7a59 // indirect + github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260430110913-f15849bb7a59 // indirect + github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260430110913-f15849bb7a59 // indirect + github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260430110913-f15849bb7a59 // indirect + github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260430110913-f15849bb7a59 // indirect + github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260430110913-f15849bb7a59 // indirect + github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260430110913-f15849bb7a59 // indirect + github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260430110913-f15849bb7a59 // indirect + github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260430110913-f15849bb7a59 // indirect + github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260430110913-f15849bb7a59 // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect github.com/sagernet/nftables v0.3.0-mod.2 // indirect github.com/spf13/pflag v1.0.9 // indirect diff --git a/go.sum b/go.sum index e5bf275cb8..1dd31b5670 100644 --- a/go.sum +++ b/go.sum @@ -168,68 +168,68 @@ github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkk github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ= github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI= -github.com/sagernet/cronet-go v0.0.0-20260413093659-e4926ba205fa h1:7SehNSF1UHbLZa5dk+1rW1aperffJzl5r6TCJIXtAaY= -github.com/sagernet/cronet-go v0.0.0-20260413093659-e4926ba205fa/go.mod h1:hwFHBEjjthyEquDULbr4c4ucMedp8Drb6Jvm2kt/0Bw= -github.com/sagernet/cronet-go/all v0.0.0-20260413093659-e4926ba205fa h1:ijk5v9N/akiMgqu734yMpv7Pk9F4Qmjh8Vfdcb4uJHE= -github.com/sagernet/cronet-go/all v0.0.0-20260413093659-e4926ba205fa/go.mod h1:+FENo4+0AOvH9e3oY6/iO7yy7USNt61dgbnI5W0TDZ0= -github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260413092954-cd09eb3e271b h1:O+PkYT88ayVWESX5tqxeMeS9OnzC3ZTic8gYiPJNXT8= -github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw= -github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260413092954-cd09eb3e271b h1:o0MsgbsJwYkbqlbfaCvmAwb8/LAXeoSP8NE/aNvR/yY= -github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM= -github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260413092954-cd09eb3e271b h1:JEQnc7cRMUahWJFtWY6n0hs1LE0KgyRv3pD0RWS8Yo8= -github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc= -github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260413092954-cd09eb3e271b h1:69+AKzuUW9hzw2nU79c2DWfuzrIZ3PJm1KAwXh+7xr0= -github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ= -github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260413092954-cd09eb3e271b h1:jp9FHUVTCJQ67Ecw3Inoct6/z1VTFXPtNYpXt47pa4E= -github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs= -github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260413092954-cd09eb3e271b h1:WN3DZoECd2UbhmYQGpOA4jx4QBXiZuN1DvL/35NT61g= -github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0= -github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260413092954-cd09eb3e271b h1:H4RKicwrIa4PwTXZOmXOg85hiCrpeFja4daOlX180pE= -github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0= -github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260413092954-cd09eb3e271b h1:Rwi+Cu+Hgwj28F1lh837gGqSqn7oU8+r5i3UJyLPkKc= -github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4= -github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260413092954-cd09eb3e271b h1:v2wcnPX3gt0PngFYXjXYAiarFckwx3pVAP6ETSpbSWE= -github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo= -github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260413092954-cd09eb3e271b h1:Bl0zZ3QZq6pPJMbQlYHDhhaGngVefRlFzxWc0p48eHo= -github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ= -github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260413092954-cd09eb3e271b h1:vf+MbGv6RvvmXUNvganykBOnDIVXxy8XgtKOOqOcxtE= -github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU= -github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260413092954-cd09eb3e271b h1:2IAc1bVFYF+B6hof34ChQKVhw7LElBxEEx7S0n+7o78= -github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI= -github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260413092954-cd09eb3e271b h1:NrJaiOS0VLmWTbUHhXDsLTqelmCW4y3xJqptPs4Sx0s= -github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ= -github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260413092954-cd09eb3e271b h1:A+ubSkca1nl2cT8pYUqCo1O7M41suNrKpWhZKCM/aIQ= -github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0= -github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260413092954-cd09eb3e271b h1:WrhGH5FDXlCAoXwN6N44yCMvy6EbIurmTmptkz3mmms= -github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s= -github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260413092954-cd09eb3e271b h1:kgwB5p5e0gdVX5iYRE7VbZS/On4qnb4UKonkGPwhkDI= -github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ= -github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260413092954-cd09eb3e271b h1:Z3dOeFlRIOeQhSh+mCYDHui1yR3S/Uw8eupczzBvxqw= -github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow= -github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260413092954-cd09eb3e271b h1:LPi6jz1k11Q67hm3Pw6aaPJ/Z6e3VtNhzrRjr5/5AQo= -github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk= -github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260413092954-cd09eb3e271b h1:55sqihyfXWN7y7p7gOEgtUz9cm1mV3SDQ90/v6ROFaA= -github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E= -github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260413092954-cd09eb3e271b h1:OTA1cbv5YIDVsYA8AAXHC4NgEc7b6pDiY+edujLWfJU= -github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8= -github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260413092954-cd09eb3e271b h1:B/rdD/1A+RgqUYUZcoGhLeMqijnBd1mUt8+5LhOH7j8= -github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w= -github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260413092954-cd09eb3e271b h1:QFRWi6FucrODS4xQ8e9GYIzGSeMFO/DAMtTCVeJiCvM= -github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0= -github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260413092954-cd09eb3e271b h1:2WJjPKZHLNIB4D17c3o9S+SP9kb3Qh0D26oWlun1+pE= -github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs= -github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260413092954-cd09eb3e271b h1:cUNTe4gNncRpYL28jzQf6qcJej40zzGQsH0o6CLUGws= -github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc= -github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260413092954-cd09eb3e271b h1:+sc1LJF0FjU2hVO5xBqqT+8qzoU08J2uHwxSle2m/Hw= -github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4= -github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260413092954-cd09eb3e271b h1:+D/uhFxllI/KTLpeNEl8dwF3omPGmUFbrqt5tJkAyp0= -github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc= -github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260413092954-cd09eb3e271b h1:nSUzzTUAZdqjGGckayk64sz+F0TGJPHvauTiAn27UKk= -github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc= -github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260413092954-cd09eb3e271b h1:PE/fYBiHzB52gnQMg0soBfQyJCzmWHti48kCe2TBt9w= -github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= -github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260413092954-cd09eb3e271b h1:hy/3lPV11pKAAojDFnb95l9NpwOym6kME7FxS9p8sXs= -github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw= +github.com/sagernet/cronet-go v0.0.0-20260430111606-b19b50ccea0c h1:Z6rd2dZ0pIYC2Sbz6c1zcDwITdQ56PkBkuAlRx3Mg5A= +github.com/sagernet/cronet-go v0.0.0-20260430111606-b19b50ccea0c/go.mod h1:hwFHBEjjthyEquDULbr4c4ucMedp8Drb6Jvm2kt/0Bw= +github.com/sagernet/cronet-go/all v0.0.0-20260430111606-b19b50ccea0c h1:S+2MG8NSwD1ietZzXlZGFENtw/NjSZgzpiercXy4k54= +github.com/sagernet/cronet-go/all v0.0.0-20260430111606-b19b50ccea0c/go.mod h1:71rv3TtJ/lZDVC9/Qqo/H8YJpAcsoEl+OJj29YxLU2M= +github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260430110913-f15849bb7a59 h1:FgyHy7cPMlP+3MvbPSx6eq8/TkeMK/i1OT7WbGoWzAo= +github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260430110913-f15849bb7a59/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw= +github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260430110913-f15849bb7a59 h1:N82Dqwee5umZ1uPGACrU+ObXOGq26dYw7ChXUkOiAiY= +github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260430110913-f15849bb7a59/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM= +github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260430110913-f15849bb7a59 h1:3CPK9qAU2UHi7aVpwYjlZgRiWpVdm4+960Ao+hMH1g4= +github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260430110913-f15849bb7a59/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc= +github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260430110913-f15849bb7a59 h1:6MIkfoApgGPLZzKvCLmdC9JEhj14cSgcGrnUq/g4uaM= +github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260430110913-f15849bb7a59/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ= +github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260430110913-f15849bb7a59 h1:tTmakYDgG9pkZhznzGMY2+8thyroldlGzn9mZzWdXBk= +github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260430110913-f15849bb7a59/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs= +github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260430110913-f15849bb7a59 h1:Q41MU6hz7wj9DWbENvZVWf8/OnQ+qjCXA22FCxYzspo= +github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260430110913-f15849bb7a59/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0= +github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260430110913-f15849bb7a59 h1:KQ/KyKuz5K9ZSgZ4FWKQ+9pAQLxhYPSt2FvT+/0slfA= +github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260430110913-f15849bb7a59/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0= +github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260430110913-f15849bb7a59 h1:OyVnropT3w+Woqva5UWq0BCH6kaOHud8yx1PtkQONJc= +github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260430110913-f15849bb7a59/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4= +github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260430110913-f15849bb7a59 h1:2qvUPs7EYQehmTH/jrno3IyDW7s1IUSBK+xjci9qxPE= +github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260430110913-f15849bb7a59/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo= +github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260430110913-f15849bb7a59 h1:yFmYtFrzwORNUvnI7M5OpL2nIXKI6L0i0zjZB/BnkN4= +github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260430110913-f15849bb7a59/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ= +github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260430110913-f15849bb7a59 h1:AGqpeL0/9djbAk7R4oEngynbnW2aDLzR2WTcDcbw5EQ= +github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260430110913-f15849bb7a59/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU= +github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260430110913-f15849bb7a59 h1:q+jC10z8zvRyEuqf8GSa/Y+wDOYUt0ciesZbvZXx78s= +github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260430110913-f15849bb7a59/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI= +github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260430110913-f15849bb7a59 h1:2paT8Gw2wl6lMX80jog3KAon0bHy7dePT6Q1y9UjHHA= +github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260430110913-f15849bb7a59/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ= +github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260430110913-f15849bb7a59 h1:HEpPZuoXjvvLwSeiwiMimfnJfbDXhrUGlEDAUTWCibE= +github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260430110913-f15849bb7a59/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0= +github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260430110913-f15849bb7a59 h1:cf4bqRqdgB8gg6yKSGv02MQBvRopsjykIgn31VORLE8= +github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260430110913-f15849bb7a59/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s= +github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260430110913-f15849bb7a59 h1:X6eDmPjxUgjtAfl231Hkfpj/wNCoPfn7YeHSMRumhSA= +github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260430110913-f15849bb7a59/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ= +github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260430110913-f15849bb7a59 h1:z2rtNM0C2Pc1cWJz1t7XVkb+17392Z2tbffwCCaP0Yk= +github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260430110913-f15849bb7a59/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow= +github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260430110913-f15849bb7a59 h1:ja8Cu9qgRyMk3QSsOWv8kxkpIpN26x08Xpi7i2TemHI= +github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260430110913-f15849bb7a59/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk= +github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260430110913-f15849bb7a59 h1:VO917vuk1lRliFmUrPbNRwXqWvUQkt96f2qIp1dG80I= +github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260430110913-f15849bb7a59/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E= +github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260430110913-f15849bb7a59 h1:wrmLoWg/+vnqINtm30Ydk5+gF+AknlJd+YH645YV4w8= +github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260430110913-f15849bb7a59/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8= +github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260430110913-f15849bb7a59 h1:Qi0oNjn+I/Pr1mpbftV6xQB5ADPBs3EAmu2wKw6B7lc= +github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260430110913-f15849bb7a59/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w= +github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260430110913-f15849bb7a59 h1:nY/RmkOfhQOfDgTqPpR3LU2I4X8UkbfDkiCnRBf8Wa8= +github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260430110913-f15849bb7a59/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0= +github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260430110913-f15849bb7a59 h1:f5mwqLetCMaCPjN+KSQwjxS3kbs6xFNo9cUNlgiLjTM= +github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260430110913-f15849bb7a59/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs= +github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260430110913-f15849bb7a59 h1:X0zP20qboBr6Y+M2aKqO/Xfkt4bG1Se0F144jhdLRUQ= +github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260430110913-f15849bb7a59/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc= +github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260430110913-f15849bb7a59 h1:JbVciM+1yzMtphPKcYB3DBhfL2sIuVnbCEdFIut4gsM= +github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260430110913-f15849bb7a59/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4= +github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260430110913-f15849bb7a59 h1:hQdTNtfMcNBNp7+1Cbesv8BTo29ZP3DRko/wC6SWR1Y= +github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260430110913-f15849bb7a59/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc= +github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260430110913-f15849bb7a59 h1:hrdE8MSmP4ry0Vi8rWGdWKRyRuR8vxK0YxgHMFBN/xM= +github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260430110913-f15849bb7a59/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc= +github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260430110913-f15849bb7a59 h1:G5dDrhlCIgbezbgrn9A6jATwFRw675bHSr4RYnpWGX8= +github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260430110913-f15849bb7a59/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= +github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260430110913-f15849bb7a59 h1:QjBwgVU268bMWKoQyYnH4Ip1MpRoaCJuhzAsm2XV7eE= +github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260430110913-f15849bb7a59/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw= github.com/sagernet/fswatch v0.1.2 h1:/TT7k4mkce1qFPxamLO842WjqBgbTBiXP2mlUjp9PFk= github.com/sagernet/fswatch v0.1.2/go.mod h1:5BpGmpUQVd3Mc5r313HRpvADHRg3/rKn5QbwFteB880= github.com/sagernet/gomobile v0.1.12 h1:XwzjZaclFF96deLqwAgK8gU3w0M2A8qxgDmhV+A0wjg= From bb60b58f00f41d83e70ed45a4cd5f62c5d780153 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 2 May 2026 18:36:58 +0800 Subject: [PATCH 70/93] Allow customizing TUN DNS mode and hijack interface DNS by default --- docs/configuration/dns/rule.md | 1 + docs/configuration/dns/rule.zh.md | 1 + docs/configuration/dns/server/hosts.md | 8 +-- docs/configuration/dns/server/hosts.zh.md | 8 +-- docs/configuration/dns/server/resolved.md | 8 +-- docs/configuration/dns/server/resolved.zh.md | 8 +-- docs/configuration/dns/server/tailscale.md | 8 +-- docs/configuration/dns/server/tailscale.zh.md | 8 +-- docs/configuration/inbound/tun.md | 55 ++++++++++++++++++- docs/configuration/inbound/tun.zh.md | 49 ++++++++++++++++- experimental/libbox/tun.go | 23 ++++++-- go.mod | 2 +- go.sum | 4 +- option/tun.go | 2 + protocol/tun/hook.go | 3 - protocol/tun/inbound.go | 54 ++++++++++++++---- route/route.go | 8 ++- 17 files changed, 187 insertions(+), 63 deletions(-) delete mode 100644 protocol/tun/hook.go diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 897bb09b88..1fc4eafaf3 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -513,6 +513,7 @@ Match specified DNS servers' preferred domains. | `local` | Match hosts entries, neighbor-resolved hosts, and mDNS local domains | | `mdns` | Match mDNS local domains (`*.local.` and IPv4/IPv6 link-local reverse zones) | | `tailscale` | Match MagicDNS hosts and DNS route suffixes | +| `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 55e2298059..c56e7f5bfc 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -505,6 +505,7 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`. | `local` | 匹配 hosts 中的条目、邻居解析得到的主机名以及 mDNS 本地域名 | | `mdns` | 匹配 mDNS 本地域名(`*.local.` 以及 IPv4/IPv6 链路本地反向区域) | | `tailscale` | 匹配 MagicDNS 主机和 DNS 路由后缀 | +| `resolved` | 匹配 systemd-resolved 链路中的分流域名和搜索域 | #### wifi_ssid diff --git a/docs/configuration/dns/server/hosts.md b/docs/configuration/dns/server/hosts.md index da76f61922..2b2cdee63b 100644 --- a/docs/configuration/dns/server/hosts.md +++ b/docs/configuration/dns/server/hosts.md @@ -89,13 +89,9 @@ Example: ], "rules": [ { - "action": "evaluate", + "preferred_by": "hosts", + "action": "route", "server": "hosts" - }, - { - "match_response": true, - "ip_accept_any": true, - "action": "respond" } ] } diff --git a/docs/configuration/dns/server/hosts.zh.md b/docs/configuration/dns/server/hosts.zh.md index 3384adc746..bd4ae7638d 100644 --- a/docs/configuration/dns/server/hosts.zh.md +++ b/docs/configuration/dns/server/hosts.zh.md @@ -89,13 +89,9 @@ hosts 文件路径列表。 ], "rules": [ { - "action": "evaluate", + "preferred_by": "hosts", + "action": "route", "server": "hosts" - }, - { - "match_response": true, - "ip_accept_any": true, - "action": "respond" } ] } diff --git a/docs/configuration/dns/server/resolved.md b/docs/configuration/dns/server/resolved.md index 75835c6b85..44a4123c43 100644 --- a/docs/configuration/dns/server/resolved.md +++ b/docs/configuration/dns/server/resolved.md @@ -61,13 +61,9 @@ If not enabled, `NXDOMAIN` will be returned for requests that do not match searc ], "rules": [ { - "action": "evaluate", + "preferred_by": "resolved", + "action": "route", "server": "resolved" - }, - { - "match_response": true, - "ip_accept_any": true, - "action": "respond" } ] } diff --git a/docs/configuration/dns/server/resolved.zh.md b/docs/configuration/dns/server/resolved.zh.md index 8747e83132..6102aa8099 100644 --- a/docs/configuration/dns/server/resolved.zh.md +++ b/docs/configuration/dns/server/resolved.zh.md @@ -60,13 +60,9 @@ icon: material/new-box ], "rules": [ { - "action": "evaluate", + "preferred_by": "resolved", + "action": "route", "server": "resolved" - }, - { - "match_response": true, - "ip_accept_any": true, - "action": "respond" } ] } diff --git a/docs/configuration/dns/server/tailscale.md b/docs/configuration/dns/server/tailscale.md index b2169ed382..d28a11c635 100644 --- a/docs/configuration/dns/server/tailscale.md +++ b/docs/configuration/dns/server/tailscale.md @@ -73,13 +73,9 @@ Default resolvers are not consulted for single-label queries regardless of `acce ], "rules": [ { - "action": "evaluate", + "preferred_by": "ts", + "action": "route", "server": "ts" - }, - { - "match_response": true, - "ip_accept_any": true, - "action": "respond" } ] } diff --git a/docs/configuration/dns/server/tailscale.zh.md b/docs/configuration/dns/server/tailscale.zh.md index e0086653d6..40fae69cc7 100644 --- a/docs/configuration/dns/server/tailscale.zh.md +++ b/docs/configuration/dns/server/tailscale.zh.md @@ -73,13 +73,9 @@ icon: material/new-box ], "rules": [ { - "action": "evaluate", + "preferred_by": "ts", + "action": "route", "server": "ts" - }, - { - "match_response": true, - "ip_accept_any": true, - "action": "respond" } ] } diff --git a/docs/configuration/inbound/tun.md b/docs/configuration/inbound/tun.md index 6dae06e18a..9af118b68a 100644 --- a/docs/configuration/inbound/tun.md +++ b/docs/configuration/inbound/tun.md @@ -5,7 +5,9 @@ icon: material/new-box !!! quote "Changes in sing-box 1.14.0" :material-plus: [include_mac_address](#include_mac_address) - :material-plus: [exclude_mac_address](#exclude_mac_address) + :material-plus: [exclude_mac_address](#exclude_mac_address) + :material-plus: [dns_mode](#dns_mode) + :material-plus: [dns_address](#dns_address) !!! quote "Changes in sing-box 1.13.3" @@ -73,6 +75,11 @@ icon: material/new-box "fdfe:dcba:9876::1/126" ], "mtu": 9000, + "dns_mode": "hijack", + "dns_address": [ + "172.18.0.2", + "fdfe:dcba:9876::2" + ], "auto_route": true, "iproute2_table_index": 2022, "iproute2_rule_index": 9000, @@ -216,6 +223,52 @@ IPv6 prefix for the tun interface. The maximum transmission unit. +#### dns_mode + +!!! question "Since sing-box 1.14.0" + +How DNS is handled on the TUN interface. + +| Mode | Description | +|------------|------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `disabled` | Do not configure native DNS and do not hijack DNS traffic. | +| `native` | Set the platform's native interface DNS where possible: per-interface DNS on Windows and Apple platforms, and `systemd-resolved` interface DNS on Linux. | +| `hijack` | Same as `native`, with additional port 53 hijacking described below. Used by default. | + +`hijack` adds the following on top of `native`: + +*On Linux*: only DNS sent to non-local destinations can be intercepted. +Traffic destined to addresses on the host's own interfaces (such as +`127.0.0.53` or the host's LAN-side IP) is delivered through the kernel +`local` routing table before any user rule applies, and `OUTPUT` NAT cannot +redirect packets going through `lo`. + +- Without `auto_redirect`, an `iproute2` rule makes port 53 skip the `main` + table's specific-route lookup, forcing DNS that would otherwise be + delivered through a directly-attached subnet through the TUN. Destination + addresses are not rewritten. +- With `auto_redirect`, an nftables rule DNATs port 53 traffic directly to + [`dns_address`](#dns_address). + +*On Windows with [`strict_route`](#strict_route)*: a WFP filter blocks port +53 traffic going through interfaces other than the TUN. + +#### dns_address + +!!! question "Since sing-box 1.14.0" + +List of DNS server addresses used by [`dns_mode`](#dns_mode). + +When unset, sing-box derives one address per family by taking the next IP after +the first IPv4/IPv6 entry in [`address`](#address). Connections toward those +derived addresses are additionally hijacked into the sing-box DNS module, +equivalent to a [`hijack-dns`](/configuration/route/rule_action/#hijack-dns) +route action; this preserves the behaviour from before this option was added. + +When set, this auto-hijack is not applied; configure an explicit +[`hijack-dns`](/configuration/route/rule_action/#hijack-dns) route rule if the +behaviour is still required. + #### gso !!! failure "Deprecated in sing-box 1.11.0" diff --git a/docs/configuration/inbound/tun.zh.md b/docs/configuration/inbound/tun.zh.md index a41e5ae9ff..7471771eec 100644 --- a/docs/configuration/inbound/tun.zh.md +++ b/docs/configuration/inbound/tun.zh.md @@ -5,7 +5,9 @@ icon: material/new-box !!! quote "sing-box 1.14.0 中的更改" :material-plus: [include_mac_address](#include_mac_address) - :material-plus: [exclude_mac_address](#exclude_mac_address) + :material-plus: [exclude_mac_address](#exclude_mac_address) + :material-plus: [dns_mode](#dns_mode) + :material-plus: [dns_address](#dns_address) !!! quote "sing-box 1.13.3 中的更改" @@ -73,6 +75,11 @@ icon: material/new-box "fdfe:dcba:9876::1/126" ], "mtu": 9000, + "dns_mode": "hijack", + "dns_address": [ + "172.18.0.2", + "fdfe:dcba:9876::2" + ], "auto_route": true, "iproute2_table_index": 2022, "iproute2_rule_index": 9000, @@ -222,6 +229,46 @@ tun 接口的 IPv6 前缀。 最大传输单元。 +#### dns_mode + +!!! question "自 sing-box 1.14.0 起" + +TUN 接口上 DNS 的处理方式。 + +| 模式 | 描述 | +|------------|-------------------------------------------------------------------------------------------------------| +| `disabled` | 不设置原生 DNS,也不劫持 DNS 流量。 | +| `native` | 尽可能设置平台的原生接口 DNS:Windows 与 Apple 上的接口 DNS,Linux 上的 `systemd-resolved` 接口 DNS。 | +| `hijack` | 与 `native` 相同,并额外执行下文所述的 53 端口劫持。默认使用。 | + +`hijack` 在 `native` 之上额外执行: + +*Linux*:只能劫持发往非本机地址的 DNS。发往本机接口地址(如 `127.0.0.53` +或本机 LAN 接口 IP)的流量由内核 `local` 路由表在所有用户规则之前直接交付, +`OUTPUT` 链 NAT 也无法对走 `lo` 的包生效。 + +- 未启用 `auto_redirect` 时:通过 `iproute2` 规则让 53 端口跳过 `main` 表的 + 具体路由查找,把本来会经直连子网直接送达的 DNS 改走 TUN —— 不重写目的地址。 +- 启用 `auto_redirect` 时:通过 nftables 规则将 53 端口流量直接 DNAT 至 + [`dns_address`](#dns_address)。 + +*Windows 启用 [`strict_route`](#strict_route) 时*:通过 WFP 过滤器阻止经由非 +TUN 接口的 53 端口流量。 + +#### dns_address + +!!! question "自 sing-box 1.14.0 起" + +[`dns_mode`](#dns_mode) 使用的 DNS 服务器地址列表。 + +未设置时,sing-box 会按地址族在 [`address`](#address) 的第一个 IPv4/IPv6 +条目后面取下一个 IP 作为 DNS 服务器地址,并将流向这些推导地址的连接额外劫持到 +sing-box DNS 模块,等价于一条 +[`hijack-dns`](/zh/configuration/route/rule_action/#hijack-dns) 路由动作;这与此选项加入之前的行为一致。 + +设置后,将不再自动劫持;如仍需此行为,请显式配置 +[`hijack-dns`](/zh/configuration/route/rule_action/#hijack-dns) 路由规则。 + #### gso !!! failure "已在 sing-box 1.11.0 废弃" diff --git a/experimental/libbox/tun.go b/experimental/libbox/tun.go index 84c6372aca..fef66b91c1 100644 --- a/experimental/libbox/tun.go +++ b/experimental/libbox/tun.go @@ -7,13 +7,19 @@ import ( "github.com/sagernet/sing-box/option" tun "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" - E "github.com/sagernet/sing/common/exceptions" +) + +const ( + DNSModeDisabled = tun.DNSModeDisabled + DNSModeNative = tun.DNSModeNative + DNSModeHijack = tun.DNSModeHijack ) type TunOptions interface { GetInet4Address() RoutePrefixIterator GetInet6Address() RoutePrefixIterator - GetDNSServerAddress() (*StringBox, error) + GetDNSMode() *StringBox + GetDNSServerAddress() (StringIterator, error) GetMTU() int32 GetAutoRoute() bool GetStrictRoute() bool @@ -89,11 +95,16 @@ func (o *tunOptions) GetInet6Address() RoutePrefixIterator { return mapRoutePrefix(o.Inet6Address) } -func (o *tunOptions) GetDNSServerAddress() (*StringBox, error) { - if len(o.Inet4Address) == 0 || o.Inet4Address[0].Bits() == 32 { - return nil, E.New("need one more IPv4 address for DNS hijacking") +func (o *tunOptions) GetDNSMode() *StringBox { + return wrapString(o.Options.DNSMode) +} + +func (o *tunOptions) GetDNSServerAddress() (StringIterator, error) { + dnsServers, err := o.Options.DNSServerAddress() + if err != nil { + return nil, err } - return wrapString(o.Inet4Address[0].Addr().Next().String()), nil + return newIterator(common.Map(dnsServers, netip.Addr.String)), nil } func (o *tunOptions) GetMTU() int32 { diff --git a/go.mod b/go.mod index 15afa043f1..13ed3ab3fa 100644 --- a/go.mod +++ b/go.mod @@ -44,7 +44,7 @@ require ( github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 - github.com/sagernet/sing-tun v0.8.10-0.20260427231103-812b89ea042d + github.com/sagernet/sing-tun v0.8.10-0.20260502074200-87904db3a2c1 github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 github.com/sagernet/smux v1.5.50-sing-box-mod.1 github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7 diff --git a/go.sum b/go.sum index 1dd31b5670..1fdaa4dd4e 100644 --- a/go.sum +++ b/go.sum @@ -256,8 +256,8 @@ github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnq github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA= -github.com/sagernet/sing-tun v0.8.10-0.20260427231103-812b89ea042d h1:rcFzy3rMpx9M/Zel+YLd2iNGHl0ElH7T8Pl7Y6oxPOQ= -github.com/sagernet/sing-tun v0.8.10-0.20260427231103-812b89ea042d/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ= +github.com/sagernet/sing-tun v0.8.10-0.20260502074200-87904db3a2c1 h1:JaW/aRriLE4fgCBLM6wFlpDcscJwRmAgHVRgN0ePOkA= +github.com/sagernet/sing-tun v0.8.10-0.20260502074200-87904db3a2c1/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY= github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478= diff --git a/option/tun.go b/option/tun.go index fda028b69e..379aa7c330 100644 --- a/option/tun.go +++ b/option/tun.go @@ -14,6 +14,8 @@ type TunInboundOptions struct { InterfaceName string `json:"interface_name,omitempty"` MTU uint32 `json:"mtu,omitempty"` Address badoption.Listable[netip.Prefix] `json:"address,omitempty"` + DNSMode string `json:"dns_mode,omitempty"` + DNSAddress badoption.Listable[netip.Addr] `json:"dns_address,omitempty"` AutoRoute bool `json:"auto_route,omitempty"` IPRoute2TableIndex int `json:"iproute2_table_index,omitempty"` IPRoute2RuleIndex int `json:"iproute2_rule_index,omitempty"` diff --git a/protocol/tun/hook.go b/protocol/tun/hook.go deleted file mode 100644 index 1afa643f2d..0000000000 --- a/protocol/tun/hook.go +++ /dev/null @@ -1,3 +0,0 @@ -package tun - -var HookBeforeCreatePlatformInterface func() diff --git a/protocol/tun/inbound.go b/protocol/tun/inbound.go index 4b113f4a78..65e87bfdc5 100644 --- a/protocol/tun/inbound.go +++ b/protocol/tun/inbound.go @@ -42,6 +42,7 @@ type Inbound struct { logger log.ContextLogger tunOptions tun.Options udpTimeout time.Duration + dnsHijackAddress []netip.Addr stack string tunIf tun.Tun tunStack tun.Stack @@ -190,6 +191,8 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo GSO: enableGSO, Inet4Address: inet4Address, Inet6Address: inet6Address, + DNSMode: options.DNSMode, + DNSAddress: options.DNSAddress, AutoRoute: options.AutoRoute, IPRoute2TableIndex: tableIndex, IPRoute2RuleIndex: ruleIndex, @@ -310,6 +313,12 @@ func (t *Inbound) Tag() string { func (t *Inbound) Start(stage adapter.StartStage) error { switch stage { + case adapter.StartStateInitialize: + if t.tunOptions.DNSModeOrDefault() != tun.DNSModeDisabled && len(t.tunOptions.DNSAddress) == 0 { + inet4DNSAddress, _ := t.tunOptions.Inet4DNSAddress() + inet6DNSAddress, _ := t.tunOptions.Inet6DNSAddress() + t.dnsHijackAddress = append(inet4DNSAddress, inet6DNSAddress...) + } case adapter.StartStateStart: if C.IsAndroid && t.platformInterface == nil { t.tunOptions.BuildAndroidRules(t.networkManager.PackageManager()) @@ -373,9 +382,6 @@ func (t *Inbound) Start(stage adapter.StartStage) error { if t.platformInterface != nil && t.platformInterface.UsePlatformInterface() { tunInterface, err = t.platformInterface.OpenInterface(&tunOptions, t.platformOptions) } else { - if HookBeforeCreatePlatformInterface != nil { - HookBeforeCreatePlatformInterface() - } tunInterface, err = tun.New(tunOptions) } monitor.Finish() @@ -490,9 +496,17 @@ func (t *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, source M.S metadata.InboundType = C.TypeTun metadata.Source = source metadata.Destination = destination - - t.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) - t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + for _, dnsHijackAddress := range t.dnsHijackAddress { + if destination.Addr == dnsHijackAddress { + metadata.Protocol = C.ProtocolDNS + } + } + if metadata.Protocol == C.ProtocolDNS { + t.logger.InfoContext(ctx, "inbound DNS connection from ", metadata.Source) + } else { + t.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) + t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + } t.router.RouteConnectionEx(ctx, conn, metadata, onClose) } @@ -503,9 +517,17 @@ func (t *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata.InboundType = C.TypeTun metadata.Source = source metadata.Destination = destination - - t.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) - t.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) + for _, dnsHijackAddress := range t.dnsHijackAddress { + if destination.Addr == dnsHijackAddress { + metadata.Protocol = C.ProtocolDNS + } + } + if metadata.Protocol == C.ProtocolDNS { + t.logger.InfoContext(ctx, "inbound DNS packet connection from ", metadata.Source) + } else { + t.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) + t.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) + } t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } @@ -548,9 +570,17 @@ func (t *autoRedirectHandler) NewConnectionEx(ctx context.Context, conn net.Conn metadata.InboundType = C.TypeTun metadata.Source = source metadata.Destination = destination - - t.logger.InfoContext(ctx, "inbound redirect connection from ", metadata.Source) - t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + for _, dnsHijackAddress := range t.dnsHijackAddress { + if destination.Addr == dnsHijackAddress { + metadata.Protocol = C.ProtocolDNS + } + } + if metadata.Protocol == C.ProtocolDNS { + t.logger.InfoContext(ctx, "inbound redirect DNS connection from ", metadata.Source) + } else { + t.logger.InfoContext(ctx, "inbound redirect connection from ", metadata.Source) + t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + } t.router.RouteConnectionEx(ctx, conn, metadata, onClose) } diff --git a/route/route.go b/route/route.go index 1809971ed4..bdc5339327 100644 --- a/route/route.go +++ b/route/route.go @@ -88,6 +88,10 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad case uot.LegacyMagicAddress: return E.New("global UoT (legacy) not supported since sing-box v1.7.0.") } + if metadata.InboundType == C.TypeTun && metadata.Protocol == C.ProtocolDNS { + N.CloseOnHandshakeFailure(conn, onClose, r.hijackDNSStream(ctx, conn, metadata)) + return nil + } if deadline.NeedAdditionalReadDeadline(conn) { conn = deadline.NewConn(conn) } @@ -219,7 +223,9 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m /*if deadline.NeedAdditionalReadDeadline(conn) { conn = deadline.NewPacketConn(bufio.NewNetPacketConn(conn)) }*/ - + if metadata.InboundType == C.TypeTun && metadata.Protocol == C.ProtocolDNS { + return r.hijackDNSPacket(ctx, conn, nil, metadata, onClose) + } selectedRule, _, _, packetBuffers, err := r.matchRule(ctx, &metadata, false, false, nil, conn) if err != nil { return err From 772ab58f8c1fd59588c07ff9d997f9ce70488d9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 2 May 2026 19:18:08 +0800 Subject: [PATCH 71/93] Update naiveproxy to v148.0.7778.96-1 --- .github/CRONET_GO_VERSION | 2 +- go.mod | 62 +++++++++---------- go.sum | 124 +++++++++++++++++++------------------- 3 files changed, 94 insertions(+), 94 deletions(-) diff --git a/.github/CRONET_GO_VERSION b/.github/CRONET_GO_VERSION index bad6f2267b..80c50a89d0 100644 --- a/.github/CRONET_GO_VERSION +++ b/.github/CRONET_GO_VERSION @@ -1 +1 @@ -6a30864fae5a9a6bff2e12a605cf6da942bc6f0a +cd9fcba9981bf69c1aab541887d2758d84dc31f3 diff --git a/go.mod b/go.mod index 13ed3ab3fa..66a735909a 100644 --- a/go.mod +++ b/go.mod @@ -31,8 +31,8 @@ require ( github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1 github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a github.com/sagernet/cors v1.2.1 - github.com/sagernet/cronet-go v0.0.0-20260430111606-b19b50ccea0c - github.com/sagernet/cronet-go/all v0.0.0-20260430111606-b19b50ccea0c + github.com/sagernet/cronet-go v0.0.0-20260502110630-bdcbb26bce76 + github.com/sagernet/cronet-go/all v0.0.0-20260502110630-bdcbb26bce76 github.com/sagernet/fswatch v0.1.2 github.com/sagernet/gomobile v0.1.12 github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 @@ -109,35 +109,35 @@ require ( github.com/prometheus-community/pro-bing v0.4.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/safchain/ethtool v0.3.0 // indirect - github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260430110913-f15849bb7a59 // indirect - github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260430110913-f15849bb7a59 // indirect - github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260430110913-f15849bb7a59 // indirect - github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260430110913-f15849bb7a59 // indirect - github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260430110913-f15849bb7a59 // indirect - github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260430110913-f15849bb7a59 // indirect - github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260430110913-f15849bb7a59 // indirect - github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260430110913-f15849bb7a59 // indirect - github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260430110913-f15849bb7a59 // indirect - github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260430110913-f15849bb7a59 // indirect - github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260430110913-f15849bb7a59 // indirect - github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260430110913-f15849bb7a59 // indirect - github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260430110913-f15849bb7a59 // indirect - github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260430110913-f15849bb7a59 // indirect - github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260430110913-f15849bb7a59 // indirect - github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260430110913-f15849bb7a59 // indirect - github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260430110913-f15849bb7a59 // indirect - github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260430110913-f15849bb7a59 // indirect - github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260430110913-f15849bb7a59 // indirect - github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260430110913-f15849bb7a59 // indirect - github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260430110913-f15849bb7a59 // indirect - github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260430110913-f15849bb7a59 // indirect - github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260430110913-f15849bb7a59 // indirect - github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260430110913-f15849bb7a59 // indirect - github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260430110913-f15849bb7a59 // indirect - github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260430110913-f15849bb7a59 // indirect - github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260430110913-f15849bb7a59 // indirect - github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260430110913-f15849bb7a59 // indirect - github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260430110913-f15849bb7a59 // indirect + github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260502110005-e9754926ef93 // indirect + github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260502110005-e9754926ef93 // indirect + github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260502110005-e9754926ef93 // indirect + github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260502110005-e9754926ef93 // indirect + github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260502110005-e9754926ef93 // indirect + github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260502110005-e9754926ef93 // indirect + github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260502110005-e9754926ef93 // indirect + github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260502110005-e9754926ef93 // indirect + github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260502110005-e9754926ef93 // indirect + github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260502110005-e9754926ef93 // indirect + github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260502110005-e9754926ef93 // indirect + github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260502110005-e9754926ef93 // indirect + github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260502110005-e9754926ef93 // indirect + github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260502110005-e9754926ef93 // indirect + github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260502110005-e9754926ef93 // indirect + github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260502110005-e9754926ef93 // indirect + github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260502110005-e9754926ef93 // indirect + github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260502110005-e9754926ef93 // indirect + github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260502110005-e9754926ef93 // indirect + github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260502110005-e9754926ef93 // indirect + github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260502110005-e9754926ef93 // indirect + github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260502110005-e9754926ef93 // indirect + github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260502110005-e9754926ef93 // indirect + github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260502110005-e9754926ef93 // indirect + github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260502110005-e9754926ef93 // indirect + github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260502110005-e9754926ef93 // indirect + github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260502110005-e9754926ef93 // indirect + github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260502110005-e9754926ef93 // indirect + github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260502110005-e9754926ef93 // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect github.com/sagernet/nftables v0.3.0-mod.2 // indirect github.com/spf13/pflag v1.0.9 // indirect diff --git a/go.sum b/go.sum index 1fdaa4dd4e..8496ea5f16 100644 --- a/go.sum +++ b/go.sum @@ -168,68 +168,68 @@ github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkk github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ= github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI= -github.com/sagernet/cronet-go v0.0.0-20260430111606-b19b50ccea0c h1:Z6rd2dZ0pIYC2Sbz6c1zcDwITdQ56PkBkuAlRx3Mg5A= -github.com/sagernet/cronet-go v0.0.0-20260430111606-b19b50ccea0c/go.mod h1:hwFHBEjjthyEquDULbr4c4ucMedp8Drb6Jvm2kt/0Bw= -github.com/sagernet/cronet-go/all v0.0.0-20260430111606-b19b50ccea0c h1:S+2MG8NSwD1ietZzXlZGFENtw/NjSZgzpiercXy4k54= -github.com/sagernet/cronet-go/all v0.0.0-20260430111606-b19b50ccea0c/go.mod h1:71rv3TtJ/lZDVC9/Qqo/H8YJpAcsoEl+OJj29YxLU2M= -github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260430110913-f15849bb7a59 h1:FgyHy7cPMlP+3MvbPSx6eq8/TkeMK/i1OT7WbGoWzAo= -github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260430110913-f15849bb7a59/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw= -github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260430110913-f15849bb7a59 h1:N82Dqwee5umZ1uPGACrU+ObXOGq26dYw7ChXUkOiAiY= -github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260430110913-f15849bb7a59/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM= -github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260430110913-f15849bb7a59 h1:3CPK9qAU2UHi7aVpwYjlZgRiWpVdm4+960Ao+hMH1g4= -github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260430110913-f15849bb7a59/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc= -github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260430110913-f15849bb7a59 h1:6MIkfoApgGPLZzKvCLmdC9JEhj14cSgcGrnUq/g4uaM= -github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260430110913-f15849bb7a59/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ= -github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260430110913-f15849bb7a59 h1:tTmakYDgG9pkZhznzGMY2+8thyroldlGzn9mZzWdXBk= -github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260430110913-f15849bb7a59/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs= -github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260430110913-f15849bb7a59 h1:Q41MU6hz7wj9DWbENvZVWf8/OnQ+qjCXA22FCxYzspo= -github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260430110913-f15849bb7a59/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0= -github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260430110913-f15849bb7a59 h1:KQ/KyKuz5K9ZSgZ4FWKQ+9pAQLxhYPSt2FvT+/0slfA= -github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260430110913-f15849bb7a59/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0= -github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260430110913-f15849bb7a59 h1:OyVnropT3w+Woqva5UWq0BCH6kaOHud8yx1PtkQONJc= -github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260430110913-f15849bb7a59/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4= -github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260430110913-f15849bb7a59 h1:2qvUPs7EYQehmTH/jrno3IyDW7s1IUSBK+xjci9qxPE= -github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260430110913-f15849bb7a59/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo= -github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260430110913-f15849bb7a59 h1:yFmYtFrzwORNUvnI7M5OpL2nIXKI6L0i0zjZB/BnkN4= -github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260430110913-f15849bb7a59/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ= -github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260430110913-f15849bb7a59 h1:AGqpeL0/9djbAk7R4oEngynbnW2aDLzR2WTcDcbw5EQ= -github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260430110913-f15849bb7a59/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU= -github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260430110913-f15849bb7a59 h1:q+jC10z8zvRyEuqf8GSa/Y+wDOYUt0ciesZbvZXx78s= -github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260430110913-f15849bb7a59/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI= -github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260430110913-f15849bb7a59 h1:2paT8Gw2wl6lMX80jog3KAon0bHy7dePT6Q1y9UjHHA= -github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260430110913-f15849bb7a59/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ= -github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260430110913-f15849bb7a59 h1:HEpPZuoXjvvLwSeiwiMimfnJfbDXhrUGlEDAUTWCibE= -github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260430110913-f15849bb7a59/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0= -github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260430110913-f15849bb7a59 h1:cf4bqRqdgB8gg6yKSGv02MQBvRopsjykIgn31VORLE8= -github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260430110913-f15849bb7a59/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s= -github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260430110913-f15849bb7a59 h1:X6eDmPjxUgjtAfl231Hkfpj/wNCoPfn7YeHSMRumhSA= -github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260430110913-f15849bb7a59/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ= -github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260430110913-f15849bb7a59 h1:z2rtNM0C2Pc1cWJz1t7XVkb+17392Z2tbffwCCaP0Yk= -github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260430110913-f15849bb7a59/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow= -github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260430110913-f15849bb7a59 h1:ja8Cu9qgRyMk3QSsOWv8kxkpIpN26x08Xpi7i2TemHI= -github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260430110913-f15849bb7a59/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk= -github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260430110913-f15849bb7a59 h1:VO917vuk1lRliFmUrPbNRwXqWvUQkt96f2qIp1dG80I= -github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260430110913-f15849bb7a59/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E= -github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260430110913-f15849bb7a59 h1:wrmLoWg/+vnqINtm30Ydk5+gF+AknlJd+YH645YV4w8= -github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260430110913-f15849bb7a59/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8= -github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260430110913-f15849bb7a59 h1:Qi0oNjn+I/Pr1mpbftV6xQB5ADPBs3EAmu2wKw6B7lc= -github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260430110913-f15849bb7a59/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w= -github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260430110913-f15849bb7a59 h1:nY/RmkOfhQOfDgTqPpR3LU2I4X8UkbfDkiCnRBf8Wa8= -github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260430110913-f15849bb7a59/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0= -github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260430110913-f15849bb7a59 h1:f5mwqLetCMaCPjN+KSQwjxS3kbs6xFNo9cUNlgiLjTM= -github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260430110913-f15849bb7a59/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs= -github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260430110913-f15849bb7a59 h1:X0zP20qboBr6Y+M2aKqO/Xfkt4bG1Se0F144jhdLRUQ= -github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260430110913-f15849bb7a59/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc= -github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260430110913-f15849bb7a59 h1:JbVciM+1yzMtphPKcYB3DBhfL2sIuVnbCEdFIut4gsM= -github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260430110913-f15849bb7a59/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4= -github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260430110913-f15849bb7a59 h1:hQdTNtfMcNBNp7+1Cbesv8BTo29ZP3DRko/wC6SWR1Y= -github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260430110913-f15849bb7a59/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc= -github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260430110913-f15849bb7a59 h1:hrdE8MSmP4ry0Vi8rWGdWKRyRuR8vxK0YxgHMFBN/xM= -github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260430110913-f15849bb7a59/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc= -github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260430110913-f15849bb7a59 h1:G5dDrhlCIgbezbgrn9A6jATwFRw675bHSr4RYnpWGX8= -github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260430110913-f15849bb7a59/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= -github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260430110913-f15849bb7a59 h1:QjBwgVU268bMWKoQyYnH4Ip1MpRoaCJuhzAsm2XV7eE= -github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260430110913-f15849bb7a59/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw= +github.com/sagernet/cronet-go v0.0.0-20260502110630-bdcbb26bce76 h1:gEHy7Tr07rlXcw26YrO99nNrbyZxZM3XiEWchQN2+JA= +github.com/sagernet/cronet-go v0.0.0-20260502110630-bdcbb26bce76/go.mod h1:hwFHBEjjthyEquDULbr4c4ucMedp8Drb6Jvm2kt/0Bw= +github.com/sagernet/cronet-go/all v0.0.0-20260502110630-bdcbb26bce76 h1:HIWLA9kqPeWdZJVVALeI8+zlmMrJ8Sgx+4WpDiFcYn4= +github.com/sagernet/cronet-go/all v0.0.0-20260502110630-bdcbb26bce76/go.mod h1:Ws1+lX4ozLw1kjMhNP6jMMMYzDQ+xj37JowIzsKjAQg= +github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260502110005-e9754926ef93 h1:Ai6n4KUL8xL5+sPPGLMA6gR6pdGE+YfyumEHmu0WOz8= +github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260502110005-e9754926ef93/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw= +github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260502110005-e9754926ef93 h1:YCsJA84Kqn+M79nugJBvLe32g7NPROwIHrCQcUeg8z0= +github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260502110005-e9754926ef93/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM= +github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260502110005-e9754926ef93 h1:iZIW+x0tyAriSheonyexh/EXgY1Yf3clZ3ZXXQlUrqI= +github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260502110005-e9754926ef93/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc= +github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260502110005-e9754926ef93 h1:5fFO+M2tKzvuqrlLA7dpYb3D+s1FYUjvZRfhGPzaxyQ= +github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260502110005-e9754926ef93/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ= +github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260502110005-e9754926ef93 h1:22KfcLvNGCB41xAEWou7NgIFUja6mxUgUWpLEO4Z3PE= +github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260502110005-e9754926ef93/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs= +github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260502110005-e9754926ef93 h1:38qlpF10SZ5ZQkHAFq+Itg6aDqi65d60U7GWeoae6Qg= +github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260502110005-e9754926ef93/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0= +github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260502110005-e9754926ef93 h1:NtZYaRFqlrQSFQpA+VhaIlwL1VMSeI5N8zEuJT+D5Mc= +github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260502110005-e9754926ef93/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0= +github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260502110005-e9754926ef93 h1:qgBCWWszFCUJDwJbUx4E7iLlyvZRgH4J+wWJWw7zz58= +github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260502110005-e9754926ef93/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4= +github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260502110005-e9754926ef93 h1:5pGx9W7N1DIONRxX0gFDcDLOsC+A2jVlbIeabdJl1l4= +github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260502110005-e9754926ef93/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo= +github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260502110005-e9754926ef93 h1:BaTS59k8zsnM8wb0Fahe3828oJtEUP6uVt5DOut1k/o= +github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260502110005-e9754926ef93/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ= +github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260502110005-e9754926ef93 h1:ZvOfpob2GE3MwT/DujqOsBo+YyuTbN5CXvhahh88DTM= +github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260502110005-e9754926ef93/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU= +github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260502110005-e9754926ef93 h1:AugS8654l2PdCSZEo1Hl//+WzkHKE7jnIu09XWpnjLE= +github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260502110005-e9754926ef93/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI= +github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260502110005-e9754926ef93 h1:+zmnmfrEKZbvzmRbKw4dDp4vlLzvto9LBQmGNx+5prk= +github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260502110005-e9754926ef93/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ= +github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260502110005-e9754926ef93 h1:sOf3QZxokQLMXEAYK2Ev1UBKw7T4dCScpejK5Sg40Fo= +github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260502110005-e9754926ef93/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0= +github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260502110005-e9754926ef93 h1:jyK14VGV4ZK+5SAITLWuwHlA+UTV6yKfnpFaEM1FaCE= +github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260502110005-e9754926ef93/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s= +github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260502110005-e9754926ef93 h1:na2c0wGTPMhFS7rSJNhokYIUq10y7q00KyEXhXsI3fc= +github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260502110005-e9754926ef93/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ= +github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260502110005-e9754926ef93 h1:+vmvZIGIaUXOyF/tFo9JGEbU7IfVENjlQduc1mYP6l8= +github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260502110005-e9754926ef93/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow= +github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260502110005-e9754926ef93 h1:/RKvCIdn3kQwojdPU8p3wslQiby0cZ2n7HnQPW8hGeo= +github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260502110005-e9754926ef93/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk= +github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260502110005-e9754926ef93 h1:76aHGGm4mJNUqJQRgYKw4Y/FMgbRr3EMaaGaAGcr5qE= +github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260502110005-e9754926ef93/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E= +github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260502110005-e9754926ef93 h1:O7MaOwIuwiqrC9uR4W+dewvkCAFa4Uh9o2Y2G5IO8Z0= +github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260502110005-e9754926ef93/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8= +github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260502110005-e9754926ef93 h1:tK68PKvoy6CRJHyunQeImVPriYsPxXdRG9RtcQRCWb8= +github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260502110005-e9754926ef93/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w= +github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260502110005-e9754926ef93 h1:tspbl1GfyatQaGpvjkowtHR/HjbEakDyTR15cNC2wmo= +github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260502110005-e9754926ef93/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0= +github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260502110005-e9754926ef93 h1:MFtSQBsTOxAkLYSA7+VNUR2VTBLxLXi15Vw9tYKiqH0= +github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260502110005-e9754926ef93/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs= +github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260502110005-e9754926ef93 h1:4i9OVY94DMslpvZP38tPSN7iZVa4iRE2wqVWPgGUJR0= +github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260502110005-e9754926ef93/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc= +github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260502110005-e9754926ef93 h1:/P/+rX2PTFI5HlIbq8kxB+HA+tqpNIx/okv4/cDZkJo= +github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260502110005-e9754926ef93/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4= +github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260502110005-e9754926ef93 h1:Szdw25xT4Wz+Xt7ZuJ1g6M4GlUpovAzSlTLEKxitoEs= +github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260502110005-e9754926ef93/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc= +github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260502110005-e9754926ef93 h1:yp3eCuMi/y5Ql4d3usN8EJJ6eU3qxQ7dqe79ExZA/j8= +github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260502110005-e9754926ef93/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc= +github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260502110005-e9754926ef93 h1:VFA4sS7Sdi8Zzk6FlT0MKcmBgI+mMo0ERBZHE9xdckM= +github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260502110005-e9754926ef93/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= +github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260502110005-e9754926ef93 h1:r2ACrUknRLL0Ysb3BbidkmQ1ZNtidBqX0N0OHqL1g6M= +github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260502110005-e9754926ef93/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw= github.com/sagernet/fswatch v0.1.2 h1:/TT7k4mkce1qFPxamLO842WjqBgbTBiXP2mlUjp9PFk= github.com/sagernet/fswatch v0.1.2/go.mod h1:5BpGmpUQVd3Mc5r313HRpvADHRg3/rKn5QbwFteB880= github.com/sagernet/gomobile v0.1.12 h1:XwzjZaclFF96deLqwAgK8gU3w0M2A8qxgDmhV+A0wjg= From 48c65a2f8ab7a563d2c5463e0cef1c2da00b72fc Mon Sep 17 00:00:00 2001 From: macronut <4027187+macronut@users.noreply.github.com> Date: Wed, 29 Apr 2026 19:24:49 +0800 Subject: [PATCH 72/93] Add more spoof method Signed-off-by: macronut <4027187+macronut@users.noreply.github.com> --- adapter/inbound.go | 3 + common/tls/client.go | 20 +- common/tls/client_test.go | 154 --------- common/tlsspoof/client_hello_test.go | 83 ----- common/tlsspoof/conn_test.go | 369 --------------------- common/tlsspoof/integration_unix_test.go | 101 +++--- common/tlsspoof/packet.go | 121 +++++-- common/tlsspoof/packet_test.go | 136 -------- common/tlsspoof/raw_darwin.go | 7 +- common/tlsspoof/raw_linux.go | 12 +- common/tlsspoof/raw_windows.go | 41 ++- common/tlsspoof/raw_windows_test.go | 112 ------- common/tlsspoof/spoof.go | 39 ++- common/tlsspoof/testdata_test.go | 4 + docs/configuration/route/rule_action.md | 28 +- docs/configuration/route/rule_action.zh.md | 26 +- docs/configuration/shared/tls.md | 13 +- docs/configuration/shared/tls.zh.md | 13 +- option/rule_action.go | 2 + route/conn.go | 12 + route/route.go | 8 + route/rule/rule_action.go | 81 +++-- 22 files changed, 362 insertions(+), 1023 deletions(-) delete mode 100644 common/tls/client_test.go delete mode 100644 common/tlsspoof/client_hello_test.go delete mode 100644 common/tlsspoof/conn_test.go delete mode 100644 common/tlsspoof/packet_test.go delete mode 100644 common/tlsspoof/raw_windows_test.go create mode 100644 common/tlsspoof/testdata_test.go diff --git a/adapter/inbound.go b/adapter/inbound.go index 923a8e668d..44b3273726 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -6,6 +6,7 @@ import ( "net/netip" "time" + "github.com/sagernet/sing-box/common/tlsspoof" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" @@ -75,6 +76,8 @@ type InboundContext struct { TLSFragment bool TLSFragmentFallbackDelay time.Duration TLSRecordFragment bool + TLSSpoof string + TLSSpoofMethod tlsspoof.Method NetworkStrategy *C.NetworkStrategy NetworkType []C.InterfaceType diff --git a/common/tls/client.go b/common/tls/client.go index 5134384197..7b303687ca 100644 --- a/common/tls/client.go +++ b/common/tls/client.go @@ -22,26 +22,20 @@ import ( var errMissingServerName = E.New("missing server_name or insecure=true") func parseTLSSpoofOptions(serverName string, options option.OutboundTLSOptions) (string, tlsspoof.Method, error) { - if options.Spoof == "" { - if options.SpoofMethod != "" { - return "", 0, E.New("`spoof_method` requires `spoof`") - } - return "", 0, nil + spoof, method, err := tlsspoof.ParseOptions(options.Spoof, options.SpoofMethod) + if err != nil { + return "", 0, err } - if !tlsspoof.PlatformSupported { - return "", 0, E.New("`spoof` is not supported on this platform") + if spoof == "" { + return "", 0, nil } if options.DisableSNI || serverName == "" || M.ParseAddr(serverName).IsValid() { return "", 0, E.New("`spoof` requires TLS ClientHello with SNI") } - if strings.EqualFold(options.Spoof, serverName) { + if strings.EqualFold(spoof, serverName) { return "", 0, E.New("`spoof` must differ from `server_name`") } - method, err := tlsspoof.ParseMethod(options.SpoofMethod) - if err != nil { - return "", 0, err - } - return options.Spoof, method, nil + return spoof, method, nil } func applyTLSSpoof(conn net.Conn, spoof string, method tlsspoof.Method) (net.Conn, error) { diff --git a/common/tls/client_test.go b/common/tls/client_test.go deleted file mode 100644 index 5bc939e29e..0000000000 --- a/common/tls/client_test.go +++ /dev/null @@ -1,154 +0,0 @@ -package tls - -import ( - "context" - "crypto/tls" - "net" - "testing" - - tf "github.com/sagernet/sing-box/common/tlsfragment" - "github.com/sagernet/sing-box/common/tlsspoof" - "github.com/sagernet/sing-box/option" - - "github.com/stretchr/testify/require" -) - -func TestParseTLSSpoofOptions_Disabled(t *testing.T) { - t.Parallel() - spoof, method, err := parseTLSSpoofOptions("example.com", option.OutboundTLSOptions{}) - require.NoError(t, err) - require.Empty(t, spoof) - require.Equal(t, tlsspoof.MethodWrongSequence, method) -} - -func TestParseTLSSpoofOptions_MethodWithoutSpoof(t *testing.T) { - t.Parallel() - _, _, err := parseTLSSpoofOptions("example.com", option.OutboundTLSOptions{ - SpoofMethod: tlsspoof.MethodNameWrongChecksum, - }) - require.Error(t, err) -} - -func TestParseTLSSpoofOptions_IPLiteralRejected(t *testing.T) { - t.Parallel() - _, _, err := parseTLSSpoofOptions("1.2.3.4", option.OutboundTLSOptions{ - Spoof: "example.com", - }) - require.Error(t, err) -} - -func TestParseTLSSpoofOptions_EmptyServerNameRejected(t *testing.T) { - t.Parallel() - _, _, err := parseTLSSpoofOptions("", option.OutboundTLSOptions{ - Spoof: "example.com", - }) - require.Error(t, err) -} - -func TestParseTLSSpoofOptions_DisableSNIRejected(t *testing.T) { - t.Parallel() - _, _, err := parseTLSSpoofOptions("example.com", option.OutboundTLSOptions{ - Spoof: "decoy.com", - DisableSNI: true, - }) - require.Error(t, err) -} - -// TestParseTLSSpoofOptions_RejectsSameSNI is the primary regression test for -// the "spoofed packet contains the original SNI" bug report: when a user -// configures spoof equal to server_name, the rewriter produces a byte-identical -// record, so the fake and real ClientHellos on the wire look the same. Reject -// at parse time. -func TestParseTLSSpoofOptions_RejectsSameSNI(t *testing.T) { - t.Parallel() - _, _, err := parseTLSSpoofOptions("example.com", option.OutboundTLSOptions{ - Spoof: "example.com", - }) - require.Error(t, err) - - _, _, err = parseTLSSpoofOptions("example.com", option.OutboundTLSOptions{ - Spoof: "EXAMPLE.com", - }) - require.Error(t, err, "comparison must be case-insensitive") -} - -func TestParseTLSSpoofOptions_UnknownMethodRejected(t *testing.T) { - t.Parallel() - _, _, err := parseTLSSpoofOptions("example.com", option.OutboundTLSOptions{ - Spoof: "decoy.com", - SpoofMethod: "nonsense", - }) - require.Error(t, err) -} - -func TestParseTLSSpoofOptions_DistinctSNIAccepted(t *testing.T) { - t.Parallel() - if !tlsspoof.PlatformSupported { - t.Skip("tlsspoof not supported on this platform") - } - spoof, method, err := parseTLSSpoofOptions("example.com", option.OutboundTLSOptions{ - Spoof: "decoy.com", - SpoofMethod: tlsspoof.MethodNameWrongSequence, - }) - require.NoError(t, err) - require.Equal(t, "decoy.com", spoof) - require.Equal(t, tlsspoof.MethodWrongSequence, method) -} - -// The following tests guard the wrap gate in STDClientConfig.Client(): -// tf.Conn must wrap the underlying connection whenever either `fragment` or -// `record_fragment` is set, so that TLS fragmentation coexists with features -// like tls_spoof that layer on top of tf.Conn. - -func newSTDClientConfigForGateTest(fragment, recordFragment bool) *STDClientConfig { - return &STDClientConfig{ - ctx: context.Background(), - config: &tls.Config{ServerName: "example.com", InsecureSkipVerify: true}, - fragment: fragment, - recordFragment: recordFragment, - } -} - -func TestSTDClient_Client_NoFragment_DoesNotWrap(t *testing.T) { - t.Parallel() - client, server := net.Pipe() - defer client.Close() - defer server.Close() - wrapped, err := newSTDClientConfigForGateTest(false, false).Client(client) - require.NoError(t, err) - _, isTF := wrapped.NetConn().(*tf.Conn) - require.False(t, isTF, "no fragment flags: must not wrap with tf.Conn") -} - -func TestSTDClient_Client_FragmentOnly_Wraps(t *testing.T) { - t.Parallel() - client, server := net.Pipe() - defer client.Close() - defer server.Close() - wrapped, err := newSTDClientConfigForGateTest(true, false).Client(client) - require.NoError(t, err) - _, isTF := wrapped.NetConn().(*tf.Conn) - require.True(t, isTF, "fragment=true: must wrap with tf.Conn") -} - -func TestSTDClient_Client_RecordFragmentOnly_Wraps(t *testing.T) { - t.Parallel() - client, server := net.Pipe() - defer client.Close() - defer server.Close() - wrapped, err := newSTDClientConfigForGateTest(false, true).Client(client) - require.NoError(t, err) - _, isTF := wrapped.NetConn().(*tf.Conn) - require.True(t, isTF, "record_fragment=true: must wrap with tf.Conn") -} - -func TestSTDClient_Client_BothFragment_Wraps(t *testing.T) { - t.Parallel() - client, server := net.Pipe() - defer client.Close() - defer server.Close() - wrapped, err := newSTDClientConfigForGateTest(true, true).Client(client) - require.NoError(t, err) - _, isTF := wrapped.NetConn().(*tf.Conn) - require.True(t, isTF, "both fragment flags: must wrap with tf.Conn") -} diff --git a/common/tlsspoof/client_hello_test.go b/common/tlsspoof/client_hello_test.go deleted file mode 100644 index 3eb7a2e040..0000000000 --- a/common/tlsspoof/client_hello_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package tlsspoof - -import ( - "bytes" - "encoding/binary" - "strings" - "testing" - - tf "github.com/sagernet/sing-box/common/tlsfragment" - - "github.com/stretchr/testify/require" -) - -// x25519MLKEM768 is the IANA code point for the post-quantum hybrid named -// group (0x11EC). The fake ClientHello must never carry it — its 1184-byte -// key share is the reason kernel-generated ClientHellos exceed one MSS, and -// the reason this builder has to force CurvePreferences. -const x25519MLKEM768 uint16 = 0x11EC - -func TestBuildFakeClientHello_ParsesWithSNI(t *testing.T) { - t.Parallel() - record, err := buildFakeClientHello("example.com") - require.NoError(t, err) - - serverName := tf.IndexTLSServerName(record) - require.NotNil(t, serverName, "output must parse as a ClientHello") - require.Equal(t, "example.com", serverName.ServerName) - - recordLen := binary.BigEndian.Uint16(record[3:5]) - require.Equal(t, len(record)-5, int(recordLen), - "record length header must match on-wire record size") - handshakeLen := int(record[6])<<16 | int(record[7])<<8 | int(record[8]) - require.Equal(t, len(record)-5-4, handshakeLen, - "handshake length header must match handshake body size") -} - -// TestBuildFakeClientHello_FitsOneSegment is the regression guard for the -// whole point of the rewrite: the fake must never need fragmenting on a -// standard 1500-byte path MTU. 1200 leaves ~260 bytes for IP+TCP headers and -// a generous safety margin — the X25519MLKEM768 ClientHello this replaces -// hit ~1400+. -func TestBuildFakeClientHello_FitsOneSegment(t *testing.T) { - t.Parallel() - for _, sni := range []string{"a.io", "example.com", strings.Repeat("a", 253)} { - record, err := buildFakeClientHello(sni) - require.NoError(t, err, "sni=%q", sni) - require.Less(t, len(record), 1200, "sni=%q built %d bytes", sni, len(record)) - } -} - -// TestBuildFakeClientHello_NoPostQuantumKeyShare catches regressions that -// would accidentally pull an X25519MLKEM768 key share (the reason the prior -// implementation had to fragment) back into the fake — e.g. if CurvePreferences -// stopped being respected by a future Go version. -func TestBuildFakeClientHello_NoPostQuantumKeyShare(t *testing.T) { - t.Parallel() - record, err := buildFakeClientHello("example.com") - require.NoError(t, err) - - var needle [2]byte - binary.BigEndian.PutUint16(needle[:], x25519MLKEM768) - require.False(t, bytes.Contains(record, needle[:]), - "output must not contain the X25519MLKEM768 code point (0x%04x)", x25519MLKEM768) -} - -// TestBuildFakeClientHello_RandomizesPerCall ensures crypto/tls generates a -// fresh random + session_id + key_share on every call, as required to avoid -// trivial fingerprinting of the spoof. -func TestBuildFakeClientHello_RandomizesPerCall(t *testing.T) { - t.Parallel() - first, err := buildFakeClientHello("example.com") - require.NoError(t, err) - second, err := buildFakeClientHello("example.com") - require.NoError(t, err) - require.NotEqual(t, first, second, - "repeated calls must produce distinct bytes (random/session_id/key_share must vary)") -} - -func TestBuildFakeClientHello_RejectsEmpty(t *testing.T) { - t.Parallel() - _, err := buildFakeClientHello("") - require.Error(t, err) -} diff --git a/common/tlsspoof/conn_test.go b/common/tlsspoof/conn_test.go deleted file mode 100644 index b41cf54753..0000000000 --- a/common/tlsspoof/conn_test.go +++ /dev/null @@ -1,369 +0,0 @@ -package tlsspoof - -import ( - "bytes" - "context" - "encoding/binary" - "encoding/hex" - "io" - "net" - "testing" - "time" - - tf "github.com/sagernet/sing-box/common/tlsfragment" - - "github.com/stretchr/testify/require" -) - -// realClientHello is a captured Chrome ClientHello for github.com. Tests that -// stack tlsspoof.Conn on top of tf.Conn still need a parseable payload to -// exercise the fragment transform. -const realClientHello = "16030105f8010005f403036e35de7389a679c54029cf452611f2211c70d9ac3897271de589ab6155f8e4ab20637d225f1ef969ad87ed78bfb9d171300bcb1703b6f314ccefb964f79b7d0961002a0a0a130213031301c02cc02bcca9c030c02fcca8c00ac009c014c013009d009c0035002fc008c012000a01000581baba00000000000f000d00000a6769746875622e636f6d00170000ff01000100000a000e000c3a3a11ec001d001700180019000b000201000010000e000c02683208687474702f312e31000500050100000000000d00160014040308040401050308050805050108060601020100120000003304ef04ed3a3a00010011ec04c0aeb2250c092a3463161cccb29d9183331a424964248579507ed23a180b0ceab2a5f5d9ce41547e497a89055471ea572867ba3a1fc3c9e45025274a20f60c6b60e62476b6afed0403af59ab83660ef4112ae20386a602010d0a5d454c0ed34c84ed4423e750213e6a2baab1bf9c4367a6007ab40a33d95220c2dcaa44f257024a5626b545db0510f4311b1a60714154909c6a61fdfca011fb2626d657aeb6070bf078508babe3b584555013e34acc56198ed4663742b3155a664a9901794c4586820a7dc162c01827291f3792e1237f801a8d1ef096013c181c4a58d2f6859ba75022d18cc4418bd4f351d5c18f83a58857d05af860c4b9ac018a5b63f17184e591532c6bc2cf2215d4a282c8a8a4f6f7aee110422c8bc9ebd3b1d609c568523aaae555db320e6c269473d87af38c256cbb9febc20aea6380c32a8916f7a373c8b1e37554e3260bf6621f6b804ee80b3c516b1d01985bf4c603b6daa9a5991de6a7a29f3a7122b8afb843a7660110fce62b43c615f5bcc2db688ba012649c0952b0a2c031e732d2b454c6b2968683cb8d244be2c9a7fa163222979eaf92722b92b862d81a3d94450c2b60c318421ebb4307c42d1f0473592a5c30e42039cc68cda9721e61aa63f49def17c15221680ed444896340133bbee67556f56b9f9d78a4df715f926a12add0cc9c862e46ea8b7316ae468282c18601b2771c9c9322f982228cf93effaacd3f80cbd12bce5fc36f56e2a3caf91e578a5fae00c9b23a8ed1a66764f4433c3628a70b8f0a6196adc60a4cb4226f07ba4c6b363fe9065563bfc1347452946386bab488686e837ab979c64f9047417fca635fe1bb4f074f256cc8af837c7b455e280426547755af90a61640169ef180aea3a77e662bb6dac1b6c3696027129b1a5edf495314e9c7f4b6110e16378ec893fa24642330a40aba1a85326101acb97c620fd8d71389e69eaed7bdb01bbe1fd428d66191150c7b2cd1ad4257391676a82ba8ce07fb2667c3b289f159003a7c7bc31d361b7b7f49a802961739d950dfcc0fa1c7abce5abdd2245101da391151490862028110465950b9e9c03d08a90998ab83267838d2e74a0593bc81f74cdf734519a05b351c0e5488c68dd810e6e9142ccc1e2f4a7f464297eb340e27acc6b9d64e12e38cce8492b3d939140b5a9e149a75597f10a23874c84323a07cdd657274378f887c85c4259b9c04cd33ba58ed630ef2a744f8e19dd34843dff331d2a6be7e2332c599289cd248a611c73d7481cd4a9bd43449a3836f14b2af18a1739e17999e4c67e85cc5bcecabb14185e5bcaff3c96098f03dc5aba819f29587758f49f940585354a2a780830528d68ccd166920dadcaa25cab5fc1907272a826aba3f08bc6b88757776812ecb6c7cec69a223ec0a13a7b62a2349a0f63ed7a27a3b15ba21d71fe6864ec6e089ae17cadd433fa3138f7ee24353c11365818f8fc34f43a05542d18efaac24bfccc1f748a0cc1a67ad379468b76fd34973dba785f5c91d618333cd810fe0700d1bbc8422029782628070a624c52c5309a4a64d625b11f8033ab28df34a1add297517fcc06b92b6817b3c5144438cf260867c57bde68c8c4b82e6a135ef676a52fbae5708002a404e6189a60e2836de565ad1b29e3819e5ed49f6810bcb28e1bd6de57306f94b79d9dae1cc4624d2a068499beef81cd5fe4b76dcbfff2a2008001d002001976128c6d5a934533f28b9914d2480aab2a8c1ab03d212529ce8b27640a716002d00020101002b000706caca03040303001b00030200015a5a000100" - -func decodeClientHello(t *testing.T) []byte { - t.Helper() - payload, err := hex.DecodeString(realClientHello) - require.NoError(t, err) - return payload -} - -type fakeSpoofer struct { - injected [][]byte - err error - closeErr error -} - -func (f *fakeSpoofer) Inject(payload []byte) error { - if f.err != nil { - return f.err - } - f.injected = append(f.injected, append([]byte(nil), payload...)) - return nil -} - -func (f *fakeSpoofer) Close() error { - return f.closeErr -} - -func readAll(t *testing.T, conn net.Conn) []byte { - t.Helper() - data, err := io.ReadAll(conn) - require.NoError(t, err) - return data -} - -func TestConn_Write_InjectsThenForwards(t *testing.T) { - t.Parallel() - payload := decodeClientHello(t) - - client, server := net.Pipe() - spoofer := &fakeSpoofer{} - wrapped, err := newConn(client, spoofer, "letsencrypt.org") - require.NoError(t, err) - - serverRead := make(chan []byte, 1) - go func() { - serverRead <- readAll(t, server) - }() - - n, err := wrapped.Write(payload) - require.NoError(t, err) - require.Equal(t, len(payload), n) - require.NoError(t, wrapped.Close()) - - forwarded := <-serverRead - require.Equal(t, payload, forwarded, "underlying conn must receive the real ClientHello unchanged") - require.Len(t, spoofer.injected, 1) - - injected := spoofer.injected[0] - serverName := tf.IndexTLSServerName(injected) - require.NotNil(t, serverName, "injected payload must parse as ClientHello") - require.Equal(t, "letsencrypt.org", serverName.ServerName) -} - -func TestConn_Write_SecondWriteDoesNotInject(t *testing.T) { - t.Parallel() - payload := decodeClientHello(t) - - client, server := net.Pipe() - spoofer := &fakeSpoofer{} - wrapped, err := newConn(client, spoofer, "letsencrypt.org") - require.NoError(t, err) - - serverRead := make(chan []byte, 1) - go func() { - serverRead <- readAll(t, server) - }() - - _, err = wrapped.Write(payload) - require.NoError(t, err) - _, err = wrapped.Write([]byte("second")) - require.NoError(t, err) - require.NoError(t, wrapped.Close()) - - forwarded := <-serverRead - require.Equal(t, append(append([]byte(nil), payload...), []byte("second")...), forwarded) - require.Len(t, spoofer.injected, 1) -} - -// TestConn_Write_SurfacesCloseError guards against the defer pattern silently -// dropping the spoofer's Close() error on the success path. -func TestConn_Write_SurfacesCloseError(t *testing.T) { - t.Parallel() - - client, server := net.Pipe() - defer client.Close() - defer server.Close() - spoofer := &fakeSpoofer{closeErr: errSpoofClose} - wrapped, err := newConn(client, spoofer, "letsencrypt.org") - require.NoError(t, err) - - go func() { _, _ = io.ReadAll(server) }() - - _, err = wrapped.Write([]byte("trigger inject")) - require.ErrorIs(t, err, errSpoofClose, - "Close() error must be wrapped into Write's return") -} - -func TestConn_NewConn_RejectsEmptySNI(t *testing.T) { - t.Parallel() - client, server := net.Pipe() - defer client.Close() - defer server.Close() - _, err := newConn(client, &fakeSpoofer{}, "") - require.Error(t, err, "empty SNI must fail at construction") -} - -var errSpoofClose = errTest("spoof-close-failed") - -type errTest string - -func (e errTest) Error() string { return string(e) } - -// recordingConn intercepts each Write call so tests can assert how many -// downstream writes occurred and in what order with respect to spoof -// injection. It does not implement WithUpstream, so tf.Conn's -// N.UnwrapReader(conn).(*net.TCPConn) returns nil and fragment-mode falls -// back to its plain Write + time.Sleep path — which is what we want to -// exercise over a net.Pipe. -type recordingConn struct { - net.Conn - writes [][]byte - timeline *[]string -} - -func (c *recordingConn) Write(p []byte) (int, error) { - c.writes = append(c.writes, append([]byte(nil), p...)) - if c.timeline != nil { - *c.timeline = append(*c.timeline, "write") - } - return c.Conn.Write(p) -} - -type tlsRecord struct { - contentType byte - payload []byte -} - -func parseTLSRecords(t *testing.T, data []byte) []tlsRecord { - t.Helper() - var records []tlsRecord - for len(data) > 0 { - require.GreaterOrEqual(t, len(data), 5, "record header incomplete") - recordLen := int(binary.BigEndian.Uint16(data[3:5])) - require.GreaterOrEqual(t, len(data), 5+recordLen, "record payload truncated") - records = append(records, tlsRecord{ - contentType: data[0], - payload: append([]byte(nil), data[5:5+recordLen]...), - }) - data = data[5+recordLen:] - } - return records -} - -// TestConn_StackedWithRecordFragment mirrors the wrapping order that -// STDClientConfig.Client() produces when record_fragment is enabled: -// tls.Client → tlsspoof.Conn → tf.Conn → raw conn. -// Asserts the decoy is injected and the real handshake arrives split into -// multiple TLS records whose payloads reassemble to the original. -func TestConn_StackedWithRecordFragment(t *testing.T) { - t.Parallel() - payload := decodeClientHello(t) - - client, server := net.Pipe() - defer server.Close() - - fragConn := tf.NewConn(client, context.Background(), false, true, time.Millisecond) - spoofer := &fakeSpoofer{} - wrapped, err := newConn(fragConn, spoofer, "letsencrypt.org") - require.NoError(t, err) - - serverRead := make(chan []byte, 1) - go func() { serverRead <- readAll(t, server) }() - - _, err = wrapped.Write(payload) - require.NoError(t, err) - require.NoError(t, wrapped.Close()) - forwarded := <-serverRead - - require.Len(t, spoofer.injected, 1, "spoof must inject exactly once") - injected := tf.IndexTLSServerName(spoofer.injected[0]) - require.NotNil(t, injected, "injected payload must parse as ClientHello") - require.Equal(t, "letsencrypt.org", injected.ServerName) - - records := parseTLSRecords(t, forwarded) - require.Greater(t, len(records), 1, "record_fragment must produce multiple records") - var reassembled []byte - for _, r := range records { - require.Equal(t, byte(0x16), r.contentType, "all records must be handshake") - reassembled = append(reassembled, r.payload...) - } - require.Equal(t, payload[5:], reassembled, "record payloads must reassemble to original handshake") -} - -// TestConn_StackedWithPacketFragment is the primary regression test for the -// fragment-only gate fix in STDClientConfig.Client(). It verifies that -// packet-level fragmentation combined with spoof produces: -// - one spoof injection carrying the decoy SNI, -// - multiple separate writes to the underlying conn, -// - an unmodified byte stream when those writes are concatenated -// (no extra record framing). -func TestConn_StackedWithPacketFragment(t *testing.T) { - t.Parallel() - payload := decodeClientHello(t) - - client, server := net.Pipe() - defer server.Close() - - rc := &recordingConn{Conn: client} - fragConn := tf.NewConn(rc, context.Background(), true, false, time.Millisecond) - spoofer := &fakeSpoofer{} - wrapped, err := newConn(fragConn, spoofer, "letsencrypt.org") - require.NoError(t, err) - - serverRead := make(chan []byte, 1) - go func() { serverRead <- readAll(t, server) }() - - _, err = wrapped.Write(payload) - require.NoError(t, err) - require.NoError(t, wrapped.Close()) - forwarded := <-serverRead - - require.Len(t, spoofer.injected, 1, "spoof must inject exactly once") - injected := tf.IndexTLSServerName(spoofer.injected[0]) - require.NotNil(t, injected) - require.Equal(t, "letsencrypt.org", injected.ServerName) - - require.Greater(t, len(rc.writes), 1, "fragment must split the ClientHello into multiple writes") - require.Equal(t, payload, bytes.Join(rc.writes, nil), - "concatenated writes must equal original bytes (no extra framing)") - require.Equal(t, payload, forwarded) -} - -// TestConn_StackedWithBothFragment exercises the combination that produces -// the strongest obfuscation: each chunk becomes its own TLS record and its -// own TCP write. -func TestConn_StackedWithBothFragment(t *testing.T) { - t.Parallel() - payload := decodeClientHello(t) - - client, server := net.Pipe() - defer server.Close() - - rc := &recordingConn{Conn: client} - fragConn := tf.NewConn(rc, context.Background(), true, true, time.Millisecond) - spoofer := &fakeSpoofer{} - wrapped, err := newConn(fragConn, spoofer, "letsencrypt.org") - require.NoError(t, err) - - serverRead := make(chan []byte, 1) - go func() { serverRead <- readAll(t, server) }() - - _, err = wrapped.Write(payload) - require.NoError(t, err) - require.NoError(t, wrapped.Close()) - forwarded := <-serverRead - - require.Len(t, spoofer.injected, 1) - injected := tf.IndexTLSServerName(spoofer.injected[0]) - require.NotNil(t, injected) - require.Equal(t, "letsencrypt.org", injected.ServerName) - - require.Greater(t, len(rc.writes), 1, "split-packet must produce multiple writes") - records := parseTLSRecords(t, forwarded) - require.Greater(t, len(records), 1, "split-record must produce multiple records") - var reassembled []byte - for _, r := range records { - require.Equal(t, byte(0x16), r.contentType) - reassembled = append(reassembled, r.payload...) - } - require.Equal(t, payload[5:], reassembled, - "record payloads must reassemble to the original handshake") -} - -// trackingSpoofer adds the spoof injection to a shared event timeline so -// TestConn_StackedInjectionOrder can prove the decoy precedes the first -// downstream write. -type trackingSpoofer struct { - injected [][]byte - timeline *[]string -} - -func (s *trackingSpoofer) Inject(payload []byte) error { - s.injected = append(s.injected, append([]byte(nil), payload...)) - *s.timeline = append(*s.timeline, "inject") - return nil -} - -func (s *trackingSpoofer) Close() error { return nil } - -// TestConn_StackedInjectionOrder asserts the documented wire order: the -// decoy injection happens before any write reaches the underlying conn. -func TestConn_StackedInjectionOrder(t *testing.T) { - t.Parallel() - payload := decodeClientHello(t) - - client, server := net.Pipe() - defer server.Close() - - var timeline []string - rc := &recordingConn{Conn: client, timeline: &timeline} - fragConn := tf.NewConn(rc, context.Background(), true, true, time.Millisecond) - spoofer := &trackingSpoofer{timeline: &timeline} - wrapped, err := newConn(fragConn, spoofer, "letsencrypt.org") - require.NoError(t, err) - - serverRead := make(chan []byte, 1) - go func() { serverRead <- readAll(t, server) }() - - _, err = wrapped.Write(payload) - require.NoError(t, err) - require.NoError(t, wrapped.Close()) - <-serverRead - - require.NotEmpty(t, timeline) - require.Equal(t, "inject", timeline[0], "decoy must be injected before any downstream write") - require.Contains(t, timeline[1:], "write", "at least one downstream write must follow the inject") -} - -func TestParseMethod(t *testing.T) { - t.Parallel() - cases := map[string]struct { - want Method - ok bool - }{ - "": {MethodWrongSequence, true}, - "wrong-sequence": {MethodWrongSequence, true}, - "wrong-checksum": {MethodWrongChecksum, true}, - "nonsense": {0, false}, - } - for input, expected := range cases { - m, err := ParseMethod(input) - if !expected.ok { - require.Error(t, err, "input=%q", input) - continue - } - require.NoError(t, err, "input=%q", input) - require.Equal(t, expected.want, m, "input=%q", input) - } -} diff --git a/common/tlsspoof/integration_unix_test.go b/common/tlsspoof/integration_unix_test.go index 0f4585fd82..6b262ff71b 100644 --- a/common/tlsspoof/integration_unix_test.go +++ b/common/tlsspoof/integration_unix_test.go @@ -6,74 +6,53 @@ import ( "encoding/hex" "io" "net" + "runtime" "testing" "time" "github.com/stretchr/testify/require" ) -func TestIntegrationSpoofer_WrongChecksum(t *testing.T) { +func TestIntegrationSpoofer(t *testing.T) { requireRoot(t) - client, serverPort := dialLocalEchoServer(t) - spoofer, err := newRawSpoofer(client, MethodWrongChecksum) - require.NoError(t, err) - defer spoofer.Close() - - fake, err := buildFakeClientHello("letsencrypt.org") - require.NoError(t, err) - - captured := tcpdumpObserver(t, loopbackInterface, serverPort, "letsencrypt.org", func() { - require.NoError(t, spoofer.Inject(fake)) - }, 3*time.Second) - require.True(t, captured, "injected fake ClientHello must be observable on loopback") -} - -func TestIntegrationSpoofer_WrongSequence(t *testing.T) { - requireRoot(t) - client, serverPort := dialLocalEchoServer(t) - spoofer, err := newRawSpoofer(client, MethodWrongSequence) - require.NoError(t, err) - defer spoofer.Close() - - fake, err := buildFakeClientHello("letsencrypt.org") - require.NoError(t, err) - - captured := tcpdumpObserver(t, loopbackInterface, serverPort, "letsencrypt.org", func() { - require.NoError(t, spoofer.Inject(fake)) - }, 3*time.Second) - require.True(t, captured, "injected fake ClientHello must be observable on loopback") -} - -func TestIntegrationSpoofer_IPv6_WrongChecksum(t *testing.T) { - requireRoot(t) - client, serverPort := dialLocalEchoServerIPv6(t) - spoofer, err := newRawSpoofer(client, MethodWrongChecksum) - require.NoError(t, err) - defer spoofer.Close() - - fake, err := buildFakeClientHello("letsencrypt.org") - require.NoError(t, err) - - captured := tcpdumpObserver(t, loopbackInterface, serverPort, "letsencrypt.org", func() { - require.NoError(t, spoofer.Inject(fake)) - }, 3*time.Second) - require.True(t, captured, "injected fake ClientHello must be observable on loopback") -} - -func TestIntegrationSpoofer_IPv6_WrongSequence(t *testing.T) { - requireRoot(t) - client, serverPort := dialLocalEchoServerIPv6(t) - spoofer, err := newRawSpoofer(client, MethodWrongSequence) - require.NoError(t, err) - defer spoofer.Close() - - fake, err := buildFakeClientHello("letsencrypt.org") - require.NoError(t, err) - - captured := tcpdumpObserver(t, loopbackInterface, serverPort, "letsencrypt.org", func() { - require.NoError(t, spoofer.Inject(fake)) - }, 3*time.Second) - require.True(t, captured, "injected fake ClientHello must be observable on loopback") + methods := []struct { + name string + method Method + }{ + {"WrongChecksum", MethodWrongChecksum}, + {"WrongSequence", MethodWrongSequence}, + {"WrongAcknowledgment", MethodWrongAcknowledgment}, + {"WrongMD5Sig", MethodWrongMD5Sig}, + {"WrongTimestamp", MethodWrongTimestamp}, + } + families := []struct { + name string + dial func(*testing.T) (net.Conn, uint16) + }{ + {"IPv4", dialLocalEchoServer}, + {"IPv6", dialLocalEchoServerIPv6}, + } + for _, family := range families { + for _, tc := range methods { + t.Run(family.name+"/"+tc.name, func(t *testing.T) { + if tc.method == MethodWrongTimestamp && runtime.GOOS == "darwin" { + t.Skip("wrong-timestamp is not supported on macOS") + } + client, serverPort := family.dial(t) + spoofer, err := newRawSpoofer(client, tc.method) + require.NoError(t, err) + defer spoofer.Close() + + fake, err := buildFakeClientHello("letsencrypt.org") + require.NoError(t, err) + + captured := tcpdumpObserver(t, loopbackInterface, serverPort, "letsencrypt.org", func() { + require.NoError(t, spoofer.Inject(fake)) + }, 3*time.Second) + require.True(t, captured, "injected fake ClientHello must be observable on loopback") + }) + } + } } // Loopback bypasses TCP checksum validation, so wrong-sequence is used instead. diff --git a/common/tlsspoof/packet.go b/common/tlsspoof/packet.go index 9bdf7a59d9..bee53f537f 100644 --- a/common/tlsspoof/packet.go +++ b/common/tlsspoof/packet.go @@ -1,6 +1,7 @@ package tlsspoof import ( + "encoding/binary" "net/netip" "github.com/sagernet/sing-tun/gtcpip/checksum" @@ -12,15 +13,24 @@ const ( defaultTTL uint8 = 64 defaultWindowSize uint16 = 0xFFFF tcpHeaderLen = header.TCPMinimumSize + + tcpOptionMD5Signature = 19 + tcpOptionMD5SignatureLength = 18 + tcpTimestampBackdate = 3600000 ) +type spoofPacketInfo struct { + seqNum uint32 + ackNum uint32 + corrupt bool + options []byte +} + func buildTCPSegment( src netip.AddrPort, dst netip.AddrPort, - seqNum uint32, - ackNum uint32, + packetInfo spoofPacketInfo, payload []byte, - corruptChecksum bool, ) []byte { if src.Addr().Is4() != dst.Addr().Is4() { panic("tlsspoof: mixed IPv4/IPv6 address family") @@ -29,9 +39,10 @@ func buildTCPSegment( frame []byte ipHeaderLen int ) + ipPayloadLen := tcpHeaderLen + len(packetInfo.options) + len(payload) if src.Addr().Is4() { ipHeaderLen = header.IPv4MinimumSize - frame = make([]byte, ipHeaderLen+tcpHeaderLen+len(payload)) + frame = make([]byte, ipHeaderLen+ipPayloadLen) ip := header.IPv4(frame[:ipHeaderLen]) ip.Encode(&header.IPv4Fields{ TotalLength: uint16(len(frame)), @@ -44,68 +55,128 @@ func buildTCPSegment( ip.SetChecksum(^ip.CalculateChecksum()) } else { ipHeaderLen = header.IPv6MinimumSize - frame = make([]byte, ipHeaderLen+tcpHeaderLen+len(payload)) + frame = make([]byte, ipHeaderLen+ipPayloadLen) ip := header.IPv6(frame[:ipHeaderLen]) ip.Encode(&header.IPv6Fields{ - PayloadLength: uint16(tcpHeaderLen + len(payload)), + PayloadLength: uint16(ipPayloadLen), TransportProtocol: header.TCPProtocolNumber, HopLimit: defaultTTL, SrcAddr: src.Addr(), DstAddr: dst.Addr(), }) } - encodeTCP(frame, ipHeaderLen, src, dst, seqNum, ackNum, payload, corruptChecksum) + encodeTCP(frame, ipHeaderLen, src, dst, packetInfo, payload) return frame } -func encodeTCP(frame []byte, ipHeaderLen int, src, dst netip.AddrPort, seqNum, ackNum uint32, payload []byte, corruptChecksum bool) { +func encodeTCP(frame []byte, ipHeaderLen int, src, dst netip.AddrPort, packetInfo spoofPacketInfo, payload []byte) { tcp := header.TCP(frame[ipHeaderLen:]) - copy(frame[ipHeaderLen+tcpHeaderLen:], payload) + copy(frame[ipHeaderLen+tcpHeaderLen:], packetInfo.options) + optionsLen := len(packetInfo.options) + copy(frame[ipHeaderLen+tcpHeaderLen+optionsLen:], payload) tcp.Encode(&header.TCPFields{ SrcPort: src.Port(), DstPort: dst.Port(), - SeqNum: seqNum, - AckNum: ackNum, - DataOffset: tcpHeaderLen, + SeqNum: packetInfo.seqNum, + AckNum: packetInfo.ackNum, + DataOffset: uint8(tcpHeaderLen + optionsLen), Flags: header.TCPFlagAck | header.TCPFlagPsh, WindowSize: defaultWindowSize, }) - applyTCPChecksum(tcp, src.Addr(), dst.Addr(), payload, corruptChecksum) + applyTCPChecksum(tcp, src.Addr(), dst.Addr(), payload, packetInfo.corrupt) } -func buildSpoofFrame(method Method, src, dst netip.AddrPort, sendNext, receiveNext uint32, payload []byte) ([]byte, error) { - sequence, corrupt, err := resolveSpoofSequence(method, sendNext, payload) +func buildSpoofFrame(method Method, src, dst netip.AddrPort, sendNext, receiveNext, timestamp uint32, tcpOptions, payload []byte) ([]byte, error) { + packetInfo, err := resolveSpoofPacketInfo(method, sendNext, receiveNext, timestamp, tcpOptions, payload) if err != nil { return nil, err } - return buildTCPSegment(src, dst, sequence, receiveNext, payload, corrupt), nil + return buildTCPSegment(src, dst, packetInfo, payload), nil } // buildSpoofTCPSegment returns a TCP segment without an IP header, for // platforms where the kernel synthesises the IP header (darwin IPv6). -func buildSpoofTCPSegment(method Method, src, dst netip.AddrPort, sendNext, receiveNext uint32, payload []byte) ([]byte, error) { - sequence, corrupt, err := resolveSpoofSequence(method, sendNext, payload) +func buildSpoofTCPSegment(method Method, src, dst netip.AddrPort, sendNext, receiveNext, timestamp uint32, payload []byte) ([]byte, error) { + packetInfo, err := resolveSpoofPacketInfo(method, sendNext, receiveNext, timestamp, nil, payload) if err != nil { return nil, err } - segment := make([]byte, tcpHeaderLen+len(payload)) - encodeTCP(segment, 0, src, dst, sequence, receiveNext, payload, corrupt) + segment := make([]byte, tcpHeaderLen+len(packetInfo.options)+len(payload)) + encodeTCP(segment, 0, src, dst, packetInfo, payload) return segment, nil } -func resolveSpoofSequence(method Method, sendNext uint32, payload []byte) (uint32, bool, error) { +func resolveSpoofPacketInfo(method Method, sendNext, receiveNext, timestamp uint32, tcpOptions, payload []byte) (spoofPacketInfo, error) { + packetInfo := spoofPacketInfo{seqNum: sendNext, ackNum: receiveNext} switch method { case MethodWrongSequence: - return sendNext - uint32(len(payload)), false, nil + packetInfo.seqNum = sendNext - uint32(len(payload)) case MethodWrongChecksum: - return sendNext, true, nil + packetInfo.corrupt = true + case MethodWrongAcknowledgment: + packetInfo.ackNum = receiveNext - uint32(defaultWindowSize/2) + case MethodWrongMD5Sig: + packetInfo.options = buildMD5SignatureOptions() + case MethodWrongTimestamp: + packetInfo.options = buildWrongTimestampOptions(timestamp, tcpOptions) default: - return 0, false, E.New("tls_spoof: unknown method ", method) + return packetInfo, E.New("tls_spoof: unknown method ", method) + } + return packetInfo, nil +} + +func buildMD5SignatureOptions() []byte { + options := make([]byte, tcpOptionMD5SignatureLength+2) + options[0] = tcpOptionMD5Signature + options[1] = tcpOptionMD5SignatureLength + return options +} + +func buildWrongTimestampOptions(timestamp uint32, tcpOptions []byte) []byte { + spoofedTimestamp := timestamp + if spoofedTimestamp > tcpTimestampBackdate { + spoofedTimestamp -= tcpTimestampBackdate + } else { + spoofedTimestamp = 0 + } + if rewriteTCPOptionTimestamp(tcpOptions, spoofedTimestamp) { + return tcpOptions + } + options := make([]byte, header.TCPOptionTSLength+2) + header.EncodeTSOption(spoofedTimestamp, 0, options) + return options +} + +// rewriteTCPOptionTimestamp finds the TS option in tcpOptions and writes +// timestamp into its TSVal field in place. The caller must own tcpOptions +// (parseTCPPacket already returns a private copy on Windows). +func rewriteTCPOptionTimestamp(tcpOptions []byte, timestamp uint32) bool { + for i := 0; i < len(tcpOptions); { + switch tcpOptions[i] { + case header.TCPOptionEOL: + return false + case header.TCPOptionNOP: + i++ + continue + } + if i+1 >= len(tcpOptions) { + return false + } + optionLen := int(tcpOptions[i+1]) + if optionLen < 2 || i+optionLen > len(tcpOptions) { + return false + } + if tcpOptions[i] == header.TCPOptionTS && optionLen == header.TCPOptionTSLength { + binary.BigEndian.PutUint32(tcpOptions[i+2:], timestamp) + return true + } + i += optionLen } + return false } func applyTCPChecksum(tcp header.TCP, srcAddr, dstAddr netip.Addr, payload []byte, corrupt bool) { - tcpLen := tcpHeaderLen + len(payload) + tcpLen := int(tcp.DataOffset()) + len(payload) pseudo := header.PseudoHeaderChecksum(header.TCPProtocolNumber, srcAddr.AsSlice(), dstAddr.AsSlice(), uint16(tcpLen)) payloadChecksum := checksum.Checksum(payload, 0) tcpChecksum := ^tcp.CalculateChecksum(checksum.Combine(pseudo, payloadChecksum)) diff --git a/common/tlsspoof/packet_test.go b/common/tlsspoof/packet_test.go deleted file mode 100644 index 5c6d5b6be4..0000000000 --- a/common/tlsspoof/packet_test.go +++ /dev/null @@ -1,136 +0,0 @@ -package tlsspoof - -import ( - "net/netip" - "testing" - - "github.com/sagernet/sing-tun/gtcpip" - "github.com/sagernet/sing-tun/gtcpip/checksum" - "github.com/sagernet/sing-tun/gtcpip/header" - - "github.com/stretchr/testify/require" -) - -func TestBuildTCPSegment_IPv4_ValidChecksum(t *testing.T) { - t.Parallel() - src := netip.MustParseAddrPort("10.0.0.1:54321") - dst := netip.MustParseAddrPort("1.2.3.4:443") - payload := []byte("fake-client-hello") - frame := buildTCPSegment(src, dst, 100_000, 200_000, payload, false) - - ip := header.IPv4(frame[:header.IPv4MinimumSize]) - require.True(t, ip.IsChecksumValid()) - - tcp := header.TCP(frame[header.IPv4MinimumSize:]) - payloadChecksum := checksum.Checksum(payload, 0) - require.True(t, tcp.IsChecksumValid( - tcpip.AddrFrom4(src.Addr().As4()), - tcpip.AddrFrom4(dst.Addr().As4()), - payloadChecksum, - uint16(len(payload)), - )) -} - -func TestBuildTCPSegment_IPv4_CorruptChecksum(t *testing.T) { - t.Parallel() - src := netip.MustParseAddrPort("10.0.0.1:54321") - dst := netip.MustParseAddrPort("1.2.3.4:443") - payload := []byte("fake-client-hello") - frame := buildTCPSegment(src, dst, 100_000, 200_000, payload, true) - - tcp := header.TCP(frame[header.IPv4MinimumSize:]) - payloadChecksum := checksum.Checksum(payload, 0) - require.False(t, tcp.IsChecksumValid( - tcpip.AddrFrom4(src.Addr().As4()), - tcpip.AddrFrom4(dst.Addr().As4()), - payloadChecksum, - uint16(len(payload)), - )) - // IP checksum must still be valid so the router forwards the packet. - require.True(t, header.IPv4(frame[:header.IPv4MinimumSize]).IsChecksumValid()) -} - -func TestBuildTCPSegment_IPv6_ValidChecksum(t *testing.T) { - t.Parallel() - src := netip.MustParseAddrPort("[fe80::1]:54321") - dst := netip.MustParseAddrPort("[2606:4700::1]:443") - payload := []byte("fake-client-hello") - frame := buildTCPSegment(src, dst, 0xDEADBEEF, 0x12345678, payload, false) - - tcp := header.TCP(frame[header.IPv6MinimumSize:]) - payloadChecksum := checksum.Checksum(payload, 0) - require.True(t, tcp.IsChecksumValid( - tcpip.AddrFrom16(src.Addr().As16()), - tcpip.AddrFrom16(dst.Addr().As16()), - payloadChecksum, - uint16(len(payload)), - )) -} - -func TestBuildTCPSegment_MixedFamilyPanics(t *testing.T) { - t.Parallel() - src := netip.MustParseAddrPort("10.0.0.1:54321") - dst := netip.MustParseAddrPort("[2606:4700::1]:443") - require.Panics(t, func() { - buildTCPSegment(src, dst, 0, 0, nil, false) - }) -} - -func TestBuildSpoofFrame_WrongSequence(t *testing.T) { - t.Parallel() - src := netip.MustParseAddrPort("10.0.0.1:54321") - dst := netip.MustParseAddrPort("1.2.3.4:443") - payload := []byte("fake-client-hello") - const sendNext uint32 = 10_000 - frame, err := buildSpoofFrame(MethodWrongSequence, src, dst, sendNext, 20_000, payload) - require.NoError(t, err) - - tcp := header.TCP(frame[header.IPv4MinimumSize:]) - require.Equal(t, sendNext-uint32(len(payload)), tcp.SequenceNumber(), - "wrong-sequence places the fake at sendNext-len(payload)") - require.True(t, tcp.Flags().Contains(header.TCPFlagAck|header.TCPFlagPsh)) - - // Checksum must still be valid — only the sequence number is wrong. - payloadChecksum := checksum.Checksum(payload, 0) - require.True(t, tcp.IsChecksumValid( - tcpip.AddrFrom4(src.Addr().As4()), - tcpip.AddrFrom4(dst.Addr().As4()), - payloadChecksum, - uint16(len(payload)), - )) -} - -func TestBuildSpoofFrame_WrongChecksum(t *testing.T) { - t.Parallel() - src := netip.MustParseAddrPort("10.0.0.1:54321") - dst := netip.MustParseAddrPort("1.2.3.4:443") - payload := []byte("fake-client-hello") - const sendNext uint32 = 5_000 - frame, err := buildSpoofFrame(MethodWrongChecksum, src, dst, sendNext, 20_000, payload) - require.NoError(t, err) - - tcp := header.TCP(frame[header.IPv4MinimumSize:]) - require.Equal(t, sendNext, tcp.SequenceNumber(), - "wrong-checksum keeps the real sequence number") - - payloadChecksum := checksum.Checksum(payload, 0) - require.False(t, tcp.IsChecksumValid( - tcpip.AddrFrom4(src.Addr().As4()), - tcpip.AddrFrom4(dst.Addr().As4()), - payloadChecksum, - uint16(len(payload)), - )) - require.True(t, header.IPv4(frame[:header.IPv4MinimumSize]).IsChecksumValid(), - "IPv4 checksum must remain valid so the router forwards the packet") -} - -func TestBuildSpoofTCPSegment_EncodesWithoutIPHeader(t *testing.T) { - t.Parallel() - src := netip.MustParseAddrPort("[fe80::1]:54321") - dst := netip.MustParseAddrPort("[2606:4700::1]:443") - payload := []byte("fake-client-hello") - segment, err := buildSpoofTCPSegment(MethodWrongSequence, src, dst, 1000, 2000, payload) - require.NoError(t, err) - require.Equal(t, tcpHeaderLen+len(payload), len(segment), - "segment must be TCP header + payload, no IP header") -} diff --git a/common/tlsspoof/raw_darwin.go b/common/tlsspoof/raw_darwin.go index ab31687692..6f1dd96b6e 100644 --- a/common/tlsspoof/raw_darwin.go +++ b/common/tlsspoof/raw_darwin.go @@ -68,6 +68,9 @@ type darwinSpoofer struct { } func newRawSpoofer(conn net.Conn, method Method) (rawSpoofer, error) { + if method == MethodWrongTimestamp { + return nil, E.New("tls_spoof: wrong-timestamp is not supported on macOS") + } _, src, dst, err := tcpEndpoints(conn) if err != nil { return nil, err @@ -159,7 +162,7 @@ func openDarwinRawSocket(src, dst netip.AddrPort) (int, unix.Sockaddr, error) { func (s *darwinSpoofer) Inject(payload []byte) error { if !s.src.Addr().Is4() { - segment, err := buildSpoofTCPSegment(s.method, s.src, s.dst, s.sendNext, s.receiveNext, payload) + segment, err := buildSpoofTCPSegment(s.method, s.src, s.dst, s.sendNext, s.receiveNext, 0, payload) if err != nil { return err } @@ -169,7 +172,7 @@ func (s *darwinSpoofer) Inject(payload []byte) error { } return nil } - frame, err := buildSpoofFrame(s.method, s.src, s.dst, s.sendNext, s.receiveNext, payload) + frame, err := buildSpoofFrame(s.method, s.src, s.dst, s.sendNext, s.receiveNext, 0, nil, payload) if err != nil { return err } diff --git a/common/tlsspoof/raw_linux.go b/common/tlsspoof/raw_linux.go index f82fbc9efb..3e4761147e 100644 --- a/common/tlsspoof/raw_linux.go +++ b/common/tlsspoof/raw_linux.go @@ -27,6 +27,7 @@ type linuxSpoofer struct { rawSockAddr unix.Sockaddr sendNext uint32 receiveNext uint32 + timestamp uint32 } func newRawSpoofer(conn net.Conn, method Method) (rawSpoofer, error) { @@ -84,6 +85,15 @@ func openLinuxRawSocket(dst netip.AddrPort) (int, unix.Sockaddr, error) { func (s *linuxSpoofer) loadSequenceNumbers(tcpConn *net.TCPConn) error { return control.Conn(tcpConn, func(raw uintptr) (err error) { fd := int(raw) + + if s.method == MethodWrongTimestamp { + timestamp, err := unix.GetsockoptInt(fd, unix.IPPROTO_TCP, unix.TCP_TIMESTAMP) + if err != nil { + return E.Cause(err, "read timestamp") + } + s.timestamp = uint32(timestamp) + } + err = unix.SetsockoptInt(fd, unix.IPPROTO_TCP, unix.TCP_REPAIR, unix.TCP_REPAIR_ON) if err != nil { return E.Cause(err, "enter TCP_REPAIR (need CAP_NET_ADMIN)") @@ -118,7 +128,7 @@ func (s *linuxSpoofer) loadSequenceNumbers(tcpConn *net.TCPConn) error { } func (s *linuxSpoofer) Inject(payload []byte) error { - frame, err := buildSpoofFrame(s.method, s.src, s.dst, s.sendNext, s.receiveNext, payload) + frame, err := buildSpoofFrame(s.method, s.src, s.dst, s.sendNext, s.receiveNext, s.timestamp, nil, payload) if err != nil { return err } diff --git a/common/tlsspoof/raw_windows.go b/common/tlsspoof/raw_windows.go index 9f6553f1b8..d7d8a2682a 100644 --- a/common/tlsspoof/raw_windows.go +++ b/common/tlsspoof/raw_windows.go @@ -6,6 +6,7 @@ import ( "errors" "net" "net/netip" + "slices" "sync" "sync/atomic" "time" @@ -113,7 +114,7 @@ func (s *windowsSpoofer) run() { return } pkt := buf[:n] - seq, ack, payloadLen, ok := parseTCPFields(pkt, addr.IPv6()) + seq, ack, tcpOptions, payloadLen, ok := parseTCPPacket(pkt, addr.IPv6()) if !ok { // Our filter is OutboundTCP(src, dst); a non-TCP or truncated // match means driver state is suspect. Re-inject so the kernel @@ -151,7 +152,11 @@ func (s *windowsSpoofer) run() { continue } - frame, err := buildSpoofFrame(s.method, s.src, s.dst, seq, ack, fake) + var timestamp uint32 + if parsed := header.ParseTCPOptions(tcpOptions); parsed.TS { + timestamp = parsed.TSVal + } + frame, err := buildSpoofFrame(s.method, s.src, s.dst, seq, ack, timestamp, tcpOptions, fake) if err != nil { s.recordErr(err) return @@ -177,46 +182,56 @@ func (s *windowsSpoofer) run() { } } -func parseTCPFields(pkt []byte, isV6 bool) (seq, ack uint32, payloadLen int, ok bool) { +func parseTCPPacket(pkt []byte, isV6 bool) (seq, ack uint32, options []byte, payloadLen int, ok bool) { if isV6 { if len(pkt) < header.IPv6MinimumSize+header.TCPMinimumSize { - return 0, 0, 0, false + return 0, 0, nil, 0, false } ip := header.IPv6(pkt) if ip.TransportProtocol() != header.TCPProtocolNumber { - return 0, 0, 0, false + return 0, 0, nil, 0, false } tcp := header.TCP(pkt[header.IPv6MinimumSize:]) tcpHdr := int(tcp.DataOffset()) if tcpHdr < header.TCPMinimumSize || header.IPv6MinimumSize+tcpHdr > len(pkt) { - return 0, 0, 0, false + return 0, 0, nil, 0, false + } + total := header.IPv6MinimumSize + int(ip.PayloadLength()) + if total == header.IPv6MinimumSize || total > len(pkt) { + total = len(pkt) } - return tcp.SequenceNumber(), tcp.AckNumber(), - len(pkt) - header.IPv6MinimumSize - tcpHdr, true + if total < header.IPv6MinimumSize+tcpHdr { + return 0, 0, nil, 0, false + } + return tcp.SequenceNumber(), tcp.AckNumber(), slices.Clone(tcp.Options()), + total - header.IPv6MinimumSize - tcpHdr, true } if len(pkt) < header.IPv4MinimumSize+header.TCPMinimumSize { - return 0, 0, 0, false + return 0, 0, nil, 0, false } ip := header.IPv4(pkt) if ip.Protocol() != uint8(header.TCPProtocolNumber) { - return 0, 0, 0, false + return 0, 0, nil, 0, false } ihl := int(ip.HeaderLength()) // ihl+TCPMinimumSize guards the TCP-header field reads below; without // this, an IPv4 packet with options (ihl>20) against a 40-byte buffer // reads past the TCP slice when calling DataOffset. if ihl < header.IPv4MinimumSize || ihl+header.TCPMinimumSize > len(pkt) { - return 0, 0, 0, false + return 0, 0, nil, 0, false } tcp := header.TCP(pkt[ihl:]) tcpHdr := int(tcp.DataOffset()) if tcpHdr < header.TCPMinimumSize || ihl+tcpHdr > len(pkt) { - return 0, 0, 0, false + return 0, 0, nil, 0, false } total := int(ip.TotalLength()) if total == 0 || total > len(pkt) { total = len(pkt) } - return tcp.SequenceNumber(), tcp.AckNumber(), + if total < ihl+tcpHdr { + return 0, 0, nil, 0, false + } + return tcp.SequenceNumber(), tcp.AckNumber(), slices.Clone(tcp.Options()), total - ihl - tcpHdr, true } diff --git a/common/tlsspoof/raw_windows_test.go b/common/tlsspoof/raw_windows_test.go deleted file mode 100644 index 58566b8759..0000000000 --- a/common/tlsspoof/raw_windows_test.go +++ /dev/null @@ -1,112 +0,0 @@ -//go:build windows && (amd64 || 386) - -package tlsspoof - -import ( - "net/netip" - "testing" - - "github.com/sagernet/sing-tun/gtcpip/header" - - "github.com/stretchr/testify/require" -) - -func TestParseTCPFieldsIPv4Valid(t *testing.T) { - t.Parallel() - src := netip.MustParseAddrPort("10.0.0.1:54321") - dst := netip.MustParseAddrPort("1.2.3.4:443") - payload := []byte("hello") - frame := buildTCPSegment(src, dst, 1000, 2000, payload, false) - - seq, ack, payloadLen, ok := parseTCPFields(frame, false) - require.True(t, ok) - require.Equal(t, uint32(1000), seq) - require.Equal(t, uint32(2000), ack) - require.Equal(t, len(payload), payloadLen) -} - -func TestParseTCPFieldsIPv4NoPayload(t *testing.T) { - t.Parallel() - src := netip.MustParseAddrPort("10.0.0.1:54321") - dst := netip.MustParseAddrPort("1.2.3.4:443") - frame := buildTCPSegment(src, dst, 42, 100, nil, false) - - seq, ack, payloadLen, ok := parseTCPFields(frame, false) - require.True(t, ok) - require.Equal(t, uint32(42), seq) - require.Equal(t, uint32(100), ack) - require.Equal(t, 0, payloadLen) -} - -func TestParseTCPFieldsIPv6Valid(t *testing.T) { - t.Parallel() - src := netip.MustParseAddrPort("[fe80::1]:54321") - dst := netip.MustParseAddrPort("[2606:4700::1]:443") - payload := []byte("hello-v6") - frame := buildTCPSegment(src, dst, 0xDEADBEEF, 0x12345678, payload, false) - - seq, ack, payloadLen, ok := parseTCPFields(frame, true) - require.True(t, ok) - require.Equal(t, uint32(0xDEADBEEF), seq) - require.Equal(t, uint32(0x12345678), ack) - require.Equal(t, len(payload), payloadLen) -} - -func TestParseTCPFieldsIPv4TooShort(t *testing.T) { - t.Parallel() - _, _, _, ok := parseTCPFields(make([]byte, header.IPv4MinimumSize+header.TCPMinimumSize-1), false) - require.False(t, ok) -} - -func TestParseTCPFieldsIPv6TooShort(t *testing.T) { - t.Parallel() - _, _, _, ok := parseTCPFields(make([]byte, header.IPv6MinimumSize+header.TCPMinimumSize-1), true) - require.False(t, ok) -} - -// buildTCPSegment only produces TCP; a UDP packet hitting parseTCPFields -// (for example from a mis-specified filter) must be rejected. -func TestParseTCPFieldsIPv4WrongProtocol(t *testing.T) { - t.Parallel() - frame := make([]byte, header.IPv4MinimumSize+header.TCPMinimumSize) - ip := header.IPv4(frame[:header.IPv4MinimumSize]) - ip.Encode(&header.IPv4Fields{ - TotalLength: uint16(len(frame)), - TTL: 64, - Protocol: 17, // UDP - SrcAddr: netip.MustParseAddr("10.0.0.1"), - DstAddr: netip.MustParseAddr("10.0.0.2"), - }) - _, _, _, ok := parseTCPFields(frame, false) - require.False(t, ok) -} - -func TestParseTCPFieldsIPv6WrongProtocol(t *testing.T) { - t.Parallel() - frame := make([]byte, header.IPv6MinimumSize+header.TCPMinimumSize) - ip := header.IPv6(frame[:header.IPv6MinimumSize]) - ip.Encode(&header.IPv6Fields{ - PayloadLength: header.TCPMinimumSize, - TransportProtocol: 17, // UDP - HopLimit: 64, - SrcAddr: netip.MustParseAddr("fe80::1"), - DstAddr: netip.MustParseAddr("fe80::2"), - }) - _, _, _, ok := parseTCPFields(frame, true) - require.False(t, ok) -} - -// ihl > 20 must not read past the TCP slice. Build an IPv4 packet with -// options header but truncate so ihl*4 + TCPMinimumSize exceeds len. -func TestParseTCPFieldsIPv4OptionsOverflow(t *testing.T) { - t.Parallel() - // Start with a valid IPv4+TCP frame, then lie about the header length. - src := netip.MustParseAddrPort("10.0.0.1:1") - dst := netip.MustParseAddrPort("10.0.0.2:2") - frame := buildTCPSegment(src, dst, 0, 0, []byte("x"), false) - ip := header.IPv4(frame[:header.IPv4MinimumSize]) - // ihl=15 → 60 bytes of IP header claimed, but buffer only has 20. - ip.SetHeaderLength(60) - _, _, _, ok := parseTCPFields(frame, false) - require.False(t, ok) -} diff --git a/common/tlsspoof/spoof.go b/common/tlsspoof/spoof.go index 1bca5693fe..e36d39e1f0 100644 --- a/common/tlsspoof/spoof.go +++ b/common/tlsspoof/spoof.go @@ -11,19 +11,48 @@ type Method int const ( MethodWrongSequence Method = iota MethodWrongChecksum + MethodWrongAcknowledgment + MethodWrongMD5Sig + MethodWrongTimestamp ) const ( - MethodNameWrongSequence = "wrong-sequence" - MethodNameWrongChecksum = "wrong-checksum" + MethodNameWrongSequence = "wrong-sequence" + MethodNameWrongChecksum = "wrong-checksum" + MethodNameWrongAcknowledgment = "wrong-ack" + MethodNameWrongMD5Sig = "wrong-md5" + MethodNameWrongTimestamp = "wrong-timestamp" ) +func ParseOptions(spoof, method string) (string, Method, error) { + if spoof == "" { + if method != "" { + return "", 0, E.New("spoof_method requires spoof") + } + return "", 0, nil + } + if !PlatformSupported { + return "", 0, E.New("tls_spoof is not supported on this platform") + } + parsedMethod, err := ParseMethod(method) + if err != nil { + return "", 0, err + } + return spoof, parsedMethod, nil +} + func ParseMethod(s string) (Method, error) { switch s { case "", MethodNameWrongSequence: return MethodWrongSequence, nil case MethodNameWrongChecksum: return MethodWrongChecksum, nil + case MethodNameWrongAcknowledgment: + return MethodWrongAcknowledgment, nil + case MethodNameWrongMD5Sig: + return MethodWrongMD5Sig, nil + case MethodNameWrongTimestamp: + return MethodWrongTimestamp, nil default: return 0, E.New("tls_spoof: unknown method: ", s) } @@ -35,6 +64,12 @@ func (m Method) String() string { return MethodNameWrongSequence case MethodWrongChecksum: return MethodNameWrongChecksum + case MethodWrongAcknowledgment: + return MethodNameWrongAcknowledgment + case MethodWrongMD5Sig: + return MethodNameWrongMD5Sig + case MethodWrongTimestamp: + return MethodNameWrongTimestamp default: return "unknown" } diff --git a/common/tlsspoof/testdata_test.go b/common/tlsspoof/testdata_test.go new file mode 100644 index 0000000000..85e74c5245 --- /dev/null +++ b/common/tlsspoof/testdata_test.go @@ -0,0 +1,4 @@ +package tlsspoof + +// realClientHello is a captured Chrome ClientHello for github.com. +const realClientHello = "16030105f8010005f403036e35de7389a679c54029cf452611f2211c70d9ac3897271de589ab6155f8e4ab20637d225f1ef969ad87ed78bfb9d171300bcb1703b6f314ccefb964f79b7d0961002a0a0a130213031301c02cc02bcca9c030c02fcca8c00ac009c014c013009d009c0035002fc008c012000a01000581baba00000000000f000d00000a6769746875622e636f6d00170000ff01000100000a000e000c3a3a11ec001d001700180019000b000201000010000e000c02683208687474702f312e31000500050100000000000d00160014040308040401050308050805050108060601020100120000003304ef04ed3a3a00010011ec04c0aeb2250c092a3463161cccb29d9183331a424964248579507ed23a180b0ceab2a5f5d9ce41547e497a89055471ea572867ba3a1fc3c9e45025274a20f60c6b60e62476b6afed0403af59ab83660ef4112ae20386a602010d0a5d454c0ed34c84ed4423e750213e6a2baab1bf9c4367a6007ab40a33d95220c2dcaa44f257024a5626b545db0510f4311b1a60714154909c6a61fdfca011fb2626d657aeb6070bf078508babe3b584555013e34acc56198ed4663742b3155a664a9901794c4586820a7dc162c01827291f3792e1237f801a8d1ef096013c181c4a58d2f6859ba75022d18cc4418bd4f351d5c18f83a58857d05af860c4b9ac018a5b63f17184e591532c6bc2cf2215d4a282c8a8a4f6f7aee110422c8bc9ebd3b1d609c568523aaae555db320e6c269473d87af38c256cbb9febc20aea6380c32a8916f7a373c8b1e37554e3260bf6621f6b804ee80b3c516b1d01985bf4c603b6daa9a5991de6a7a29f3a7122b8afb843a7660110fce62b43c615f5bcc2db688ba012649c0952b0a2c031e732d2b454c6b2968683cb8d244be2c9a7fa163222979eaf92722b92b862d81a3d94450c2b60c318421ebb4307c42d1f0473592a5c30e42039cc68cda9721e61aa63f49def17c15221680ed444896340133bbee67556f56b9f9d78a4df715f926a12add0cc9c862e46ea8b7316ae468282c18601b2771c9c9322f982228cf93effaacd3f80cbd12bce5fc36f56e2a3caf91e578a5fae00c9b23a8ed1a66764f4433c3628a70b8f0a6196adc60a4cb4226f07ba4c6b363fe9065563bfc1347452946386bab488686e837ab979c64f9047417fca635fe1bb4f074f256cc8af837c7b455e280426547755af90a61640169ef180aea3a77e662bb6dac1b6c3696027129b1a5edf495314e9c7f4b6110e16378ec893fa24642330a40aba1a85326101acb97c620fd8d71389e69eaed7bdb01bbe1fd428d66191150c7b2cd1ad4257391676a82ba8ce07fb2667c3b289f159003a7c7bc31d361b7b7f49a802961739d950dfcc0fa1c7abce5abdd2245101da391151490862028110465950b9e9c03d08a90998ab83267838d2e74a0593bc81f74cdf734519a05b351c0e5488c68dd810e6e9142ccc1e2f4a7f464297eb340e27acc6b9d64e12e38cce8492b3d939140b5a9e149a75597f10a23874c84323a07cdd657274378f887c85c4259b9c04cd33ba58ed630ef2a744f8e19dd34843dff331d2a6be7e2332c599289cd248a611c73d7481cd4a9bd43449a3836f14b2af18a1739e17999e4c67e85cc5bcecabb14185e5bcaff3c96098f03dc5aba819f29587758f49f940585354a2a780830528d68ccd166920dadcaa25cab5fc1907272a826aba3f08bc6b88757776812ecb6c7cec69a223ec0a13a7b62a2349a0f63ed7a27a3b15ba21d71fe6864ec6e089ae17cadd433fa3138f7ee24353c11365818f8fc34f43a05542d18efaac24bfccc1f748a0cc1a67ad379468b76fd34973dba785f5c91d618333cd810fe0700d1bbc8422029782628070a624c52c5309a4a64d625b11f8033ab28df34a1add297517fcc06b92b6817b3c5144438cf260867c57bde68c8c4b82e6a135ef676a52fbae5708002a404e6189a60e2836de565ad1b29e3819e5ed49f6810bcb28e1bd6de57306f94b79d9dae1cc4624d2a068499beef81cd5fe4b76dcbfff2a2008001d002001976128c6d5a934533f28b9914d2480aab2a8c1ab03d212529ce8b27640a716002d00020101002b000706caca03040303001b00030200015a5a000100" diff --git a/docs/configuration/route/rule_action.md b/docs/configuration/route/rule_action.md index f4dfad3840..b362eefe58 100644 --- a/docs/configuration/route/rule_action.md +++ b/docs/configuration/route/rule_action.md @@ -10,7 +10,9 @@ icon: material/new-box !!! quote "Changes in sing-box 1.14.0" :material-plus: [resolve.disable_optimistic_cache](#disable_optimistic_cache) - :material-plus: [resolve.timeout](#timeout) + :material-plus: [resolve.timeout](#timeout) + :material-plus: [tls_spoof](#tls_spoof) + :material-plus: [tls_spoof_method](#tls_spoof_method) !!! quote "Changes in sing-box 1.12.0" @@ -149,7 +151,9 @@ Not available when `method` is set to drop. "udp_timeout": "", "tls_fragment": false, "tls_fragment_fallback_delay": "", - "tls_record_fragment": "" + "tls_record_fragment": "", + "tls_spoof": "", + "tls_spoof_method": "" } ``` @@ -248,6 +252,26 @@ The fallback value used when TLS segmentation cannot automatically determine the Fragment TLS handshake into multiple TLS records to bypass firewalls. +#### tls_spoof + +!!! question "Since sing-box 1.14.0" + +==Linux/macOS/Windows only, requires elevated privileges== + +Inject a forged TLS ClientHello carrying this SNI before the real one, +to fool SNI-filtering middleboxes that permit specific hostnames. + +See outbound TLS [`spoof`](/configuration/shared/tls/#spoof) for details +and required privileges. + +#### tls_spoof_method + +!!! question "Since sing-box 1.14.0" + +How the forged segment is rejected by the real server. See outbound TLS +[`spoof_method`](/configuration/shared/tls/#spoof_method) for the full table +of accepted values and platform notes. + ### sniff ```json diff --git a/docs/configuration/route/rule_action.zh.md b/docs/configuration/route/rule_action.zh.md index f832a30d7e..3ef8d8d435 100644 --- a/docs/configuration/route/rule_action.zh.md +++ b/docs/configuration/route/rule_action.zh.md @@ -10,7 +10,9 @@ icon: material/new-box !!! quote "sing-box 1.14.0 中的更改" :material-plus: [resolve.disable_optimistic_cache](#disable_optimistic_cache) - :material-plus: [resolve.timeout](#timeout) + :material-plus: [resolve.timeout](#timeout) + :material-plus: [tls_spoof](#tls_spoof) + :material-plus: [tls_spoof_method](#tls_spoof_method) !!! quote "sing-box 1.12.0 中的更改" @@ -142,7 +144,9 @@ icon: material/new-box "udp_timeout": "", "tls_fragment": false, "tls_fragment_fallback_delay": "", - "tls_record_fragment": false + "tls_record_fragment": false, + "tls_spoof": "", + "tls_spoof_method": "" } ``` @@ -240,6 +244,24 @@ UDP 连接超时时间。 通过分段 TLS 握手数据包到多个 TLS 记录来绕过防火墙检测。 +#### tls_spoof + +!!! question "自 sing-box 1.14.0 起" + +==仅 Linux/macOS/Windows,需要管理员权限== + +在真实 ClientHello 之前注入携带本字段所指定 SNI 的伪造 TLS ClientHello, +用于欺骗仅放行特定主机名的 SNI 过滤中间盒。 + +详情与所需权限参阅出站 TLS [`spoof`](/zh/configuration/shared/tls/#spoof)。 + +#### tls_spoof_method + +!!! question "自 sing-box 1.14.0 起" + +控制伪造报文被真实服务器拒绝的方式。完整取值表与平台说明参阅出站 TLS +[`spoof_method`](/zh/configuration/shared/tls/#spoof_method)。 + ### sniff ```json diff --git a/docs/configuration/shared/tls.md b/docs/configuration/shared/tls.md index 3b74e890ae..70e3b5c4fe 100644 --- a/docs/configuration/shared/tls.md +++ b/docs/configuration/shared/tls.md @@ -702,12 +702,13 @@ driver on first use. Windows on ARM64 is not supported. How the forged segment is rejected by the real server. -| Value | Behavior | -|----------------------------|----------------------------------------------------------------------------------------| -| `wrong-sequence` (default) | The forged segment's TCP sequence number is placed before the server's receive window. | -| `wrong-checksum` | The forged segment's TCP checksum is deliberately invalid. | - -Conflict with `spoof` unset. +| Value | Behavior | +|----------------------------|----------------------------------------------------------------------------------------------------------------| +| `wrong-sequence` (default) | The forged segment's TCP sequence number is placed before the server's receive window. | +| `wrong-checksum` | The forged segment's TCP checksum is deliberately invalid. | +| `wrong-ack` | The forged segment's TCP acknowledgment number is placed before the server's send window. | +| `wrong-md5` | The forged segment carries a TCP-MD5 signature option, which the server rejects since no MD5 key is negotiated. | +| `wrong-timestamp` | The forged segment carries a backdated TCP timestamp, which the server rejects as a PAWS replay. Linux/Windows only; not supported on macOS. | ### ACME Fields diff --git a/docs/configuration/shared/tls.zh.md b/docs/configuration/shared/tls.zh.md index 3cc411da45..40762c4f74 100644 --- a/docs/configuration/shared/tls.zh.md +++ b/docs/configuration/shared/tls.zh.md @@ -695,12 +695,13 @@ Windows 上首次使用时需要 Administrator 以安装内嵌的 WinDivert 内 控制伪造报文被真实服务器拒绝的方式。 -| 取值 | 行为 | -|----------------------------|------------------------------------------------| -| `wrong-sequence`(默认) | 伪造报文的 TCP 序列号位于服务器接收窗口之前。 | -| `wrong-checksum` | 伪造报文的 TCP 校验和被故意设为无效。 | - -与 `spoof` 未设置冲突。 +| 取值 | 行为 | +|--------------------------|-------------------------------------------------------------------| +| `wrong-sequence`(默认) | 伪造报文的 TCP 序列号位于服务器接收窗口之前。 | +| `wrong-checksum` | 伪造报文的 TCP 校验和被故意设为无效。 | +| `wrong-ack` | 伪造报文的 TCP 确认号位于服务器发送窗口之前。 | +| `wrong-md5` | 伪造报文携带 TCP-MD5 签名选项,未协商 MD5 密钥的服务器将拒绝。 | +| `wrong-timestamp` | 伪造报文携带回退的 TCP 时间戳,服务器按 PAWS 规则视为重放并拒绝。仅支持 Linux/Windows,不支持 macOS。 | ### ACME 字段 diff --git a/option/rule_action.go b/option/rule_action.go index 303f77d6cc..a6f181f2d7 100644 --- a/option/rule_action.go +++ b/option/rule_action.go @@ -182,6 +182,8 @@ type RawRouteOptionsActionOptions struct { TLSFragment bool `json:"tls_fragment,omitempty"` TLSFragmentFallbackDelay badoption.Duration `json:"tls_fragment_fallback_delay,omitempty"` TLSRecordFragment bool `json:"tls_record_fragment,omitempty"` + TLSSpoof string `json:"tls_spoof,omitempty"` + TLSSpoofMethod string `json:"tls_spoof_method,omitempty"` } type RouteOptionsActionOptions RawRouteOptionsActionOptions diff --git a/route/conn.go b/route/conn.go index 59afe5394c..2ac8e9eb5f 100644 --- a/route/conn.go +++ b/route/conn.go @@ -14,6 +14,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/tlsfragment" + "github.com/sagernet/sing-box/common/tlsspoof" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" @@ -128,6 +129,17 @@ func (m *ConnectionManager) NewConnection(ctx context.Context, this N.Dialer, co if metadata.TLSFragment || metadata.TLSRecordFragment { remoteConn = tf.NewConn(remoteConn, ctx, metadata.TLSFragment, metadata.TLSRecordFragment, metadata.TLSFragmentFallbackDelay) } + if metadata.TLSSpoof != "" { + spoofConn, spoofErr := tlsspoof.NewConn(remoteConn, metadata.TLSSpoofMethod, metadata.TLSSpoof) + if spoofErr != nil { + spoofErr = E.Cause(spoofErr, "tls_spoof setup") + remoteConn.Close() + N.CloseOnHandshakeFailure(conn, onClose, spoofErr) + m.logger.ErrorContext(ctx, spoofErr) + return + } + remoteConn = spoofConn + } var done atomic.Bool if m.kickWriteHandshake(ctx, conn, remoteConn, false, &done, onClose) { return diff --git a/route/route.go b/route/route.go index bdc5339327..924b6cc47d 100644 --- a/route/route.go +++ b/route/route.go @@ -489,6 +489,10 @@ match: routeOptions = &action.RuleActionRouteOptions case *R.RuleActionRouteOptions: routeOptions = action + case *R.RuleActionBypass: + if action.Outbound != "" { + routeOptions = &action.RuleActionRouteOptions + } } if routeOptions != nil { // TODO: add nat @@ -538,6 +542,10 @@ match: if routeOptions.TLSRecordFragment { metadata.TLSRecordFragment = true } + if routeOptions.TLSSpoof != "" { + metadata.TLSSpoof = routeOptions.TLSSpoof + metadata.TLSSpoofMethod = routeOptions.TLSSpoofMethod + } } switch action := currentRule.Action().(type) { case *R.RuleActionSniff: diff --git a/route/rule/rule_action.go b/route/rule/rule_action.go index 2187d80d06..207945c410 100644 --- a/route/rule/rule_action.go +++ b/route/rule/rule_action.go @@ -11,6 +11,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/sniff" + "github.com/sagernet/sing-box/common/tlsspoof" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-tun" @@ -24,52 +25,54 @@ import ( "github.com/miekg/dns" ) +func newRuleActionRouteOptions(options option.RawRouteOptionsActionOptions) (RuleActionRouteOptions, error) { + spoof, spoofMethod, err := tlsspoof.ParseOptions(options.TLSSpoof, options.TLSSpoofMethod) + if err != nil { + return RuleActionRouteOptions{}, err + } + return RuleActionRouteOptions{ + OverrideAddress: M.ParseSocksaddrHostPort(options.OverrideAddress, 0), + OverridePort: options.OverridePort, + NetworkStrategy: (*C.NetworkStrategy)(options.NetworkStrategy), + FallbackDelay: time.Duration(options.FallbackDelay), + UDPDisableDomainUnmapping: options.UDPDisableDomainUnmapping, + UDPConnect: options.UDPConnect, + UDPTimeout: time.Duration(options.UDPTimeout), + TLSFragment: options.TLSFragment, + TLSFragmentFallbackDelay: time.Duration(options.TLSFragmentFallbackDelay), + TLSRecordFragment: options.TLSRecordFragment, + TLSSpoof: spoof, + TLSSpoofMethod: spoofMethod, + }, nil +} + func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action option.RuleAction) (adapter.RuleAction, error) { switch action.Action { case "": return nil, nil case C.RuleActionTypeRoute: + routeOptions, err := newRuleActionRouteOptions(action.RouteOptions.RawRouteOptionsActionOptions) + if err != nil { + return nil, err + } return &RuleActionRoute{ - Outbound: action.RouteOptions.Outbound, - RuleActionRouteOptions: RuleActionRouteOptions{ - OverrideAddress: M.ParseSocksaddrHostPort(action.RouteOptions.OverrideAddress, 0), - OverridePort: action.RouteOptions.OverridePort, - NetworkStrategy: (*C.NetworkStrategy)(action.RouteOptions.NetworkStrategy), - FallbackDelay: time.Duration(action.RouteOptions.FallbackDelay), - UDPDisableDomainUnmapping: action.RouteOptions.UDPDisableDomainUnmapping, - UDPConnect: action.RouteOptions.UDPConnect, - TLSFragment: action.RouteOptions.TLSFragment, - TLSFragmentFallbackDelay: time.Duration(action.RouteOptions.TLSFragmentFallbackDelay), - TLSRecordFragment: action.RouteOptions.TLSRecordFragment, - }, + Outbound: action.RouteOptions.Outbound, + RuleActionRouteOptions: routeOptions, }, nil case C.RuleActionTypeRouteOptions: - return &RuleActionRouteOptions{ - OverrideAddress: M.ParseSocksaddrHostPort(action.RouteOptionsOptions.OverrideAddress, 0), - OverridePort: action.RouteOptionsOptions.OverridePort, - NetworkStrategy: (*C.NetworkStrategy)(action.RouteOptionsOptions.NetworkStrategy), - FallbackDelay: time.Duration(action.RouteOptionsOptions.FallbackDelay), - UDPDisableDomainUnmapping: action.RouteOptionsOptions.UDPDisableDomainUnmapping, - UDPConnect: action.RouteOptionsOptions.UDPConnect, - UDPTimeout: time.Duration(action.RouteOptionsOptions.UDPTimeout), - TLSFragment: action.RouteOptionsOptions.TLSFragment, - TLSFragmentFallbackDelay: time.Duration(action.RouteOptionsOptions.TLSFragmentFallbackDelay), - TLSRecordFragment: action.RouteOptionsOptions.TLSRecordFragment, - }, nil + routeOptions, err := newRuleActionRouteOptions(option.RawRouteOptionsActionOptions(action.RouteOptionsOptions)) + if err != nil { + return nil, err + } + return &routeOptions, nil case C.RuleActionTypeBypass: + routeOptions, err := newRuleActionRouteOptions(action.BypassOptions.RawRouteOptionsActionOptions) + if err != nil { + return nil, err + } return &RuleActionBypass{ - Outbound: action.BypassOptions.Outbound, - RuleActionRouteOptions: RuleActionRouteOptions{ - OverrideAddress: M.ParseSocksaddrHostPort(action.BypassOptions.OverrideAddress, 0), - OverridePort: action.BypassOptions.OverridePort, - NetworkStrategy: (*C.NetworkStrategy)(action.BypassOptions.NetworkStrategy), - FallbackDelay: time.Duration(action.BypassOptions.FallbackDelay), - UDPDisableDomainUnmapping: action.BypassOptions.UDPDisableDomainUnmapping, - UDPConnect: action.BypassOptions.UDPConnect, - TLSFragment: action.BypassOptions.TLSFragment, - TLSFragmentFallbackDelay: time.Duration(action.BypassOptions.TLSFragmentFallbackDelay), - TLSRecordFragment: action.BypassOptions.TLSRecordFragment, - }, + Outbound: action.BypassOptions.Outbound, + RuleActionRouteOptions: routeOptions, }, nil case C.RuleActionTypeDirect: directDialer, err := dialer.New(ctx, option.DialerOptions(action.DirectOptions), false) @@ -225,6 +228,8 @@ type RuleActionRouteOptions struct { TLSFragment bool TLSFragmentFallbackDelay time.Duration TLSRecordFragment bool + TLSSpoof string + TLSSpoofMethod tlsspoof.Method } func (r *RuleActionRouteOptions) Type() string { @@ -273,6 +278,10 @@ func (r *RuleActionRouteOptions) Descriptions() []string { if r.TLSRecordFragment { descriptions = append(descriptions, "tls-record-fragment") } + if r.TLSSpoof != "" { + descriptions = append(descriptions, F.ToString("tls-spoof=", r.TLSSpoof)) + descriptions = append(descriptions, F.ToString("tls-spoof-method=", r.TLSSpoofMethod.String())) + } return descriptions } From 9ee56ae7f50c74058e629938cfab0cc47b2cd11a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 28 Apr 2026 18:35:22 +0800 Subject: [PATCH 73/93] Bump version --- docs/changelog.md | 63 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index e51e6ed94b..47e4b0b08e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,69 @@ icon: material/alert-decagram --- +#### 1.14.0-alpha.21 + +* Allow customizing TUN DNS mode and hijack interface DNS by default **1** +* Add mDNS DNS server **2** +* Add `preferred_by` DNS rule item **3** +* Add neighbor-based hostname resolution for the local DNS server **4** +* Update NaiveProxy to 148.0.7778.96-1 +* Add more TLS spoof methods and route rule action support **5** +** Fixes and improvements + +**1**: + +Adds [`dns_mode`](/configuration/inbound/tun/#dns_mode) and +[`dns_address`](/configuration/inbound/tun/#dns_address) on the TUN inbound. +The default `hijack` mode now sets the platform's native interface DNS +(`systemd-resolved` on Linux, per-interface DNS on Windows and Apple) and +installs platform-level DNS hijacking (an `iproute2` rule on Linux, +nftables DNAT when `auto_redirect` is enabled, WFP filters on Windows when +`strict_route` is enabled). Earlier versions did not touch the interface +DNS or the platform firewall. + +**2**: + +The new [mDNS DNS server](/configuration/dns/server/mdns/) sends queries via +multicast on the local network. The default +[local DNS server](/configuration/dns/server/local/) also routes queries for +`*.local.` and IPv4/IPv6 link-local reverse zones via mDNS on non-Apple +platforms (and via the system resolver on Apple), so an explicit `mdns` +server is only needed to reference it from +[`preferred_by`](/configuration/dns/rule/#preferred_by) or to use it +standalone. + +**3**: + +The new [`preferred_by`](/configuration/dns/rule/#preferred_by) DNS rule +item matches domains that the listed DNS servers consider their preferred +names. Supported server types are `hosts`, `local`, `mdns`, `tailscale`, and +`resolved`. The [Tailscale](/configuration/dns/server/tailscale/), +[Hosts](/configuration/dns/server/hosts/) and +[Resolved](/configuration/dns/server/resolved/) example pages have been +updated to use this rule item in place of the previous `evaluate` + +`ip_accept_any` + `respond` pattern. + +**4**: + +Adds [`neighbor_domain`](/configuration/dns/server/local/#neighbor_domain) +on the local DNS server. Listed suffixes (each starting with `.`) cause +A/AAAA queries for single-label hosts under those suffixes to be answered +from the [neighbor resolver](/configuration/shared/neighbor/) instead of +the upstream (for example `[".", ".lan"]`). + +**5**: + +Adds `wrong-ack`, `wrong-md5`, and `wrong-timestamp` +[spoof methods](/configuration/shared/tls/#spoof_method), and adds +[`tls_spoof`](/configuration/route/rule_action/#tls_spoof) / +[`tls_spoof_method`](/configuration/route/rule_action/#tls_spoof_method) +to route rule actions for per-rule TLS spoofing without outbound TLS settings. + +#### 1.14.0-alpha.20 + +** Fixes and improvements + #### 1.14.0-alpha.19 * Preserve comments between formatting From 0ee7592343ba9bd94216eafac51eb6d7fb68120c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 3 May 2026 21:36:37 +0800 Subject: [PATCH 74/93] release: Add replace_macos_standalone make target --- Makefile | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Makefile b/Makefile index 6ec7bc9b09..1fb1bfa1af 100644 --- a/Makefile +++ b/Makefile @@ -174,14 +174,31 @@ upload_macos_pkg: ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Intel.pkg" ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Universal.pkg" +replace_macos_pkg: + mkdir -p dist/SFM + cp ../sing-box-for-apple/build/SFM-Apple.pkg "dist/SFM/SFM-${VERSION}-Apple.pkg" + cp ../sing-box-for-apple/build/SFM-Intel.pkg "dist/SFM/SFM-${VERSION}-Intel.pkg" + cp ../sing-box-for-apple/build/SFM-Universal.pkg "dist/SFM/SFM-${VERSION}-Universal.pkg" + ghr --replace "v${VERSION}" "dist/SFM/SFM-${VERSION}-Apple.pkg" + ghr --replace "v${VERSION}" "dist/SFM/SFM-${VERSION}-Intel.pkg" + ghr --replace "v${VERSION}" "dist/SFM/SFM-${VERSION}-Universal.pkg" + upload_macos_dsyms: mkdir -p dist/SFM cd ../sing-box-for-apple/build/SFM.System-universal.xcarchive && zip -r SFM.dSYMs.zip dSYMs cp ../sing-box-for-apple/build/SFM.System-universal.xcarchive/SFM.dSYMs.zip "dist/SFM/SFM-${VERSION}.dSYMs.zip" ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}.dSYMs.zip" +replace_macos_dsyms: + mkdir -p dist/SFM + cd ../sing-box-for-apple/build/SFM.System-universal.xcarchive && zip -r SFM.dSYMs.zip dSYMs + cp ../sing-box-for-apple/build/SFM.System-universal.xcarchive/SFM.dSYMs.zip "dist/SFM/SFM-${VERSION}.dSYMs.zip" + ghr --replace "v${VERSION}" "dist/SFM/SFM-${VERSION}.dSYMs.zip" + release_macos_standalone: build_macos_pkg notarize_macos_pkg upload_macos_pkg upload_macos_dsyms +replace_macos_standalone: build_macos_pkg notarize_macos_pkg upload_macos_pkg upload_macos_dsyms + build_tvos: cd ../sing-box-for-apple && \ rm -rf build/SFT.xcarchive && \ From 31252a7e95630669a98a1601a959d51dc564640a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 3 May 2026 21:54:38 +0800 Subject: [PATCH 75/93] Fix cronet close and crash --- .github/CRONET_GO_VERSION | 2 +- go.mod | 64 +++++++++---------- go.sum | 128 +++++++++++++++++++------------------- 3 files changed, 97 insertions(+), 97 deletions(-) diff --git a/.github/CRONET_GO_VERSION b/.github/CRONET_GO_VERSION index 80c50a89d0..435750bd32 100644 --- a/.github/CRONET_GO_VERSION +++ b/.github/CRONET_GO_VERSION @@ -1 +1 @@ -cd9fcba9981bf69c1aab541887d2758d84dc31f3 +d96fab9b6594c94b1c08dd564cb7d72966ca3612 diff --git a/go.mod b/go.mod index 66a735909a..7d29f75f8f 100644 --- a/go.mod +++ b/go.mod @@ -31,8 +31,8 @@ require ( github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1 github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a github.com/sagernet/cors v1.2.1 - github.com/sagernet/cronet-go v0.0.0-20260502110630-bdcbb26bce76 - github.com/sagernet/cronet-go/all v0.0.0-20260502110630-bdcbb26bce76 + github.com/sagernet/cronet-go v0.0.0-20260504082225-c80e757c7008 + github.com/sagernet/cronet-go/all v0.0.0-20260504082225-c80e757c7008 github.com/sagernet/fswatch v0.1.2 github.com/sagernet/gomobile v0.1.12 github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 @@ -79,7 +79,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/ebitengine/purego v0.9.1 // 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 github.com/fxamacker/cbor/v2 v2.7.0 // indirect @@ -109,35 +109,35 @@ require ( github.com/prometheus-community/pro-bing v0.4.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/safchain/ethtool v0.3.0 // indirect - github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260502110005-e9754926ef93 // indirect - github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260502110005-e9754926ef93 // indirect - github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260502110005-e9754926ef93 // indirect - github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260502110005-e9754926ef93 // indirect - github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260502110005-e9754926ef93 // indirect - github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260502110005-e9754926ef93 // indirect - github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260502110005-e9754926ef93 // indirect - github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260502110005-e9754926ef93 // indirect - github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260502110005-e9754926ef93 // indirect - github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260502110005-e9754926ef93 // indirect - github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260502110005-e9754926ef93 // indirect - github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260502110005-e9754926ef93 // indirect - github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260502110005-e9754926ef93 // indirect - github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260502110005-e9754926ef93 // indirect - github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260502110005-e9754926ef93 // indirect - github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260502110005-e9754926ef93 // indirect - github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260502110005-e9754926ef93 // indirect - github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260502110005-e9754926ef93 // indirect - github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260502110005-e9754926ef93 // indirect - github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260502110005-e9754926ef93 // indirect - github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260502110005-e9754926ef93 // indirect - github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260502110005-e9754926ef93 // indirect - github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260502110005-e9754926ef93 // indirect - github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260502110005-e9754926ef93 // indirect - github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260502110005-e9754926ef93 // indirect - github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260502110005-e9754926ef93 // indirect - github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260502110005-e9754926ef93 // indirect - github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260502110005-e9754926ef93 // indirect - github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260502110005-e9754926ef93 // indirect + github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260504081344-a7d8efd828f8 // indirect + github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260504081344-a7d8efd828f8 // indirect + github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260504081344-a7d8efd828f8 // indirect + github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260504081344-a7d8efd828f8 // indirect + github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260504081344-a7d8efd828f8 // indirect + github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260504081344-a7d8efd828f8 // indirect + github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260504081344-a7d8efd828f8 // indirect + github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260504081344-a7d8efd828f8 // indirect + github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260504081344-a7d8efd828f8 // indirect + github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260504081344-a7d8efd828f8 // indirect + github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260504081344-a7d8efd828f8 // indirect + github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260504081344-a7d8efd828f8 // indirect + github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260504081344-a7d8efd828f8 // indirect + github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260504081344-a7d8efd828f8 // indirect + github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260504081344-a7d8efd828f8 // indirect + github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260504081344-a7d8efd828f8 // indirect + github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260504081344-a7d8efd828f8 // indirect + github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260504081344-a7d8efd828f8 // indirect + github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260504081344-a7d8efd828f8 // indirect + github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260504081344-a7d8efd828f8 // indirect + github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260504081344-a7d8efd828f8 // indirect + github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260504081344-a7d8efd828f8 // indirect + github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260504081344-a7d8efd828f8 // indirect + github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260504081344-a7d8efd828f8 // indirect + github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260504081344-a7d8efd828f8 // indirect + github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260504081344-a7d8efd828f8 // indirect + github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260504081344-a7d8efd828f8 // indirect + github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260504081344-a7d8efd828f8 // indirect + github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260504081344-a7d8efd828f8 // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect github.com/sagernet/nftables v0.3.0-mod.2 // indirect github.com/spf13/pflag v1.0.9 // indirect diff --git a/go.sum b/go.sum index 8496ea5f16..442363e333 100644 --- a/go.sum +++ b/go.sum @@ -46,8 +46,8 @@ github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 h1:CaO/zOnF8VvUfEbhRatPcwKVWamvbY github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4= 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.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= -github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +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/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= @@ -168,68 +168,68 @@ github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkk github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ= github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI= -github.com/sagernet/cronet-go v0.0.0-20260502110630-bdcbb26bce76 h1:gEHy7Tr07rlXcw26YrO99nNrbyZxZM3XiEWchQN2+JA= -github.com/sagernet/cronet-go v0.0.0-20260502110630-bdcbb26bce76/go.mod h1:hwFHBEjjthyEquDULbr4c4ucMedp8Drb6Jvm2kt/0Bw= -github.com/sagernet/cronet-go/all v0.0.0-20260502110630-bdcbb26bce76 h1:HIWLA9kqPeWdZJVVALeI8+zlmMrJ8Sgx+4WpDiFcYn4= -github.com/sagernet/cronet-go/all v0.0.0-20260502110630-bdcbb26bce76/go.mod h1:Ws1+lX4ozLw1kjMhNP6jMMMYzDQ+xj37JowIzsKjAQg= -github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260502110005-e9754926ef93 h1:Ai6n4KUL8xL5+sPPGLMA6gR6pdGE+YfyumEHmu0WOz8= -github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260502110005-e9754926ef93/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw= -github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260502110005-e9754926ef93 h1:YCsJA84Kqn+M79nugJBvLe32g7NPROwIHrCQcUeg8z0= -github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260502110005-e9754926ef93/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM= -github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260502110005-e9754926ef93 h1:iZIW+x0tyAriSheonyexh/EXgY1Yf3clZ3ZXXQlUrqI= -github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260502110005-e9754926ef93/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc= -github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260502110005-e9754926ef93 h1:5fFO+M2tKzvuqrlLA7dpYb3D+s1FYUjvZRfhGPzaxyQ= -github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260502110005-e9754926ef93/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ= -github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260502110005-e9754926ef93 h1:22KfcLvNGCB41xAEWou7NgIFUja6mxUgUWpLEO4Z3PE= -github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260502110005-e9754926ef93/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs= -github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260502110005-e9754926ef93 h1:38qlpF10SZ5ZQkHAFq+Itg6aDqi65d60U7GWeoae6Qg= -github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260502110005-e9754926ef93/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0= -github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260502110005-e9754926ef93 h1:NtZYaRFqlrQSFQpA+VhaIlwL1VMSeI5N8zEuJT+D5Mc= -github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260502110005-e9754926ef93/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0= -github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260502110005-e9754926ef93 h1:qgBCWWszFCUJDwJbUx4E7iLlyvZRgH4J+wWJWw7zz58= -github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260502110005-e9754926ef93/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4= -github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260502110005-e9754926ef93 h1:5pGx9W7N1DIONRxX0gFDcDLOsC+A2jVlbIeabdJl1l4= -github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260502110005-e9754926ef93/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo= -github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260502110005-e9754926ef93 h1:BaTS59k8zsnM8wb0Fahe3828oJtEUP6uVt5DOut1k/o= -github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260502110005-e9754926ef93/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ= -github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260502110005-e9754926ef93 h1:ZvOfpob2GE3MwT/DujqOsBo+YyuTbN5CXvhahh88DTM= -github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260502110005-e9754926ef93/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU= -github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260502110005-e9754926ef93 h1:AugS8654l2PdCSZEo1Hl//+WzkHKE7jnIu09XWpnjLE= -github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260502110005-e9754926ef93/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI= -github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260502110005-e9754926ef93 h1:+zmnmfrEKZbvzmRbKw4dDp4vlLzvto9LBQmGNx+5prk= -github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260502110005-e9754926ef93/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ= -github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260502110005-e9754926ef93 h1:sOf3QZxokQLMXEAYK2Ev1UBKw7T4dCScpejK5Sg40Fo= -github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260502110005-e9754926ef93/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0= -github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260502110005-e9754926ef93 h1:jyK14VGV4ZK+5SAITLWuwHlA+UTV6yKfnpFaEM1FaCE= -github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260502110005-e9754926ef93/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s= -github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260502110005-e9754926ef93 h1:na2c0wGTPMhFS7rSJNhokYIUq10y7q00KyEXhXsI3fc= -github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260502110005-e9754926ef93/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ= -github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260502110005-e9754926ef93 h1:+vmvZIGIaUXOyF/tFo9JGEbU7IfVENjlQduc1mYP6l8= -github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260502110005-e9754926ef93/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow= -github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260502110005-e9754926ef93 h1:/RKvCIdn3kQwojdPU8p3wslQiby0cZ2n7HnQPW8hGeo= -github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260502110005-e9754926ef93/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk= -github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260502110005-e9754926ef93 h1:76aHGGm4mJNUqJQRgYKw4Y/FMgbRr3EMaaGaAGcr5qE= -github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260502110005-e9754926ef93/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E= -github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260502110005-e9754926ef93 h1:O7MaOwIuwiqrC9uR4W+dewvkCAFa4Uh9o2Y2G5IO8Z0= -github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260502110005-e9754926ef93/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8= -github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260502110005-e9754926ef93 h1:tK68PKvoy6CRJHyunQeImVPriYsPxXdRG9RtcQRCWb8= -github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260502110005-e9754926ef93/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w= -github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260502110005-e9754926ef93 h1:tspbl1GfyatQaGpvjkowtHR/HjbEakDyTR15cNC2wmo= -github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260502110005-e9754926ef93/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0= -github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260502110005-e9754926ef93 h1:MFtSQBsTOxAkLYSA7+VNUR2VTBLxLXi15Vw9tYKiqH0= -github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260502110005-e9754926ef93/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs= -github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260502110005-e9754926ef93 h1:4i9OVY94DMslpvZP38tPSN7iZVa4iRE2wqVWPgGUJR0= -github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260502110005-e9754926ef93/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc= -github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260502110005-e9754926ef93 h1:/P/+rX2PTFI5HlIbq8kxB+HA+tqpNIx/okv4/cDZkJo= -github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260502110005-e9754926ef93/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4= -github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260502110005-e9754926ef93 h1:Szdw25xT4Wz+Xt7ZuJ1g6M4GlUpovAzSlTLEKxitoEs= -github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260502110005-e9754926ef93/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc= -github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260502110005-e9754926ef93 h1:yp3eCuMi/y5Ql4d3usN8EJJ6eU3qxQ7dqe79ExZA/j8= -github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260502110005-e9754926ef93/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc= -github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260502110005-e9754926ef93 h1:VFA4sS7Sdi8Zzk6FlT0MKcmBgI+mMo0ERBZHE9xdckM= -github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260502110005-e9754926ef93/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= -github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260502110005-e9754926ef93 h1:r2ACrUknRLL0Ysb3BbidkmQ1ZNtidBqX0N0OHqL1g6M= -github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260502110005-e9754926ef93/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw= +github.com/sagernet/cronet-go v0.0.0-20260504082225-c80e757c7008 h1:VJ073eJpvH4vsPuVswDn5UcBsLPLh7kXbvTYe2gVOfw= +github.com/sagernet/cronet-go v0.0.0-20260504082225-c80e757c7008/go.mod h1:T/mwtrpC4JlWfScw73CmSBvHzIvc7BatQ1MhRr+cYNw= +github.com/sagernet/cronet-go/all v0.0.0-20260504082225-c80e757c7008 h1:zN//kYChT5AL7a55MijB28nORo2GpMjozoU0oAVkMdE= +github.com/sagernet/cronet-go/all v0.0.0-20260504082225-c80e757c7008/go.mod h1:OJNky/hhb7cQv79x4HhpF+LyiOt7PAnnV5eZYxbnTwA= +github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260504081344-a7d8efd828f8 h1:rzk4DK7pSmgp4+p2UMPbqV6cbh66Ly3s+EhML2skX/E= +github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw= +github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260504081344-a7d8efd828f8 h1:E7UXAEykQ5cRpiQZeZ2OXOT4KkyqU6Z/xLmxiTd5dI4= +github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM= +github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260504081344-a7d8efd828f8 h1:GHdB3P+7r2pwHeA+93i71j19IUqld40LF26fB6UVulM= +github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc= +github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260504081344-a7d8efd828f8 h1:EkCd6jIAKsw9LDbwn41/EEz11wOCKlQXLBIx1zbR6Vk= +github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ= +github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260504081344-a7d8efd828f8 h1:8uUm7AaYk8quv5JmZ3rlfYjhcgZY3CFC9hZrzHAxkoQ= +github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs= +github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260504081344-a7d8efd828f8 h1:9lQg3peAjTNlc+QGrqlj/VukeVHVvMsgv4zSKQtNGJw= +github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0= +github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260504081344-a7d8efd828f8 h1:A09GwZpGBxQEhzx6LpR4Hfley5JMtc2P1ZYfl2Dl40U= +github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0= +github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260504081344-a7d8efd828f8 h1:/38BhdER+KEj/LEjgn9VbSiXlu6HNi9WRmiAeIodQcI= +github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4= +github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260504081344-a7d8efd828f8 h1:M9hVmIp4MiLrzLjBZ05DF25RxUJlcT6Fu5pl7jUdI28= +github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo= +github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260504081344-a7d8efd828f8 h1:bILzUWCCwq2RzpIh6JxmXN4A2lJtWMZJbxupdtphj6s= +github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ= +github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260504081344-a7d8efd828f8 h1:rUK4NnmSkRvHxolibnmA/CF3bxt1FdiQKjC/ZsRv8Qc= +github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU= +github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260504081344-a7d8efd828f8 h1:uGcq7LeAVlXt/MjLgzhD6eLjp0VuFnsOHd0Ho6o85NY= +github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI= +github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260504081344-a7d8efd828f8 h1:rg/O1t+N/QDeLs2PC8rHp74bvRwhH+nZT+JUIGNsLS0= +github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ= +github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260504081344-a7d8efd828f8 h1:Htq1OACFoiLU9OhAXJa23rIZx6wWv/rs9JRuhpkwPEY= +github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0= +github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260504081344-a7d8efd828f8 h1:FO82Tguucsh5nWlcise+zMS8MNSHL6RpPGPUfwp3oJY= +github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s= +github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260504081344-a7d8efd828f8 h1:F4eKuYPkL7kWUC76wfDhJ457NXRgMl9rLhJ3d9vXW2E= +github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ= +github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260504081344-a7d8efd828f8 h1:yTE5wcQRCAViNpszYpx+oUFUrOr4Kzjb0Mw4yPKXy+o= +github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow= +github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260504081344-a7d8efd828f8 h1:kwSGjoUzAL2RZqbbxAB+/5cwTvi1Dm54oz1D9ehWxEI= +github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk= +github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260504081344-a7d8efd828f8 h1:M/k9zDoP6b4KiIWoP2NsxV8pdDd0UP8kaoD3DiVcA6U= +github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E= +github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260504081344-a7d8efd828f8 h1:4xgOIUBNPmtmBffTIWzJazKcRHgik6tuuS3IrYnPqaE= +github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8= +github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260504081344-a7d8efd828f8 h1:3wqBjp6EtM9JZciOHwC0D78YFCOvBzk0chAE+mjS1RU= +github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w= +github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260504081344-a7d8efd828f8 h1:EeLBaVrvie+SFkaIVRkP6dMnKX5QHkqjgdFZAxX0g18= +github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0= +github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260504081344-a7d8efd828f8 h1:xKxrRhi5k/p2uOkrp/iA3EtSx05CcA8851fz5yHpm00= +github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs= +github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260504081344-a7d8efd828f8 h1:d+VZ4X5QaJvfTUQBsef3uWxBeEu+Lh9TZLY40jswKfk= +github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc= +github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260504081344-a7d8efd828f8 h1:ufyrbpyhxvjyA+Fe8KCfllEI0vyq0FZTqpP3IwnSskk= +github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4= +github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260504081344-a7d8efd828f8 h1:3J3O1J0zYploVeTrP6Ddz3mXm66woh7eyNcAbcc6LNY= +github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc= +github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260504081344-a7d8efd828f8 h1:tuREhEWa9KlwErDdrFFNVc5Sn9bMjfqdBnr2G40tO7g= +github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc= +github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260504081344-a7d8efd828f8 h1:UPjLdQ//cUBXdUwLtDSXfzLAFVYD/19Jt/YF+nZtPhs= +github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= +github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260504081344-a7d8efd828f8 h1:7cmmYMddOUces3noUNOlAjAROOSZz5BQdM7gTp1Sbso= +github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw= github.com/sagernet/fswatch v0.1.2 h1:/TT7k4mkce1qFPxamLO842WjqBgbTBiXP2mlUjp9PFk= github.com/sagernet/fswatch v0.1.2/go.mod h1:5BpGmpUQVd3Mc5r313HRpvADHRg3/rKn5QbwFteB880= github.com/sagernet/gomobile v0.1.12 h1:XwzjZaclFF96deLqwAgK8gU3w0M2A8qxgDmhV+A0wjg= From 228eb2df78c78c3c9128b8d88a878d9fc40d6a22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 4 May 2026 19:14:14 +0800 Subject: [PATCH 76/93] dns: Fix deadline --- dns/transport/tcp.go | 18 ++++++++++++++++++ dns/transport/tls.go | 15 ++++++++------- dns/transport/udp.go | 2 ++ 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/dns/transport/tcp.go b/dns/transport/tcp.go index 59333de8df..f8249437a1 100644 --- a/dns/transport/tcp.go +++ b/dns/transport/tcp.go @@ -4,6 +4,8 @@ import ( "context" "encoding/binary" "io" + "net" + "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/dialer" @@ -13,6 +15,7 @@ import ( "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio/deadline" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" @@ -71,6 +74,7 @@ func (t *TCPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M return nil, E.Cause(err, "dial TCP connection") } defer conn.Close() + defer setConnDeadline(ctx, conn, deadline.NeedAdditionalReadDeadline(conn))() err = WriteMessage(conn, 0, message) if err != nil { return nil, E.Cause(err, "write request") @@ -82,6 +86,20 @@ func (t *TCPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M return response, nil } +func setConnDeadline(ctx context.Context, conn net.Conn, needClose bool) func() { + if needClose { + stop := context.AfterFunc(ctx, func() { + conn.Close() + }) + return func() { stop() } + } + if d, ok := ctx.Deadline(); ok { + conn.SetDeadline(d) + return func() { conn.SetDeadline(time.Time{}) } + } + return func() {} +} + func ReadMessage(reader io.Reader) (*mDNS.Msg, error) { var responseLen uint16 err := binary.Read(reader, binary.BigEndian, &responseLen) diff --git a/dns/transport/tls.go b/dns/transport/tls.go index 43978b6ff9..b7ef25fb79 100644 --- a/dns/transport/tls.go +++ b/dns/transport/tls.go @@ -2,7 +2,6 @@ package transport import ( "context" - "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/dialer" @@ -12,6 +11,7 @@ import ( "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/bufio/deadline" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" @@ -38,7 +38,8 @@ type TLSTransport struct { type tlsDNSConn struct { tls.Conn - queryId uint16 + queryId uint16 + needDeadlineClose bool } func NewTLS(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteTLSDNSServerOptions) (adapter.DNSTransport, error) { @@ -104,7 +105,10 @@ func (t *TLSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M if err != nil { return nil, E.Cause(err, "dial TLS connection") } - return &tlsDNSConn{Conn: tlsConn}, nil + return &tlsDNSConn{ + Conn: tlsConn, + needDeadlineClose: deadline.NeedAdditionalReadDeadline(tlsConn.NetConn()), + }, nil }) if err != nil { return nil, err @@ -125,9 +129,7 @@ func (t *TLSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M } func (t *TLSTransport) exchange(ctx context.Context, message *mDNS.Msg, conn *tlsDNSConn) (*mDNS.Msg, error) { - if deadline, ok := ctx.Deadline(); ok { - conn.SetDeadline(deadline) - } + defer setConnDeadline(ctx, conn, conn.needDeadlineClose)() conn.queryId++ err := WriteMessage(conn, conn.queryId, message) if err != nil { @@ -137,6 +139,5 @@ func (t *TLSTransport) exchange(ctx context.Context, message *mDNS.Msg, conn *tl if err != nil { return nil, E.Cause(err, "read response") } - conn.SetDeadline(time.Time{}) return response, nil } diff --git a/dns/transport/udp.go b/dns/transport/udp.go index c9f520e310..7203b5ad4d 100644 --- a/dns/transport/udp.go +++ b/dns/transport/udp.go @@ -13,6 +13,7 @@ import ( "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio/deadline" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" @@ -130,6 +131,7 @@ func (t *UDPTransport) exchangeTCP(ctx context.Context, message *mDNS.Msg) (*mDN return nil, E.Cause(err, "dial TCP connection") } defer conn.Close() + defer setConnDeadline(ctx, conn, deadline.NeedAdditionalReadDeadline(conn))() err = WriteMessage(conn, message.Id, message) if err != nil { return nil, E.Cause(err, "write request") From 4807ee90072f62073bd2687279d91f7b40eafa85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 5 May 2026 10:48:12 +0800 Subject: [PATCH 77/93] Bump version --- docs/changelog.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index 47e4b0b08e..6dea7ec943 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,10 @@ icon: material/alert-decagram --- +#### 1.14.0-alpha.22 + +* Fixes and improvements + #### 1.14.0-alpha.21 * Allow customizing TUN DNS mode and hijack interface DNS by default **1** @@ -10,7 +14,7 @@ icon: material/alert-decagram * Add neighbor-based hostname resolution for the local DNS server **4** * Update NaiveProxy to 148.0.7778.96-1 * Add more TLS spoof methods and route rule action support **5** -** Fixes and improvements +* Fixes and improvements **1**: From 3e5991744d3d43d64fd07834f757cff054468a34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 5 May 2026 20:19:19 +0800 Subject: [PATCH 78/93] Fix reset network --- experimental/clashapi/connections.go | 8 ++++---- experimental/clashapi/server.go | 4 +++- experimental/libbox/command_server.go | 2 +- route/network.go | 13 +++++++++---- route/router.go | 1 - service/oomkiller/service.go | 6 +++--- service/oomkiller/timer.go | 10 +++++----- 7 files changed, 25 insertions(+), 19 deletions(-) diff --git a/experimental/clashapi/connections.go b/experimental/clashapi/connections.go index 14274b311b..e60e6622f5 100644 --- a/experimental/clashapi/connections.go +++ b/experimental/clashapi/connections.go @@ -18,10 +18,10 @@ import ( "github.com/gofrs/uuid/v5" ) -func connectionRouter(ctx context.Context, router adapter.Router, trafficManager *trafficontrol.Manager) http.Handler { +func connectionRouter(ctx context.Context, network adapter.NetworkManager, trafficManager *trafficontrol.Manager) http.Handler { r := chi.NewRouter() r.Get("/", getConnections(ctx, trafficManager)) - r.Delete("/", closeAllConnections(router, trafficManager)) + r.Delete("/", closeAllConnections(network, trafficManager)) r.Delete("/{id}", closeConnection(trafficManager)) return r } @@ -96,13 +96,13 @@ func closeConnection(trafficManager *trafficontrol.Manager) func(w http.Response } } -func closeAllConnections(router adapter.Router, trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) { +func closeAllConnections(network adapter.NetworkManager, trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { snapshot := trafficManager.Snapshot() for _, c := range snapshot.Connections { c.Close() } - router.ResetNetwork() + network.ResetNetwork() render.NoContent(w, r) } } diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index 20cea0bf9e..5278619649 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -43,6 +43,7 @@ var _ adapter.ClashServer = (*Server)(nil) type Server struct { ctx context.Context + network adapter.NetworkManager router adapter.Router dnsRouter adapter.DNSRouter outbound adapter.OutboundManager @@ -69,6 +70,7 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op chiRouter := chi.NewRouter() s := &Server{ ctx: ctx, + network: service.FromContext[adapter.NetworkManager](ctx), router: service.FromContext[adapter.Router](ctx), dnsRouter: service.FromContext[adapter.DNSRouter](ctx), outbound: service.FromContext[adapter.OutboundManager](ctx), @@ -124,7 +126,7 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op r.Mount("/configs", configRouter(s, logFactory)) r.Mount("/proxies", proxyRouter(s, s.router)) r.Mount("/rules", ruleRouter(s.router)) - r.Mount("/connections", connectionRouter(s.ctx, s.router, trafficManager)) + r.Mount("/connections", connectionRouter(s.ctx, s.network, trafficManager)) r.Mount("/providers/proxies", proxyProviderRouter()) r.Mount("/providers/rules", ruleProviderRouter()) r.Mount("/script", scriptRouter()) diff --git a/experimental/libbox/command_server.go b/experimental/libbox/command_server.go index 60ec17a8f0..dc07cb4abc 100644 --- a/experimental/libbox/command_server.go +++ b/experimental/libbox/command_server.go @@ -243,7 +243,7 @@ func (s *CommandServer) ResetNetwork() { if instance == nil || instance.Box() == nil { return } - instance.Box().Router().ResetNetwork() + instance.Box().Network().ResetNetwork() } func (s *CommandServer) UpdateWIFIState() { diff --git a/route/network.go b/route/network.go index 6598ead48d..41352435fd 100644 --- a/route/network.go +++ b/route/network.go @@ -34,10 +34,11 @@ import ( var _ adapter.NetworkManager = (*NetworkManager)(nil) type NetworkManager struct { - logger logger.ContextLogger - interfaceFinder *control.DefaultInterfaceFinder - networkInterfaces common.TypedValue[[]adapter.NetworkInterface] - + ctx context.Context + logger logger.ContextLogger + router adapter.Router + interfaceFinder *control.DefaultInterfaceFinder + networkInterfaces common.TypedValue[[]adapter.NetworkInterface] autoDetectInterface bool defaultOptions adapter.NetworkOptions autoRedirectOutputMark uint32 @@ -70,6 +71,7 @@ func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, options return nil, E.New("`default_mark` is only supported on linux") } nm := &NetworkManager{ + ctx: ctx, logger: logger, interfaceFinder: control.NewDefaultInterfaceFinder(), autoDetectInterface: options.AutoDetectInterface, @@ -138,6 +140,7 @@ func (r *NetworkManager) Start(stage adapter.StartStage) error { monitor := taskmonitor.New(r.logger, C.StartTimeout) switch stage { case adapter.StartStateInitialize: + r.router = service.FromContext[adapter.Router](r.ctx) if r.networkMonitor != nil { monitor.Start("initialize network monitor") err := r.networkMonitor.Start() @@ -478,6 +481,8 @@ func (r *NetworkManager) ResetNetwork() { listener.InterfaceUpdated() } } + + r.router.ResetNetwork() } func (r *NetworkManager) notifyInterfaceUpdate(defaultInterface *control.Interface, flags int) { diff --git a/route/router.go b/route/router.go index 2d50c22e4b..76a8f1bb09 100644 --- a/route/router.go +++ b/route/router.go @@ -280,7 +280,6 @@ func (r *Router) NeighborResolver() adapter.NeighborResolver { } func (r *Router) ResetNetwork() { - r.network.ResetNetwork() r.httpClientManager.ResetNetwork() r.dns.ResetNetwork() } diff --git a/service/oomkiller/service.go b/service/oomkiller/service.go index 7c19562e36..03fbdb1981 100644 --- a/service/oomkiller/service.go +++ b/service/oomkiller/service.go @@ -27,7 +27,7 @@ type Service struct { boxService.Adapter ctx context.Context logger log.ContextLogger - router adapter.Router + network adapter.NetworkManager timerConfig timerConfig adaptiveTimer *adaptiveTimer lastReportTime atomic.Int64 @@ -44,13 +44,13 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio Adapter: boxService.NewAdapter(boxConstant.TypeOOMKiller, tag), ctx: ctx, logger: logger, - router: service.FromContext[adapter.Router](ctx), + network: service.FromContext[adapter.NetworkManager](ctx), timerConfig: config, }, nil } func (s *Service) createTimer() { - s.adaptiveTimer = newAdaptiveTimer(s.logger, s.router, s.timerConfig, s.writeOOMReport) + s.adaptiveTimer = newAdaptiveTimer(s.logger, s.network, s.timerConfig, s.writeOOMReport) } func (s *Service) startTimer() { diff --git a/service/oomkiller/timer.go b/service/oomkiller/timer.go index f2070ce3f0..47fb685631 100644 --- a/service/oomkiller/timer.go +++ b/service/oomkiller/timer.go @@ -100,7 +100,7 @@ func buildTimerConfig(options option.OOMKillerServiceOptions, memoryLimit uint64 type adaptiveTimer struct { timerConfig logger log.ContextLogger - router adapter.Router + network adapter.NetworkManager onTriggered func(uint64) limitThresholds pressureThresholds @@ -115,11 +115,11 @@ type adaptiveTimer struct { pressureBaselineTime time.Time } -func newAdaptiveTimer(logger log.ContextLogger, router adapter.Router, config timerConfig, onTriggered func(uint64)) *adaptiveTimer { +func newAdaptiveTimer(logger log.ContextLogger, network adapter.NetworkManager, config timerConfig, onTriggered func(uint64)) *adaptiveTimer { t := &adaptiveTimer{ timerConfig: config, logger: logger, - router: router, + network: network, onTriggered: onTriggered, } if config.policyMode == policyModeMemoryLimit || config.policyMode == policyModeNetworkExtension { @@ -218,14 +218,14 @@ func (t *adaptiveTimer) poll() { t.logger.Warn("memory growth rate critical (report only), usage: ", byteformats.FormatMemoryBytes(sample.usage), t.logDetails(sample)) } else { t.logger.Error("memory growth rate critical, usage: ", byteformats.FormatMemoryBytes(sample.usage), t.logDetails(sample), ", resetting network") - t.router.ResetNetwork() + t.network.ResetNetwork() } } else { if t.killerDisabled { t.logger.Warn("memory threshold reached (report only), usage: ", byteformats.FormatMemoryBytes(sample.usage), t.logDetails(sample)) } else { t.logger.Error("memory threshold reached, usage: ", byteformats.FormatMemoryBytes(sample.usage), t.logDetails(sample), ", resetting network") - t.router.ResetNetwork() + t.network.ResetNetwork() } } badCleanup() From e0c137e232552cc568537029e77317e748b855c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 6 May 2026 12:32:51 +0800 Subject: [PATCH 79/93] Fix missing deadline for naive --- .github/CRONET_GO_VERSION | 2 +- go.mod | 62 +++++++++---------- go.sum | 124 +++++++++++++++++++------------------- 3 files changed, 94 insertions(+), 94 deletions(-) diff --git a/.github/CRONET_GO_VERSION b/.github/CRONET_GO_VERSION index 435750bd32..dc8aa112e5 100644 --- a/.github/CRONET_GO_VERSION +++ b/.github/CRONET_GO_VERSION @@ -1 +1 @@ -d96fab9b6594c94b1c08dd564cb7d72966ca3612 +92d97ac7e5afaca20df950d085839763e10ccaf6 diff --git a/go.mod b/go.mod index 7d29f75f8f..67e626531d 100644 --- a/go.mod +++ b/go.mod @@ -31,8 +31,8 @@ require ( github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1 github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a github.com/sagernet/cors v1.2.1 - github.com/sagernet/cronet-go v0.0.0-20260504082225-c80e757c7008 - github.com/sagernet/cronet-go/all v0.0.0-20260504082225-c80e757c7008 + github.com/sagernet/cronet-go v0.0.0-20260505140453-33a9cf45ce16 + github.com/sagernet/cronet-go/all v0.0.0-20260505140453-33a9cf45ce16 github.com/sagernet/fswatch v0.1.2 github.com/sagernet/gomobile v0.1.12 github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 @@ -109,35 +109,35 @@ require ( github.com/prometheus-community/pro-bing v0.4.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/safchain/ethtool v0.3.0 // indirect - github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260504081344-a7d8efd828f8 // indirect - github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260504081344-a7d8efd828f8 // indirect - github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260504081344-a7d8efd828f8 // indirect - github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260504081344-a7d8efd828f8 // indirect - github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260504081344-a7d8efd828f8 // indirect - github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260504081344-a7d8efd828f8 // indirect - github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260504081344-a7d8efd828f8 // indirect - github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260504081344-a7d8efd828f8 // indirect - github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260504081344-a7d8efd828f8 // indirect - github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260504081344-a7d8efd828f8 // indirect - github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260504081344-a7d8efd828f8 // indirect - github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260504081344-a7d8efd828f8 // indirect - github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260504081344-a7d8efd828f8 // indirect - github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260504081344-a7d8efd828f8 // indirect - github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260504081344-a7d8efd828f8 // indirect - github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260504081344-a7d8efd828f8 // indirect - github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260504081344-a7d8efd828f8 // indirect - github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260504081344-a7d8efd828f8 // indirect - github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260504081344-a7d8efd828f8 // indirect - github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260504081344-a7d8efd828f8 // indirect - github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260504081344-a7d8efd828f8 // indirect - github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260504081344-a7d8efd828f8 // indirect - github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260504081344-a7d8efd828f8 // indirect - github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260504081344-a7d8efd828f8 // indirect - github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260504081344-a7d8efd828f8 // indirect - github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260504081344-a7d8efd828f8 // indirect - github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260504081344-a7d8efd828f8 // indirect - github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260504081344-a7d8efd828f8 // indirect - github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260504081344-a7d8efd828f8 // indirect + github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260505135725-f39085455d92 // indirect + github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260505135725-f39085455d92 // indirect + github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260505135725-f39085455d92 // indirect + github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260505135725-f39085455d92 // indirect + github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260505135725-f39085455d92 // indirect + github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260505135725-f39085455d92 // indirect + github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260505135725-f39085455d92 // indirect + github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260505135725-f39085455d92 // indirect + github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260505135725-f39085455d92 // indirect + github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260505135725-f39085455d92 // indirect + github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260505135725-f39085455d92 // indirect + github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260505135725-f39085455d92 // indirect + github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260505135725-f39085455d92 // indirect + github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260505135725-f39085455d92 // indirect + github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260505135725-f39085455d92 // indirect + github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260505135725-f39085455d92 // indirect + github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260505135725-f39085455d92 // indirect + github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260505135725-f39085455d92 // indirect + github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260505135725-f39085455d92 // indirect + github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260505135725-f39085455d92 // indirect + github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260505135725-f39085455d92 // indirect + github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260505135725-f39085455d92 // indirect + github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260505135725-f39085455d92 // indirect + github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260505135725-f39085455d92 // indirect + github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260505135725-f39085455d92 // indirect + github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260505135725-f39085455d92 // indirect + github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260505135725-f39085455d92 // indirect + github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260505135725-f39085455d92 // indirect + github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260505135725-f39085455d92 // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect github.com/sagernet/nftables v0.3.0-mod.2 // indirect github.com/spf13/pflag v1.0.9 // indirect diff --git a/go.sum b/go.sum index 442363e333..cb457a74fc 100644 --- a/go.sum +++ b/go.sum @@ -168,68 +168,68 @@ github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkk github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ= github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI= -github.com/sagernet/cronet-go v0.0.0-20260504082225-c80e757c7008 h1:VJ073eJpvH4vsPuVswDn5UcBsLPLh7kXbvTYe2gVOfw= -github.com/sagernet/cronet-go v0.0.0-20260504082225-c80e757c7008/go.mod h1:T/mwtrpC4JlWfScw73CmSBvHzIvc7BatQ1MhRr+cYNw= -github.com/sagernet/cronet-go/all v0.0.0-20260504082225-c80e757c7008 h1:zN//kYChT5AL7a55MijB28nORo2GpMjozoU0oAVkMdE= -github.com/sagernet/cronet-go/all v0.0.0-20260504082225-c80e757c7008/go.mod h1:OJNky/hhb7cQv79x4HhpF+LyiOt7PAnnV5eZYxbnTwA= -github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260504081344-a7d8efd828f8 h1:rzk4DK7pSmgp4+p2UMPbqV6cbh66Ly3s+EhML2skX/E= -github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw= -github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260504081344-a7d8efd828f8 h1:E7UXAEykQ5cRpiQZeZ2OXOT4KkyqU6Z/xLmxiTd5dI4= -github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM= -github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260504081344-a7d8efd828f8 h1:GHdB3P+7r2pwHeA+93i71j19IUqld40LF26fB6UVulM= -github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc= -github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260504081344-a7d8efd828f8 h1:EkCd6jIAKsw9LDbwn41/EEz11wOCKlQXLBIx1zbR6Vk= -github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ= -github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260504081344-a7d8efd828f8 h1:8uUm7AaYk8quv5JmZ3rlfYjhcgZY3CFC9hZrzHAxkoQ= -github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs= -github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260504081344-a7d8efd828f8 h1:9lQg3peAjTNlc+QGrqlj/VukeVHVvMsgv4zSKQtNGJw= -github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0= -github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260504081344-a7d8efd828f8 h1:A09GwZpGBxQEhzx6LpR4Hfley5JMtc2P1ZYfl2Dl40U= -github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0= -github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260504081344-a7d8efd828f8 h1:/38BhdER+KEj/LEjgn9VbSiXlu6HNi9WRmiAeIodQcI= -github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4= -github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260504081344-a7d8efd828f8 h1:M9hVmIp4MiLrzLjBZ05DF25RxUJlcT6Fu5pl7jUdI28= -github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo= -github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260504081344-a7d8efd828f8 h1:bILzUWCCwq2RzpIh6JxmXN4A2lJtWMZJbxupdtphj6s= -github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ= -github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260504081344-a7d8efd828f8 h1:rUK4NnmSkRvHxolibnmA/CF3bxt1FdiQKjC/ZsRv8Qc= -github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU= -github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260504081344-a7d8efd828f8 h1:uGcq7LeAVlXt/MjLgzhD6eLjp0VuFnsOHd0Ho6o85NY= -github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI= -github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260504081344-a7d8efd828f8 h1:rg/O1t+N/QDeLs2PC8rHp74bvRwhH+nZT+JUIGNsLS0= -github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ= -github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260504081344-a7d8efd828f8 h1:Htq1OACFoiLU9OhAXJa23rIZx6wWv/rs9JRuhpkwPEY= -github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0= -github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260504081344-a7d8efd828f8 h1:FO82Tguucsh5nWlcise+zMS8MNSHL6RpPGPUfwp3oJY= -github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s= -github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260504081344-a7d8efd828f8 h1:F4eKuYPkL7kWUC76wfDhJ457NXRgMl9rLhJ3d9vXW2E= -github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ= -github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260504081344-a7d8efd828f8 h1:yTE5wcQRCAViNpszYpx+oUFUrOr4Kzjb0Mw4yPKXy+o= -github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow= -github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260504081344-a7d8efd828f8 h1:kwSGjoUzAL2RZqbbxAB+/5cwTvi1Dm54oz1D9ehWxEI= -github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk= -github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260504081344-a7d8efd828f8 h1:M/k9zDoP6b4KiIWoP2NsxV8pdDd0UP8kaoD3DiVcA6U= -github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E= -github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260504081344-a7d8efd828f8 h1:4xgOIUBNPmtmBffTIWzJazKcRHgik6tuuS3IrYnPqaE= -github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8= -github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260504081344-a7d8efd828f8 h1:3wqBjp6EtM9JZciOHwC0D78YFCOvBzk0chAE+mjS1RU= -github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w= -github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260504081344-a7d8efd828f8 h1:EeLBaVrvie+SFkaIVRkP6dMnKX5QHkqjgdFZAxX0g18= -github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0= -github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260504081344-a7d8efd828f8 h1:xKxrRhi5k/p2uOkrp/iA3EtSx05CcA8851fz5yHpm00= -github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs= -github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260504081344-a7d8efd828f8 h1:d+VZ4X5QaJvfTUQBsef3uWxBeEu+Lh9TZLY40jswKfk= -github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc= -github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260504081344-a7d8efd828f8 h1:ufyrbpyhxvjyA+Fe8KCfllEI0vyq0FZTqpP3IwnSskk= -github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4= -github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260504081344-a7d8efd828f8 h1:3J3O1J0zYploVeTrP6Ddz3mXm66woh7eyNcAbcc6LNY= -github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc= -github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260504081344-a7d8efd828f8 h1:tuREhEWa9KlwErDdrFFNVc5Sn9bMjfqdBnr2G40tO7g= -github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc= -github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260504081344-a7d8efd828f8 h1:UPjLdQ//cUBXdUwLtDSXfzLAFVYD/19Jt/YF+nZtPhs= -github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= -github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260504081344-a7d8efd828f8 h1:7cmmYMddOUces3noUNOlAjAROOSZz5BQdM7gTp1Sbso= -github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260504081344-a7d8efd828f8/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw= +github.com/sagernet/cronet-go v0.0.0-20260505140453-33a9cf45ce16 h1:eq9/CwqNUIwK+9NjTZxVNyqnK0S9jSSNAC8+qZEmKAg= +github.com/sagernet/cronet-go v0.0.0-20260505140453-33a9cf45ce16/go.mod h1:T/mwtrpC4JlWfScw73CmSBvHzIvc7BatQ1MhRr+cYNw= +github.com/sagernet/cronet-go/all v0.0.0-20260505140453-33a9cf45ce16 h1:qZJownM+DPd/0b9hfIOWpwgQDiDGaUCBfnMqJ0HBwhE= +github.com/sagernet/cronet-go/all v0.0.0-20260505140453-33a9cf45ce16/go.mod h1:WowirEFpH2FsoRqJuJEypNd8Rfo/Tx1pe2fi0xI3BTs= +github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260505135725-f39085455d92 h1:/6JbsIpOeCA4Nc5wj+MCxq3zLp0UAt+D9l2eoGuyMBU= +github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260505135725-f39085455d92/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw= +github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260505135725-f39085455d92 h1:m1xmxBeOvWiFE79t765TfQ9r98RjDieCtpXRJ1Kd7hY= +github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260505135725-f39085455d92/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM= +github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260505135725-f39085455d92 h1:n7edaokpJS5NwAwku9f5dwo+PsokobHtArBMRNH4v9I= +github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260505135725-f39085455d92/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc= +github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260505135725-f39085455d92 h1:KF4AjWLN101YTFU5hZeiz+2qSOl5r1ORbqTEThbCJ3g= +github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260505135725-f39085455d92/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ= +github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260505135725-f39085455d92 h1:Eoc4tM+hMA7rRZYhw/CQTTC+vKdhGEr23r4BScS2ARs= +github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260505135725-f39085455d92/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs= +github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260505135725-f39085455d92 h1:yLELJ2IsddWqIhttuvAInIk9FPgsg9EJBZwlgf9pbB0= +github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260505135725-f39085455d92/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0= +github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260505135725-f39085455d92 h1:uNn7Fxl5HLtAElXzuZmg8APaDCHnM54VzMPqbke5x+8= +github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260505135725-f39085455d92/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0= +github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260505135725-f39085455d92 h1:H3oCO8x27JPAsdpgJn39WOVc44gHDVJ6Us1hhY9rr2Y= +github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260505135725-f39085455d92/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4= +github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260505135725-f39085455d92 h1:orfOCA/o1ranJcZ8KPAWCI+IIEgQG45GSK6oztG1Fdc= +github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260505135725-f39085455d92/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo= +github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260505135725-f39085455d92 h1:s9QrD5N0VgHIrphiH9VUvIAoO1AFpvdom6Ulpj/injM= +github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260505135725-f39085455d92/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ= +github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260505135725-f39085455d92 h1:87uZhvYpHuDsWOd9RUQ+E0E2op+pkWn2WuniYHzHfyA= +github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260505135725-f39085455d92/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU= +github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260505135725-f39085455d92 h1:BmZdqfos9MJNZoa3vuSMI+ZhvAKQX6zuTn8k1cMGGjQ= +github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260505135725-f39085455d92/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI= +github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260505135725-f39085455d92 h1:q4AxdtpMXS+v2xsGqR7cLL4YYqZDFGT3kjV/8z6G9xc= +github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260505135725-f39085455d92/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ= +github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260505135725-f39085455d92 h1:QPnGHp7XOYMs5GoSlrePaipTFufPcjKGnUicTBLdT3s= +github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260505135725-f39085455d92/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0= +github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260505135725-f39085455d92 h1:5CpZFrfXMdWvDTAmuTRoSyY6MSHHvuGvt+ZMUztxts8= +github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260505135725-f39085455d92/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s= +github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260505135725-f39085455d92 h1:NpigJfFJxtNQxaArUvHfcmxexV/rxF7ExCZf4K5XSJg= +github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260505135725-f39085455d92/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ= +github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260505135725-f39085455d92 h1:pbhDbfxKjTpDSQyOW7cTHC0JhKGRgkgSH3FPLYKvl8A= +github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260505135725-f39085455d92/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow= +github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260505135725-f39085455d92 h1:Chv2n6Dvqt9owso1NRVM8muhxq1nOQM1jHh0Ip8Ak3g= +github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260505135725-f39085455d92/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk= +github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260505135725-f39085455d92 h1:tNJLTPx61AZ8V3ilCAwkkIlo8rWQGaaawYsrhnyOt+8= +github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260505135725-f39085455d92/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E= +github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260505135725-f39085455d92 h1:2Ggy52AQqWhV+zNjhrUNgn3OFxLffdDkjYretEP5PuM= +github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260505135725-f39085455d92/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8= +github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260505135725-f39085455d92 h1:m16yOJQkkyo5LDnTi8JlMgNzCzinEVuGOXcWrHXFbNw= +github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260505135725-f39085455d92/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w= +github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260505135725-f39085455d92 h1:GUM5+vkxf7jJhLneLGZjMBNGX7z12kDhn65jYreYRTo= +github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260505135725-f39085455d92/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0= +github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260505135725-f39085455d92 h1:6dvaguIY7qF0Mf/GZ3D0KAjz2GA0fRqmSjfzZ9FEWiU= +github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260505135725-f39085455d92/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs= +github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260505135725-f39085455d92 h1:UWURfgvjks/PGG1yO1q48A58kMzpgdQ1e5/w73FtqNQ= +github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260505135725-f39085455d92/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc= +github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260505135725-f39085455d92 h1:fpueIFNflXSXm2PjTOmLR4dhdNUc3CHq0HkyTYdGJ1w= +github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260505135725-f39085455d92/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4= +github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260505135725-f39085455d92 h1:qCFCL4s2TCoScRi2xAkBnp5Jh2n67j0yGIDNy/7KJRI= +github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260505135725-f39085455d92/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc= +github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260505135725-f39085455d92 h1:ho/o2GydFvAjuxM76a2I/1f2kukK/6usa6fGus93ROk= +github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260505135725-f39085455d92/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc= +github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260505135725-f39085455d92 h1:sFbVqze7871HseJVZtAVkRd/6rnk7aF1bO97cX/sV7w= +github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260505135725-f39085455d92/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= +github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260505135725-f39085455d92 h1:2V3z9vsVf/jBm+acgCEDRONpf/zfrGSGXLnD69ub16Y= +github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260505135725-f39085455d92/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw= github.com/sagernet/fswatch v0.1.2 h1:/TT7k4mkce1qFPxamLO842WjqBgbTBiXP2mlUjp9PFk= github.com/sagernet/fswatch v0.1.2/go.mod h1:5BpGmpUQVd3Mc5r313HRpvADHRg3/rKn5QbwFteB880= github.com/sagernet/gomobile v0.1.12 h1:XwzjZaclFF96deLqwAgK8gU3w0M2A8qxgDmhV+A0wjg= From 18f10561c93e19ac37241f9525526d44bbb2f2f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 8 May 2026 12:35:14 +0800 Subject: [PATCH 80/93] Skip kickWriteHandshake for server first protocols --- route/conn.go | 58 +++++++++++++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/route/conn.go b/route/conn.go index 2ac8e9eb5f..c2ab90d54d 100644 --- a/route/conn.go +++ b/route/conn.go @@ -13,6 +13,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/sniff" "github.com/sagernet/sing-box/common/tlsfragment" "github.com/sagernet/sing-box/common/tlsspoof" C "github.com/sagernet/sing-box/constant" @@ -140,11 +141,12 @@ func (m *ConnectionManager) NewConnection(ctx context.Context, this N.Dialer, co } remoteConn = spoofConn } + serverFirst := sniff.Skip(&metadata) var done atomic.Bool - if m.kickWriteHandshake(ctx, conn, remoteConn, false, &done, onClose) { + if m.kickWriteHandshake(ctx, conn, remoteConn, serverFirst, false, &done, onClose) { return } - if m.kickWriteHandshake(ctx, remoteConn, conn, true, &done, onClose) { + if m.kickWriteHandshake(ctx, remoteConn, conn, serverFirst, true, &done, onClose) { return } go m.connectionCopy(ctx, conn, remoteConn, false, &done, onClose) @@ -305,37 +307,43 @@ func (m *ConnectionManager) connectionCopy(ctx context.Context, source net.Conn, } } -func (m *ConnectionManager) kickWriteHandshake(ctx context.Context, source net.Conn, destination net.Conn, direction bool, done *atomic.Bool, onClose N.CloseHandlerFunc) bool { +func (m *ConnectionManager) kickWriteHandshake(ctx context.Context, source net.Conn, destination net.Conn, serverFirst bool, direction bool, done *atomic.Bool, onClose N.CloseHandlerFunc) bool { if !N.NeedHandshakeForWrite(destination) { return false } var ( - cachedBuffer *buf.Buffer + err error wrotePayload bool ) - sourceReader, readCounters := N.UnwrapCountReader(source, nil) - destinationWriter, writeCounters := N.UnwrapCountWriter(destination, nil) - if cachedReader, ok := sourceReader.(N.CachedReader); ok { - cachedBuffer = cachedReader.ReadCached() - } - var err error - if cachedBuffer != nil { - wrotePayload = true - dataLen := cachedBuffer.Len() - _, err = destinationWriter.Write(cachedBuffer.Bytes()) - cachedBuffer.Release() - if err == nil { - for _, counter := range readCounters { - counter(int64(dataLen)) - } - for _, counter := range writeCounters { - counter(int64(dataLen)) - } - } - } else { + if serverFirst { _ = destination.SetWriteDeadline(time.Now().Add(C.ReadPayloadTimeout)) - _, err = destinationWriter.Write(nil) + _, err = destination.Write(nil) _ = destination.SetWriteDeadline(time.Time{}) + } else { + var cachedBuffer *buf.Buffer + sourceReader, readCounters := N.UnwrapCountReader(source, nil) + destinationWriter, writeCounters := N.UnwrapCountWriter(destination, nil) + if cachedReader, ok := sourceReader.(N.CachedReader); ok { + cachedBuffer = cachedReader.ReadCached() + } + if cachedBuffer != nil { + wrotePayload = true + dataLen := cachedBuffer.Len() + _, err = destinationWriter.Write(cachedBuffer.Bytes()) + cachedBuffer.Release() + if err == nil { + for _, counter := range readCounters { + counter(int64(dataLen)) + } + for _, counter := range writeCounters { + counter(int64(dataLen)) + } + } + } else { + _ = destination.SetWriteDeadline(time.Now().Add(C.ReadPayloadTimeout)) + _, err = destinationWriter.Write(nil) + _ = destination.SetWriteDeadline(time.Time{}) + } } if err == nil { return false From 34c90e6f2ea7dae1609630ea798b742ea450d34d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 10 May 2026 13:31:53 +0800 Subject: [PATCH 81/93] Add hysteria2 realm service and support --- constant/proxy.go | 1 + docs/configuration/inbound/hysteria2.md | 56 +- docs/configuration/inbound/hysteria2.zh.md | 56 +- docs/configuration/outbound/hysteria2.md | 60 +- docs/configuration/outbound/hysteria2.zh.md | 60 +- docs/configuration/service/hysteria-realm.md | 66 +++ .../service/hysteria-realm.zh.md | 66 +++ docs/configuration/service/index.md | 15 +- docs/configuration/service/index.zh.md | 15 +- go.mod | 2 +- go.sum | 4 +- include/quic.go | 5 + include/quic_stub.go | 7 + include/registry.go | 1 + mkdocs.yml | 1 + option/hysteria2.go | 26 +- protocol/hysteria2/inbound.go | 15 +- protocol/hysteria2/outbound.go | 35 +- protocol/hysteria2/realm.go | 173 ++++++ protocol/hysteria2/realm_server.go | 536 ++++++++++++++++++ 20 files changed, 1167 insertions(+), 33 deletions(-) create mode 100644 docs/configuration/service/hysteria-realm.md create mode 100644 docs/configuration/service/hysteria-realm.zh.md create mode 100644 protocol/hysteria2/realm.go create mode 100644 protocol/hysteria2/realm_server.go diff --git a/constant/proxy.go b/constant/proxy.go index ffec80250b..868a3bb85d 100644 --- a/constant/proxy.go +++ b/constant/proxy.go @@ -32,6 +32,7 @@ const ( TypeCCM = "ccm" TypeOCM = "ocm" TypeOOMKiller = "oom-killer" + TypeHysteriaRealm = "hysteria-realm" TypeACME = "acme" TypeCloudflareOriginCA = "cloudflare-origin-ca" ) diff --git a/docs/configuration/inbound/hysteria2.md b/docs/configuration/inbound/hysteria2.md index 62fbb209ef..4d24940226 100644 --- a/docs/configuration/inbound/hysteria2.md +++ b/docs/configuration/inbound/hysteria2.md @@ -4,7 +4,8 @@ icon: material/alert-decagram !!! quote "Changes in sing-box 1.14.0" - :material-plus: [bbr_profile](#bbr_profile) + :material-plus: [bbr_profile](#bbr_profile) + :material-plus: [realm](#realm) !!! quote "Changes in sing-box 1.11.0" @@ -39,7 +40,14 @@ icon: material/alert-decagram "masquerade": "", // or {} "bbr_profile": "", - "brutal_debug": false + "brutal_debug": false, + "realm": { + "server_url": "https://realm.example.com", + "token": "", + "realm_id": "", + "stun_servers": [], + "http_client": {} + } } ``` @@ -164,3 +172,47 @@ BBR congestion control algorithm profile, one of `conservative` `standard` `aggr #### brutal_debug Enable debug information logging for Hysteria Brutal CC. + +#### realm + +!!! question "Since sing-box 1.14.0" + +Register this inbound to a Hysteria Realm rendezvous service to enable NAT traversal. + +The inbound discovers its public addresses via STUN, registers them on the realm, and uses UDP hole-punching to accept incoming clients without a publicly reachable listen address. + +See [Hysteria Realm](/configuration/service/hysteria-realm/) for the rendezvous service. + +#### realm.server_url + +==Required== + +Realm rendezvous service URL. + +#### realm.token + +Bearer token for the realm. Must match one of `users[].token` configured on the realm. + +#### realm.realm_id + +==Required== + +Slot identifier on the realm. + +1–64 characters, must match `^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$`. + +Outbounds must use the same `realm_id` to find this server. + +#### realm.stun_servers + +==Required== + +List of STUN servers (`host` or `host:port`) used to discover public addresses. + +Port defaults to `3478`. + +#### realm.http_client + +HTTP client used to talk to the realm. + +See [HTTP Client](/configuration/shared/http-client/) for details. diff --git a/docs/configuration/inbound/hysteria2.zh.md b/docs/configuration/inbound/hysteria2.zh.md index 0c5fdb014f..cfe7f9f0d5 100644 --- a/docs/configuration/inbound/hysteria2.zh.md +++ b/docs/configuration/inbound/hysteria2.zh.md @@ -4,7 +4,8 @@ icon: material/alert-decagram !!! quote "sing-box 1.14.0 中的更改" - :material-plus: [bbr_profile](#bbr_profile) + :material-plus: [bbr_profile](#bbr_profile) + :material-plus: [realm](#realm) !!! quote "sing-box 1.11.0 中的更改" @@ -39,7 +40,14 @@ icon: material/alert-decagram "masquerade": "", // 或 {} "bbr_profile": "", - "brutal_debug": false + "brutal_debug": false, + "realm": { + "server_url": "https://realm.example.com", + "token": "", + "realm_id": "", + "stun_servers": [], + "http_client": {} + } } ``` @@ -161,3 +169,47 @@ BBR 拥塞控制算法配置,可选 `conservative` `standard` `aggressive`。 #### brutal_debug 启用 Hysteria Brutal CC 的调试信息日志记录。 + +#### realm + +!!! question "自 sing-box 1.14.0 起" + +将此入站注册到 Hysteria Realm 会合服务,以启用 NAT 穿透。 + +入站通过 STUN 发现自己的公网地址并注册到 realm,借助 UDP 打洞接受客户端连接,无需可公网直达的监听地址。 + +会合服务参阅 [Hysteria Realm](/zh/configuration/service/hysteria-realm/)。 + +#### realm.server_url + +==必填== + +Realm 会合服务 URL。 + +#### realm.token + +Realm 的 Bearer 令牌,需与 realm 上配置的 `users[].token` 之一匹配。 + +#### realm.realm_id + +==必填== + +Realm 上的槽位标识符。 + +1–64 字符,需匹配 `^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$`。 + +出站需使用相同的 `realm_id` 才能找到本服务器。 + +#### realm.stun_servers + +==必填== + +用于发现公网地址的 STUN 服务器列表(`host` 或 `host:port`)。 + +端口默认为 `3478`。 + +#### realm.http_client + +与 realm 通信使用的 HTTP 客户端。 + +参阅 [HTTP 客户端](/zh/configuration/shared/http-client/) 了解详情。 diff --git a/docs/configuration/outbound/hysteria2.md b/docs/configuration/outbound/hysteria2.md index 2d5a9bcb1b..4e162b5545 100644 --- a/docs/configuration/outbound/hysteria2.md +++ b/docs/configuration/outbound/hysteria2.md @@ -1,7 +1,8 @@ !!! quote "Changes in sing-box 1.14.0" :material-plus: [hop_interval_max](#hop_interval_max) - :material-plus: [bbr_profile](#bbr_profile) + :material-plus: [bbr_profile](#bbr_profile) + :material-plus: [realm](#realm) !!! quote "Changes in sing-box 1.11.0" @@ -36,6 +37,13 @@ "bbr_profile": "", "brutal_debug": false, + "realm": { + "server_url": "https://realm.example.com", + "token": "", + "realm_id": "", + "stun_servers": [], + "http_client": {} + }, ... // Dial Fields } @@ -61,6 +69,8 @@ The server address. +Conflicts with `realm`. + #### server_port ==Required== @@ -69,13 +79,15 @@ The server port. Ignored if `server_ports` is set. +Conflicts with `realm`. + #### server_ports !!! question "Since sing-box 1.11.0" Server port range list. -Conflicts with `server_port`. +Conflicts with `server_port` and `realm`. #### hop_interval @@ -143,6 +155,50 @@ BBR congestion control algorithm profile, one of `conservative` `standard` `aggr Enable debug information logging for Hysteria Brutal CC. +#### realm + +!!! question "Since sing-box 1.14.0" + +Connect to a Hysteria2 server through a Hysteria Realm rendezvous service. + +The outbound queries the realm for the server's current public addresses, performs UDP hole-punching, and proceeds with the normal QUIC handshake. + +Conflicts with `server`, `server_port` and `server_ports`. + +The TLS SNI defaults to the host portion of `server_url`. Set `tls.server_name` to match the certificate the Hysteria2 server presents. + +See [Hysteria Realm](/configuration/service/hysteria-realm/) for the rendezvous service. + +#### realm.server_url + +==Required== + +Realm rendezvous service URL. + +#### realm.token + +Bearer token for the realm. Must match one of `users[].token` configured on the realm. + +#### realm.realm_id + +==Required== + +The same slot identifier the target Hysteria2 server registered. + +#### realm.stun_servers + +==Required== + +List of STUN servers (`host` or `host:port`) used to discover this client's public addresses. + +Port defaults to `3478`. + +#### realm.http_client + +HTTP client used to talk to the realm. + +See [HTTP Client](/configuration/shared/http-client/) for details. + ### Dial Fields See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/outbound/hysteria2.zh.md b/docs/configuration/outbound/hysteria2.zh.md index aa0e6e11f9..aff60a8a13 100644 --- a/docs/configuration/outbound/hysteria2.zh.md +++ b/docs/configuration/outbound/hysteria2.zh.md @@ -1,7 +1,8 @@ !!! quote "sing-box 1.14.0 中的更改" :material-plus: [hop_interval_max](#hop_interval_max) - :material-plus: [bbr_profile](#bbr_profile) + :material-plus: [bbr_profile](#bbr_profile) + :material-plus: [realm](#realm) !!! quote "sing-box 1.11.0 中的更改" @@ -36,6 +37,13 @@ "bbr_profile": "", "brutal_debug": false, + "realm": { + "server_url": "https://realm.example.com", + "token": "", + "realm_id": "", + "stun_servers": [], + "http_client": {} + }, ... // 拨号字段 } @@ -59,6 +67,8 @@ 服务器地址。 +与 `realm` 冲突。 + #### server_port ==必填== @@ -67,13 +77,15 @@ 如果设置了 `server_ports`,则忽略此项。 +与 `realm` 冲突。 + #### server_ports !!! question "自 sing-box 1.11.0 起" 服务器端口范围列表。 -与 `server_port` 冲突。 +与 `server_port` 和 `realm` 冲突。 #### hop_interval @@ -141,6 +153,50 @@ BBR 拥塞控制算法配置,可选 `conservative` `standard` `aggressive`。 启用 Hysteria Brutal CC 的调试信息日志记录。 +#### realm + +!!! question "自 sing-box 1.14.0 起" + +通过 Hysteria Realm 会合服务连接 Hysteria2 服务器。 + +出站从 realm 查询服务器当前的公网地址,执行 UDP 打洞,然后进行常规的 QUIC 握手。 + +与 `server`、`server_port` 和 `server_ports` 冲突。 + +TLS SNI 默认使用 `server_url` 中的主机名。需设置 `tls.server_name` 以匹配 Hysteria2 服务器证书覆盖的名字。 + +会合服务参阅 [Hysteria Realm](/zh/configuration/service/hysteria-realm/)。 + +#### realm.server_url + +==必填== + +Realm 会合服务 URL。 + +#### realm.token + +Realm 的 Bearer 令牌,需与 realm 上配置的 `users[].token` 之一匹配。 + +#### realm.realm_id + +==必填== + +目标 Hysteria2 服务器注册时使用的相同槽位标识符。 + +#### realm.stun_servers + +==必填== + +用于发现本客户端公网地址的 STUN 服务器列表(`host` 或 `host:port`)。 + +端口默认为 `3478`。 + +#### realm.http_client + +与 realm 通信使用的 HTTP 客户端。 + +参阅 [HTTP 客户端](/zh/configuration/shared/http-client/) 了解详情。 + ### 拨号字段 参阅 [拨号字段](/zh/configuration/shared/dial/)。 diff --git a/docs/configuration/service/hysteria-realm.md b/docs/configuration/service/hysteria-realm.md new file mode 100644 index 0000000000..a69d130693 --- /dev/null +++ b/docs/configuration/service/hysteria-realm.md @@ -0,0 +1,66 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.14.0" + +# Hysteria Realm + +Hysteria Realm is a rendezvous service for Hysteria2 NAT traversal. + +A Hysteria2 server behind NAT registers its STUN-discovered public addresses to a stable realm endpoint; clients query the realm to learn the server's current addresses and perform UDP hole-punching to establish a direct QUIC connection. + +The realm only carries control-plane signaling. Once hole-punching succeeds, all proxy traffic flows directly between client and server. + +### Structure + +```json +{ + "type": "hysteria-realm", + + ... // Listen Fields + + "tls": {}, + "users": [ + { + "name": "", + "token": "", + "max_realms": 0 + } + ] +} +``` + +### Listen Fields + +See [Listen Fields](/configuration/shared/listen/) for details. + +### Fields + +#### tls + +TLS configuration, see [TLS](/configuration/shared/tls/#inbound). + +When configured, the realm serves HTTP/2 over TLS; otherwise plain HTTP/1.1. + +#### users + +==Required== + +Authorized users. + +#### users.name + +==Required== + +Username, used in logs and as the quota key. + +#### users.token + +==Required== + +Bearer token presented by Hysteria2 inbounds and outbounds via `Authorization: Bearer `. + +#### users.max_realms + +Maximum number of realm slots this user may hold concurrently. diff --git a/docs/configuration/service/hysteria-realm.zh.md b/docs/configuration/service/hysteria-realm.zh.md new file mode 100644 index 0000000000..a73ddbdbf5 --- /dev/null +++ b/docs/configuration/service/hysteria-realm.zh.md @@ -0,0 +1,66 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.14.0 起" + +# Hysteria Realm + +Hysteria Realm 是用于 Hysteria2 NAT 穿透的会合服务。 + +位于 NAT 后面的 Hysteria2 服务器将其通过 STUN 发现的公网地址注册到一个稳定的 realm 端点;客户端从 realm 查询服务器当前的地址并执行 UDP 打洞,以建立直连的 QUIC 连接。 + +Realm 只承载控制信令。打洞成功后,所有代理流量在客户端和服务器之间直连传输。 + +### 结构 + +```json +{ + "type": "hysteria-realm", + + ... // 监听字段 + + "tls": {}, + "users": [ + { + "name": "", + "token": "", + "max_realms": 0 + } + ] +} +``` + +### 监听字段 + +参阅 [监听字段](/zh/configuration/shared/listen/) 了解详情。 + +### 字段 + +#### tls + +TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。 + +配置后,realm 将通过 TLS 提供 HTTP/2 服务;否则提供明文 HTTP/1.1。 + +#### users + +==必填== + +授权用户。 + +#### users.name + +==必填== + +用户名,用于日志记录和配额键。 + +#### users.token + +==必填== + +Hysteria2 入站和出站通过 `Authorization: Bearer ` 出示的 Bearer 令牌。 + +#### users.max_realms + +此用户可同时持有的 realm 槽位数量上限。 diff --git a/docs/configuration/service/index.md b/docs/configuration/service/index.md index de3583b2ba..dea66d9712 100644 --- a/docs/configuration/service/index.md +++ b/docs/configuration/service/index.md @@ -21,13 +21,14 @@ icon: material/new-box ### Fields -| Type | Format | -|------------|------------------------| -| `ccm` | [CCM](./ccm) | -| `derp` | [DERP](./derp) | -| `ocm` | [OCM](./ocm) | -| `resolved` | [Resolved](./resolved) | -| `ssm-api` | [SSM API](./ssm-api) | +| Type | Format | +|-------------------|---------------------------------------| +| `ccm` | [CCM](./ccm) | +| `derp` | [DERP](./derp) | +| `hysteria-realm` | [Hysteria Realm](./hysteria-realm) | +| `ocm` | [OCM](./ocm) | +| `resolved` | [Resolved](./resolved) | +| `ssm-api` | [SSM API](./ssm-api) | #### tag diff --git a/docs/configuration/service/index.zh.md b/docs/configuration/service/index.zh.md index a0d18cbba7..240dce2cd1 100644 --- a/docs/configuration/service/index.zh.md +++ b/docs/configuration/service/index.zh.md @@ -21,13 +21,14 @@ icon: material/new-box ### 字段 -| 类型 | 格式 | -|-----------|------------------------| -| `ccm` | [CCM](./ccm) | -| `derp` | [DERP](./derp) | -| `ocm` | [OCM](./ocm) | -| `resolved`| [Resolved](./resolved) | -| `ssm-api` | [SSM API](./ssm-api) | +| 类型 | 格式 | +|-------------------|---------------------------------------| +| `ccm` | [CCM](./ccm) | +| `derp` | [DERP](./derp) | +| `hysteria-realm` | [Hysteria Realm](./hysteria-realm) | +| `ocm` | [OCM](./ocm) | +| `resolved` | [Resolved](./resolved) | +| `ssm-api` | [SSM API](./ssm-api) | #### tag diff --git a/go.mod b/go.mod index 67e626531d..bac338d098 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( github.com/sagernet/sing v0.8.10-0.20260428084616-2bc976d03e39 github.com/sagernet/sing-cloudflared v0.1.0 github.com/sagernet/sing-mux v0.3.4 - github.com/sagernet/sing-quic v0.6.2-0.20260412143638-8f65b6be7cd6 + github.com/sagernet/sing-quic v0.6.2-0.20260510042401-4055a2bb2381 github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 diff --git a/go.sum b/go.sum index cb457a74fc..be6ebec39b 100644 --- a/go.sum +++ b/go.sum @@ -248,8 +248,8 @@ github.com/sagernet/sing-cloudflared v0.1.0 h1:to+2fcCx8zu4X/DirRw9Ihc+FrEZ7oEyI github.com/sagernet/sing-cloudflared v0.1.0/go.mod h1:bH2NKX+NpDTY1Zkxfboxw6MXB/ZywaNLmrDJYgKMJ2Y= github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s= github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= -github.com/sagernet/sing-quic v0.6.2-0.20260412143638-8f65b6be7cd6 h1:j3ISQRDyY5rs27NzUS/le+DHR0iOO0K0x+mWDLzu4Ok= -github.com/sagernet/sing-quic v0.6.2-0.20260412143638-8f65b6be7cd6/go.mod h1:r5Adw0EMUyhGBCjPI2JEupDtC040DrrvreXtua7Ifdc= +github.com/sagernet/sing-quic v0.6.2-0.20260510042401-4055a2bb2381 h1:bwja5I7Sgr4Z1nyCLlN6T5eBhhSE/ilYZmkEFIoPAhQ= +github.com/sagernet/sing-quic v0.6.2-0.20260510042401-4055a2bb2381/go.mod h1:+oqD54aHel4ALKkp1hVXWCgLU/EjLojvm6AUzDfvj0I= github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE= github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI= github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo= diff --git a/include/quic.go b/include/quic.go index 6a3f301755..42182c299d 100644 --- a/include/quic.go +++ b/include/quic.go @@ -5,6 +5,7 @@ package include import ( "github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/adapter/outbound" + "github.com/sagernet/sing-box/adapter/service" "github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/dns/transport/quic" "github.com/sagernet/sing-box/protocol/hysteria" @@ -30,3 +31,7 @@ func registerQUICTransports(registry *dns.TransportRegistry) { quic.RegisterTransport(registry) quic.RegisterHTTP3Transport(registry) } + +func registerQUICServices(registry *service.Registry) { + hysteria2.RegisterRealmService(registry) +} diff --git a/include/quic_stub.go b/include/quic_stub.go index d2c03b98e4..b603cf48d0 100644 --- a/include/quic_stub.go +++ b/include/quic_stub.go @@ -10,6 +10,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/adapter/outbound" + "github.com/sagernet/sing-box/adapter/service" "github.com/sagernet/sing-box/common/listener" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" @@ -69,3 +70,9 @@ func registerQUICTransports(registry *dns.TransportRegistry) { return nil, C.ErrQUICNotIncluded }) } + +func registerQUICServices(registry *service.Registry) { + service.Register[option.HysteriaRealmServiceOptions](registry, C.TypeHysteriaRealm, func(ctx context.Context, logger log.ContextLogger, tag string, options option.HysteriaRealmServiceOptions) (adapter.Service, error) { + return nil, C.ErrQUICNotIncluded + }) +} diff --git a/include/registry.go b/include/registry.go index 91d66bbd5b..2d478bfe8d 100644 --- a/include/registry.go +++ b/include/registry.go @@ -136,6 +136,7 @@ func ServiceRegistry() *service.Registry { resolved.RegisterService(registry) ssmapi.RegisterService(registry) + registerQUICServices(registry) registerDERPService(registry) registerCCMService(registry) registerOCMService(registry) diff --git a/mkdocs.yml b/mkdocs.yml index 4280979e0e..a14ada33e4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -192,6 +192,7 @@ nav: - SSM API: configuration/service/ssm-api.md - CCM: configuration/service/ccm.md - OCM: configuration/service/ocm.md + - Hysteria Realm: configuration/service/hysteria-realm.md markdown_extensions: - toc: slugify: !!python/object/apply:pymdownx.slugs.slugify diff --git a/option/hysteria2.go b/option/hysteria2.go index e1a54e4b8a..eee8105438 100644 --- a/option/hysteria2.go +++ b/option/hysteria2.go @@ -22,6 +22,15 @@ type Hysteria2InboundOptions struct { Masquerade *Hysteria2Masquerade `json:"masquerade,omitempty"` BBRProfile string `json:"bbr_profile,omitempty"` BrutalDebug bool `json:"brutal_debug,omitempty"` + Realm *Hysteria2Realm `json:"realm,omitempty"` +} + +type Hysteria2Realm struct { + ServerURL string `json:"server_url"` + Token string `json:"token,omitempty"` + RealmID string `json:"realm_id"` + STUNServers badoption.Listable[string] `json:"stun_servers"` + HTTPClient *HTTPClientOptions `json:"http_client,omitempty"` } type Hysteria2Obfs struct { @@ -124,6 +133,19 @@ type Hysteria2OutboundOptions struct { Network NetworkList `json:"network,omitempty"` OutboundTLSOptionsContainer QUICOptions - BBRProfile string `json:"bbr_profile,omitempty"` - BrutalDebug bool `json:"brutal_debug,omitempty"` + BBRProfile string `json:"bbr_profile,omitempty"` + BrutalDebug bool `json:"brutal_debug,omitempty"` + Realm *Hysteria2Realm `json:"realm,omitempty"` +} + +type HysteriaRealmUser struct { + Name string `json:"name"` + Token string `json:"token"` + MaxRealms int `json:"max_realms,omitempty"` +} + +type HysteriaRealmServiceOptions struct { + ListenOptions + InboundTLSOptionsContainer + Users []HysteriaRealmUser `json:"users"` } diff --git a/protocol/hysteria2/inbound.go b/protocol/hysteria2/inbound.go index a94c26dd79..a6e800c5a0 100644 --- a/protocol/hysteria2/inbound.go +++ b/protocol/hysteria2/inbound.go @@ -114,7 +114,11 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo } else { udpTimeout = C.UDPTimeout } - service, err := hysteria2.NewService[int](hysteria2.ServiceOptions{ + realmOptions, err := buildRealmOptions(ctx, logger, options.Realm) + if err != nil { + return nil, err + } + hysteriaService, err := hysteria2.NewService[int](hysteria2.ServiceOptions{ Context: ctx, Logger: logger, BrutalDebug: options.BrutalDebug, @@ -136,6 +140,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo Handler: inbound, MasqueradeHandler: masqueradeHandler, BBRProfile: options.BBRProfile, + RealmOptions: realmOptions, }) if err != nil { return nil, err @@ -148,8 +153,8 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo userNameList = append(userNameList, user.Name) userPasswordList = append(userPasswordList, user.Password) } - service.UpdateUsers(userList, userPasswordList) - inbound.service = service + hysteriaService.UpdateUsers(userList, userPasswordList) + inbound.service = hysteriaService inbound.userNameList = userNameList return inbound, nil } @@ -215,6 +220,10 @@ func (h *Inbound) Start(stage adapter.StartStage) error { return h.service.Start(packetConn) } +func (h *Inbound) InterfaceUpdated() { + h.service.Reset() +} + func (h *Inbound) Close() error { return common.Close( h.listener, diff --git a/protocol/hysteria2/outbound.go b/protocol/hysteria2/outbound.go index fe23109a23..28bf4a5c7d 100644 --- a/protocol/hysteria2/outbound.go +++ b/protocol/hysteria2/outbound.go @@ -3,6 +3,7 @@ package hysteria2 import ( "context" "net" + "net/url" "os" "time" @@ -45,7 +46,11 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL if options.TLS == nil || !options.TLS.Enabled { return nil, C.ErrTLSRequired } - tlsConfig, err := tls.NewClient(ctx, logger, options.Server, common.PtrValueOrDefault(options.TLS)) + tlsServerAddress, tlsOptions, err := outboundTLSOptions(options) + if err != nil { + return nil, err + } + tlsConfig, err := tls.NewClient(ctx, logger, tlsServerAddress, tlsOptions) if err != nil { return nil, err } @@ -65,6 +70,10 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL if err != nil { return nil, err } + realmOptions, err := buildRealmOptions(ctx, logger, options.Realm) + if err != nil { + return nil, err + } networkList := options.Network.Build() client, err := hysteria2.NewClient(hysteria2.ClientOptions{ Context: ctx, @@ -89,8 +98,9 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL InitialPacketSize: options.InitialPacketSize, DisablePathMTUDiscovery: options.DisablePathMTUDiscovery, }, - UDPDisabled: !common.Contains(networkList, N.NetworkUDP), - BBRProfile: options.BBRProfile, + UDPDisabled: !common.Contains(networkList, N.NetworkUDP), + BBRProfile: options.BBRProfile, + RealmOptions: realmOptions, }) if err != nil { return nil, err @@ -102,6 +112,25 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL }, nil } +func outboundTLSOptions(options option.Hysteria2OutboundOptions) (string, option.OutboundTLSOptions, error) { + tlsOptions := common.PtrValueOrDefault(options.TLS) + if options.Realm == nil { + return options.Server, tlsOptions, nil + } + if options.Server != "" || options.ServerPort != 0 || len(options.ServerPorts) > 0 { + return "", tlsOptions, E.New("realm conflicts with server, server_port, and server_ports") + } + serverURL, err := url.Parse(options.Realm.ServerURL) + if err != nil { + return "", tlsOptions, E.Cause(err, "parse realm server_url") + } + serverName := serverURL.Hostname() + if serverName == "" { + return "", tlsOptions, E.New("missing host in realm server_url") + } + return serverName, tlsOptions, nil +} + func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { switch N.NetworkName(network) { case N.NetworkTCP: diff --git a/protocol/hysteria2/realm.go b/protocol/hysteria2/realm.go new file mode 100644 index 0000000000..7eb0f74254 --- /dev/null +++ b/protocol/hysteria2/realm.go @@ -0,0 +1,173 @@ +package hysteria2 + +import ( + "context" + "errors" + "net" + "net/http" + + "github.com/sagernet/sing-box/adapter" + boxService "github.com/sagernet/sing-box/adapter/service" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-quic/hysteria2/realm" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" + aTLS "github.com/sagernet/sing/common/tls" + sHTTP "github.com/sagernet/sing/protocol/http" + "github.com/sagernet/sing/service" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/render" + "golang.org/x/net/http2" +) + +func RegisterRealmService(registry *boxService.Registry) { + boxService.Register[option.HysteriaRealmServiceOptions](registry, C.TypeHysteriaRealm, NewRealmService) +} + +func buildRealmOptions(ctx context.Context, logger log.ContextLogger, options *option.Hysteria2Realm) (*realm.Options, error) { + if options == nil { + return nil, nil + } + transport, err := service.FromContext[adapter.HTTPClientManager](ctx).ResolveTransport(ctx, logger, common.PtrValueOrDefault(options.HTTPClient)) + if err != nil { + return nil, E.Cause(err, "create realm http client") + } + return &realm.Options{ + ServerURL: options.ServerURL, + Token: options.Token, + RealmID: options.RealmID, + STUNServers: options.STUNServers, + HTTPClient: &http.Client{Transport: transport}, + Logger: logger, + }, nil +} + +type RealmService struct { + boxService.Adapter + ctx context.Context + cancel context.CancelFunc + logger log.ContextLogger + listener *listener.Listener + tlsConfig tls.ServerConfig + httpServer *http.Server + server *server +} + +func NewRealmService(ctx context.Context, logger log.ContextLogger, tag string, options option.HysteriaRealmServiceOptions) (adapter.Service, error) { + if len(options.Users) == 0 { + return nil, E.New("missing users") + } + tokenMap := make(map[string]*realmUser, len(options.Users)) + for i, user := range options.Users { + if user.Name == "" { + return nil, E.New("missing name for user[", i, "]") + } + if user.Token == "" { + return nil, E.New("missing token for user[", i, "]") + } + tokenMap[user.Token] = &realmUser{ + name: user.Name, + maxRealms: user.MaxRealms, + } + } + server := newServer(logger, tokenMap) + ctx, cancel := context.WithCancel(ctx) + chiRouter := chi.NewRouter() + chiRouter.Use(middleware.RequestSize(maxRequestBodyBytes)) + chiRouter.Use(func(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + logger.DebugContext(r.Context(), r.Method, " ", r.RequestURI, " ", sHTTP.SourceAddress(r)) + handler.ServeHTTP(w, r) + }) + }) + chiRouter.Route("/v1/{id}", func(r chi.Router) { + r.Use(validateRealmID) + r.With(server.authUser).Post("/", server.handleRegister) + r.With(server.authSession).Delete("/", server.handleDeregister) + r.With(server.authSession).Get("/events", server.handleEvents) + r.With(server.authSession).Post("/heartbeat", server.handleHeartbeat) + r.With(server.authUser).Post("/connect", server.handleConnect) + r.With(server.authSession).Post("/connects/{nonce}", server.handleConnectResponse) + }) + chiRouter.NotFound(func(w http.ResponseWriter, r *http.Request) { + render.Status(r, http.StatusNotFound) + render.JSON(w, r, render.M{"error": "not_found", "message": "unknown path"}) + }) + chiRouter.MethodNotAllowed(func(w http.ResponseWriter, r *http.Request) { + render.Status(r, http.StatusMethodNotAllowed) + render.JSON(w, r, render.M{"error": "bad_request", "message": "method not allowed"}) + }) + s := &RealmService{ + Adapter: boxService.NewAdapter(C.TypeHysteriaRealm, tag), + ctx: ctx, + cancel: cancel, + logger: logger, + listener: listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: []string{N.NetworkTCP}, + Listen: options.ListenOptions, + }), + httpServer: &http.Server{ + Handler: chiRouter, + ConnContext: func(ctx context.Context, _ net.Conn) context.Context { + return log.ContextWithNewID(ctx) + }, + }, + server: server, + } + if options.TLS != nil { + tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS)) + if err != nil { + return nil, err + } + s.tlsConfig = tlsConfig + } + return s, nil +} + +func (s *RealmService) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + if s.tlsConfig != nil { + err := s.tlsConfig.Start() + if err != nil { + return E.Cause(err, "create TLS config") + } + } + tcpListener, err := s.listener.ListenTCP() + if err != nil { + return err + } + if s.tlsConfig != nil { + if !common.Contains(s.tlsConfig.NextProtos(), http2.NextProtoTLS) { + s.tlsConfig.SetNextProtos(append([]string{"h2"}, s.tlsConfig.NextProtos()...)) + } + tcpListener = aTLS.NewListener(tcpListener, s.tlsConfig) + } + go func() { + err = s.httpServer.Serve(tcpListener) + if err != nil && !errors.Is(err, http.ErrServerClosed) { + s.logger.Error("serve error: ", err) + } + }() + return nil +} + +func (s *RealmService) Close() error { + s.cancel() + err := common.Close(common.PtrOrNil(s.httpServer)) + s.server.closeAll() + return E.Errors(err, common.Close( + common.PtrOrNil(s.listener), + s.tlsConfig, + )) +} diff --git a/protocol/hysteria2/realm_server.go b/protocol/hysteria2/realm_server.go new file mode 100644 index 0000000000..5143b86b6e --- /dev/null +++ b/protocol/hysteria2/realm_server.go @@ -0,0 +1,536 @@ +package hysteria2 + +import ( + "context" + "crypto/rand" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/netip" + "regexp" + "strings" + "sync" + "time" + + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" +) + +const ( + sessionTTL = time.Minute + realmNamePattern = `^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$` + maxRequestBodyBytes = 4 << 10 + maxAddresses = 8 + nonceHexLength = 32 + obfsHexLength = 64 + eventChannelSize = 16 + maxPendingAttempts = 16 + connectResponseTimeout = 10 * time.Second +) + +var realmPattern = regexp.MustCompile(realmNamePattern) + +type contextKey int + +const ( + contextKeyUser contextKey = iota + contextKeySession +) + +type realmUser struct { + name string + maxRealms int +} + +type realmSession struct { + id string + realmID string + username string + addresses []string + expires time.Time + events chan realmEvent + timer *time.Timer + done chan struct{} + closed bool + pending map[string]chan punchResponsePayload +} + +type realmEvent struct { + kind string + data any +} + +type punchEvent struct { + Addresses []string `json:"addresses"` + Nonce string `json:"nonce"` + Obfs string `json:"obfs"` +} + +type punchResponsePayload struct { + addresses []string +} + +type server struct { + access sync.Mutex + realms map[string]*realmSession + sessions map[string]*realmSession + userCounts map[string]int + logger log.ContextLogger + tokenMap map[string]*realmUser +} + +func newServer(logger log.ContextLogger, tokenMap map[string]*realmUser) *server { + return &server{ + realms: make(map[string]*realmSession), + sessions: make(map[string]*realmSession), + userCounts: make(map[string]int), + logger: logger, + tokenMap: tokenMap, + } +} + +func validateRealmID(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "id") + if !realmPattern.MatchString(id) { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, render.M{"error": "bad_request", "message": "invalid realm name"}) + return + } + next.ServeHTTP(w, r) + }) +} + +func (s *server) authBearer(name string, key contextKey, lookup func(r *http.Request, token string) (any, bool)) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + header := r.Header.Get("Authorization") + bearer, token, found := strings.Cut(header, " ") + if bearer != "Bearer" || !found { + render.Status(r, http.StatusUnauthorized) + render.JSON(w, r, render.M{"error": "invalid_token", "message": "invalid " + name + " token"}) + return + } + value, authenticated := lookup(r, token) + if !authenticated { + render.Status(r, http.StatusUnauthorized) + render.JSON(w, r, render.M{"error": "invalid_token", "message": "invalid " + name + " token"}) + return + } + next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), key, value))) + }) + } +} + +func (s *server) authUser(next http.Handler) http.Handler { + return s.authBearer("realm", contextKeyUser, func(_ *http.Request, token string) (any, bool) { + user, authenticated := s.tokenMap[token] + return user, authenticated + })(next) +} + +func (s *server) authSession(next http.Handler) http.Handler { + return s.authBearer("session", contextKeySession, func(r *http.Request, token string) (any, bool) { + sess := s.getSessionByToken(token) + if sess == nil || sess.realmID != chi.URLParam(r, "id") { + return nil, false + } + return sess, true + })(next) +} + +func (s *server) getSessionByToken(token string) *realmSession { + s.access.Lock() + defer s.access.Unlock() + sess := s.sessions[token] + if sess == nil || sess.closed || time.Now().After(sess.expires) { + return nil + } + return sess +} + +func (s *server) removeSessionLocked(sess *realmSession) { + if sess.closed { + return + } + sess.closed = true + close(sess.done) + if s.realms[sess.realmID] == sess { + delete(s.realms, sess.realmID) + } + if _, found := s.sessions[sess.id]; found { + s.userCounts[sess.username]-- + if s.userCounts[sess.username] <= 0 { + delete(s.userCounts, sess.username) + } + } + delete(s.sessions, sess.id) + sess.timer.Stop() + close(sess.events) + for nonce, ch := range sess.pending { + close(ch) + delete(sess.pending, nonce) + } +} + +func (s *server) removeSession(sess *realmSession) { + s.access.Lock() + defer s.access.Unlock() + s.removeSessionLocked(sess) +} + +func (s *server) removeExpiredSession(sess *realmSession) bool { + s.access.Lock() + defer s.access.Unlock() + if sess.closed || !time.Now().After(sess.expires) { + return false + } + s.removeSessionLocked(sess) + return true +} + +func (s *server) closeAll() { + s.access.Lock() + defer s.access.Unlock() + for _, sess := range s.sessions { + s.removeSessionLocked(sess) + } +} + +func (s *server) registerPending(sess *realmSession, nonce string) (chan punchResponsePayload, bool) { + s.access.Lock() + defer s.access.Unlock() + if sess.closed || len(sess.pending) >= maxPendingAttempts { + return nil, false + } + if _, exists := sess.pending[nonce]; exists { + return nil, false + } + ch := make(chan punchResponsePayload, 1) + sess.pending[nonce] = ch + return ch, true +} + +func (s *server) deliverPending(sess *realmSession, nonce string, payload punchResponsePayload) bool { + s.access.Lock() + defer s.access.Unlock() + if sess.closed { + return false + } + ch, found := sess.pending[nonce] + if !found { + return false + } + delete(sess.pending, nonce) + select { + case ch <- payload: + default: + } + return true +} + +func (s *server) cancelPending(sess *realmSession, nonce string) { + s.access.Lock() + defer s.access.Unlock() + delete(sess.pending, nonce) +} + +func (s *server) sendEvent(sess *realmSession, ev realmEvent) bool { + s.access.Lock() + defer s.access.Unlock() + if sess.closed { + return false + } + select { + case sess.events <- ev: + return true + default: + return false + } +} + +func (s *server) handleRegister(w http.ResponseWriter, r *http.Request) { + user := r.Context().Value(contextKeyUser).(*realmUser) + id := chi.URLParam(r, "id") + var req struct { + Addresses []string `json:"addresses"` + } + err := render.DecodeJSON(r.Body, &req) + if err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, render.M{"error": "bad_request", "message": "invalid json"}) + return + } + err = validateAddresses(req.Addresses) + if err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, render.M{"error": "bad_request", "message": err.Error()}) + return + } + s.access.Lock() + if _, exists := s.realms[id]; exists { + s.access.Unlock() + render.Status(r, http.StatusConflict) + render.JSON(w, r, render.M{"error": "realm_taken", "message": "realm already registered"}) + return + } + if user.maxRealms > 0 && s.userCounts[user.name] >= user.maxRealms { + s.access.Unlock() + render.Status(r, http.StatusTooManyRequests) + render.JSON(w, r, render.M{"error": "realm_limit_reached", "message": "per-user realm limit reached"}) + return + } + var b [16]byte + _, err = rand.Read(b[:]) + if err != nil { + s.access.Unlock() + render.Status(r, http.StatusInternalServerError) + render.JSON(w, r, render.M{"error": "internal", "message": "entropy failure"}) + return + } + sess := &realmSession{ + id: hex.EncodeToString(b[:]), + realmID: id, + username: user.name, + addresses: append([]string(nil), req.Addresses...), + expires: time.Now().Add(sessionTTL), + events: make(chan realmEvent, eventChannelSize), + done: make(chan struct{}), + pending: make(map[string]chan punchResponsePayload), + } + s.realms[id] = sess + s.sessions[sess.id] = sess + s.userCounts[user.name]++ + sess.timer = time.AfterFunc(sessionTTL, func() { + if s.removeExpiredSession(sess) { + s.logger.Debug("[", sess.username, "] session expired realm=", sess.realmID) + } + }) + s.access.Unlock() + s.logger.InfoContext(r.Context(), "[", user.name, "] registered realm=", id) + render.JSON(w, r, render.M{ + "session_id": sess.id, + "ttl": int(sessionTTL.Seconds()), + }) +} + +func (s *server) handleDeregister(w http.ResponseWriter, r *http.Request) { + sess := r.Context().Value(contextKeySession).(*realmSession) + s.logger.InfoContext(r.Context(), "[", sess.username, "] deregistered realm=", sess.realmID) + s.removeSession(sess) + render.NoContent(w, r) +} + +func (s *server) handleHeartbeat(w http.ResponseWriter, r *http.Request) { + sess := r.Context().Value(contextKeySession).(*realmSession) + var req struct { + Addresses []string `json:"addresses"` + } + err := render.DecodeJSON(r.Body, &req) + if err != nil && !errors.Is(err, io.EOF) { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, render.M{"error": "bad_request", "message": "invalid json"}) + return + } + if req.Addresses != nil { + err = validateAddresses(req.Addresses) + if err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, render.M{"error": "bad_request", "message": err.Error()}) + return + } + } + s.access.Lock() + sess.expires = time.Now().Add(sessionTTL) + if req.Addresses != nil { + sess.addresses = append([]string(nil), req.Addresses...) + } + sess.timer.Reset(sessionTTL) + s.access.Unlock() + s.logger.DebugContext(r.Context(), "[", sess.username, "] heartbeat realm=", sess.realmID) + s.sendEvent(sess, realmEvent{kind: "heartbeat_ack", data: render.M{"ttl": int(sessionTTL.Seconds())}}) + render.JSON(w, r, render.M{"ttl": int(sessionTTL.Seconds())}) +} + +func (s *server) handleEvents(w http.ResponseWriter, r *http.Request) { + sess := r.Context().Value(contextKeySession).(*realmSession) + flusher, supportsFlusher := w.(http.Flusher) + if !supportsFlusher { + render.Status(r, http.StatusInternalServerError) + render.JSON(w, r, render.M{"error": "internal", "message": "streaming unsupported"}) + return + } + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + w.WriteHeader(http.StatusOK) + flusher.Flush() + ctx := r.Context() + for { + select { + case <-ctx.Done(): + return + case ev, open := <-sess.events: + if !open { + return + } + data, _ := json.Marshal(ev.data) + fmt.Fprintf(w, "event: %s\ndata: %s\n\n", ev.kind, data) + flusher.Flush() + } + } +} + +func (s *server) handleConnect(w http.ResponseWriter, r *http.Request) { + user := r.Context().Value(contextKeyUser).(*realmUser) + id := chi.URLParam(r, "id") + var req struct { + Addresses []string `json:"addresses"` + Nonce string `json:"nonce"` + Obfs string `json:"obfs"` + } + err := render.DecodeJSON(r.Body, &req) + if err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, render.M{"error": "bad_request", "message": "invalid json"}) + return + } + err = validateAddresses(req.Addresses) + if err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, render.M{"error": "bad_request", "message": err.Error()}) + return + } + err = validateHexField("nonce", req.Nonce, nonceHexLength) + if err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, render.M{"error": "bad_request", "message": err.Error()}) + return + } + err = validateHexField("obfs", req.Obfs, obfsHexLength) + if err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, render.M{"error": "bad_request", "message": err.Error()}) + return + } + s.access.Lock() + // Any authenticated realm user may connect to a registered realm. The user name + // is for logging and per-user registration quota, not an ownership boundary here. + sess := s.realms[id] + if sess == nil || sess.closed || time.Now().After(sess.expires) { + s.access.Unlock() + render.Status(r, http.StatusNotFound) + render.JSON(w, r, render.M{"error": "realm_not_found", "message": "realm not registered"}) + return + } + serverAddresses := append([]string(nil), sess.addresses...) + s.access.Unlock() + + respCh, ready := s.registerPending(sess, req.Nonce) + if !ready { + render.Status(r, http.StatusServiceUnavailable) + render.JSON(w, r, render.M{"error": "rate_limited", "message": "too many in-flight connect attempts"}) + return + } + defer s.cancelPending(sess, req.Nonce) + + if !s.sendEvent(sess, realmEvent{kind: "punch", data: punchEvent{Addresses: req.Addresses, Nonce: req.Nonce, Obfs: req.Obfs}}) { + render.Status(r, http.StatusServiceUnavailable) + render.JSON(w, r, render.M{"error": "rate_limited", "message": "server event buffer full"}) + return + } + s.logger.DebugContext(r.Context(), "[", user.name, "] connect realm=", id) + + timer := time.NewTimer(connectResponseTimeout) + defer timer.Stop() + select { + case payload, open := <-respCh: + if !open { + render.Status(r, http.StatusNotFound) + render.JSON(w, r, render.M{"error": "realm_not_found", "message": "realm not registered"}) + return + } + if len(payload.addresses) > 0 { + serverAddresses = payload.addresses + } + case <-timer.C: + case <-sess.done: + render.Status(r, http.StatusNotFound) + render.JSON(w, r, render.M{"error": "realm_not_found", "message": "realm not registered"}) + return + case <-r.Context().Done(): + return + } + render.JSON(w, r, render.M{ + "addresses": serverAddresses, + "nonce": req.Nonce, + "obfs": req.Obfs, + }) +} + +func (s *server) handleConnectResponse(w http.ResponseWriter, r *http.Request) { + sess := r.Context().Value(contextKeySession).(*realmSession) + nonce := chi.URLParam(r, "nonce") + err := validateHexField("nonce", nonce, nonceHexLength) + if err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, render.M{"error": "bad_request", "message": err.Error()}) + return + } + var req struct { + Addresses []string `json:"addresses"` + } + err = render.DecodeJSON(r.Body, &req) + if err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, render.M{"error": "bad_request", "message": "invalid json"}) + return + } + err = validateAddresses(req.Addresses) + if err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, render.M{"error": "bad_request", "message": err.Error()}) + return + } + delivered := s.deliverPending(sess, nonce, punchResponsePayload{addresses: append([]string(nil), req.Addresses...)}) + if !delivered { + render.Status(r, http.StatusNotFound) + render.JSON(w, r, render.M{"error": "attempt_not_found", "message": "no pending attempt for nonce"}) + return + } + s.logger.DebugContext(r.Context(), "[", sess.username, "] connect-response realm=", sess.realmID) + render.NoContent(w, r) +} + +func validateAddresses(addresses []string) error { + if len(addresses) == 0 { + return E.New("at least one address required") + } + if len(addresses) > maxAddresses { + return E.New("too many addresses (max ", maxAddresses, ")") + } + for _, address := range addresses { + _, err := netip.ParseAddrPort(address) + if err != nil { + return E.New("invalid address: ", address) + } + } + return nil +} + +func validateHexField(name, value string, length int) error { + if len(value) != length { + return E.New(name, " must be ", length, " hex characters") + } + _, err := hex.DecodeString(value) + if err != nil { + return E.New(name, " must be valid hex") + } + return nil +} From 2e1a7a53ae414e1e67708bbc0ecce798e62d15de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 11 May 2026 16:24:34 +0800 Subject: [PATCH 82/93] Update hysteria2 realm --- adapter/dns.go | 10 ++--- docs/configuration/inbound/hysteria2.md | 11 +++++- docs/configuration/inbound/hysteria2.zh.md | 11 +++++- docs/configuration/outbound/hysteria2.md | 2 +- docs/configuration/outbound/hysteria2.zh.md | 2 +- go.mod | 2 +- go.sum | 4 +- option/hysteria2.go | 13 +++++-- protocol/hysteria2/inbound.go | 35 +++++++++++++++-- protocol/hysteria2/outbound.go | 42 +++++++++++++++++++-- protocol/hysteria2/realm.go | 20 ---------- 11 files changed, 109 insertions(+), 43 deletions(-) diff --git a/adapter/dns.go b/adapter/dns.go index afee5aa8a1..a613de0c44 100644 --- a/adapter/dns.go +++ b/adapter/dns.go @@ -42,16 +42,16 @@ type DNSQueryOptions struct { ClientSubnet netip.Prefix } -func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) { - if options == nil { - return &DNSQueryOptions{}, nil +func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (DNSQueryOptions, error) { + if options == nil || options.Server == "" { + return DNSQueryOptions{}, nil } transportManager := service.FromContext[DNSTransportManager](ctx) transport, loaded := transportManager.Transport(options.Server) if !loaded { - return nil, E.New("domain resolver not found: " + options.Server) + return DNSQueryOptions{}, E.New("domain resolver not found: " + options.Server) } - return &DNSQueryOptions{ + return DNSQueryOptions{ Transport: transport, Strategy: C.DomainStrategy(options.Strategy), DisableCache: options.DisableCache, diff --git a/docs/configuration/inbound/hysteria2.md b/docs/configuration/inbound/hysteria2.md index 4d24940226..0769e3c40b 100644 --- a/docs/configuration/inbound/hysteria2.md +++ b/docs/configuration/inbound/hysteria2.md @@ -46,6 +46,7 @@ icon: material/alert-decagram "token": "", "realm_id": "", "stun_servers": [], + "stun_domain_resolver": "", // or {} "http_client": {} } } @@ -209,7 +210,15 @@ Outbounds must use the same `realm_id` to find this server. List of STUN servers (`host` or `host:port`) used to discover public addresses. -Port defaults to `3478`. +#### realm.stun_domain_resolver + +Set domain resolver to use for resolving STUN server domain names. + +This option uses the same format as the [route DNS rule action](/configuration/dns/rule_action/#route) without the `action` field. + +Setting this option directly to a string is equivalent to setting `server` of this options. + +If empty, the default domain resolver is used. #### realm.http_client diff --git a/docs/configuration/inbound/hysteria2.zh.md b/docs/configuration/inbound/hysteria2.zh.md index cfe7f9f0d5..4af8a9eed4 100644 --- a/docs/configuration/inbound/hysteria2.zh.md +++ b/docs/configuration/inbound/hysteria2.zh.md @@ -46,6 +46,7 @@ icon: material/alert-decagram "token": "", "realm_id": "", "stun_servers": [], + "stun_domain_resolver": "", // 或 {} "http_client": {} } } @@ -206,7 +207,15 @@ Realm 上的槽位标识符。 用于发现公网地址的 STUN 服务器列表(`host` 或 `host:port`)。 -端口默认为 `3478`。 +#### realm.stun_domain_resolver + +用于解析 STUN 服务器域名的域名解析器。 + +此选项的格式与 [路由 DNS 规则动作](/zh/configuration/dns/rule_action/#route) 相同,但不包含 `action` 字段。 + +若直接将此选项设置为字符串,则等同于设置该选项的 `server` 字段。 + +如果为空,则使用默认域名解析器。 #### realm.http_client diff --git a/docs/configuration/outbound/hysteria2.md b/docs/configuration/outbound/hysteria2.md index 4e162b5545..3bcd42eaa6 100644 --- a/docs/configuration/outbound/hysteria2.md +++ b/docs/configuration/outbound/hysteria2.md @@ -191,7 +191,7 @@ The same slot identifier the target Hysteria2 server registered. List of STUN servers (`host` or `host:port`) used to discover this client's public addresses. -Port defaults to `3478`. +Domain names are resolved using [`domain_resolver`](/configuration/shared/dial/#domain_resolver) from Dial Fields. #### realm.http_client diff --git a/docs/configuration/outbound/hysteria2.zh.md b/docs/configuration/outbound/hysteria2.zh.md index aff60a8a13..a39f3be50c 100644 --- a/docs/configuration/outbound/hysteria2.zh.md +++ b/docs/configuration/outbound/hysteria2.zh.md @@ -189,7 +189,7 @@ Realm 的 Bearer 令牌,需与 realm 上配置的 `users[].token` 之一匹配 用于发现本客户端公网地址的 STUN 服务器列表(`host` 或 `host:port`)。 -端口默认为 `3478`。 +域名通过 [拨号字段](/zh/configuration/shared/dial/) 中的 [`domain_resolver`](/zh/configuration/shared/dial/#domain_resolver) 解析。 #### realm.http_client diff --git a/go.mod b/go.mod index bac338d098..1f365c4f69 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( github.com/sagernet/sing v0.8.10-0.20260428084616-2bc976d03e39 github.com/sagernet/sing-cloudflared v0.1.0 github.com/sagernet/sing-mux v0.3.4 - github.com/sagernet/sing-quic v0.6.2-0.20260510042401-4055a2bb2381 + github.com/sagernet/sing-quic v0.6.2-0.20260511084842-9deddf006024 github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 diff --git a/go.sum b/go.sum index be6ebec39b..03a2cfb73c 100644 --- a/go.sum +++ b/go.sum @@ -248,8 +248,8 @@ github.com/sagernet/sing-cloudflared v0.1.0 h1:to+2fcCx8zu4X/DirRw9Ihc+FrEZ7oEyI github.com/sagernet/sing-cloudflared v0.1.0/go.mod h1:bH2NKX+NpDTY1Zkxfboxw6MXB/ZywaNLmrDJYgKMJ2Y= github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s= github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= -github.com/sagernet/sing-quic v0.6.2-0.20260510042401-4055a2bb2381 h1:bwja5I7Sgr4Z1nyCLlN6T5eBhhSE/ilYZmkEFIoPAhQ= -github.com/sagernet/sing-quic v0.6.2-0.20260510042401-4055a2bb2381/go.mod h1:+oqD54aHel4ALKkp1hVXWCgLU/EjLojvm6AUzDfvj0I= +github.com/sagernet/sing-quic v0.6.2-0.20260511084842-9deddf006024 h1:P1iab6udg2I2igIrn+mNKpPZNcuejSqno3jwJ/94upw= +github.com/sagernet/sing-quic v0.6.2-0.20260511084842-9deddf006024/go.mod h1:+oqD54aHel4ALKkp1hVXWCgLU/EjLojvm6AUzDfvj0I= github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE= github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI= github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo= diff --git a/option/hysteria2.go b/option/hysteria2.go index eee8105438..b3f1208eea 100644 --- a/option/hysteria2.go +++ b/option/hysteria2.go @@ -19,10 +19,10 @@ type Hysteria2InboundOptions struct { IgnoreClientBandwidth bool `json:"ignore_client_bandwidth,omitempty"` InboundTLSOptionsContainer QUICOptions - Masquerade *Hysteria2Masquerade `json:"masquerade,omitempty"` - BBRProfile string `json:"bbr_profile,omitempty"` - BrutalDebug bool `json:"brutal_debug,omitempty"` - Realm *Hysteria2Realm `json:"realm,omitempty"` + Masquerade *Hysteria2Masquerade `json:"masquerade,omitempty"` + BBRProfile string `json:"bbr_profile,omitempty"` + BrutalDebug bool `json:"brutal_debug,omitempty"` + Realm *Hysteria2InboundRealm `json:"realm,omitempty"` } type Hysteria2Realm struct { @@ -33,6 +33,11 @@ type Hysteria2Realm struct { HTTPClient *HTTPClientOptions `json:"http_client,omitempty"` } +type Hysteria2InboundRealm struct { + Hysteria2Realm + STUNDomainResolver *DomainResolveOptions `json:"stun_domain_resolver,omitempty"` +} + type Hysteria2Obfs struct { Type string `json:"type,omitempty"` Password string `json:"password,omitempty"` diff --git a/protocol/hysteria2/inbound.go b/protocol/hysteria2/inbound.go index a6e800c5a0..d18b3fa62c 100644 --- a/protocol/hysteria2/inbound.go +++ b/protocol/hysteria2/inbound.go @@ -5,6 +5,7 @@ import ( "net" "net/http" "net/http/httputil" + "net/netip" "net/url" "time" @@ -18,11 +19,13 @@ import ( qtls "github.com/sagernet/sing-quic" "github.com/sagernet/sing-quic/hysteria" "github.com/sagernet/sing-quic/hysteria2" + "github.com/sagernet/sing-quic/hysteria2/realm" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/auth" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" ) func RegisterInbound(registry *inbound.Registry) { @@ -114,9 +117,35 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo } else { udpTimeout = C.UDPTimeout } - realmOptions, err := buildRealmOptions(ctx, logger, options.Realm) - if err != nil { - return nil, err + var realmOptions *realm.Options + if options.Realm != nil { + queryOptions, err := adapter.DNSQueryOptionsFrom(ctx, options.Realm.STUNDomainResolver) + if err != nil { + return nil, err + } + httpClientTransport, err := service.FromContext[adapter.HTTPClientManager](ctx).ResolveTransport(ctx, logger, common.PtrValueOrDefault(options.Realm.HTTPClient)) + if err != nil { + return nil, E.Cause(err, "create realm http client") + } + dnsRouter := service.FromContext[adapter.DNSRouter](ctx) + realmOptions = &realm.Options{ + ServerURL: options.Realm.ServerURL, + Token: options.Realm.Token, + RealmID: options.Realm.RealmID, + STUNServers: options.Realm.STUNServers, + HTTPClient: &http.Client{Transport: httpClientTransport}, + Resolver: func(ctx context.Context, host string, ipv4, ipv6 bool) ([]netip.Addr, error) { + dnsOptions := queryOptions + switch { + case ipv4 && !ipv6: + dnsOptions.Strategy = C.DomainStrategyIPv4Only + case !ipv4 && ipv6: + dnsOptions.Strategy = C.DomainStrategyIPv6Only + } + return dnsRouter.Lookup(ctx, host, dnsOptions) + }, + Logger: logger, + } } hysteriaService, err := hysteria2.NewService[int](hysteria2.ServiceOptions{ Context: ctx, diff --git a/protocol/hysteria2/outbound.go b/protocol/hysteria2/outbound.go index 28bf4a5c7d..f4d4d58d39 100644 --- a/protocol/hysteria2/outbound.go +++ b/protocol/hysteria2/outbound.go @@ -3,6 +3,8 @@ package hysteria2 import ( "context" "net" + "net/http" + "net/netip" "net/url" "os" "time" @@ -18,12 +20,14 @@ import ( qtls "github.com/sagernet/sing-quic" "github.com/sagernet/sing-quic/hysteria" "github.com/sagernet/sing-quic/hysteria2" + "github.com/sagernet/sing-quic/hysteria2/realm" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/bufio" 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" ) func RegisterOutbound(registry *outbound.Registry) { @@ -66,13 +70,43 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL return nil, E.New("unknown obfs type: ", options.Obfs.Type) } } - outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain()) + outboundDialer, err := dialer.NewWithOptions(dialer.Options{ + Context: ctx, + Options: options.DialerOptions, + RemoteIsDomain: options.ServerIsDomain(), + }) if err != nil { return nil, err } - realmOptions, err := buildRealmOptions(ctx, logger, options.Realm) - if err != nil { - return nil, err + var realmOptions *realm.Options + if options.Realm != nil { + queryOptions, err := adapter.DNSQueryOptionsFrom(ctx, options.DialerOptions.DomainResolver) + if err != nil { + return nil, err + } + httpClientTransport, err := service.FromContext[adapter.HTTPClientManager](ctx).ResolveTransport(ctx, logger, common.PtrValueOrDefault(options.Realm.HTTPClient)) + if err != nil { + return nil, E.Cause(err, "create realm http client") + } + dnsRouter := service.FromContext[adapter.DNSRouter](ctx) + realmOptions = &realm.Options{ + ServerURL: options.Realm.ServerURL, + Token: options.Realm.Token, + RealmID: options.Realm.RealmID, + STUNServers: options.Realm.STUNServers, + HTTPClient: &http.Client{Transport: httpClientTransport}, + Resolver: func(ctx context.Context, host string, ipv4, ipv6 bool) ([]netip.Addr, error) { + dnsOptions := queryOptions + switch { + case ipv4 && !ipv6: + dnsOptions.Strategy = C.DomainStrategyIPv4Only + case !ipv4 && ipv6: + dnsOptions.Strategy = C.DomainStrategyIPv6Only + } + return dnsRouter.Lookup(ctx, host, dnsOptions) + }, + Logger: logger, + } } networkList := options.Network.Build() client, err := hysteria2.NewClient(hysteria2.ClientOptions{ diff --git a/protocol/hysteria2/realm.go b/protocol/hysteria2/realm.go index 7eb0f74254..960bfbc914 100644 --- a/protocol/hysteria2/realm.go +++ b/protocol/hysteria2/realm.go @@ -13,13 +13,11 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing-quic/hysteria2/realm" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" N "github.com/sagernet/sing/common/network" aTLS "github.com/sagernet/sing/common/tls" sHTTP "github.com/sagernet/sing/protocol/http" - "github.com/sagernet/sing/service" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" @@ -31,24 +29,6 @@ func RegisterRealmService(registry *boxService.Registry) { boxService.Register[option.HysteriaRealmServiceOptions](registry, C.TypeHysteriaRealm, NewRealmService) } -func buildRealmOptions(ctx context.Context, logger log.ContextLogger, options *option.Hysteria2Realm) (*realm.Options, error) { - if options == nil { - return nil, nil - } - transport, err := service.FromContext[adapter.HTTPClientManager](ctx).ResolveTransport(ctx, logger, common.PtrValueOrDefault(options.HTTPClient)) - if err != nil { - return nil, E.Cause(err, "create realm http client") - } - return &realm.Options{ - ServerURL: options.ServerURL, - Token: options.Token, - RealmID: options.RealmID, - STUNServers: options.STUNServers, - HTTPClient: &http.Client{Transport: transport}, - Logger: logger, - }, nil -} - type RealmService struct { boxService.Adapter ctx context.Context From 7b3a1de7bc94d420ab05fb235c8d68e7756dcf23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 11 May 2026 20:59:49 +0800 Subject: [PATCH 83/93] dns: Fix conn pool leak --- dns/transport/conn_pool.go | 323 ++++++++++--------------------------- 1 file changed, 88 insertions(+), 235 deletions(-) diff --git a/dns/transport/conn_pool.go b/dns/transport/conn_pool.go index 6161e9bdbd..ff288b7730 100644 --- a/dns/transport/conn_pool.go +++ b/dns/transport/conn_pool.go @@ -4,7 +4,6 @@ import ( "context" "net" "sync" - "time" "github.com/sagernet/sing/common/x/list" ) @@ -53,19 +52,6 @@ type connPoolConnect[T comparable] struct { err error } -type connPoolDialContext struct { - context.Context - parent context.Context -} - -func (c connPoolDialContext) Deadline() (time.Time, bool) { - return c.parent.Deadline() -} - -func (c connPoolDialContext) Value(key any) any { - return c.parent.Value(key) -} - func NewConnPool[T comparable](options ConnPoolOptions[T]) *ConnPool[T] { return &ConnPool[T]{ options: options, @@ -108,67 +94,27 @@ func (p *ConnPool[T]) AcquireShared(ctx context.Context, dial func(context.Conte } func (p *ConnPool[T]) Release(conn T, reuse bool) { - var ( - closeConn bool - closeErr error - ) - p.access.Lock() - if p.closed || p.state == nil { - closeConn = true - closeErr = net.ErrClosed + if p.closed { p.access.Unlock() - if closeConn { - p.options.Close(conn, closeErr) - } + p.options.Close(conn, net.ErrClosed) return } - - currentState := p.state - _, tracked := currentState.all[conn] - if !tracked { - closeConn = true - closeErr = p.closeCause(currentState) + state := p.state + if _, tracked := state.all[conn]; !tracked { p.access.Unlock() - if closeConn { - p.options.Close(conn, closeErr) - } + p.options.Close(conn, net.ErrClosed) return } - if !reuse || !p.options.IsAlive(conn) { - delete(currentState.all, conn) - switch p.options.Mode { - case ConnPoolSingle: - if currentState.hasShared && currentState.shared == conn { - var zero T - currentState.shared = zero - currentState.hasShared = false - currentState.sharedClaimed = false - currentState.sharedCtx = nil - if currentState.sharedCancel != nil { - currentState.sharedCancel(net.ErrClosed) - currentState.sharedCancel = nil - } - } - case ConnPoolOrdered: - if element, loaded := currentState.idleElements[conn]; loaded { - currentState.idle.Remove(element) - delete(currentState.idleElements, conn) - } - } - closeConn = true - closeErr = net.ErrClosed + p.removeConn(state, conn, net.ErrClosed) p.access.Unlock() - if closeConn { - p.options.Close(conn, closeErr) - } + p.options.Close(conn, net.ErrClosed) return } - if p.options.Mode == ConnPoolOrdered { - if _, loaded := currentState.idleElements[conn]; !loaded { - currentState.idleElements[conn] = currentState.idle.PushBack(conn) + if _, loaded := state.idleElements[conn]; !loaded { + state.idleElements[conn] = state.idle.PushBack(conn) } } p.access.Unlock() @@ -176,42 +122,43 @@ func (p *ConnPool[T]) Release(conn T, reuse bool) { func (p *ConnPool[T]) Invalidate(conn T, cause error) { p.access.Lock() - if p.closed || p.state == nil { + if p.closed { p.access.Unlock() p.options.Close(conn, cause) return } - - currentState := p.state - _, tracked := currentState.all[conn] - if !tracked { + state := p.state + if _, tracked := state.all[conn]; !tracked { p.access.Unlock() return } + p.removeConn(state, conn, cause) + p.access.Unlock() + p.options.Close(conn, cause) +} - delete(currentState.all, conn) +// removeConn must be called with p.access held. +func (p *ConnPool[T]) removeConn(state *connPoolState[T], conn T, cause error) { + delete(state.all, conn) switch p.options.Mode { case ConnPoolSingle: - if currentState.hasShared && currentState.shared == conn { + if state.hasShared && state.shared == conn { var zero T - currentState.shared = zero - currentState.hasShared = false - currentState.sharedClaimed = false - currentState.sharedCtx = nil - if currentState.sharedCancel != nil { - currentState.sharedCancel(cause) - currentState.sharedCancel = nil + state.shared = zero + state.hasShared = false + state.sharedClaimed = false + state.sharedCtx = nil + if state.sharedCancel != nil { + state.sharedCancel(cause) + state.sharedCancel = nil } } case ConnPoolOrdered: - if element, loaded := currentState.idleElements[conn]; loaded { - currentState.idle.Remove(element) - delete(currentState.idleElements, conn) + if element, loaded := state.idleElements[conn]; loaded { + state.idle.Remove(element) + delete(state.idleElements, conn) } } - p.access.Unlock() - - p.options.Close(conn, cause) } func (p *ConnPool[T]) Reset() { @@ -220,7 +167,6 @@ func (p *ConnPool[T]) Reset() { p.access.Unlock() return } - oldState := p.state p.state = newConnPoolState[T](p.options.Mode) p.access.Unlock() @@ -234,7 +180,6 @@ func (p *ConnPool[T]) Close() error { p.access.Unlock() return nil } - p.closed = true oldState := p.state p.state = nil @@ -247,40 +192,47 @@ func (p *ConnPool[T]) Close() error { func (p *ConnPool[T]) acquireOrdered(ctx context.Context, dial func(context.Context) (T, error)) (T, bool, error) { var zero T for { - var ( - staleConn T - hasStale bool - ) - p.access.Lock() if p.closed { p.access.Unlock() return zero, false, net.ErrClosed } - - currentState := p.state - if element := currentState.idle.Front(); element != nil { - conn := currentState.idle.Remove(element) - delete(currentState.idleElements, conn) + current := p.state + if element := current.idle.Front(); element != nil { + conn := current.idle.Remove(element) + delete(current.idleElements, conn) if p.options.IsAlive(conn) { p.access.Unlock() return conn, false, nil } - delete(currentState.all, conn) - staleConn = conn - hasStale = true + delete(current.all, conn) + p.access.Unlock() + p.options.Close(conn, net.ErrClosed) + continue } p.access.Unlock() - if hasStale { - p.options.Close(staleConn, net.ErrClosed) - continue + dialCtx, dialCancel := context.WithCancelCause(ctx) + stopStateCancel := context.AfterFunc(current.ctx, func() { + dialCancel(context.Cause(current.ctx)) + }) + conn, err := dial(dialCtx) + stateCancelStopped := stopStateCancel() + dialErr := context.Cause(dialCtx) + if dialErr == nil && !stateCancelStopped { + dialErr = context.Cause(current.ctx) } - - conn, err := p.dial(ctx, currentState, dial) + dialCancel(nil) if err != nil { + if dialErr != nil { + return zero, false, dialErr + } return zero, false, err } + if dialErr != nil { + p.options.Close(conn, dialErr) + return zero, false, dialErr + } p.access.Lock() if p.closed { @@ -288,13 +240,12 @@ func (p *ConnPool[T]) acquireOrdered(ctx context.Context, dial func(context.Cont p.options.Close(conn, net.ErrClosed) return zero, false, net.ErrClosed } - if p.state != currentState { - cause := p.closeCause(currentState) + if p.state != current { p.access.Unlock() - p.options.Close(conn, cause) - return zero, false, cause + p.options.Close(conn, net.ErrClosed) + return zero, false, net.ErrClosed } - currentState.all[conn] = struct{}{} + current.all[conn] = struct{}{} p.access.Unlock() return conn, true, nil } @@ -303,21 +254,12 @@ func (p *ConnPool[T]) acquireOrdered(ctx context.Context, dial func(context.Cont func (p *ConnPool[T]) acquireShared(ctx context.Context, dial func(context.Context) (T, error)) (T, context.Context, bool, error) { var zero T for { - var ( - staleConn T - hasStale bool - state *connPoolConnect[T] - current *connPoolState[T] - startDial bool - ) - p.access.Lock() if p.closed { p.access.Unlock() return zero, nil, false, net.ErrClosed } - - current = p.state + current := p.state if current.hasShared { conn := current.shared if p.options.IsAlive(conn) { @@ -327,35 +269,19 @@ func (p *ConnPool[T]) acquireShared(ctx context.Context, dial func(context.Conte p.access.Unlock() return conn, connCtx, created, nil } - delete(current.all, conn) - var zeroConn T - current.shared = zeroConn - current.hasShared = false - current.sharedClaimed = false - current.sharedCtx = nil - if current.sharedCancel != nil { - current.sharedCancel(net.ErrClosed) - current.sharedCancel = nil - } - staleConn = conn - hasStale = true + p.removeConn(current, conn, net.ErrClosed) p.access.Unlock() - p.options.Close(staleConn, net.ErrClosed) + p.options.Close(conn, net.ErrClosed) continue } - if current.connecting == nil { - current.connecting = &connPoolConnect[T]{ - done: make(chan struct{}), - } - startDial = true + startDial := current.connecting == nil + if startDial { + current.connecting = &connPoolConnect[T]{done: make(chan struct{})} } - state = current.connecting + state := current.connecting p.access.Unlock() - if hasStale { - continue - } if startDial { go p.connectSingle(current, state, ctx, dial) } @@ -381,35 +307,39 @@ func (p *ConnPool[T]) acquireShared(ctx context.Context, dial func(context.Conte } func (p *ConnPool[T]) connectSingle(current *connPoolState[T], state *connPoolConnect[T], ctx context.Context, dial func(context.Context) (T, error)) { - conn, err := p.dial(ctx, current, dial) - if err != nil { - p.access.Lock() - if current.connecting == state { - current.connecting = nil + dialCtx, dialCancel := context.WithCancelCause(ctx) + stopStateCancel := context.AfterFunc(current.ctx, func() { + dialCancel(context.Cause(current.ctx)) + }) + conn, err := dial(dialCtx) + stateCancelStopped := stopStateCancel() + dialErr := context.Cause(dialCtx) + if dialErr == nil && !stateCancelStopped { + dialErr = context.Cause(current.ctx) + } + dialCancel(nil) + if dialErr != nil { + if err == nil { + p.options.Close(conn, dialErr) } - state.err = err - p.access.Unlock() - close(state.done) - return + err = dialErr } var closeErr error - p.access.Lock() - if current.connecting == state { - current.connecting = nil - } - if p.closed { + current.connecting = nil + if err != nil { + state.err = err + } else if p.closed { closeErr = net.ErrClosed state.err = closeErr } else if p.state != current { - closeErr = p.closeCause(current) + closeErr = net.ErrClosed state.err = closeErr } else { sharedCtx, sharedCancel := context.WithCancelCause(current.ctx) current.shared = conn current.hasShared = true - current.sharedClaimed = false current.sharedCtx = sharedCtx current.sharedCancel = sharedCancel current.all[conn] = struct{}{} @@ -439,9 +369,8 @@ func (p *ConnPool[T]) collectShared(current *connPoolState[T], state *connPoolCo return zero, nil, false, false, net.ErrClosed } if p.state != current { - cause := p.closeCause(current) p.access.Unlock() - return zero, nil, false, false, cause + return zero, nil, false, false, net.ErrClosed } if !current.hasShared { p.access.Unlock() @@ -450,16 +379,7 @@ func (p *ConnPool[T]) collectShared(current *connPoolState[T], state *connPoolCo conn := current.shared if !p.options.IsAlive(conn) { - delete(current.all, conn) - var zeroConn T - current.shared = zeroConn - current.hasShared = false - current.sharedClaimed = false - current.sharedCtx = nil - if current.sharedCancel != nil { - current.sharedCancel(net.ErrClosed) - current.sharedCancel = nil - } + p.removeConn(current, conn, net.ErrClosed) p.access.Unlock() p.options.Close(conn, net.ErrClosed) return zero, nil, false, true, nil @@ -472,76 +392,9 @@ func (p *ConnPool[T]) collectShared(current *connPoolState[T], state *connPoolCo return conn, connCtx, created, false, nil } -func (p *ConnPool[T]) dial(ctx context.Context, current *connPoolState[T], dial func(context.Context) (T, error)) (T, error) { - var zero T - - if err := ctx.Err(); err != nil { - return zero, err - } - if cause := context.Cause(current.ctx); cause != nil { - return zero, cause - } - - dialCtx, cancel := context.WithCancelCause(current.ctx) - var ( - stateAccess sync.Mutex - dialComplete bool - ) - stopCancel := context.AfterFunc(ctx, func() { - stateAccess.Lock() - if !dialComplete { - cancel(context.Cause(ctx)) - } - stateAccess.Unlock() - }) - - select { - case <-ctx.Done(): - stateAccess.Lock() - dialComplete = true - stateAccess.Unlock() - stopCancel() - cancel(context.Cause(ctx)) - return zero, ctx.Err() - default: - } - - conn, err := dial(connPoolDialContext{ - Context: dialCtx, - parent: ctx, - }) - stateAccess.Lock() - dialComplete = true - stateAccess.Unlock() - stopCancel() - if err != nil { - if cause := context.Cause(dialCtx); cause != nil { - return zero, cause - } - return zero, err - } - if cause := context.Cause(dialCtx); cause != nil { - p.options.Close(conn, cause) - return zero, cause - } - return conn, nil -} - func (p *ConnPool[T]) closeState(state *connPoolState[T], cause error) { - if state == nil { - return - } - state.cancel(cause) - if state.sharedCancel != nil { - state.sharedCancel(cause) - } for conn := range state.all { p.options.Close(conn, cause) } } - -func (p *ConnPool[T]) closeCause(state *connPoolState[T]) error { - _ = state - return net.ErrClosed -} From f0592034a677b6bc25cf2fe2a2b8607f2040e971 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 11 May 2026 21:58:09 +0800 Subject: [PATCH 84/93] cronet: Fix start cleanup --- .github/CRONET_GO_VERSION | 2 +- go.mod | 62 +++++++++---------- go.sum | 124 +++++++++++++++++++------------------- 3 files changed, 94 insertions(+), 94 deletions(-) diff --git a/.github/CRONET_GO_VERSION b/.github/CRONET_GO_VERSION index dc8aa112e5..1857e6ef36 100644 --- a/.github/CRONET_GO_VERSION +++ b/.github/CRONET_GO_VERSION @@ -1 +1 @@ -92d97ac7e5afaca20df950d085839763e10ccaf6 +e7f6f6f5b7ce226f686f6cb5d068a63da6657ccd diff --git a/go.mod b/go.mod index 1f365c4f69..5c5621574e 100644 --- a/go.mod +++ b/go.mod @@ -31,8 +31,8 @@ require ( github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1 github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a github.com/sagernet/cors v1.2.1 - github.com/sagernet/cronet-go v0.0.0-20260505140453-33a9cf45ce16 - github.com/sagernet/cronet-go/all v0.0.0-20260505140453-33a9cf45ce16 + github.com/sagernet/cronet-go v0.0.0-20260511132223-8584e57d8c9c + github.com/sagernet/cronet-go/all v0.0.0-20260511132223-8584e57d8c9c github.com/sagernet/fswatch v0.1.2 github.com/sagernet/gomobile v0.1.12 github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 @@ -109,35 +109,35 @@ require ( github.com/prometheus-community/pro-bing v0.4.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/safchain/ethtool v0.3.0 // indirect - github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260505135725-f39085455d92 // indirect - github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260505135725-f39085455d92 // indirect - github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260505135725-f39085455d92 // indirect - github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260505135725-f39085455d92 // indirect - github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260505135725-f39085455d92 // indirect - github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260505135725-f39085455d92 // indirect - github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260505135725-f39085455d92 // indirect - github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260505135725-f39085455d92 // indirect - github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260505135725-f39085455d92 // indirect - github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260505135725-f39085455d92 // indirect - github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260505135725-f39085455d92 // indirect - github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260505135725-f39085455d92 // indirect - github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260505135725-f39085455d92 // indirect - github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260505135725-f39085455d92 // indirect - github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260505135725-f39085455d92 // indirect - github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260505135725-f39085455d92 // indirect - github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260505135725-f39085455d92 // indirect - github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260505135725-f39085455d92 // indirect - github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260505135725-f39085455d92 // indirect - github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260505135725-f39085455d92 // indirect - github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260505135725-f39085455d92 // indirect - github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260505135725-f39085455d92 // indirect - github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260505135725-f39085455d92 // indirect - github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260505135725-f39085455d92 // indirect - github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260505135725-f39085455d92 // indirect - github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260505135725-f39085455d92 // indirect - github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260505135725-f39085455d92 // indirect - github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260505135725-f39085455d92 // indirect - github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260505135725-f39085455d92 // indirect + github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260511131435-f6b58ac3ef24 // indirect + github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260511131435-f6b58ac3ef24 // indirect + github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260511131435-f6b58ac3ef24 // indirect + github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260511131435-f6b58ac3ef24 // indirect + github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260511131435-f6b58ac3ef24 // indirect + github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260511131435-f6b58ac3ef24 // indirect + github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260511131435-f6b58ac3ef24 // indirect + github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260511131435-f6b58ac3ef24 // indirect + github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260511131435-f6b58ac3ef24 // indirect + github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260511131435-f6b58ac3ef24 // indirect + github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260511131435-f6b58ac3ef24 // indirect + github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260511131435-f6b58ac3ef24 // indirect + github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260511131435-f6b58ac3ef24 // indirect + github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260511131435-f6b58ac3ef24 // indirect + github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260511131435-f6b58ac3ef24 // indirect + github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260511131435-f6b58ac3ef24 // indirect + github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260511131435-f6b58ac3ef24 // indirect + github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260511131435-f6b58ac3ef24 // indirect + github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260511131435-f6b58ac3ef24 // indirect + github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260511131435-f6b58ac3ef24 // indirect + github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260511131435-f6b58ac3ef24 // indirect + github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260511131435-f6b58ac3ef24 // indirect + github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260511131435-f6b58ac3ef24 // indirect + github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260511131435-f6b58ac3ef24 // indirect + github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260511131435-f6b58ac3ef24 // indirect + github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260511131435-f6b58ac3ef24 // indirect + github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260511131435-f6b58ac3ef24 // indirect + github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260511131435-f6b58ac3ef24 // indirect + github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260511131435-f6b58ac3ef24 // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect github.com/sagernet/nftables v0.3.0-mod.2 // indirect github.com/spf13/pflag v1.0.9 // indirect diff --git a/go.sum b/go.sum index 03a2cfb73c..c02c0fd4b9 100644 --- a/go.sum +++ b/go.sum @@ -168,68 +168,68 @@ github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkk github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ= github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI= -github.com/sagernet/cronet-go v0.0.0-20260505140453-33a9cf45ce16 h1:eq9/CwqNUIwK+9NjTZxVNyqnK0S9jSSNAC8+qZEmKAg= -github.com/sagernet/cronet-go v0.0.0-20260505140453-33a9cf45ce16/go.mod h1:T/mwtrpC4JlWfScw73CmSBvHzIvc7BatQ1MhRr+cYNw= -github.com/sagernet/cronet-go/all v0.0.0-20260505140453-33a9cf45ce16 h1:qZJownM+DPd/0b9hfIOWpwgQDiDGaUCBfnMqJ0HBwhE= -github.com/sagernet/cronet-go/all v0.0.0-20260505140453-33a9cf45ce16/go.mod h1:WowirEFpH2FsoRqJuJEypNd8Rfo/Tx1pe2fi0xI3BTs= -github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260505135725-f39085455d92 h1:/6JbsIpOeCA4Nc5wj+MCxq3zLp0UAt+D9l2eoGuyMBU= -github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260505135725-f39085455d92/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw= -github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260505135725-f39085455d92 h1:m1xmxBeOvWiFE79t765TfQ9r98RjDieCtpXRJ1Kd7hY= -github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260505135725-f39085455d92/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM= -github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260505135725-f39085455d92 h1:n7edaokpJS5NwAwku9f5dwo+PsokobHtArBMRNH4v9I= -github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260505135725-f39085455d92/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc= -github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260505135725-f39085455d92 h1:KF4AjWLN101YTFU5hZeiz+2qSOl5r1ORbqTEThbCJ3g= -github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260505135725-f39085455d92/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ= -github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260505135725-f39085455d92 h1:Eoc4tM+hMA7rRZYhw/CQTTC+vKdhGEr23r4BScS2ARs= -github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260505135725-f39085455d92/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs= -github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260505135725-f39085455d92 h1:yLELJ2IsddWqIhttuvAInIk9FPgsg9EJBZwlgf9pbB0= -github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260505135725-f39085455d92/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0= -github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260505135725-f39085455d92 h1:uNn7Fxl5HLtAElXzuZmg8APaDCHnM54VzMPqbke5x+8= -github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260505135725-f39085455d92/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0= -github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260505135725-f39085455d92 h1:H3oCO8x27JPAsdpgJn39WOVc44gHDVJ6Us1hhY9rr2Y= -github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260505135725-f39085455d92/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4= -github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260505135725-f39085455d92 h1:orfOCA/o1ranJcZ8KPAWCI+IIEgQG45GSK6oztG1Fdc= -github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260505135725-f39085455d92/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo= -github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260505135725-f39085455d92 h1:s9QrD5N0VgHIrphiH9VUvIAoO1AFpvdom6Ulpj/injM= -github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260505135725-f39085455d92/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ= -github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260505135725-f39085455d92 h1:87uZhvYpHuDsWOd9RUQ+E0E2op+pkWn2WuniYHzHfyA= -github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260505135725-f39085455d92/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU= -github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260505135725-f39085455d92 h1:BmZdqfos9MJNZoa3vuSMI+ZhvAKQX6zuTn8k1cMGGjQ= -github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260505135725-f39085455d92/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI= -github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260505135725-f39085455d92 h1:q4AxdtpMXS+v2xsGqR7cLL4YYqZDFGT3kjV/8z6G9xc= -github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260505135725-f39085455d92/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ= -github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260505135725-f39085455d92 h1:QPnGHp7XOYMs5GoSlrePaipTFufPcjKGnUicTBLdT3s= -github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260505135725-f39085455d92/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0= -github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260505135725-f39085455d92 h1:5CpZFrfXMdWvDTAmuTRoSyY6MSHHvuGvt+ZMUztxts8= -github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260505135725-f39085455d92/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s= -github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260505135725-f39085455d92 h1:NpigJfFJxtNQxaArUvHfcmxexV/rxF7ExCZf4K5XSJg= -github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260505135725-f39085455d92/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ= -github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260505135725-f39085455d92 h1:pbhDbfxKjTpDSQyOW7cTHC0JhKGRgkgSH3FPLYKvl8A= -github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260505135725-f39085455d92/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow= -github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260505135725-f39085455d92 h1:Chv2n6Dvqt9owso1NRVM8muhxq1nOQM1jHh0Ip8Ak3g= -github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260505135725-f39085455d92/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk= -github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260505135725-f39085455d92 h1:tNJLTPx61AZ8V3ilCAwkkIlo8rWQGaaawYsrhnyOt+8= -github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260505135725-f39085455d92/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E= -github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260505135725-f39085455d92 h1:2Ggy52AQqWhV+zNjhrUNgn3OFxLffdDkjYretEP5PuM= -github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260505135725-f39085455d92/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8= -github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260505135725-f39085455d92 h1:m16yOJQkkyo5LDnTi8JlMgNzCzinEVuGOXcWrHXFbNw= -github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260505135725-f39085455d92/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w= -github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260505135725-f39085455d92 h1:GUM5+vkxf7jJhLneLGZjMBNGX7z12kDhn65jYreYRTo= -github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260505135725-f39085455d92/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0= -github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260505135725-f39085455d92 h1:6dvaguIY7qF0Mf/GZ3D0KAjz2GA0fRqmSjfzZ9FEWiU= -github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260505135725-f39085455d92/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs= -github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260505135725-f39085455d92 h1:UWURfgvjks/PGG1yO1q48A58kMzpgdQ1e5/w73FtqNQ= -github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260505135725-f39085455d92/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc= -github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260505135725-f39085455d92 h1:fpueIFNflXSXm2PjTOmLR4dhdNUc3CHq0HkyTYdGJ1w= -github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260505135725-f39085455d92/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4= -github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260505135725-f39085455d92 h1:qCFCL4s2TCoScRi2xAkBnp5Jh2n67j0yGIDNy/7KJRI= -github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260505135725-f39085455d92/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc= -github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260505135725-f39085455d92 h1:ho/o2GydFvAjuxM76a2I/1f2kukK/6usa6fGus93ROk= -github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260505135725-f39085455d92/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc= -github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260505135725-f39085455d92 h1:sFbVqze7871HseJVZtAVkRd/6rnk7aF1bO97cX/sV7w= -github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260505135725-f39085455d92/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= -github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260505135725-f39085455d92 h1:2V3z9vsVf/jBm+acgCEDRONpf/zfrGSGXLnD69ub16Y= -github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260505135725-f39085455d92/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw= +github.com/sagernet/cronet-go v0.0.0-20260511132223-8584e57d8c9c h1:Vc1u5tPXVUex0wwYzfcV8TlXIHcKcfDyE9ngfWzh3iw= +github.com/sagernet/cronet-go v0.0.0-20260511132223-8584e57d8c9c/go.mod h1:T/mwtrpC4JlWfScw73CmSBvHzIvc7BatQ1MhRr+cYNw= +github.com/sagernet/cronet-go/all v0.0.0-20260511132223-8584e57d8c9c h1:ALV2gKO0QR9KVvb1t6dmaF9C4TSkXzoXnjzM2vli7U4= +github.com/sagernet/cronet-go/all v0.0.0-20260511132223-8584e57d8c9c/go.mod h1:c6CQ9Mg+YAUfTgG6pavyP+DCoyD4rUXK/NGPEzGBO5w= +github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260511131435-f6b58ac3ef24 h1:Quk9xxZ1bAN7j5j7xvP2ALHk+4cWCIAuxyJsdeJzr+k= +github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260511131435-f6b58ac3ef24/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw= +github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260511131435-f6b58ac3ef24 h1:CPgOjpPtMOK1wxDC8BxVc9+dOsa9E8cZOWXNoVp8jSk= +github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260511131435-f6b58ac3ef24/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM= +github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260511131435-f6b58ac3ef24 h1:6wW+5lzrLGVOaipa8MmWOiaEor4Azm7GSoyxhO10hGQ= +github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260511131435-f6b58ac3ef24/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc= +github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260511131435-f6b58ac3ef24 h1:PGQQoF8LJv1bLPCsZUUgt13mBAvnyBoJZfmYgBn7UO0= +github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260511131435-f6b58ac3ef24/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ= +github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260511131435-f6b58ac3ef24 h1:JHSEpItTeG4hsEVMqsL6+FSximT0/WlzQgZd7lAn9j4= +github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260511131435-f6b58ac3ef24/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs= +github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260511131435-f6b58ac3ef24 h1:TmaXryugo7HW2vIVMfV12rRjCkVcrG4/nmloNirljbA= +github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260511131435-f6b58ac3ef24/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0= +github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260511131435-f6b58ac3ef24 h1:TOx4KM6CIdYJIy4NjYoKDN7Ru8G+NWJQSdF5tFVJtAw= +github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260511131435-f6b58ac3ef24/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0= +github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260511131435-f6b58ac3ef24 h1:l3iMHZqpcn64+VSRu2T5CXcZFEAL7RAnTFhbRux2/Ec= +github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260511131435-f6b58ac3ef24/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4= +github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260511131435-f6b58ac3ef24 h1:0FsWYWgHy4jTeKnaEz+oSVGQFJXvJcDrjq1+N96jxGM= +github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260511131435-f6b58ac3ef24/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo= +github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260511131435-f6b58ac3ef24 h1:2pO/rcMODOH9w5hI4J5xGVu3pv5+HFpfr7piuUZIGAU= +github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260511131435-f6b58ac3ef24/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ= +github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260511131435-f6b58ac3ef24 h1:G9aD7qmMY0UgEHxgoeQib8TKbmHQvtPz0nXIYbG95xA= +github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260511131435-f6b58ac3ef24/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU= +github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260511131435-f6b58ac3ef24 h1:BpJVJuzrjnFz1WStVXM911pYYeufOO0xG+jqhiQCTUo= +github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260511131435-f6b58ac3ef24/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI= +github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260511131435-f6b58ac3ef24 h1:lzlgU8UyqZerT/LO8S2dUdnoAawHFyFM/Ajv6qOhZ0Y= +github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260511131435-f6b58ac3ef24/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ= +github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260511131435-f6b58ac3ef24 h1:+8bXZneY6Ivr4l2d8h4LpQ8ucOpmL71tuTiyIbsXAfY= +github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260511131435-f6b58ac3ef24/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0= +github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260511131435-f6b58ac3ef24 h1:hQYTLrzCny4DAVu+GAnR2m4lFKoP88zp7M5iCFNlLSE= +github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260511131435-f6b58ac3ef24/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s= +github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260511131435-f6b58ac3ef24 h1:WLq7ZKWDEKWuquRt3lUyFFm6ntXrbg56gKu0V1Fu6eQ= +github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260511131435-f6b58ac3ef24/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ= +github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260511131435-f6b58ac3ef24 h1:bUsncZnKl74X2gx1EZpcMiVIJp4ZSheA7fR6YwbIhEs= +github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260511131435-f6b58ac3ef24/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow= +github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260511131435-f6b58ac3ef24 h1:DYX0LmEP2dbwo4MDcVeUJMNDNjaSLG71CX+yupkgRXQ= +github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260511131435-f6b58ac3ef24/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk= +github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260511131435-f6b58ac3ef24 h1:2pPqZD/A93M2Fs7hL7kDpPNg73uS3+mDmYcKTIFKJO4= +github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260511131435-f6b58ac3ef24/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E= +github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260511131435-f6b58ac3ef24 h1:L8irBcbcMe4QMG4FtcGpOOsuLA7inqe55BkhjHX8QeM= +github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260511131435-f6b58ac3ef24/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8= +github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260511131435-f6b58ac3ef24 h1:4voROLdyI4y+QYa60iYdIWn2sdzPMVlKRwnKMUp+2ps= +github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260511131435-f6b58ac3ef24/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w= +github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260511131435-f6b58ac3ef24 h1:rDAGN8YCTKV2U3Tcmi38UmEO3pTwqYc6b59eud3wW58= +github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260511131435-f6b58ac3ef24/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0= +github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260511131435-f6b58ac3ef24 h1:n99kKYMwhyT1xDYVfbHps7m9J2uRi0ovILDw0m/aPho= +github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260511131435-f6b58ac3ef24/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs= +github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260511131435-f6b58ac3ef24 h1:3r2e30PBKckpLVNGOYgmDIGLLG+sWK3bgVRRI4hfwpU= +github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260511131435-f6b58ac3ef24/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc= +github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260511131435-f6b58ac3ef24 h1:SHPkPUIFBkfwWHpHS8/79t2bUvvKfCIkM+yD9gWiOv8= +github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260511131435-f6b58ac3ef24/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4= +github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260511131435-f6b58ac3ef24 h1:2bgmysC7jdQHcN972DBLIQdbOIJ9RzatqpupiEQgd1k= +github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260511131435-f6b58ac3ef24/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc= +github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260511131435-f6b58ac3ef24 h1:7rYi2soP3ox/MKqUM1VCVvIQBEADv6TLdI2SGIgkzu4= +github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260511131435-f6b58ac3ef24/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc= +github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260511131435-f6b58ac3ef24 h1:oGO8sSwbhE+mOj6laS+HuIp4kx56d6eh+t7/fscLOw8= +github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260511131435-f6b58ac3ef24/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= +github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260511131435-f6b58ac3ef24 h1:2htmtFeP9nJKtNUljXLDyWzxFfWM7cnApQY76V8uxPA= +github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260511131435-f6b58ac3ef24/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw= github.com/sagernet/fswatch v0.1.2 h1:/TT7k4mkce1qFPxamLO842WjqBgbTBiXP2mlUjp9PFk= github.com/sagernet/fswatch v0.1.2/go.mod h1:5BpGmpUQVd3Mc5r313HRpvADHRg3/rKn5QbwFteB880= github.com/sagernet/gomobile v0.1.12 h1:XwzjZaclFF96deLqwAgK8gU3w0M2A8qxgDmhV+A0wjg= From 8875b52d51510f10a3ae180e45027cc5a608ce66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 12 May 2026 00:10:40 +0800 Subject: [PATCH 85/93] dns: Refactor reordered pool --- dns/transport/conn_pool.go | 130 +++++++++++++++++++++++++------------ dns/transport/tls.go | 5 +- go.mod | 2 +- 3 files changed, 92 insertions(+), 45 deletions(-) diff --git a/dns/transport/conn_pool.go b/dns/transport/conn_pool.go index ff288b7730..0e7a20a8c3 100644 --- a/dns/transport/conn_pool.go +++ b/dns/transport/conn_pool.go @@ -6,6 +6,8 @@ import ( "sync" "github.com/sagernet/sing/common/x/list" + + "golang.org/x/sync/semaphore" ) type ConnPoolMode int @@ -16,14 +18,18 @@ const ( ) type ConnPoolOptions[T comparable] struct { - Mode ConnPoolMode - IsAlive func(T) bool - Close func(T, error) + Mode ConnPoolMode + // MaxInflight caps concurrent in-progress dials. Only honored in ConnPoolOrdered mode. + MaxInflight int + IsAlive func(T) bool + Close func(T, error) } type ConnPool[T comparable] struct { options ConnPoolOptions[T] + sem *semaphore.Weighted + access sync.Mutex closed bool state *connPoolState[T] @@ -53,10 +59,14 @@ type connPoolConnect[T comparable] struct { } func NewConnPool[T comparable](options ConnPoolOptions[T]) *ConnPool[T] { - return &ConnPool[T]{ + p := &ConnPool[T]{ options: options, - state: newConnPoolState[T](options.Mode), } + if options.Mode == ConnPoolOrdered && options.MaxInflight > 0 { + p.sem = semaphore.NewWeighted(int64(options.MaxInflight)) + } + p.state = newConnPoolState[T](options.Mode) + return p } func newConnPoolState[T comparable](mode ConnPoolMode) *connPoolState[T] { @@ -113,7 +123,7 @@ func (p *ConnPool[T]) Release(conn T, reuse bool) { return } if p.options.Mode == ConnPoolOrdered { - if _, loaded := state.idleElements[conn]; !loaded { + if _, idle := state.idleElements[conn]; !idle { state.idleElements[conn] = state.idle.PushBack(conn) } } @@ -137,6 +147,31 @@ func (p *ConnPool[T]) Invalidate(conn T, cause error) { p.options.Close(conn, cause) } +func (p *ConnPool[T]) acquireSlot(ctx context.Context, state *connPoolState[T]) error { + if p.sem == nil { + return nil + } + acquireCtx, cancel := context.WithCancel(ctx) + stopStateCancel := context.AfterFunc(state.ctx, cancel) + err := p.sem.Acquire(acquireCtx, 1) + stopStateCancel() + cancel() + if err == nil { + return nil + } + ctxErr := ctx.Err() + if ctxErr != nil { + return ctxErr + } + return context.Cause(state.ctx) +} + +func (p *ConnPool[T]) releaseSlot() { + if p.sem != nil { + p.sem.Release(1) + } +} + // removeConn must be called with p.access held. func (p *ConnPool[T]) removeConn(state *connPoolState[T], conn T, cause error) { delete(state.all, conn) @@ -199,56 +234,65 @@ func (p *ConnPool[T]) acquireOrdered(ctx context.Context, dial func(context.Cont } current := p.state if element := current.idle.Front(); element != nil { - conn := current.idle.Remove(element) - delete(current.idleElements, conn) - if p.options.IsAlive(conn) { + idleConn := current.idle.Remove(element) + delete(current.idleElements, idleConn) + if p.options.IsAlive(idleConn) { p.access.Unlock() - return conn, false, nil + return idleConn, false, nil } - delete(current.all, conn) + delete(current.all, idleConn) p.access.Unlock() - p.options.Close(conn, net.ErrClosed) + p.options.Close(idleConn, net.ErrClosed) continue } p.access.Unlock() + return p.dialAndInstall(ctx, current, dial) + } +} - dialCtx, dialCancel := context.WithCancelCause(ctx) - stopStateCancel := context.AfterFunc(current.ctx, func() { - dialCancel(context.Cause(current.ctx)) - }) - conn, err := dial(dialCtx) - stateCancelStopped := stopStateCancel() - dialErr := context.Cause(dialCtx) - if dialErr == nil && !stateCancelStopped { - dialErr = context.Cause(current.ctx) - } - dialCancel(nil) - if err != nil { - if dialErr != nil { - return zero, false, dialErr - } - return zero, false, err - } +func (p *ConnPool[T]) dialAndInstall(ctx context.Context, current *connPoolState[T], dial func(context.Context) (T, error)) (T, bool, error) { + var zero T + err := p.acquireSlot(ctx, current) + if err != nil { + return zero, false, err + } + defer p.releaseSlot() + dialCtx, dialCancel := context.WithCancelCause(ctx) + stopStateCancel := context.AfterFunc(current.ctx, func() { + dialCancel(context.Cause(current.ctx)) + }) + conn, err := dial(dialCtx) + stateCancelStopped := stopStateCancel() + dialErr := context.Cause(dialCtx) + if dialErr == nil && !stateCancelStopped { + dialErr = context.Cause(current.ctx) + } + dialCancel(nil) + if err != nil { if dialErr != nil { - p.options.Close(conn, dialErr) return zero, false, dialErr } + return zero, false, err + } + if dialErr != nil { + p.options.Close(conn, dialErr) + return zero, false, dialErr + } - p.access.Lock() - if p.closed { - p.access.Unlock() - p.options.Close(conn, net.ErrClosed) - return zero, false, net.ErrClosed - } - if p.state != current { - p.access.Unlock() - p.options.Close(conn, net.ErrClosed) - return zero, false, net.ErrClosed - } - current.all[conn] = struct{}{} + p.access.Lock() + if p.closed { p.access.Unlock() - return conn, true, nil + p.options.Close(conn, net.ErrClosed) + return zero, false, net.ErrClosed } + if p.state != current { + p.access.Unlock() + p.options.Close(conn, net.ErrClosed) + return zero, false, net.ErrClosed + } + current.all[conn] = struct{}{} + p.access.Unlock() + return conn, true, nil } func (p *ConnPool[T]) acquireShared(ctx context.Context, dial func(context.Context) (T, error)) (T, context.Context, bool, error) { diff --git a/dns/transport/tls.go b/dns/transport/tls.go index b7ef25fb79..8ce4151444 100644 --- a/dns/transport/tls.go +++ b/dns/transport/tls.go @@ -22,6 +22,8 @@ import ( var _ adapter.DNSTransport = (*TLSTransport)(nil) +const tlsDNSMaxInflight = 8 + func RegisterTLS(registry *dns.TransportRegistry) { dns.RegisterTransport[option.RemoteTLSDNSServerOptions](registry, C.DNSTypeTLS, NewTLS) } @@ -71,7 +73,8 @@ func NewTLSRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer serverAddr: serverAddr, tlsConfig: tlsConfig, connections: NewConnPool(ConnPoolOptions[*tlsDNSConn]{ - Mode: ConnPoolOrdered, + Mode: ConnPoolOrdered, + MaxInflight: tlsDNSMaxInflight, IsAlive: func(conn *tlsDNSConn) bool { return conn != nil }, diff --git a/go.mod b/go.mod index 5c5621574e..923c086e8d 100644 --- a/go.mod +++ b/go.mod @@ -59,6 +59,7 @@ require ( golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 golang.org/x/mod v0.33.0 golang.org/x/net v0.50.0 + golang.org/x/sync v0.19.0 golang.org/x/sys v0.41.0 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 google.golang.org/grpc v1.79.1 @@ -159,7 +160,6 @@ require ( go.uber.org/zap/exp v0.3.0 // indirect go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect golang.org/x/oauth2 v0.34.0 // indirect - golang.org/x/sync v0.19.0 // indirect golang.org/x/term v0.40.0 // indirect golang.org/x/text v0.34.0 // indirect golang.org/x/time v0.11.0 // indirect From 056c45c2cae74612181aee08c1b621e6da32b94e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 11 May 2026 17:04:37 +0800 Subject: [PATCH 86/93] Bump version --- docs/changelog.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 6dea7ec943..899efc4cff 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,8 +4,22 @@ icon: material/alert-decagram #### 1.14.0-alpha.22 +* Add Hysteria Realm service and Hysteria2 NAT traversal support **1** * Fixes and improvements +**1**: + +The new [Hysteria Realm service](/configuration/service/hysteria-realm/) +is a rendezvous service for Hysteria2 NAT traversal. A Hysteria2 server +behind NAT registers its STUN-discovered public addresses on a stable +realm endpoint via the new +[`realm`](/configuration/inbound/hysteria2/#realm) inbound field; +clients query the realm via the new +[`realm`](/configuration/outbound/hysteria2/#realm) outbound field to +learn the server's current addresses and perform UDP hole-punching to +establish a direct QUIC connection. Once hole-punching succeeds, all +proxy traffic flows directly between client and server. + #### 1.14.0-alpha.21 * Allow customizing TUN DNS mode and hijack interface DNS by default **1** From 6af341af271173e085dcbf7af34e265183a310e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 12 May 2026 15:01:49 +0800 Subject: [PATCH 87/93] Fix naive inbound close --- protocol/naive/inbound.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protocol/naive/inbound.go b/protocol/naive/inbound.go index 5613f19600..41f4179889 100644 --- a/protocol/naive/inbound.go +++ b/protocol/naive/inbound.go @@ -140,7 +140,7 @@ func (n *Inbound) Start(stage adapter.StartStage) error { func (n *Inbound) Close() error { return common.Close( - &n.listener, + n.listener, common.PtrOrNil(n.httpServer), n.h3Server, n.tlsConfig, From 96edb9a774f4d4a02d75883664903f1d14c8c119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 12 May 2026 15:01:56 +0800 Subject: [PATCH 88/93] Fix TLS server close --- common/tls/std_server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/tls/std_server.go b/common/tls/std_server.go index b673c367c7..cd515d1117 100644 --- a/common/tls/std_server.go +++ b/common/tls/std_server.go @@ -320,7 +320,7 @@ func (c *STDServerConfig) certificateUpdated(path string) error { } func (c *STDServerConfig) Close() error { - return common.Close(c.certificateProvider, c.acmeService, c.watcher) + return common.Close(c.certificateProvider, c.acmeService, common.PtrOrNil(c.watcher)) } func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) { From f2015697efa8aea534e5583d983da6f338f11287 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 12 May 2026 20:09:53 +0800 Subject: [PATCH 89/93] Fix tailscale crash at start --- protocol/tailscale/endpoint.go | 15 +++++++++++++++ protocol/wireguard/endpoint.go | 24 +++++++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/protocol/tailscale/endpoint.go b/protocol/tailscale/endpoint.go index f265a470d1..59e52079b6 100644 --- a/protocol/tailscale/endpoint.go +++ b/protocol/tailscale/endpoint.go @@ -110,6 +110,7 @@ type Endpoint struct { systemInterfaceName string systemInterfaceMTU uint32 serverStarted bool + started atomic.Bool systemTun tun.Tun systemDialer *dialer.DefaultDialer fallbackTCPCloser func() @@ -429,6 +430,7 @@ func (t *Endpoint) postStart() error { } t.filter = localBackend.ExportFilter() go t.watchState() + t.started.Store(true) return nil } @@ -492,6 +494,7 @@ func (t *Endpoint) watchState() { func (t *Endpoint) Close() error { var err error + t.started.Store(false) if t.serverStarted { err = common.Close(common.PtrOrNil(t.server)) t.serverStarted = false @@ -516,6 +519,9 @@ func (t *Endpoint) DialContext(ctx context.Context, network string, destination case N.NetworkUDP: t.logger.InfoContext(ctx, "outbound packet connection to ", destination) } + if !t.started.Load() { + return nil, E.New("Tailscale is not ready yet") + } if destination.IsDomain() { destinationAddresses, err := t.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{}) if err != nil { @@ -572,6 +578,9 @@ func (t *Endpoint) DialContext(ctx context.Context, network string, destination } func (t *Endpoint) listenPacketWithAddress(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + if !t.started.Load() { + return nil, E.New("Tailscale is not ready yet") + } if t.systemDialer != nil { return t.systemDialer.ListenPacket(ctx, destination) } @@ -639,6 +648,9 @@ func (t *Endpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (n } func (t *Endpoint) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { + if !t.started.Load() { + return nil, E.New("Tailscale is not ready yet") + } tsFilter := t.filter.Load() if tsFilter != nil { var ipProto ipproto.Proto @@ -732,6 +744,9 @@ func (t *Endpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, } func (t *Endpoint) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { + if !t.started.Load() { + return nil, E.New("Tailscale is not ready yet") + } ctx := log.ContextWithNewID(t.ctx) var destination tun.DirectRouteDestination var err error diff --git a/protocol/wireguard/endpoint.go b/protocol/wireguard/endpoint.go index 9fdc4814ae..2975b05cb5 100644 --- a/protocol/wireguard/endpoint.go +++ b/protocol/wireguard/endpoint.go @@ -4,6 +4,7 @@ import ( "context" "net" "net/netip" + "sync/atomic" "time" "github.com/sagernet/sing-box/adapter" @@ -41,6 +42,7 @@ type Endpoint struct { logger logger.ContextLogger localAddresses []netip.Prefix endpoint *wireguard.Endpoint + started atomic.Bool } func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.WireGuardEndpointOptions) (adapter.Endpoint, error) { @@ -120,16 +122,24 @@ func (w *Endpoint) Start(stage adapter.StartStage) error { case adapter.StartStateStart: return w.endpoint.Start(false) case adapter.StartStatePostStart: - return w.endpoint.Start(true) + err := w.endpoint.Start(true) + if err != nil { + return err + } + w.started.Store(true) } return nil } func (w *Endpoint) Close() error { + w.started.Store(false) return w.endpoint.Close() } func (w *Endpoint) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { + if !w.started.Load() { + return nil, E.New("WireGuard is not ready yet") + } var ipVersion uint8 if !destination.IsIPv6() { ipVersion = 4 @@ -210,6 +220,9 @@ func (w *Endpoint) DialContext(ctx context.Context, network string, destination case N.NetworkUDP: w.logger.InfoContext(ctx, "outbound packet connection to ", destination) } + if !w.started.Load() { + return nil, E.New("WireGuard is not ready yet") + } if destination.IsDomain() { destinationAddresses, err := w.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{}) if err != nil { @@ -224,6 +237,9 @@ func (w *Endpoint) DialContext(ctx context.Context, network string, destination func (w *Endpoint) ListenPacketWithDestination(ctx context.Context, destination M.Socksaddr) (net.PacketConn, netip.Addr, error) { w.logger.InfoContext(ctx, "outbound packet connection to ", destination) + if !w.started.Load() { + return nil, netip.Addr{}, E.New("WireGuard is not ready yet") + } if destination.IsDomain() { destinationAddresses, err := w.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{}) if err != nil { @@ -257,9 +273,15 @@ func (w *Endpoint) PreferredDomain(domain string) bool { } func (w *Endpoint) PreferredAddress(address netip.Addr) bool { + if !w.started.Load() { + return false + } return w.endpoint.Lookup(address) != nil } func (w *Endpoint) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { + if !w.started.Load() { + return nil, E.New("WireGuard is not ready yet") + } return w.endpoint.NewDirectRouteConnection(metadata, routeContext, timeout) } From cad899775021eeca9d30e3747833e939e4ae442a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 12 May 2026 20:10:30 +0800 Subject: [PATCH 90/93] realm: Add stun retry and lazy server start --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 923c086e8d..d318268aa3 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( github.com/sagernet/sing v0.8.10-0.20260428084616-2bc976d03e39 github.com/sagernet/sing-cloudflared v0.1.0 github.com/sagernet/sing-mux v0.3.4 - github.com/sagernet/sing-quic v0.6.2-0.20260511084842-9deddf006024 + github.com/sagernet/sing-quic v0.6.2-0.20260512113342-74f3e685d5a7 github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 diff --git a/go.sum b/go.sum index c02c0fd4b9..479c0fb38f 100644 --- a/go.sum +++ b/go.sum @@ -248,8 +248,8 @@ github.com/sagernet/sing-cloudflared v0.1.0 h1:to+2fcCx8zu4X/DirRw9Ihc+FrEZ7oEyI github.com/sagernet/sing-cloudflared v0.1.0/go.mod h1:bH2NKX+NpDTY1Zkxfboxw6MXB/ZywaNLmrDJYgKMJ2Y= github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s= github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= -github.com/sagernet/sing-quic v0.6.2-0.20260511084842-9deddf006024 h1:P1iab6udg2I2igIrn+mNKpPZNcuejSqno3jwJ/94upw= -github.com/sagernet/sing-quic v0.6.2-0.20260511084842-9deddf006024/go.mod h1:+oqD54aHel4ALKkp1hVXWCgLU/EjLojvm6AUzDfvj0I= +github.com/sagernet/sing-quic v0.6.2-0.20260512113342-74f3e685d5a7 h1:nDVG/86RW7LNVk8PZkASi16ntTiusV+n8gpscmnzwH4= +github.com/sagernet/sing-quic v0.6.2-0.20260512113342-74f3e685d5a7/go.mod h1:+oqD54aHel4ALKkp1hVXWCgLU/EjLojvm6AUzDfvj0I= github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE= github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI= github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo= From 44033f9f2eebb6e573141c97408cca3499dec421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 12 May 2026 22:18:24 +0800 Subject: [PATCH 91/93] Fix hysteria2 realm server --- docs/configuration/service/hysteria-realm.md | 7 +++++++ docs/configuration/service/hysteria-realm.zh.md | 7 +++++++ option/hysteria2.go | 1 + protocol/hysteria2/realm.go | 10 +++++++++- 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/configuration/service/hysteria-realm.md b/docs/configuration/service/hysteria-realm.md index a69d130693..0fe01f1e7c 100644 --- a/docs/configuration/service/hysteria-realm.md +++ b/docs/configuration/service/hysteria-realm.md @@ -21,6 +21,9 @@ The realm only carries control-plane signaling. Once hole-punching succeeds, all ... // Listen Fields "tls": {}, + + ... // HTTP2 Fields + "users": [ { "name": "", @@ -35,6 +38,10 @@ The realm only carries control-plane signaling. Once hole-punching succeeds, all See [Listen Fields](/configuration/shared/listen/) for details. +### HTTP2 Fields + +See [HTTP2 Fields](/configuration/shared/http2/) for details. + ### Fields #### tls diff --git a/docs/configuration/service/hysteria-realm.zh.md b/docs/configuration/service/hysteria-realm.zh.md index a73ddbdbf5..c01d744bf4 100644 --- a/docs/configuration/service/hysteria-realm.zh.md +++ b/docs/configuration/service/hysteria-realm.zh.md @@ -21,6 +21,9 @@ Realm 只承载控制信令。打洞成功后,所有代理流量在客户端 ... // 监听字段 "tls": {}, + + ... // HTTP2 字段 + "users": [ { "name": "", @@ -35,6 +38,10 @@ Realm 只承载控制信令。打洞成功后,所有代理流量在客户端 参阅 [监听字段](/zh/configuration/shared/listen/) 了解详情。 +### HTTP2 字段 + +参阅 [HTTP2 字段](/zh/configuration/shared/http2/) 了解详情。 + ### 字段 #### tls diff --git a/option/hysteria2.go b/option/hysteria2.go index b3f1208eea..86730632a9 100644 --- a/option/hysteria2.go +++ b/option/hysteria2.go @@ -152,5 +152,6 @@ type HysteriaRealmUser struct { type HysteriaRealmServiceOptions struct { ListenOptions InboundTLSOptionsContainer + HTTP2Options Users []HysteriaRealmUser `json:"users"` } diff --git a/protocol/hysteria2/realm.go b/protocol/hysteria2/realm.go index 960bfbc914..5b7a82e84f 100644 --- a/protocol/hysteria2/realm.go +++ b/protocol/hysteria2/realm.go @@ -5,6 +5,7 @@ import ( "errors" "net" "net/http" + "time" "github.com/sagernet/sing-box/adapter" boxService "github.com/sagernet/sing-box/adapter/service" @@ -23,6 +24,7 @@ import ( "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/render" "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" ) func RegisterRealmService(registry *boxService.Registry) { @@ -96,7 +98,13 @@ func NewRealmService(ctx context.Context, logger log.ContextLogger, tag string, Listen: options.ListenOptions, }), httpServer: &http.Server{ - Handler: chiRouter, + Handler: h2c.NewHandler(chiRouter, &http2.Server{ + IdleTimeout: time.Duration(options.IdleTimeout), + ReadIdleTimeout: time.Duration(options.KeepAlivePeriod), + MaxUploadBufferPerStream: int32(options.StreamReceiveWindow.Value()), + MaxUploadBufferPerConnection: int32(options.ConnectionReceiveWindow.Value()), + MaxConcurrentStreams: uint32(options.MaxConcurrentStreams), + }), ConnContext: func(ctx context.Context, _ net.Conn) context.Context { return log.ContextWithNewID(ctx) }, From b4d2d897ec06240b992b4042df43bb3e3502c55d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 12 May 2026 20:10:51 +0800 Subject: [PATCH 92/93] Bump version --- docs/changelog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 899efc4cff..e51315d5bc 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,10 @@ icon: material/alert-decagram --- +#### 1.14.0-alpha.23 + +* Fixes and improvements + #### 1.14.0-alpha.22 * Add Hysteria Realm service and Hysteria2 NAT traversal support **1** From e37a708895efb32d365559799790c7c43a14f8b1 Mon Sep 17 00:00:00 2001 From: Arthur Facredyn Date: Thu, 14 May 2026 23:25:44 +0200 Subject: [PATCH 93/93] fakeip: persist metadata on every save interval, not just on Close MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FakeIPSaveMetadataAsync had two bugs that compound: 1. The 10s timer is debounced: every call invokes .Reset(), pushing the deadline another 10s. Under any active workload (every new domain triggers an allocation triggers a save call) the timer never fires — the only path that ever writes metadata is Close(). 2. Even if the timer fired, the closure captures the metadata pointer from the FIRST call. Subsequent calls only Reset() the timer; the closure is never updated. A delayed fire would persist a stale snapshot from the start of the session. Combined, the on-disk fakeip counter only advances on clean shutdown. On mobile that almost never happens (process kill, OOM, phone reboot), so the counter stays at whatever the last clean Close() wrote while the buckets accumulate well past it. Because Store.Create() doesn't check whether the next IP is already allocated — it just calls FakeIPStore which silently overwrites — the next start loads the stale counter and silently clobbers the reverse-map entries between (counter, actual_max]. Forward-map (fakeip_domain*) still points to those IPs for the old domains, so any app that cached the previous DNS answer (real-world: Instagram, hours of TTL) hits a fake IP that now reverse-maps to a different domain → router dials the wrong outbound → TLS cert mismatch → those hosts break while everything else looks fine. Fix: track the latest metadata in a mutex-protected field on CacheFile, let the timer fire on its own schedule (no Reset), and on fire snapshot the latest pointer and clear the timer so the next allocation reschedules. Metadata now tracks reality within one FakeIPMetadataSaveInterval (10s) of any allocation activity. Verified by reproducing on Android: pre-patch, counter on disk stuck at .6 while buckets held .2–.40. Post-patch, counter advances within ~10s of new allocations and survives force-kill / restart cycles with the saved value matching the bucket max, so the next allocation picks an unused IP instead of overwriting an existing one. --- experimental/cachefile/cache.go | 2 ++ experimental/cachefile/fakeip.go | 13 ++++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/experimental/cachefile/cache.go b/experimental/cachefile/cache.go index 3198fc6ae0..a9b3ff294e 100644 --- a/experimental/cachefile/cache.go +++ b/experimental/cachefile/cache.go @@ -54,6 +54,8 @@ type CacheFile struct { DB *bbolt.DB resetAccess sync.Mutex saveMetadataTimer *time.Timer + saveMetadataAccess sync.Mutex + latestFakeIPMetadata *adapter.FakeIPMetadata saveFakeIPAccess sync.RWMutex saveDomain map[netip.Addr]string saveAddress4 map[string]netip.Addr diff --git a/experimental/cachefile/fakeip.go b/experimental/cachefile/fakeip.go index 7a4bd384f1..182999521e 100644 --- a/experimental/cachefile/fakeip.go +++ b/experimental/cachefile/fakeip.go @@ -59,13 +59,20 @@ func (c *CacheFile) FakeIPSaveMetadata(metadata *adapter.FakeIPMetadata) error { } func (c *CacheFile) FakeIPSaveMetadataAsync(metadata *adapter.FakeIPMetadata) { + c.saveMetadataAccess.Lock() + c.latestFakeIPMetadata = metadata if c.saveMetadataTimer == nil { c.saveMetadataTimer = time.AfterFunc(C.FakeIPMetadataSaveInterval, func() { - _ = c.FakeIPSaveMetadata(metadata) + c.saveMetadataAccess.Lock() + m := c.latestFakeIPMetadata + c.saveMetadataTimer = nil + c.saveMetadataAccess.Unlock() + if m != nil { + _ = c.FakeIPSaveMetadata(m) + } }) - } else { - c.saveMetadataTimer.Reset(C.FakeIPMetadataSaveInterval) } + c.saveMetadataAccess.Unlock() } func (c *CacheFile) FakeIPStore(address netip.Addr, domain string) error {