Repository: reF1nd/sing-box Branch: testing Commit: 6b07a229185d Files: 1205 Total size: 5.1 MB Directory structure: gitextract_yza4ly9v/ ├── .fpm_openwrt ├── .fpm_pacman ├── .fpm_systemd ├── .github/ │ ├── CRONET_GO_VERSION │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ └── bug_report_zh.yml │ ├── build_alpine_apk.sh │ ├── build_openwrt_apk.sh │ ├── deb2ipk.sh │ ├── detect_track.sh │ ├── renovate.json │ ├── setup_go_for_macos1013.sh │ ├── setup_go_for_windows7.sh │ ├── update_clients.sh │ ├── update_cronet.sh │ ├── update_cronet_dev.sh │ ├── update_dependencies.sh │ └── workflows/ │ ├── build.yml │ ├── docker.yml │ ├── lint.yml │ ├── linux.yml │ ├── stale.yml │ └── test.yml ├── .gitignore ├── .gitmodules ├── .golangci.yml ├── Dockerfile ├── Dockerfile.binary ├── LICENSE ├── Makefile ├── README.md ├── adapter/ │ ├── certificate/ │ │ ├── adapter.go │ │ ├── manager.go │ │ └── registry.go │ ├── certificate.go │ ├── certificate_darwin.go │ ├── certificate_provider.go │ ├── connections.go │ ├── dns.go │ ├── endpoint/ │ │ ├── adapter.go │ │ ├── manager.go │ │ └── registry.go │ ├── endpoint.go │ ├── experimental.go │ ├── fakeip.go │ ├── fakeip_metadata.go │ ├── handler.go │ ├── http.go │ ├── inbound/ │ │ ├── adapter.go │ │ ├── manager.go │ │ └── registry.go │ ├── inbound.go │ ├── inbound_test.go │ ├── lifecycle.go │ ├── lifecycle_legacy.go │ ├── neighbor.go │ ├── network.go │ ├── outbound/ │ │ ├── adapter.go │ │ ├── manager.go │ │ └── registry.go │ ├── outbound.go │ ├── platform.go │ ├── prestart.go │ ├── router.go │ ├── rule.go │ ├── service/ │ │ ├── adapter.go │ │ ├── manager.go │ │ └── registry.go │ ├── service.go │ ├── ssm.go │ ├── tailscale.go │ ├── time.go │ ├── upstream.go │ ├── upstream_legacy.go │ └── v2ray.go ├── box.go ├── cmd/ │ ├── internal/ │ │ ├── app_store_connect/ │ │ │ └── main.go │ │ ├── build/ │ │ │ └── main.go │ │ ├── build_libbox/ │ │ │ └── main.go │ │ ├── build_shared/ │ │ │ ├── sdk.go │ │ │ └── tag.go │ │ ├── format_docs/ │ │ │ └── main.go │ │ ├── protogen/ │ │ │ └── main.go │ │ ├── read_tag/ │ │ │ └── main.go │ │ ├── tun_bench/ │ │ │ └── main.go │ │ ├── update_android_version/ │ │ │ └── main.go │ │ ├── update_apple_version/ │ │ │ └── main.go │ │ └── update_certificates/ │ │ └── main.go │ └── sing-box/ │ ├── cmd.go │ ├── cmd_check.go │ ├── cmd_format.go │ ├── cmd_generate.go │ ├── cmd_generate_ech.go │ ├── cmd_generate_tls.go │ ├── cmd_generate_vapid.go │ ├── cmd_generate_wireguard.go │ ├── cmd_geoip.go │ ├── cmd_geoip_export.go │ ├── cmd_geoip_list.go │ ├── cmd_geoip_lookup.go │ ├── cmd_geosite.go │ ├── cmd_geosite_export.go │ ├── cmd_geosite_list.go │ ├── cmd_geosite_lookup.go │ ├── cmd_geosite_matcher.go │ ├── cmd_merge.go │ ├── cmd_rule_set.go │ ├── cmd_rule_set_compile.go │ ├── cmd_rule_set_convert.go │ ├── cmd_rule_set_decompile.go │ ├── cmd_rule_set_format.go │ ├── cmd_rule_set_match.go │ ├── cmd_rule_set_merge.go │ ├── cmd_rule_set_upgrade.go │ ├── cmd_run.go │ ├── cmd_tools.go │ ├── cmd_tools_connect.go │ ├── cmd_tools_fetch.go │ ├── cmd_tools_fetch_http3.go │ ├── cmd_tools_fetch_http3_stub.go │ ├── cmd_tools_networkquality.go │ ├── cmd_tools_stun.go │ ├── cmd_tools_synctime.go │ ├── cmd_version.go │ ├── generate_completions.go │ └── main.go ├── common/ │ ├── badtls/ │ │ ├── raw_conn.go │ │ ├── raw_half_conn.go │ │ ├── read_wait.go │ │ ├── read_wait_stub.go │ │ ├── registry.go │ │ └── registry_utls.go │ ├── badversion/ │ │ ├── version.go │ │ ├── version_json.go │ │ └── version_test.go │ ├── certificate/ │ │ ├── anchors_darwin.h │ │ ├── anchors_darwin.m │ │ ├── chrome.go │ │ ├── chrome.pem │ │ ├── mozilla.go │ │ ├── mozilla.pem │ │ ├── store.go │ │ ├── store_darwin.go │ │ └── store_other.go │ ├── compatible/ │ │ └── map.go │ ├── convertor/ │ │ └── adguard/ │ │ ├── convertor.go │ │ └── convertor_test.go │ ├── dialer/ │ │ ├── default.go │ │ ├── default_parallel_interface.go │ │ ├── default_parallel_network.go │ │ ├── detour.go │ │ ├── dialer.go │ │ ├── resolve.go │ │ ├── router.go │ │ ├── tfo.go │ │ └── wireguard.go │ ├── geoip/ │ │ └── reader.go │ ├── geosite/ │ │ ├── compat_test.go │ │ ├── geosite_test.go │ │ ├── reader.go │ │ ├── rule.go │ │ └── writer.go │ ├── httpclient/ │ │ ├── apple_transport_darwin.go │ │ ├── apple_transport_darwin.h │ │ ├── apple_transport_darwin.m │ │ ├── apple_transport_darwin_test.go │ │ ├── apple_transport_stub.go │ │ ├── client.go │ │ ├── context.go │ │ ├── helpers.go │ │ ├── helpers_test.go │ │ ├── http1_transport.go │ │ ├── http2_config.go │ │ ├── http2_fallback_transport.go │ │ ├── http2_fallback_transport_test.go │ │ ├── http2_transport.go │ │ ├── http3_transport.go │ │ ├── http3_transport_stub.go │ │ ├── http3_transport_test.go │ │ ├── managed_transport.go │ │ └── manager.go │ ├── interrupt/ │ │ ├── conn.go │ │ ├── context.go │ │ └── group.go │ ├── ja3/ │ │ ├── LICENSE │ │ ├── README.md │ │ ├── error.go │ │ ├── ja3.go │ │ └── parser.go │ ├── ktls/ │ │ ├── ktls.go │ │ ├── ktls_alert.go │ │ ├── ktls_cipher_suites_linux.go │ │ ├── ktls_close.go │ │ ├── ktls_const.go │ │ ├── ktls_handshake_messages.go │ │ ├── ktls_key_update.go │ │ ├── ktls_linux.go │ │ ├── ktls_prf.go │ │ ├── ktls_read.go │ │ ├── ktls_read_wait.go │ │ ├── ktls_stub_nolinkname.go │ │ ├── ktls_stub_nonlinux.go │ │ ├── ktls_stub_oldgo.go │ │ └── ktls_write.go │ ├── listener/ │ │ ├── listener.go │ │ ├── listener_tcp.go │ │ └── listener_udp.go │ ├── mux/ │ │ ├── client.go │ │ └── router.go │ ├── networkquality/ │ │ ├── http.go │ │ ├── http3.go │ │ ├── http3_stub.go │ │ └── networkquality.go │ ├── pipelistener/ │ │ └── listener.go │ ├── process/ │ │ ├── searcher.go │ │ ├── searcher_android.go │ │ ├── searcher_darwin.go │ │ ├── searcher_darwin_shared.go │ │ ├── searcher_linux.go │ │ ├── searcher_linux_shared.go │ │ ├── searcher_linux_shared_test.go │ │ ├── searcher_stub.go │ │ └── searcher_windows.go │ ├── proxybridge/ │ │ └── bridge.go │ ├── redir/ │ │ ├── redir_darwin.go │ │ ├── redir_linux.go │ │ ├── redir_other.go │ │ ├── tproxy_linux.go │ │ └── tproxy_other.go │ ├── schannel/ │ │ ├── doc.go │ │ ├── schannel_windows.go │ │ ├── schannel_windows_test.go │ │ ├── syscall_windows.go │ │ ├── types_windows.go │ │ └── zsyscall_windows.go │ ├── settings/ │ │ ├── proxy_android.go │ │ ├── proxy_darwin.go │ │ ├── proxy_linux.go │ │ ├── proxy_stub.go │ │ ├── proxy_windows.go │ │ ├── system_proxy.go │ │ ├── wifi.go │ │ ├── wifi_linux.go │ │ ├── wifi_linux_connman.go │ │ ├── wifi_linux_iwd.go │ │ ├── wifi_linux_nm.go │ │ ├── wifi_linux_wpa.go │ │ ├── wifi_stub.go │ │ └── wifi_windows.go │ ├── sniff/ │ │ ├── bittorrent.go │ │ ├── bittorrent_test.go │ │ ├── dns.go │ │ ├── dns_test.go │ │ ├── dtls.go │ │ ├── dtls_test.go │ │ ├── http.go │ │ ├── http_test.go │ │ ├── internal/ │ │ │ └── qtls/ │ │ │ └── qtls.go │ │ ├── ntp.go │ │ ├── ntp_test.go │ │ ├── quic.go │ │ ├── quic_blacklist.go │ │ ├── quic_capture_test.go │ │ ├── quic_test.go │ │ ├── rdp.go │ │ ├── rdp_test.go │ │ ├── sniff.go │ │ ├── ssh.go │ │ ├── ssh_test.go │ │ ├── stun.go │ │ ├── stun_test.go │ │ └── tls.go │ ├── srs/ │ │ ├── binary.go │ │ ├── compat_test.go │ │ ├── ip_cidr.go │ │ └── ip_set.go │ ├── stun/ │ │ └── stun.go │ ├── taskmonitor/ │ │ └── monitor.go │ ├── tls/ │ │ ├── acme.go │ │ ├── acme_logger.go │ │ ├── acme_stub.go │ │ ├── apple_client.go │ │ ├── apple_client_platform.go │ │ ├── apple_client_platform_benchmark_test.go │ │ ├── apple_client_platform_darwin.h │ │ ├── apple_client_platform_darwin.m │ │ ├── apple_client_platform_dispatch_test.go │ │ ├── apple_client_platform_dispatch_testhelper_darwin.go │ │ ├── apple_client_platform_test.go │ │ ├── apple_client_stub.go │ │ ├── client.go │ │ ├── common.go │ │ ├── config.go │ │ ├── ech.go │ │ ├── ech_shared.go │ │ ├── ech_tag_stub.go │ │ ├── ktls.go │ │ ├── mkcert.go │ │ ├── reality_client.go │ │ ├── reality_server.go │ │ ├── reality_stub.go │ │ ├── server.go │ │ ├── std_client.go │ │ ├── std_server.go │ │ ├── system_client.go │ │ ├── system_client_engine.go │ │ ├── time_wrapper.go │ │ ├── utls_client.go │ │ ├── utls_client_test.go │ │ ├── utls_stub.go │ │ ├── windows_client.go │ │ ├── windows_client_stub.go │ │ └── windows_client_test.go │ ├── tlsfragment/ │ │ ├── conn.go │ │ ├── conn_test.go │ │ ├── index.go │ │ ├── index_test.go │ │ ├── wait_darwin.go │ │ ├── wait_linux.go │ │ ├── wait_stub.go │ │ └── wait_windows.go │ ├── tlsspoof/ │ │ ├── README.md │ │ ├── client_hello.go │ │ ├── endpoints.go │ │ ├── integration_darwin_test.go │ │ ├── integration_linux_test.go │ │ ├── integration_test.go │ │ ├── integration_tls_test.go │ │ ├── integration_unix_test.go │ │ ├── integration_windows_test.go │ │ ├── packet.go │ │ ├── packet_darwin.go │ │ ├── raw_darwin.go │ │ ├── raw_linux.go │ │ ├── raw_stub.go │ │ ├── raw_unix.go │ │ ├── raw_windows.go │ │ ├── spoof.go │ │ └── testdata_test.go │ ├── uot/ │ │ └── router.go │ ├── urltest/ │ │ └── urltest.go │ └── windivert/ │ ├── address_test.go │ ├── assets/ │ │ ├── LICENSE.txt │ │ ├── WinDivert32.sys │ │ └── WinDivert64.sys │ ├── assets_386.go │ ├── assets_amd64.go │ ├── assets_unsupported.go │ ├── driver_windows.go │ ├── filter.go │ ├── filter_test.go │ ├── handle_windows.go │ ├── handle_windows_test.go │ ├── integration_windows_test.go │ └── windivert.go ├── constant/ │ ├── certificate.go │ ├── cgo.go │ ├── cgo_disabled.go │ ├── dhcp.go │ ├── dns.go │ ├── err.go │ ├── goos/ │ │ ├── gengoos.go │ │ ├── goos.go │ │ ├── zgoos_aix.go │ │ ├── zgoos_android.go │ │ ├── zgoos_darwin.go │ │ ├── zgoos_dragonfly.go │ │ ├── zgoos_freebsd.go │ │ ├── zgoos_hurd.go │ │ ├── zgoos_illumos.go │ │ ├── zgoos_ios.go │ │ ├── zgoos_js.go │ │ ├── zgoos_linux.go │ │ ├── zgoos_netbsd.go │ │ ├── zgoos_openbsd.go │ │ ├── zgoos_plan9.go │ │ ├── zgoos_solaris.go │ │ ├── zgoos_windows.go │ │ └── zgoos_zos.go │ ├── hysteria2.go │ ├── network.go │ ├── os.go │ ├── path.go │ ├── path_unix.go │ ├── protocol.go │ ├── proxy.go │ ├── quic.go │ ├── quic_stub.go │ ├── rule.go │ ├── speed.go │ ├── time.go │ ├── timeout.go │ ├── tls.go │ ├── v2ray.go │ └── version.go ├── daemon/ │ ├── deprecated.go │ ├── instance.go │ ├── platform.go │ ├── started_service.go │ ├── started_service.pb.go │ ├── started_service.proto │ └── started_service_grpc.pb.go ├── debug.go ├── debug_http.go ├── debug_stub.go ├── debug_unix.go ├── dns/ │ ├── client.go │ ├── client_log.go │ ├── client_truncate.go │ ├── extension_edns0_subnet.go │ ├── rcode.go │ ├── repro_test.go │ ├── router.go │ ├── router_test.go │ ├── transport/ │ │ ├── conn_pool.go │ │ ├── dhcp/ │ │ │ ├── dhcp.go │ │ │ └── dhcp_shared.go │ │ ├── fakeip/ │ │ │ ├── fakeip.go │ │ │ ├── memory.go │ │ │ └── store.go │ │ ├── hosts/ │ │ │ ├── hosts.go │ │ │ ├── hosts_file.go │ │ │ ├── hosts_test.go │ │ │ ├── hosts_unix.go │ │ │ ├── hosts_windows.go │ │ │ └── testdata/ │ │ │ └── hosts │ │ ├── https.go │ │ ├── https_transport.go │ │ ├── local/ │ │ │ ├── local.go │ │ │ ├── local_darwin_cgo.go │ │ │ ├── local_darwin_stun.go │ │ │ ├── local_dhcp.go │ │ │ ├── local_neighbor.go │ │ │ ├── local_nodhcp.go │ │ │ ├── local_other.go │ │ │ ├── local_resolved.go │ │ │ ├── local_resolved_linux.go │ │ │ ├── local_resolved_stub.go │ │ │ ├── local_shared.go │ │ │ ├── resolv.go │ │ │ ├── resolv_default.go │ │ │ ├── resolv_test.go │ │ │ ├── resolv_unix.go │ │ │ └── resolv_windows.go │ │ ├── mdns/ │ │ │ └── mdns.go │ │ ├── quic/ │ │ │ ├── http3.go │ │ │ └── quic.go │ │ ├── tcp.go │ │ ├── tls.go │ │ └── udp.go │ ├── transport_adapter.go │ ├── transport_dialer.go │ ├── transport_manager.go │ └── transport_registry.go ├── docs/ │ ├── CNAME │ ├── changelog.md │ ├── clients/ │ │ ├── android/ │ │ │ ├── features.md │ │ │ └── index.md │ │ ├── apple/ │ │ │ ├── features.md │ │ │ └── index.md │ │ ├── general.md │ │ ├── index.md │ │ ├── index.zh.md │ │ └── privacy.md │ ├── configuration/ │ │ ├── certificate/ │ │ │ ├── index.md │ │ │ └── index.zh.md │ │ ├── dns/ │ │ │ ├── fakeip.md │ │ │ ├── fakeip.zh.md │ │ │ ├── index.md │ │ │ ├── index.zh.md │ │ │ ├── rule.md │ │ │ ├── rule.zh.md │ │ │ ├── rule_action.md │ │ │ ├── rule_action.zh.md │ │ │ └── server/ │ │ │ ├── dhcp.md │ │ │ ├── dhcp.zh.md │ │ │ ├── fakeip.md │ │ │ ├── fakeip.zh.md │ │ │ ├── hosts.md │ │ │ ├── hosts.zh.md │ │ │ ├── http3.md │ │ │ ├── http3.zh.md │ │ │ ├── https.md │ │ │ ├── https.zh.md │ │ │ ├── index.md │ │ │ ├── index.zh.md │ │ │ ├── legacy.md │ │ │ ├── legacy.zh.md │ │ │ ├── local.md │ │ │ ├── local.zh.md │ │ │ ├── mdns.md │ │ │ ├── mdns.zh.md │ │ │ ├── quic.md │ │ │ ├── quic.zh.md │ │ │ ├── resolved.md │ │ │ ├── resolved.zh.md │ │ │ ├── tailscale.md │ │ │ ├── tailscale.zh.md │ │ │ ├── tcp.md │ │ │ ├── tcp.zh.md │ │ │ ├── tls.md │ │ │ ├── tls.zh.md │ │ │ ├── udp.md │ │ │ └── udp.zh.md │ │ ├── endpoint/ │ │ │ ├── index.md │ │ │ ├── index.zh.md │ │ │ ├── tailscale.md │ │ │ ├── tailscale.zh.md │ │ │ ├── wireguard.md │ │ │ └── wireguard.zh.md │ │ ├── experimental/ │ │ │ ├── cache-file.md │ │ │ ├── cache-file.zh.md │ │ │ ├── clash-api.md │ │ │ ├── clash-api.zh.md │ │ │ ├── index.md │ │ │ ├── index.zh.md │ │ │ ├── v2ray-api.md │ │ │ └── v2ray-api.zh.md │ │ ├── inbound/ │ │ │ ├── anytls.md │ │ │ ├── anytls.zh.md │ │ │ ├── cloudflared.md │ │ │ ├── cloudflared.zh.md │ │ │ ├── direct.md │ │ │ ├── direct.zh.md │ │ │ ├── http.md │ │ │ ├── http.zh.md │ │ │ ├── hysteria.md │ │ │ ├── hysteria.zh.md │ │ │ ├── hysteria2.md │ │ │ ├── hysteria2.zh.md │ │ │ ├── index.md │ │ │ ├── index.zh.md │ │ │ ├── mixed.md │ │ │ ├── mixed.zh.md │ │ │ ├── naive.md │ │ │ ├── naive.zh.md │ │ │ ├── redirect.md │ │ │ ├── redirect.zh.md │ │ │ ├── shadowsocks.md │ │ │ ├── shadowsocks.zh.md │ │ │ ├── shadowtls.md │ │ │ ├── shadowtls.zh.md │ │ │ ├── socks.md │ │ │ ├── socks.zh.md │ │ │ ├── tproxy.md │ │ │ ├── tproxy.zh.md │ │ │ ├── trojan.md │ │ │ ├── trojan.zh.md │ │ │ ├── tuic.md │ │ │ ├── tuic.zh.md │ │ │ ├── tun.md │ │ │ ├── tun.zh.md │ │ │ ├── vless.md │ │ │ ├── vless.zh.md │ │ │ ├── vmess.md │ │ │ └── vmess.zh.md │ │ ├── index.md │ │ ├── index.zh.md │ │ ├── log/ │ │ │ ├── index.md │ │ │ └── index.zh.md │ │ ├── ntp/ │ │ │ ├── index.md │ │ │ └── index.zh.md │ │ ├── outbound/ │ │ │ ├── anytls.md │ │ │ ├── anytls.zh.md │ │ │ ├── block.md │ │ │ ├── block.zh.md │ │ │ ├── direct.md │ │ │ ├── direct.zh.md │ │ │ ├── dns.md │ │ │ ├── dns.zh.md │ │ │ ├── http.md │ │ │ ├── http.zh.md │ │ │ ├── hysteria.md │ │ │ ├── hysteria.zh.md │ │ │ ├── hysteria2.md │ │ │ ├── hysteria2.zh.md │ │ │ ├── index.md │ │ │ ├── index.zh.md │ │ │ ├── naive.md │ │ │ ├── naive.zh.md │ │ │ ├── selector.md │ │ │ ├── selector.zh.md │ │ │ ├── shadowsocks.md │ │ │ ├── shadowsocks.zh.md │ │ │ ├── shadowtls.md │ │ │ ├── shadowtls.zh.md │ │ │ ├── socks.md │ │ │ ├── socks.zh.md │ │ │ ├── ssh.md │ │ │ ├── ssh.zh.md │ │ │ ├── tor.md │ │ │ ├── tor.zh.md │ │ │ ├── trojan.md │ │ │ ├── trojan.zh.md │ │ │ ├── tuic.md │ │ │ ├── tuic.zh.md │ │ │ ├── urltest.md │ │ │ ├── urltest.zh.md │ │ │ ├── vless.md │ │ │ ├── vless.zh.md │ │ │ ├── vmess.md │ │ │ ├── vmess.zh.md │ │ │ ├── wireguard.md │ │ │ └── wireguard.zh.md │ │ ├── route/ │ │ │ ├── geoip.md │ │ │ ├── geoip.zh.md │ │ │ ├── geosite.md │ │ │ ├── geosite.zh.md │ │ │ ├── index.md │ │ │ ├── index.zh.md │ │ │ ├── rule.md │ │ │ ├── rule.zh.md │ │ │ ├── rule_action.md │ │ │ ├── rule_action.zh.md │ │ │ ├── sniff.md │ │ │ └── sniff.zh.md │ │ ├── rule-set/ │ │ │ ├── adguard.md │ │ │ ├── adguard.zh.md │ │ │ ├── headless-rule.md │ │ │ ├── headless-rule.zh.md │ │ │ ├── index.md │ │ │ ├── index.zh.md │ │ │ ├── source-format.md │ │ │ └── source-format.zh.md │ │ ├── service/ │ │ │ ├── ccm.md │ │ │ ├── ccm.zh.md │ │ │ ├── derp.md │ │ │ ├── derp.zh.md │ │ │ ├── hysteria-realm.md │ │ │ ├── hysteria-realm.zh.md │ │ │ ├── index.md │ │ │ ├── index.zh.md │ │ │ ├── ocm.md │ │ │ ├── ocm.zh.md │ │ │ ├── resolved.md │ │ │ ├── resolved.zh.md │ │ │ ├── ssm-api.md │ │ │ └── ssm-api.zh.md │ │ └── shared/ │ │ ├── certificate-provider/ │ │ │ ├── acme.md │ │ │ ├── acme.zh.md │ │ │ ├── cloudflare-origin-ca.md │ │ │ ├── cloudflare-origin-ca.zh.md │ │ │ ├── index.md │ │ │ ├── index.zh.md │ │ │ ├── tailscale.md │ │ │ └── tailscale.zh.md │ │ ├── dial.md │ │ ├── dial.zh.md │ │ ├── dns01_challenge.md │ │ ├── dns01_challenge.zh.md │ │ ├── http-client.md │ │ ├── http-client.zh.md │ │ ├── http2.md │ │ ├── http2.zh.md │ │ ├── listen.md │ │ ├── listen.zh.md │ │ ├── multiplex.md │ │ ├── multiplex.zh.md │ │ ├── neighbor.md │ │ ├── neighbor.zh.md │ │ ├── pre-match.md │ │ ├── pre-match.zh.md │ │ ├── quic.md │ │ ├── quic.zh.md │ │ ├── tcp-brutal.md │ │ ├── tcp-brutal.zh.md │ │ ├── tls.md │ │ ├── tls.zh.md │ │ ├── udp-over-tcp.md │ │ ├── udp-over-tcp.zh.md │ │ ├── v2ray-transport.md │ │ ├── v2ray-transport.zh.md │ │ ├── wifi-state.md │ │ └── wifi-state.zh.md │ ├── deprecated.md │ ├── deprecated.zh.md │ ├── index.md │ ├── index.zh.md │ ├── installation/ │ │ ├── build-from-source.md │ │ ├── build-from-source.zh.md │ │ ├── docker.md │ │ ├── docker.zh.md │ │ ├── package-manager.md │ │ ├── package-manager.zh.md │ │ └── tools/ │ │ ├── arch-install.sh │ │ ├── deb-install.sh │ │ ├── install.sh │ │ ├── rpm-install.sh │ │ └── sing-box.repo │ ├── manual/ │ │ ├── misc/ │ │ │ └── tunnelvision.md │ │ ├── proxy/ │ │ │ ├── client.md │ │ │ └── server.md │ │ └── proxy-protocol/ │ │ ├── hysteria2.md │ │ ├── shadowsocks.md │ │ └── trojan.md │ ├── migration.md │ ├── migration.zh.md │ ├── sponsors.md │ ├── support.md │ └── support.zh.md ├── experimental/ │ ├── cachefile/ │ │ ├── cache.go │ │ ├── dns_cache.go │ │ ├── fakeip.go │ │ └── rdrc.go │ ├── clashapi/ │ │ ├── api_meta.go │ │ ├── api_meta_group.go │ │ ├── api_meta_upgrade.go │ │ ├── cache.go │ │ ├── common.go │ │ ├── configs.go │ │ ├── connections.go │ │ ├── ctxkeys.go │ │ ├── dns.go │ │ ├── errors.go │ │ ├── profile.go │ │ ├── provider.go │ │ ├── proxies.go │ │ ├── ruleprovider.go │ │ ├── rules.go │ │ ├── script.go │ │ ├── server.go │ │ ├── server_fs.go │ │ ├── server_resources.go │ │ └── trafficontrol/ │ │ ├── manager.go │ │ └── tracker.go │ ├── clashapi.go │ ├── deprecated/ │ │ ├── constants.go │ │ ├── manager.go │ │ └── stderr.go │ ├── libbox/ │ │ ├── build_info.go │ │ ├── command.go │ │ ├── command_client.go │ │ ├── command_server.go │ │ ├── command_types.go │ │ ├── command_types_nq.go │ │ ├── command_types_stun.go │ │ ├── command_types_tailscale.go │ │ ├── command_types_tailscale_ping.go │ │ ├── config.go │ │ ├── connection_owner_darwin.go │ │ ├── debug.go │ │ ├── deprecated.go │ │ ├── dns.go │ │ ├── fdroid.go │ │ ├── fdroid_mirrors.go │ │ ├── ffi.json │ │ ├── http.go │ │ ├── internal/ │ │ │ ├── oomprofile/ │ │ │ │ ├── builder.go │ │ │ │ ├── defs_darwin_amd64.go │ │ │ │ ├── defs_darwin_arm64.go │ │ │ │ ├── linkname.go │ │ │ │ ├── mapping_darwin.go │ │ │ │ ├── mapping_linux.go │ │ │ │ ├── mapping_windows.go │ │ │ │ ├── oomprofile.go │ │ │ │ └── protobuf.go │ │ │ └── procfs/ │ │ │ └── procfs.go │ │ ├── iterator.go │ │ ├── link_flags_stub.go │ │ ├── link_flags_unix.go │ │ ├── log.go │ │ ├── monitor.go │ │ ├── neighbor.go │ │ ├── neighbor_darwin.go │ │ ├── neighbor_linux.go │ │ ├── neighbor_stub.go │ │ ├── neighbor_unix.go │ │ ├── networkquality.go │ │ ├── oom_report.go │ │ ├── panic.go │ │ ├── pidfd_android.go │ │ ├── platform.go │ │ ├── pprof.go │ │ ├── profile_import.go │ │ ├── remote_profile.go │ │ ├── report.go │ │ ├── semver.go │ │ ├── semver_test.go │ │ ├── service.go │ │ ├── service_other.go │ │ ├── service_windows.go │ │ ├── setup.go │ │ ├── signal_handler_darwin.go │ │ ├── signal_handler_stub.go │ │ ├── stun.go │ │ ├── tun.go │ │ ├── tun_darwin.go │ │ ├── tun_name_darwin.go │ │ ├── tun_name_linux.go │ │ └── tun_name_other.go │ ├── locale/ │ │ ├── locale.go │ │ └── locale_zh_CN.go │ ├── v2rayapi/ │ │ ├── server.go │ │ ├── stats.go │ │ ├── stats.pb.go │ │ ├── stats.proto │ │ └── stats_grpc.pb.go │ └── v2rayapi.go ├── go.mod ├── go.sum ├── include/ │ ├── acme.go │ ├── acme_stub.go │ ├── ccm.go │ ├── ccm_stub.go │ ├── ccm_stub_darwin.go │ ├── clashapi.go │ ├── clashapi_stub.go │ ├── cloudflared.go │ ├── cloudflared_stub.go │ ├── dhcp.go │ ├── dhcp_stub.go │ ├── naive_outbound.go │ ├── naive_outbound_stub.go │ ├── ocm.go │ ├── ocm_stub.go │ ├── oom_killer.go │ ├── quic.go │ ├── quic_stub.go │ ├── registry.go │ ├── tailscale.go │ ├── tailscale_stub.go │ ├── tz_android.go │ ├── tz_ios.go │ ├── v2rayapi.go │ ├── v2rayapi_stub.go │ ├── wireguard.go │ └── wireguard_stub.go ├── log/ │ ├── export.go │ ├── factory.go │ ├── format.go │ ├── id.go │ ├── level.go │ ├── log.go │ ├── nop.go │ ├── observable.go │ ├── override.go │ └── platform.go ├── mkdocs.yml ├── option/ │ ├── acme.go │ ├── anytls.go │ ├── ccm.go │ ├── certificate.go │ ├── certificate_provider.go │ ├── cloudflared.go │ ├── debug.go │ ├── direct.go │ ├── dns.go │ ├── dns_record.go │ ├── dns_record_test.go │ ├── dns_test.go │ ├── endpoint.go │ ├── experimental.go │ ├── group.go │ ├── http.go │ ├── hysteria.go │ ├── hysteria2.go │ ├── inbound.go │ ├── multiplex.go │ ├── naive.go │ ├── ntp.go │ ├── ocm.go │ ├── oom_killer.go │ ├── options.go │ ├── origin_ca.go │ ├── outbound.go │ ├── platform.go │ ├── redir.go │ ├── resolved.go │ ├── route.go │ ├── rule.go │ ├── rule_action.go │ ├── rule_action_test.go │ ├── rule_dns.go │ ├── rule_nested.go │ ├── rule_nested_test.go │ ├── rule_set.go │ ├── service.go │ ├── shadowsocks.go │ ├── shadowsocksr.go │ ├── shadowtls.go │ ├── simple.go │ ├── ssh.go │ ├── ssmapi.go │ ├── tailscale.go │ ├── tls.go │ ├── tls_acme.go │ ├── tor.go │ ├── trojan.go │ ├── tuic.go │ ├── tun.go │ ├── tun_platform.go │ ├── types.go │ ├── udp_over_tcp.go │ ├── v2ray.go │ ├── v2ray_transport.go │ ├── vless.go │ ├── vmess.go │ └── wireguard.go ├── protocol/ │ ├── anytls/ │ │ ├── inbound.go │ │ └── outbound.go │ ├── block/ │ │ └── outbound.go │ ├── cloudflare/ │ │ └── inbound.go │ ├── direct/ │ │ ├── inbound.go │ │ └── outbound.go │ ├── dns/ │ │ ├── handle.go │ │ └── outbound.go │ ├── group/ │ │ ├── selector.go │ │ └── urltest.go │ ├── http/ │ │ ├── inbound.go │ │ └── outbound.go │ ├── hysteria/ │ │ ├── inbound.go │ │ ├── outbound.go │ │ └── quic.go │ ├── hysteria2/ │ │ ├── inbound.go │ │ ├── outbound.go │ │ ├── realm.go │ │ └── realm_server.go │ ├── mixed/ │ │ └── inbound.go │ ├── naive/ │ │ ├── inbound.go │ │ ├── inbound_conn.go │ │ ├── outbound.go │ │ └── quic/ │ │ └── inbound_init.go │ ├── redirect/ │ │ ├── redirect.go │ │ └── tproxy.go │ ├── shadowsocks/ │ │ ├── inbound.go │ │ ├── inbound_multi.go │ │ ├── inbound_relay.go │ │ └── outbound.go │ ├── shadowtls/ │ │ ├── inbound.go │ │ └── outbound.go │ ├── socks/ │ │ ├── inbound.go │ │ └── outbound.go │ ├── ssh/ │ │ └── outbound.go │ ├── tailscale/ │ │ ├── certificate_provider.go │ │ ├── dns_transport.go │ │ ├── endpoint.go │ │ ├── hostinfo_tvos.go │ │ ├── ping.go │ │ ├── status.go │ │ ├── tun_device_unix.go │ │ └── tun_device_windows.go │ ├── tor/ │ │ └── outbound.go │ ├── trojan/ │ │ ├── inbound.go │ │ └── outbound.go │ ├── tuic/ │ │ ├── inbound.go │ │ └── outbound.go │ ├── tun/ │ │ └── inbound.go │ ├── vless/ │ │ ├── inbound.go │ │ └── outbound.go │ ├── vmess/ │ │ ├── inbound.go │ │ └── outbound.go │ └── wireguard/ │ └── endpoint.go ├── release/ │ ├── DEFAULT_BUILD_TAGS │ ├── DEFAULT_BUILD_TAGS_OTHERS │ ├── DEFAULT_BUILD_TAGS_WINDOWS │ ├── LDFLAGS │ ├── completions/ │ │ ├── sing-box.bash │ │ ├── sing-box.fish │ │ └── sing-box.zsh │ ├── config/ │ │ ├── config.json │ │ ├── openwrt.conf │ │ ├── openwrt.init │ │ ├── openwrt.keep │ │ ├── openwrt.prerm │ │ ├── sing-box-split-dns.xml │ │ ├── sing-box.confd │ │ ├── sing-box.initd │ │ ├── sing-box.postinst │ │ ├── sing-box.rules │ │ ├── sing-box.service │ │ ├── sing-box.sysusers │ │ └── sing-box@.service │ └── local/ │ ├── common.sh │ ├── debug.sh │ ├── enable.sh │ ├── install.sh │ ├── install_go.sh │ ├── reinstall.sh │ ├── sing-box.service │ ├── uninstall.sh │ └── update.sh ├── route/ │ ├── conn.go │ ├── dns.go │ ├── neighbor_resolver_darwin.go │ ├── neighbor_resolver_hostname.go │ ├── neighbor_resolver_lease.go │ ├── neighbor_resolver_linux.go │ ├── neighbor_resolver_parse.go │ ├── neighbor_resolver_platform.go │ ├── neighbor_resolver_stub.go │ ├── neighbor_table_darwin.go │ ├── neighbor_table_linux.go │ ├── network.go │ ├── platform_searcher.go │ ├── process_cache.go │ ├── route.go │ ├── router.go │ ├── rule/ │ │ ├── match_state.go │ │ ├── rule_abstract.go │ │ ├── rule_abstract_test.go │ │ ├── rule_action.go │ │ ├── rule_default.go │ │ ├── rule_default_interface_address.go │ │ ├── rule_dns.go │ │ ├── rule_headless.go │ │ ├── rule_interface_address.go │ │ ├── rule_item_adguard.go │ │ ├── rule_item_auth_user.go │ │ ├── rule_item_cidr.go │ │ ├── rule_item_clash_mode.go │ │ ├── rule_item_client.go │ │ ├── rule_item_domain.go │ │ ├── rule_item_domain_keyword.go │ │ ├── rule_item_domain_regex.go │ │ ├── rule_item_inbound.go │ │ ├── rule_item_ip_accept_any.go │ │ ├── rule_item_ip_is_private.go │ │ ├── rule_item_ipversion.go │ │ ├── rule_item_network.go │ │ ├── rule_item_network_is_constrained.go │ │ ├── rule_item_network_is_expensive.go │ │ ├── rule_item_network_type.go │ │ ├── rule_item_outbound.go │ │ ├── rule_item_package_name.go │ │ ├── rule_item_package_name_regex.go │ │ ├── rule_item_port.go │ │ ├── rule_item_port_range.go │ │ ├── rule_item_preferred_by.go │ │ ├── rule_item_preferred_by_dns.go │ │ ├── rule_item_process_name.go │ │ ├── rule_item_process_path.go │ │ ├── rule_item_process_path_regex.go │ │ ├── rule_item_protocol.go │ │ ├── rule_item_query_type.go │ │ ├── rule_item_response_rcode.go │ │ ├── rule_item_response_record.go │ │ ├── rule_item_rule_set.go │ │ ├── rule_item_rule_set_test.go │ │ ├── rule_item_source_hostname.go │ │ ├── rule_item_source_mac_address.go │ │ ├── rule_item_user.go │ │ ├── rule_item_user_id.go │ │ ├── rule_item_wifi_bssid.go │ │ ├── rule_item_wifi_ssid.go │ │ ├── rule_nested_action.go │ │ ├── rule_nested_action_test.go │ │ ├── rule_network_interface_address.go │ │ ├── rule_set.go │ │ ├── rule_set_local.go │ │ ├── rule_set_remote.go │ │ ├── rule_set_semantics_test.go │ │ └── rule_set_update_validation_test.go │ └── rule_conds.go ├── service/ │ ├── acme/ │ │ ├── service.go │ │ └── stub.go │ ├── ccm/ │ │ ├── credential.go │ │ ├── credential_darwin.go │ │ ├── credential_other.go │ │ ├── service.go │ │ ├── service_usage.go │ │ └── service_user.go │ ├── derp/ │ │ └── service.go │ ├── ocm/ │ │ ├── credential.go │ │ ├── credential_darwin.go │ │ ├── credential_other.go │ │ ├── service.go │ │ ├── service_usage.go │ │ ├── service_user.go │ │ └── service_websocket.go │ ├── oomkiller/ │ │ ├── badcleanup.go │ │ ├── badcleanup_stub.go │ │ ├── policy.go │ │ ├── service.go │ │ ├── service_darwin.go │ │ ├── service_stub.go │ │ ├── timer.go │ │ └── timer_darwin.go │ ├── origin_ca/ │ │ └── service.go │ ├── resolved/ │ │ ├── resolve1.go │ │ ├── service.go │ │ ├── stub.go │ │ └── transport.go │ └── ssmapi/ │ ├── api.go │ ├── cache.go │ ├── server.go │ ├── traffic.go │ └── user.go ├── test/ │ ├── box_test.go │ ├── brutal_test.go │ ├── clash_darwin_test.go │ ├── clash_other_test.go │ ├── clash_test.go │ ├── config/ │ │ ├── hysteria-client.json │ │ ├── hysteria-server.json │ │ ├── hysteria2-client.yml │ │ ├── hysteria2-server.yml │ │ ├── naive-nginx.conf │ │ ├── naive-quic.json │ │ ├── naive.json │ │ ├── nginx.conf │ │ ├── shadowsocksr.json │ │ ├── trojan.json │ │ ├── tuic-client.json │ │ ├── tuic-server.json │ │ ├── vless-server.json │ │ ├── vless-tls-client.json │ │ ├── vless-tls-server.json │ │ ├── vmess-client.json │ │ ├── vmess-grpc-client.json │ │ ├── vmess-grpc-server.json │ │ ├── vmess-mux-client.json │ │ ├── vmess-server.json │ │ ├── vmess-ws-client.json │ │ ├── vmess-ws-server.json │ │ └── wireguard.conf │ ├── direct_test.go │ ├── docker_test.go │ ├── domain_inbound_test.go │ ├── ech_test.go │ ├── go.mod │ ├── go.sum │ ├── http_test.go │ ├── hysteria2_test.go │ ├── hysteria_test.go │ ├── inbound_detour_test.go │ ├── ktls_test.go │ ├── mkcert.go │ ├── mux_cool_test.go │ ├── mux_test.go │ ├── naive_self_test.go │ ├── naive_test.go │ ├── reality_test.go │ ├── shadowsocks_legacy_test.go │ ├── shadowsocks_test.go │ ├── shadowtls_test.go │ ├── socks_test.go │ ├── ss_plugin_test.go │ ├── tfo_test.go │ ├── tls_test.go │ ├── trojan_test.go │ ├── tuic_test.go │ ├── v2ray_api_test.go │ ├── v2ray_grpc_test.go │ ├── v2ray_httpupgrade_test.go │ ├── v2ray_transport_test.go │ ├── v2ray_ws_test.go │ ├── vmess_test.go │ └── wrapper_test.go └── transport/ ├── simple-obfs/ │ ├── README.md │ ├── http.go │ └── tls.go ├── sip003/ │ ├── args.go │ ├── obfs.go │ ├── plugin.go │ └── v2ray.go ├── trojan/ │ ├── mux.go │ ├── protocol.go │ ├── protocol_wait.go │ ├── service.go │ └── service_wait.go ├── v2ray/ │ ├── grpc.go │ ├── grpc_lite.go │ ├── quic.go │ └── transport.go ├── v2raygrpc/ │ ├── client.go │ ├── conn.go │ ├── credentials/ │ │ ├── credentials.go │ │ ├── spiffe.go │ │ ├── syscallconn.go │ │ └── util.go │ ├── custom_name.go │ ├── server.go │ ├── stream.pb.go │ ├── stream.proto │ ├── stream_grpc.pb.go │ └── tls_credentials.go ├── v2raygrpclite/ │ ├── client.go │ ├── conn.go │ └── server.go ├── v2rayhttp/ │ ├── client.go │ ├── conn.go │ ├── force_close.go │ ├── pool.go │ └── server.go ├── v2rayhttpupgrade/ │ ├── client.go │ └── server.go ├── v2rayquic/ │ ├── client.go │ ├── init.go │ ├── server.go │ └── stream.go ├── v2raywebsocket/ │ ├── client.go │ ├── conn.go │ ├── server.go │ └── writer.go └── wireguard/ ├── client_bind.go ├── device.go ├── device_nat.go ├── device_stack.go ├── device_stack_gonet.go ├── device_stack_stub.go ├── device_system.go ├── device_system_stack.go ├── endpoint.go └── endpoint_options.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .fpm_openwrt ================================================ -s dir --name sing-box --category net --license GPL-3.0-or-later --description "The universal proxy platform." --url "https://sing-box.sagernet.org/" --maintainer "nekohasekai " --no-deb-generate-changes --config-files /etc/config/sing-box --config-files /etc/sing-box/config.json --depends ca-bundle --depends kmod-inet-diag --depends kmod-tun --depends firewall4 --depends kmod-nft-queue --before-remove release/config/openwrt.prerm release/config/config.json=/etc/sing-box/config.json release/config/openwrt.conf=/etc/config/sing-box release/config/openwrt.init=/etc/init.d/sing-box release/config/openwrt.keep=/lib/upgrade/keep.d/sing-box release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish release/completions/sing-box.zsh=/usr/share/zsh/site-functions/_sing-box LICENSE=/usr/share/licenses/sing-box/LICENSE ================================================ FILE: .fpm_pacman ================================================ -s dir --name sing-box --category net --license GPL-3.0-or-later --description "The universal proxy platform." --url "https://sing-box.sagernet.org/" --maintainer "nekohasekai " --config-files etc/sing-box/config.json --after-install release/config/sing-box.postinst release/config/config.json=/etc/sing-box/config.json release/config/sing-box.service=/usr/lib/systemd/system/sing-box.service release/config/sing-box@.service=/usr/lib/systemd/system/sing-box@.service release/config/sing-box.sysusers=/usr/lib/sysusers.d/sing-box.conf release/config/sing-box.rules=usr/share/polkit-1/rules.d/sing-box.rules release/config/sing-box-split-dns.xml=/usr/share/dbus-1/system.d/sing-box-split-dns.conf release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish release/completions/sing-box.zsh=/usr/share/zsh/site-functions/_sing-box LICENSE=/usr/share/licenses/sing-box/LICENSE ================================================ FILE: .fpm_systemd ================================================ -s dir --name sing-box --category net --license GPL-3.0-or-later --description "The universal proxy platform." --url "https://sing-box.sagernet.org/" --vendor SagerNet --maintainer "nekohasekai " --deb-field "Bug: https://github.com/SagerNet/sing-box/issues" --no-deb-generate-changes --config-files /etc/sing-box/config.json --after-install release/config/sing-box.postinst release/config/config.json=/etc/sing-box/config.json release/config/sing-box.service=/usr/lib/systemd/system/sing-box.service release/config/sing-box@.service=/usr/lib/systemd/system/sing-box@.service release/config/sing-box.sysusers=/usr/lib/sysusers.d/sing-box.conf release/config/sing-box.rules=usr/share/polkit-1/rules.d/sing-box.rules release/config/sing-box-split-dns.xml=/usr/share/dbus-1/system.d/sing-box-split-dns.conf release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish release/completions/sing-box.zsh=/usr/share/zsh/site-functions/_sing-box LICENSE=/usr/share/licenses/sing-box/LICENSE ================================================ FILE: .github/CRONET_GO_VERSION ================================================ 2faf34666c2cc8234f10f2ab6d4c4d6104d34ae2 ================================================ FILE: .github/FUNDING.yml ================================================ github: nekohasekai ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: Bug report description: "Report sing-box bug" body: - type: dropdown attributes: label: Operating system description: Operating system type options: - iOS - macOS - Apple tvOS - Android - Windows - Linux - Others validations: required: true - type: input attributes: label: System version description: Please provide the operating system version validations: required: true - type: dropdown attributes: label: Installation type description: Please provide the sing-box installation type options: - Original sing-box Command Line - sing-box for iOS Graphical Client - sing-box for macOS Graphical Client - sing-box for Apple tvOS Graphical Client - sing-box for Android Graphical Client - Third-party graphical clients that advertise themselves as using sing-box (Windows) - Third-party graphical clients that advertise themselves as using sing-box (Android) - Others validations: required: true - type: input attributes: description: Graphical client version label: If you are using a graphical client, please provide the version of the client. - type: textarea attributes: label: Version description: If you are using the original command line program, please provide the output of the `sing-box version` command. render: shell - type: textarea attributes: label: Description description: Please provide a detailed description of the error. validations: required: true - type: textarea attributes: label: Reproduction description: Please provide the steps to reproduce the error, including the configuration files and procedures that can locally (not dependent on the remote server) reproduce the error using the original command line program of sing-box. validations: required: true - type: textarea attributes: label: Logs description: |- In addition, if you encounter a crash with the graphical client, please also provide crash logs. For Apple platform clients, please check `Settings - View Service Log` for crash logs. For the Android client, please check the `/sdcard/Android/data/io.nekohasekai.sfa/files/stderr.log` file for crash logs. render: shell - type: checkboxes id: supporter attributes: label: Supporter options: - label: I am a [sponsor](https://github.com/sponsors/nekohasekai/) - type: checkboxes attributes: label: Integrity requirements description: |- Please check all of the following options to prove that you have read and understood the requirements, otherwise this issue will be closed. Sing-box is not a project aimed to please users who can't make any meaningful contributions and gain unethical influence. If you deceive here to deliberately waste the time of the developers, you will be permanently blocked. options: - label: I confirm that I have read the documentation, understand the meaning of all the configuration items I wrote, and did not pile up seemingly useful options or default values. required: true - label: I confirm that I have provided the server and client configuration files and process that can be reproduced locally, instead of a complicated client configuration file that has been stripped of sensitive data. required: true - label: I confirm that I have provided the simplest configuration that can be used to reproduce the error I reported, instead of depending on remote servers, TUN, graphical interface clients, or other closed-source software. required: true - label: I confirm that I have provided the complete configuration files and logs, rather than just providing parts I think are useful out of confidence in my own intelligence. required: true ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report_zh.yml ================================================ name: 错误反馈 description: "提交 sing-box 漏洞" body: - type: dropdown attributes: label: 操作系统 description: 请提供操作系统类型 options: - iOS - macOS - Apple tvOS - Android - Windows - Linux - 其他 validations: required: true - type: input attributes: label: 系统版本 description: 请提供操作系统版本 validations: required: true - type: dropdown attributes: label: 安装类型 description: 请提供该 sing-box 安装类型 options: - sing-box 原始命令行程序 - sing-box for iOS 图形客户端程序 - sing-box for macOS 图形客户端程序 - sing-box for Apple tvOS 图形客户端程序 - sing-box for Android 图形客户端程序 - 宣传使用 sing-box 的第三方图形客户端程序 (Windows) - 宣传使用 sing-box 的第三方图形客户端程序 (Android) - 其他 validations: required: true - type: input attributes: description: 图形客户端版本 label: 如果您使用图形客户端程序,请提供该程序版本。 - type: textarea attributes: label: 版本 description: 如果您使用原始命令行程序,请提供 `sing-box version` 命令的输出。 render: shell - type: textarea attributes: label: 描述 description: 请提供错误的详细描述。 validations: required: true - type: textarea attributes: label: 重现方式 description: 请提供重现错误的步骤,必须包括可以在本地(不依赖与远程服务器)使用 sing-box 原始命令行程序重现错误的配置文件与流程。 validations: required: true - type: textarea attributes: label: 日志 description: |- 此外,如果您遭遇图形界面应用程序崩溃,请附加提供崩溃日志。 对于 Apple 平台图形客户端程序,请检查 `Settings - View Service Log` 以导出崩溃日志。 对于 Android 图形客户端程序,请检查 `/sdcard/Android/data/io.nekohasekai.sfa/files/stderr.log` 文件以导出崩溃日志。 render: shell - type: checkboxes id: supporter attributes: label: 支持我们 options: - label: 我已经 [赞助](https://github.com/sponsors/nekohasekai/) - type: checkboxes attributes: label: 完整性要求 description: |- 请勾选以下所有选项以证明您已经阅读并理解了以下要求,否则该 issue 将被关闭。 sing-box 不是讨好无法作出任何意义上的贡献的最终用户并获取非道德影响力的项目,如果您在此处欺骗以故意浪费开发者的时间,您将被永久封锁。 options: - label: 我保证阅读了文档,了解所有我编写的配置文件项的含义,而不是大量堆砌看似有用的选项或默认值。 required: true - label: 我保证提供了可以在本地重现该问题的服务器、客户端配置文件与流程,而不是一个脱敏的复杂客户端配置文件。 required: true - label: 我保证提供了可用于重现我报告的错误的最简配置,而不是依赖远程服务器、TUN、图形界面客户端或者其他闭源软件。 required: true - label: 我保证提供了完整的配置文件与日志,而不是出于对自身智力的自信而仅提供了部分认为有用的部分。 required: true ================================================ FILE: .github/build_alpine_apk.sh ================================================ #!/usr/bin/env bash set -e -o pipefail prepare_apk_root() { # apk mkpkg resolves owner/group names through --root/etc/{passwd,group}. APK_ROOT_DIR=$(mktemp -d) mkdir -p "$APK_ROOT_DIR/etc" cat > "$APK_ROOT_DIR/etc/passwd" < "$APK_ROOT_DIR/etc/group" < " exit 1 fi PROJECT=$(cd "$(dirname "$0")/.."; pwd) # Convert version to APK format: # 1.13.0-beta.8 -> 1.13.0_beta8-r0 # 1.13.0-rc.3 -> 1.13.0_rc3-r0 # 1.13.0 -> 1.13.0-r0 APK_VERSION=$(echo "$VERSION" | sed -E 's/-([a-z]+)\.([0-9]+)/_\1\2/') APK_VERSION="${APK_VERSION}-r0" ROOT_DIR=$(mktemp -d) prepare_apk_root trap 'rm -rf "$ROOT_DIR" "$APK_ROOT_DIR"' EXIT # Binary install -Dm755 "$BINARY_PATH" "$ROOT_DIR/usr/bin/sing-box" # Config files install -Dm644 "$PROJECT/release/config/config.json" "$ROOT_DIR/etc/sing-box/config.json" install -Dm755 "$PROJECT/release/config/sing-box.initd" "$ROOT_DIR/etc/init.d/sing-box" install -Dm644 "$PROJECT/release/config/sing-box.confd" "$ROOT_DIR/etc/conf.d/sing-box" # Service files install -Dm644 "$PROJECT/release/config/sing-box.service" "$ROOT_DIR/usr/lib/systemd/system/sing-box.service" install -Dm644 "$PROJECT/release/config/sing-box@.service" "$ROOT_DIR/usr/lib/systemd/system/sing-box@.service" # Completions install -Dm644 "$PROJECT/release/completions/sing-box.bash" "$ROOT_DIR/usr/share/bash-completion/completions/sing-box.bash" install -Dm644 "$PROJECT/release/completions/sing-box.fish" "$ROOT_DIR/usr/share/fish/vendor_completions.d/sing-box.fish" install -Dm644 "$PROJECT/release/completions/sing-box.zsh" "$ROOT_DIR/usr/share/zsh/site-functions/_sing-box" # License install -Dm644 "$PROJECT/LICENSE" "$ROOT_DIR/usr/share/licenses/sing-box/LICENSE" # APK metadata PACKAGES_DIR="$ROOT_DIR/lib/apk/packages" mkdir -p "$PACKAGES_DIR" # .conffiles cat > "$PACKAGES_DIR/.conffiles" <<'EOF' /etc/conf.d/sing-box /etc/init.d/sing-box /etc/sing-box/config.json EOF # .conffiles_static (sha256 checksums) while IFS= read -r conffile; do sha256=$(sha256sum "$ROOT_DIR$conffile" | cut -d' ' -f1) echo "$conffile $sha256" done < "$PACKAGES_DIR/.conffiles" > "$PACKAGES_DIR/.conffiles_static" # .list (all files, excluding lib/apk/packages/ metadata) (cd "$ROOT_DIR" && find . -type f -o -type l) \ | sed 's|^\./|/|' \ | grep -v '^/lib/apk/packages/' \ | sort > "$PACKAGES_DIR/.list" # Build APK apk --root "$APK_ROOT_DIR" mkpkg \ --info "name:sing-box" \ --info "version:${APK_VERSION}" \ --info "description:The universal proxy platform." \ --info "arch:${ARCHITECTURE}" \ --info "license:GPL-3.0-or-later with name use or association addition" \ --info "origin:sing-box" \ --info "url:https://sing-box.sagernet.org/" \ --info "maintainer:nekohasekai " \ --files "$ROOT_DIR" \ --output "$OUTPUT_PATH" ================================================ FILE: .github/build_openwrt_apk.sh ================================================ #!/usr/bin/env bash set -e -o pipefail prepare_apk_root() { # apk mkpkg resolves owner/group names through --root/etc/{passwd,group}. APK_ROOT_DIR=$(mktemp -d) mkdir -p "$APK_ROOT_DIR/etc" cat > "$APK_ROOT_DIR/etc/passwd" < "$APK_ROOT_DIR/etc/group" < " exit 1 fi PROJECT=$(cd "$(dirname "$0")/.."; pwd) # Convert version to APK format: # 1.13.0-beta.8 -> 1.13.0_beta8-r0 # 1.13.0-rc.3 -> 1.13.0_rc3-r0 # 1.13.0 -> 1.13.0-r0 APK_VERSION=$(echo "$VERSION" | sed -E 's/-([a-z]+)\.([0-9]+)/_\1\2/') APK_VERSION="${APK_VERSION}-r0" ROOT_DIR=$(mktemp -d) prepare_apk_root trap 'rm -rf "$ROOT_DIR" "$APK_ROOT_DIR"' EXIT # Binary install -Dm755 "$BINARY_PATH" "$ROOT_DIR/usr/bin/sing-box" # Config files install -Dm644 "$PROJECT/release/config/config.json" "$ROOT_DIR/etc/sing-box/config.json" install -Dm644 "$PROJECT/release/config/openwrt.conf" "$ROOT_DIR/etc/config/sing-box" install -Dm755 "$PROJECT/release/config/openwrt.init" "$ROOT_DIR/etc/init.d/sing-box" install -Dm644 "$PROJECT/release/config/openwrt.keep" "$ROOT_DIR/lib/upgrade/keep.d/sing-box" # Completions install -Dm644 "$PROJECT/release/completions/sing-box.bash" "$ROOT_DIR/usr/share/bash-completion/completions/sing-box.bash" install -Dm644 "$PROJECT/release/completions/sing-box.fish" "$ROOT_DIR/usr/share/fish/vendor_completions.d/sing-box.fish" install -Dm644 "$PROJECT/release/completions/sing-box.zsh" "$ROOT_DIR/usr/share/zsh/site-functions/_sing-box" # License install -Dm644 "$PROJECT/LICENSE" "$ROOT_DIR/usr/share/licenses/sing-box/LICENSE" # APK metadata PACKAGES_DIR="$ROOT_DIR/lib/apk/packages" mkdir -p "$PACKAGES_DIR" # .conffiles cat > "$PACKAGES_DIR/.conffiles" <<'EOF' /etc/config/sing-box /etc/sing-box/config.json EOF # .conffiles_static (sha256 checksums) while IFS= read -r conffile; do sha256=$(sha256sum "$ROOT_DIR$conffile" | cut -d' ' -f1) echo "$conffile $sha256" done < "$PACKAGES_DIR/.conffiles" > "$PACKAGES_DIR/.conffiles_static" # .list (all files, excluding lib/apk/packages/ metadata) (cd "$ROOT_DIR" && find . -type f -o -type l) \ | sed 's|^\./|/|' \ | grep -v '^/lib/apk/packages/' \ | sort > "$PACKAGES_DIR/.list" # Build APK apk --root "$APK_ROOT_DIR" mkpkg \ --info "name:sing-box" \ --info "version:${APK_VERSION}" \ --info "description:The universal proxy platform." \ --info "arch:${ARCHITECTURE}" \ --info "license:GPL-3.0-or-later" \ --info "origin:sing-box" \ --info "url:https://sing-box.sagernet.org/" \ --info "maintainer:nekohasekai " \ --info "depends:ca-bundle kmod-inet-diag kmod-tun firewall4 kmod-nft-queue" \ --info "provider-priority:100" \ --script "pre-deinstall:${PROJECT}/release/config/openwrt.prerm" \ --files "$ROOT_DIR" \ --output "$OUTPUT_PATH" ================================================ FILE: .github/deb2ipk.sh ================================================ #!/usr/bin/env bash # mod from https://gist.github.com/pldubouilh/c5703052986bfdd404005951dee54683 set -e -o pipefail PROJECT=$(dirname "$0")/../.. TMP_PATH=`mktemp -d` cp $2 $TMP_PATH pushd $TMP_PATH DEB_NAME=`ls *.deb` ar x $DEB_NAME mkdir control pushd control tar xf ../control.tar.gz rm md5sums sed "s/Architecture:\\ \w*/Architecture:\\ $1/g" ./control -i cat control tar czf ../control.tar.gz ./* popd DEB_NAME=${DEB_NAME%.deb} tar czf $DEB_NAME.ipk control.tar.gz data.tar.gz debian-binary popd cp $TMP_PATH/$DEB_NAME.ipk $3 rm -r $TMP_PATH ================================================ FILE: .github/detect_track.sh ================================================ #!/usr/bin/env bash set -euo pipefail branches=$(git branch -r --contains HEAD) if echo "$branches" | grep -q 'origin/stable'; then track=stable elif echo "$branches" | grep -q 'origin/testing'; then track=testing elif echo "$branches" | grep -q 'origin/oldstable'; then track=oldstable else echo "ERROR: HEAD is not on any known release branch (stable/testing/oldstable)" >&2 exit 1 fi if [[ "$track" == "stable" ]]; then tag=$(git describe --tags --exact-match HEAD 2>/dev/null || true) if [[ -n "$tag" && "$tag" == *"-"* ]]; then track=beta fi fi case "$track" in stable) name=sing-box; docker_tag=latest ;; beta) name=sing-box-beta; docker_tag=latest-beta ;; testing) name=sing-box-testing; docker_tag=latest-testing ;; oldstable) name=sing-box-oldstable; docker_tag=latest-oldstable ;; esac echo "track=${track} name=${name} docker_tag=${docker_tag}" >&2 echo "TRACK=${track}" >> "$GITHUB_ENV" echo "NAME=${name}" >> "$GITHUB_ENV" echo "DOCKER_TAG=${docker_tag}" >> "$GITHUB_ENV" ================================================ FILE: .github/renovate.json ================================================ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "commitMessagePrefix": "[dependencies]", "extends": [ "config:base", ":disableRateLimiting" ], "baseBranches": [ "unstable" ], "golang": { "enabled": false }, "packageRules": [ { "matchManagers": [ "github-actions" ], "groupName": "github-actions" }, { "matchManagers": [ "dockerfile" ], "groupName": "Dockerfile" } ] } ================================================ FILE: .github/setup_go_for_macos1013.sh ================================================ #!/usr/bin/env bash set -euo pipefail VERSION="1.25.9" PATCH_COMMITS=( "afe69d3cec1c6dcf0f1797b20546795730850070" "1ed289b0cf87dc5aae9c6fe1aa5f200a83412938" ) CURL_ARGS=( -fL --silent --show-error ) if [[ -n "${GITHUB_TOKEN:-}" ]]; then CURL_ARGS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}") fi mkdir -p "$HOME/go" cd "$HOME/go" wget "https://dl.google.com/go/go${VERSION}.darwin-arm64.tar.gz" tar -xzf "go${VERSION}.darwin-arm64.tar.gz" #cp -a go go_bootstrap mv go go_osx cd go_osx # these patch URLs only work on golang1.25.x # that means after golang1.26 release it must be changed # see: https://github.com/SagerNet/go/commits/release-branch.go1.25/ # revert: # 33d3f603c1: "cmd/link/internal/ld: use 12.0.0 OS/SDK versions for macOS linking" # 937368f84e: "crypto/x509: change how we retrieve chains on darwin" for patch_commit in "${PATCH_COMMITS[@]}"; do curl "${CURL_ARGS[@]}" "https://github.com/SagerNet/go/commit/${patch_commit}.diff" | patch --verbose -p 1 done # Rebuild is not needed: we build with CGO_ENABLED=1, so Apple's external # linker handles LC_BUILD_VERSION via MACOSX_DEPLOYMENT_TARGET, and the # stdlib (crypto/x509) is compiled from patched src automatically. #cd src #GOROOT_BOOTSTRAP="$HOME/go/go_bootstrap" ./make.bash #cd ../.. #rm -rf go_bootstrap "go${VERSION}.darwin-arm64.tar.gz" ================================================ FILE: .github/setup_go_for_windows7.sh ================================================ #!/usr/bin/env bash set -euo pipefail VERSION="1.25.9" PATCH_COMMITS=( "466f6c7a29bc098b0d4c987b803c779222894a11" "1bdabae205052afe1dadb2ad6f1ba612cdbc532a" "a90777dcf692dd2168577853ba743b4338721b06" "f6bddda4e8ff58a957462a1a09562924d5f3d05c" "bed309eff415bcb3c77dd4bc3277b682b89a388d" "34b899c2fb39b092db4fa67c4417e41dc046be4b" ) CURL_ARGS=( -fL --silent --show-error ) if [[ -n "${GITHUB_TOKEN:-}" ]]; then CURL_ARGS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}") fi mkdir -p "$HOME/go" cd "$HOME/go" wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz" tar -xzf "go${VERSION}.linux-amd64.tar.gz" mv go go_win7 cd go_win7 # modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557 # these patch URLs only work on golang1.25.x # that means after golang1.26 release it must be changed # see: https://github.com/MetaCubeX/go/commits/release-branch.go1.25/ # revert: # 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng" # 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7" # 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround" # a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries" # fixes: # bed309eff415bcb3c77dd4bc3277b682b89a388d: "Fix os.RemoveAll not working on Windows7" # 34b899c2fb39b092db4fa67c4417e41dc046be4b: "Revert \"os: remove 5ms sleep on Windows in (*Process).Wait\"" for patch_commit in "${PATCH_COMMITS[@]}"; do curl "${CURL_ARGS[@]}" "https://github.com/MetaCubeX/go/commit/${patch_commit}.diff" | patch --verbose -p 1 done ================================================ FILE: .github/update_clients.sh ================================================ #!/usr/bin/env bash PROJECTS=$(dirname "$0")/../.. function updateClient() { pushd clients/$1 git fetch git reset FETCH_HEAD --hard popd git add clients/$1 } updateClient "apple" updateClient "android" ================================================ FILE: .github/update_cronet.sh ================================================ #!/usr/bin/env bash set -e -o pipefail SCRIPT_DIR=$(dirname "$0") PROJECTS=$SCRIPT_DIR/../.. git -C $PROJECTS/cronet-go fetch origin main git -C $PROJECTS/cronet-go fetch origin go go get -x github.com/sagernet/cronet-go/all@$(git -C $PROJECTS/cronet-go rev-parse origin/go) go get -x github.com/sagernet/cronet-go@$(git -C $PROJECTS/cronet-go rev-parse origin/go) go mod tidy git -C $PROJECTS/cronet-go rev-parse origin/go > "$SCRIPT_DIR/CRONET_GO_VERSION" ================================================ FILE: .github/update_cronet_dev.sh ================================================ #!/usr/bin/env bash set -e -o pipefail SCRIPT_DIR=$(dirname "$0") PROJECTS=$SCRIPT_DIR/../.. git -C $PROJECTS/cronet-go fetch origin dev git -C $PROJECTS/cronet-go fetch origin go_dev go get -x github.com/sagernet/cronet-go/all@$(git -C $PROJECTS/cronet-go rev-parse origin/go_dev) go get -x github.com/sagernet/cronet-go@$(git -C $PROJECTS/cronet-go rev-parse origin/go_dev) go mod tidy git -C $PROJECTS/cronet-go rev-parse origin/dev > "$SCRIPT_DIR/CRONET_GO_VERSION" ================================================ FILE: .github/update_dependencies.sh ================================================ #!/usr/bin/env bash PROJECTS=$(dirname "$0")/../.. go get -x github.com/sagernet/$1@$(git -C $PROJECTS/$1 rev-parse HEAD) go mod tidy ================================================ FILE: .github/workflows/build.yml ================================================ name: Build on: workflow_dispatch: inputs: version: description: "Version name" required: true type: string build: description: "Build type" required: true type: choice default: "All" options: - All - Binary - Android - Apple - app-store - iOS - macOS - tvOS - macOS-standalone - publish-android push: branches: - stable - testing - unstable concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}-${{ inputs.build }} cancel-in-progress: true jobs: calculate_version: name: Calculate version runs-on: ubuntu-latest outputs: version: ${{ steps.outputs.outputs.version }} steps: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 with: fetch-depth: 0 - name: Setup Go uses: actions/setup-go@v5 with: go-version: ~1.25.9 - name: Check input version if: github.event_name == 'workflow_dispatch' run: |- echo "version=${{ inputs.version }}" echo "version=${{ inputs.version }}" >> "$GITHUB_ENV" - name: Calculate version if: github.event_name != 'workflow_dispatch' run: |- go run -v ./cmd/internal/read_tag --ci --nightly - name: Set outputs id: outputs run: |- echo "version=$version" >> "$GITHUB_OUTPUT" build: name: Build binary if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Binary' runs-on: ubuntu-latest needs: - calculate_version strategy: matrix: include: - { os: linux, arch: amd64, variant: purego, naive: true } - { os: linux, arch: amd64, variant: glibc, naive: true } - { os: linux, arch: amd64, variant: musl, naive: true, debian: amd64, rpm: x86_64, pacman: x86_64, alpine: x86_64, openwrt: "x86_64" } - { os: linux, arch: arm64, variant: purego, naive: true } - { os: linux, arch: arm64, variant: glibc, naive: true } - { os: linux, arch: arm64, variant: musl, naive: true, debian: arm64, rpm: aarch64, pacman: aarch64, alpine: aarch64, openwrt: "aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" } - { os: linux, arch: "386", go386: sse2 } - { os: linux, arch: "386", variant: glibc, naive: true, go386: sse2 } - { os: linux, arch: "386", variant: musl, naive: true, go386: sse2, debian: i386, rpm: i386, alpine: x86, openwrt: "i386_pentium4" } - { os: linux, arch: arm, goarm: "7" } - { os: linux, arch: arm, variant: glibc, naive: true, goarm: "7" } - { os: linux, arch: arm, variant: musl, naive: true, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl, alpine: armv7, openwrt: "arm_cortex-a5_vfpv4 arm_cortex-a7_neon-vfpv4 arm_cortex-a7_vfpv4 arm_cortex-a8_vfpv3 arm_cortex-a9_neon arm_cortex-a9_vfpv3-d16 arm_cortex-a15_neon-vfpv4" } - { os: linux, arch: mipsle, gomips: hardfloat, naive: true, variant: glibc } - { os: linux, arch: mipsle, gomips: softfloat, naive: true, variant: musl, debian: mipsel, rpm: mipsel, openwrt: "mipsel_24kc mipsel_74kc mipsel_mips32" } - { os: linux, arch: mips64le, gomips: hardfloat, naive: true, variant: glibc, debian: mips64el, rpm: mips64el } - { os: linux, arch: riscv64, naive: true, variant: glibc } - { os: linux, arch: riscv64, naive: true, variant: musl, debian: riscv64, rpm: riscv64, alpine: riscv64, openwrt: "riscv64_generic" } - { os: linux, arch: loong64, naive: true, variant: glibc } - { os: linux, arch: loong64, naive: true, variant: musl, debian: loongarch64, rpm: loongarch64, alpine: loongarch64, openwrt: "loongarch64_generic" } - { os: linux, arch: "386", go386: softfloat, openwrt: "i386_pentium-mmx" } - { os: linux, arch: arm, goarm: "5", openwrt: "arm_arm926ej-s arm_cortex-a7 arm_cortex-a9 arm_fa526 arm_xscale" } - { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl, openwrt: "arm_arm1176jzf-s_vfp" } - { os: linux, arch: mips, gomips: softfloat, openwrt: "mips_24kc mips_4kec mips_mips32" } - { os: linux, arch: mipsle, gomips: hardfloat, openwrt: "mipsel_24kc_24kf" } - { os: linux, arch: mipsle, gomips: softfloat } - { os: linux, arch: mips64, gomips: softfloat, openwrt: "mips64_mips64r2 mips64_octeonplus" } - { os: linux, arch: mips64le, gomips: hardfloat } - { os: linux, arch: mips64le, gomips: softfloat, openwrt: "mips64el_mips64r2" } - { os: linux, arch: s390x, debian: s390x, rpm: s390x } - { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le } - { os: linux, arch: riscv64 } - { os: linux, arch: loong64 } - { os: windows, arch: amd64, legacy_win7: true, legacy_name: "windows-7" } - { os: windows, arch: "386", legacy_win7: true, legacy_name: "windows-7" } - { os: android, arch: arm64, ndk: "aarch64-linux-android23" } - { os: android, arch: arm, ndk: "armv7a-linux-androideabi23" } - { os: android, arch: amd64, ndk: "x86_64-linux-android23" } - { os: android, arch: "386", ndk: "i686-linux-android23" } steps: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 with: fetch-depth: 0 - name: Setup Go if: ${{ ! matrix.legacy_win7 }} uses: actions/setup-go@v5 with: go-version: ~1.25.9 - name: Cache Go for Windows 7 if: matrix.legacy_win7 id: cache-go-for-windows7 uses: actions/cache@v4 with: path: | ~/go/go_win7 key: go_win7_1258 - name: Setup Go for Windows 7 if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true' env: GITHUB_TOKEN: ${{ github.token }} run: |- .github/setup_go_for_windows7.sh - name: Setup Go for Windows 7 if: matrix.legacy_win7 run: |- echo "PATH=$HOME/go/go_win7/bin:$PATH" >> $GITHUB_ENV echo "GOROOT=$HOME/go/go_win7" >> $GITHUB_ENV - name: Setup Android NDK if: matrix.os == 'android' uses: nttld/setup-ndk@v1 with: ndk-version: r28 local-cache: true - name: Clone cronet-go if: matrix.naive run: | set -xeuo pipefail CRONET_GO_VERSION=$(cat .github/CRONET_GO_VERSION) git init ~/cronet-go git -C ~/cronet-go remote add origin https://github.com/sagernet/cronet-go.git git -C ~/cronet-go fetch --depth=1 origin "$CRONET_GO_VERSION" git -C ~/cronet-go checkout FETCH_HEAD git -C ~/cronet-go submodule update --init --recursive --depth=1 - name: Regenerate Debian keyring if: matrix.naive run: | set -xeuo pipefail rm -f ~/cronet-go/naiveproxy/src/build/linux/sysroot_scripts/keyring.gpg cd ~/cronet-go GPG_TTY=/dev/null ./naiveproxy/src/build/linux/sysroot_scripts/generate_keyring.sh - name: Cache Chromium toolchain if: matrix.naive id: cache-chromium-toolchain uses: actions/cache@v4 with: path: | ~/cronet-go/naiveproxy/src/third_party/llvm-build/ ~/cronet-go/naiveproxy/src/gn/out/ ~/cronet-go/naiveproxy/src/chrome/build/pgo_profiles/ ~/cronet-go/naiveproxy/src/out/sysroot-build/ key: chromium-toolchain-${{ matrix.arch }}-${{ matrix.variant }}-${{ hashFiles('.github/CRONET_GO_VERSION') }} - name: Download Chromium toolchain if: matrix.naive run: | set -xeuo pipefail cd ~/cronet-go if [[ "${{ matrix.variant }}" == "musl" ]]; then go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl download-toolchain else go run ./cmd/build-naive --target=linux/${{ matrix.arch }} download-toolchain fi - name: Set Chromium toolchain environment if: matrix.naive run: | set -xeuo pipefail cd ~/cronet-go if [[ "${{ matrix.variant }}" == "musl" ]]; then go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl env >> $GITHUB_ENV else go run ./cmd/build-naive --target=linux/${{ matrix.arch }} env >> $GITHUB_ENV fi - name: Set tag run: |- git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV" git tag v${{ needs.calculate_version.outputs.version }} -f - name: Set build tags run: | set -xeuo pipefail if [[ "${{ matrix.naive }}" == "true" ]]; then TAGS=$(cat release/DEFAULT_BUILD_TAGS) else TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS) fi if [[ "${{ matrix.variant }}" == "purego" ]]; then TAGS="${TAGS},with_purego" elif [[ "${{ matrix.variant }}" == "musl" ]]; then TAGS="${TAGS},with_musl" fi echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}" - name: Set shared ldflags run: | echo "LDFLAGS_SHARED=$(cat release/LDFLAGS)" >> "${GITHUB_ENV}" - name: Build (purego) if: matrix.variant == 'purego' run: | set -xeuo pipefail mkdir -p dist go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \ -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \ ./cmd/sing-box env: CGO_ENABLED: "0" GOOS: ${{ matrix.os }} GOARCH: ${{ matrix.arch }} GO386: ${{ matrix.go386 }} GOARM: ${{ matrix.goarm }} GOMIPS: ${{ matrix.gomips }} GOMIPS64: ${{ matrix.gomips }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Extract libcronet.so if: matrix.variant == 'purego' && matrix.naive run: | cd ~/cronet-go CGO_ENABLED=0 go run -v ./cmd/build-naive extract-lib --target ${{ matrix.os }}/${{ matrix.arch }} -o $GITHUB_WORKSPACE/dist - name: Build (glibc) if: matrix.variant == 'glibc' run: | set -xeuo pipefail mkdir -p dist go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \ -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \ ./cmd/sing-box env: CGO_ENABLED: "1" GOOS: linux GOARCH: ${{ matrix.arch }} GO386: ${{ matrix.go386 }} GOARM: ${{ matrix.goarm }} GOMIPS: ${{ matrix.gomips }} GOMIPS64: ${{ matrix.gomips }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Build (musl) if: matrix.variant == 'musl' run: | set -xeuo pipefail mkdir -p dist go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \ -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \ ./cmd/sing-box env: CGO_ENABLED: "1" GOOS: linux GOARCH: ${{ matrix.arch }} GO386: ${{ matrix.go386 }} GOARM: ${{ matrix.goarm }} GOMIPS: ${{ matrix.gomips }} GOMIPS64: ${{ matrix.gomips }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Build (non-variant) if: matrix.os != 'android' && matrix.variant == '' run: | set -xeuo pipefail mkdir -p dist go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \ -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \ ./cmd/sing-box env: CGO_ENABLED: "0" GOOS: ${{ matrix.os }} GOARCH: ${{ matrix.arch }} GO386: ${{ matrix.go386 }} GOARM: ${{ matrix.goarm }} GOMIPS: ${{ matrix.gomips }} GOMIPS64: ${{ matrix.gomips }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Build Android if: matrix.os == 'android' run: | set -xeuo pipefail go install -v ./cmd/internal/build export CC='${{ matrix.ndk }}-clang' export CXX="${CC}++" mkdir -p dist GOOS=$BUILD_GOOS GOARCH=$BUILD_GOARCH build go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \ -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \ ./cmd/sing-box env: CGO_ENABLED: "1" BUILD_GOOS: ${{ matrix.os }} BUILD_GOARCH: ${{ matrix.arch }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Set name run: |- DIR_NAME="sing-box-${{ needs.calculate_version.outputs.version }}-${{ matrix.os }}-${{ matrix.arch }}" if [[ -n "${{ matrix.goarm }}" ]]; then DIR_NAME="${DIR_NAME}v${{ matrix.goarm }}" elif [[ -n "${{ matrix.go386 }}" && "${{ matrix.go386 }}" != 'sse2' ]]; then DIR_NAME="${DIR_NAME}-${{ matrix.go386 }}" elif [[ -n "${{ matrix.gomips }}" && "${{ matrix.gomips }}" != 'hardfloat' ]]; then DIR_NAME="${DIR_NAME}-${{ matrix.gomips }}" elif [[ -n "${{ matrix.legacy_name }}" ]]; then DIR_NAME="${DIR_NAME}-legacy-${{ matrix.legacy_name }}" fi if [[ "${{ matrix.variant }}" == "glibc" ]]; then DIR_NAME="${DIR_NAME}-glibc" elif [[ "${{ matrix.variant }}" == "musl" ]]; then DIR_NAME="${DIR_NAME}-musl" fi echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}" PKG_VERSION="${{ needs.calculate_version.outputs.version }}" PKG_VERSION="${PKG_VERSION//-/\~}" echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}" - name: Package DEB if: matrix.debian != '' run: | set -xeuo pipefail sudo gem install fpm sudo apt-get update sudo apt-get install -y debsigs cp .fpm_systemd .fpm fpm -t deb \ -v "$PKG_VERSION" \ -p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.debian }}.deb" \ --architecture ${{ matrix.debian }} \ dist/sing-box=/usr/bin/sing-box curl -Lo '/tmp/debsigs.diff' 'https://gitlab.com/debsigs/debsigs/-/commit/160138f5de1ec110376d3c807b60a37388bc7c90.diff' sudo patch /usr/bin/debsigs < '/tmp/debsigs.diff' rm -rf $HOME/.gnupg gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import < $HOME/.rpmmacros <> $GITHUB_ENV echo "GOROOT=$HOME/go/go_osx" >> $GITHUB_ENV - name: Set tag run: |- git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV" git tag v${{ needs.calculate_version.outputs.version }} -f - name: Set build tags run: | set -xeuo pipefail if [[ "${{ matrix.legacy_osx }}" != "true" ]]; then TAGS=$(cat release/DEFAULT_BUILD_TAGS) else TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS) fi echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}" - name: Set shared ldflags run: | echo "LDFLAGS_SHARED=$(cat release/LDFLAGS)" >> "${GITHUB_ENV}" - name: Build run: | set -xeuo pipefail mkdir -p dist go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \ -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \ ./cmd/sing-box env: CGO_ENABLED: "1" GOOS: darwin GOARCH: ${{ matrix.arch }} MACOSX_DEPLOYMENT_TARGET: ${{ matrix.legacy_osx && '10.13' || '' }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Set name run: |- DIR_NAME="sing-box-${{ needs.calculate_version.outputs.version }}-darwin-${{ matrix.arch }}" if [[ -n "${{ matrix.legacy_name }}" ]]; then DIR_NAME="${DIR_NAME}-legacy-${{ matrix.legacy_name }}" fi echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}" - name: Archive run: | set -xeuo pipefail cd dist mkdir -p "${DIR_NAME}" cp ../LICENSE "${DIR_NAME}" cp sing-box "${DIR_NAME}" tar -czvf "${DIR_NAME}.tar.gz" "${DIR_NAME}" rm -r "${DIR_NAME}" - name: Cleanup run: rm dist/sing-box - name: Upload artifact uses: actions/upload-artifact@v4 with: name: binary-darwin_${{ matrix.arch }}${{ matrix.legacy_name && format('-legacy-{0}', matrix.legacy_name) }} path: "dist" build_windows: name: Build Windows binaries if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Binary' runs-on: windows-latest needs: - calculate_version strategy: matrix: include: - { arch: amd64, naive: true } - { arch: "386" } - { arch: arm64, naive: true } steps: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 with: fetch-depth: 0 - name: Setup Go uses: actions/setup-go@v5 with: go-version: ^1.25.4 - name: Set tag run: |- git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$env:GITHUB_ENV" git tag v${{ needs.calculate_version.outputs.version }} -f - name: Build if: matrix.naive run: | $TAGS = Get-Content release/DEFAULT_BUILD_TAGS_WINDOWS $LDFLAGS_SHARED = Get-Content release/LDFLAGS mkdir -p dist go build -v -trimpath -o dist/sing-box.exe -tags "$TAGS" ` -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' $LDFLAGS_SHARED -s -w -buildid=" ` ./cmd/sing-box env: CGO_ENABLED: "0" GOOS: windows GOARCH: ${{ matrix.arch }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Build if: ${{ !matrix.naive }} run: | $TAGS = Get-Content release/DEFAULT_BUILD_TAGS_OTHERS $LDFLAGS_SHARED = Get-Content release/LDFLAGS mkdir -p dist go build -v -trimpath -o dist/sing-box.exe -tags "$TAGS" ` -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' $LDFLAGS_SHARED -s -w -buildid=" ` ./cmd/sing-box env: CGO_ENABLED: "0" GOOS: windows GOARCH: ${{ matrix.arch }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Extract libcronet.dll if: matrix.naive run: | $CRONET_GO_VERSION = Get-Content .github/CRONET_GO_VERSION $env:CGO_ENABLED = "0" go run -v "github.com/sagernet/cronet-go/cmd/build-naive@$CRONET_GO_VERSION" extract-lib --target windows/${{ matrix.arch }} -o dist - name: Archive if: matrix.naive run: | $DIR_NAME = "sing-box-${{ needs.calculate_version.outputs.version }}-windows-${{ matrix.arch }}" mkdir "dist/$DIR_NAME" Copy-Item LICENSE "dist/$DIR_NAME" Copy-Item "dist/sing-box.exe" "dist/$DIR_NAME" Copy-Item "dist/libcronet.dll" "dist/$DIR_NAME" Compress-Archive -Path "dist/$DIR_NAME" -DestinationPath "dist/$DIR_NAME.zip" Remove-Item -Recurse "dist/$DIR_NAME" - name: Archive if: ${{ !matrix.naive }} run: | $DIR_NAME = "sing-box-${{ needs.calculate_version.outputs.version }}-windows-${{ matrix.arch }}" mkdir "dist/$DIR_NAME" Copy-Item LICENSE "dist/$DIR_NAME" Copy-Item "dist/sing-box.exe" "dist/$DIR_NAME" Compress-Archive -Path "dist/$DIR_NAME" -DestinationPath "dist/$DIR_NAME.zip" Remove-Item -Recurse "dist/$DIR_NAME" - name: Cleanup if: matrix.naive run: Remove-Item dist/sing-box.exe, dist/libcronet.dll - name: Cleanup if: ${{ !matrix.naive }} run: Remove-Item dist/sing-box.exe - name: Upload artifact uses: actions/upload-artifact@v4 with: name: binary-windows_${{ matrix.arch }} path: "dist" build_android: name: Build Android if: (github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android') && github.ref != 'refs/heads/oldstable' runs-on: ubuntu-latest needs: - calculate_version steps: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 with: fetch-depth: 0 submodules: 'recursive' - name: Setup Go uses: actions/setup-go@v5 with: go-version: ~1.25.9 - name: Setup Android NDK id: setup-ndk uses: nttld/setup-ndk@v1 with: ndk-version: r28 - name: Setup OpenJDK run: |- sudo apt update && sudo apt install -y openjdk-17-jdk-headless /usr/lib/jvm/java-17-openjdk-amd64/bin/java --version - name: Set tag run: |- git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV" git tag v${{ needs.calculate_version.outputs.version }} -f - name: Build library run: |- make lib_install export PATH="$PATH:$(go env GOPATH)/bin" make lib_android env: JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64 ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} - name: Checkout main branch if: github.ref == 'refs/heads/stable' && github.event_name != 'workflow_dispatch' run: |- cd clients/android git checkout main - name: Checkout dev branch if: github.ref == 'refs/heads/testing' run: |- cd clients/android git checkout dev - name: Gradle cache uses: actions/cache@v4 with: path: ~/.gradle key: gradle-${{ hashFiles('**/*.gradle') }} - name: Update version if: github.event_name == 'workflow_dispatch' run: |- go run -v ./cmd/internal/update_android_version --ci - name: Update nightly version if: github.event_name != 'workflow_dispatch' run: |- go run -v ./cmd/internal/update_android_version --ci --nightly - name: Build run: |- mkdir clients/android/app/libs cp *.aar clients/android/app/libs cd clients/android ./gradlew :app:assembleOtherRelease :app:assembleOtherLegacyRelease env: JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64 ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }} - name: Prepare upload run: |- mkdir -p dist #cp clients/android/app/build/outputs/apk/play/release/*.apk dist cp clients/android/app/build/outputs/apk/other/release/*.apk dist cp clients/android/app/build/outputs/apk/otherLegacy/release/*.apk dist VERSION_CODE=$(grep VERSION_CODE clients/android/version.properties | cut -d= -f2) VERSION_NAME=$(grep VERSION_NAME clients/android/version.properties | cut -d= -f2) cat > dist/SFA-version-metadata.json << EOF { "version_code": ${VERSION_CODE}, "version_name": "${VERSION_NAME}" } EOF cat dist/SFA-version-metadata.json - name: Upload artifact uses: actions/upload-artifact@v4 with: name: binary-android-apks path: 'dist' publish_android: name: Publish Android if: github.event_name == 'workflow_dispatch' && inputs.build == 'publish-android' && github.ref != 'refs/heads/oldstable' runs-on: ubuntu-latest needs: - calculate_version steps: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 with: fetch-depth: 0 submodules: 'recursive' - name: Setup Go uses: actions/setup-go@v5 with: go-version: ~1.25.9 - name: Setup Android NDK id: setup-ndk uses: nttld/setup-ndk@v1 with: ndk-version: r28 - name: Setup OpenJDK run: |- sudo apt update && sudo apt install -y openjdk-17-jdk-headless /usr/lib/jvm/java-17-openjdk-amd64/bin/java --version - name: Set tag run: |- git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV" git tag v${{ needs.calculate_version.outputs.version }} -f - name: Build library run: |- make lib_install export PATH="$PATH:$(go env GOPATH)/bin" make lib_android env: JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64 ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} - name: Checkout main branch if: github.ref == 'refs/heads/stable' && github.event_name != 'workflow_dispatch' run: |- cd clients/android git checkout main - name: Checkout dev branch if: github.ref == 'refs/heads/testing' run: |- cd clients/android git checkout dev - name: Gradle cache uses: actions/cache@v4 with: path: ~/.gradle key: gradle-${{ hashFiles('**/*.gradle') }} - name: Build run: |- go run -v ./cmd/internal/update_android_version --ci mkdir clients/android/app/libs cp *.aar clients/android/app/libs cd clients/android echo -n "$SERVICE_ACCOUNT_CREDENTIALS" | base64 --decode > service-account-credentials.json ./gradlew :app:publishPlayReleaseBundle env: JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64 ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }} SERVICE_ACCOUNT_CREDENTIALS: ${{ secrets.SERVICE_ACCOUNT_CREDENTIALS }} build_apple: name: Build Apple clients runs-on: macos-26 if: false # github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store' || inputs.build == 'iOS' || inputs.build == 'macOS' || inputs.build == 'tvOS' || inputs.build == 'macOS-standalone' needs: - calculate_version strategy: matrix: include: - name: iOS if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store'|| inputs.build == 'iOS' }} platform: ios scheme: SFI destination: 'generic/platform=iOS' archive: build/SFI.xcarchive upload: SFI/Upload.plist - name: macOS if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store'|| inputs.build == 'macOS' }} platform: macos scheme: SFM destination: 'generic/platform=macOS' archive: build/SFM.xcarchive upload: SFI/Upload.plist - name: tvOS if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store'|| inputs.build == 'tvOS' }} platform: tvos scheme: SFT destination: 'generic/platform=tvOS' archive: build/SFT.xcarchive upload: SFI/Upload.plist - name: macOS-standalone if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'macOS-standalone' }} platform: macos scheme: SFM.System destination: 'generic/platform=macOS' archive: build/SFM.System.xcarchive export: SFM.System/Export.plist export_path: build/SFM.System steps: - name: Checkout if: matrix.if uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 with: fetch-depth: 0 submodules: 'recursive' - name: Setup Go if: matrix.if uses: actions/setup-go@v5 with: go-version: ~1.25.9 - name: Set tag if: matrix.if run: |- git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV" git tag v${{ needs.calculate_version.outputs.version }} -f echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV" - name: Checkout main branch if: matrix.if && github.ref == 'refs/heads/stable' && github.event_name != 'workflow_dispatch' run: |- cd clients/apple git checkout main - name: Checkout dev branch if: matrix.if && github.ref == 'refs/heads/testing' run: |- cd clients/apple git checkout dev - name: Setup certificates if: matrix.if run: |- CERTIFICATE_PATH=$RUNNER_TEMP/Certificates.p12 KEYCHAIN_PATH=$RUNNER_TEMP/certificates.keychain-db echo -n "$CERTIFICATES_P12" | base64 --decode -o $CERTIFICATE_PATH security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH security set-keychain-settings -lut 21600 $KEYCHAIN_PATH security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH security list-keychain -d user -s $KEYCHAIN_PATH PROFILES_ZIP_PATH=$RUNNER_TEMP/Profiles.zip echo -n "$PROVISIONING_PROFILES" | base64 --decode -o $PROFILES_ZIP_PATH PROFILES_PATH="$HOME/Library/MobileDevice/Provisioning Profiles" mkdir -p "$PROFILES_PATH" unzip $PROFILES_ZIP_PATH -d "$PROFILES_PATH" ASC_KEY_PATH=$RUNNER_TEMP/Key.p12 echo -n "$ASC_KEY" | base64 --decode -o $ASC_KEY_PATH xcrun notarytool store-credentials "notarytool-password" \ --key $ASC_KEY_PATH \ --key-id $ASC_KEY_ID \ --issuer $ASC_KEY_ISSUER_ID echo "ASC_KEY_PATH=$ASC_KEY_PATH" >> "$GITHUB_ENV" echo "ASC_KEY_ID=$ASC_KEY_ID" >> "$GITHUB_ENV" echo "ASC_KEY_ISSUER_ID=$ASC_KEY_ISSUER_ID" >> "$GITHUB_ENV" env: CERTIFICATES_P12: ${{ secrets.CERTIFICATES_P12 }} P12_PASSWORD: ${{ secrets.P12_PASSWORD }} KEYCHAIN_PASSWORD: ${{ secrets.P12_PASSWORD }} PROVISIONING_PROFILES: ${{ secrets.PROVISIONING_PROFILES }} ASC_KEY: ${{ secrets.ASC_KEY }} ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} ASC_KEY_ISSUER_ID: ${{ secrets.ASC_KEY_ISSUER_ID }} - name: Build library if: matrix.if run: |- make lib_install export PATH="$PATH:$(go env GOPATH)/bin" go run ./cmd/internal/build_libbox -target apple -platform ${{ matrix.platform }} mv Libbox.xcframework clients/apple - name: Update macOS version if: matrix.if && matrix.name == 'macOS' && github.event_name == 'workflow_dispatch' run: |- MACOS_PROJECT_VERSION=$(go run -v ./cmd/internal/app_store_connect next_macos_project_version) echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION" echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION" >> "$GITHUB_ENV" - name: Update version if: matrix.if && matrix.name != 'iOS' run: |- go run -v ./cmd/internal/update_apple_version --ci - name: Build if: matrix.if run: |- cd clients/apple xcodebuild archive \ -scheme "${{ matrix.scheme }}" \ -configuration Release \ -destination "${{ matrix.destination }}" \ -archivePath "${{ matrix.archive }}" \ -allowProvisioningUpdates \ -authenticationKeyPath $ASC_KEY_PATH \ -authenticationKeyID $ASC_KEY_ID \ -authenticationKeyIssuerID $ASC_KEY_ISSUER_ID - name: Upload to App Store Connect if: matrix.if && matrix.name != 'macOS-standalone' && github.event_name == 'workflow_dispatch' run: |- go run -v ./cmd/internal/app_store_connect cancel_app_store ${{ matrix.platform }} cd clients/apple xcodebuild -exportArchive \ -archivePath "${{ matrix.archive }}" \ -exportOptionsPlist ${{ matrix.upload }} \ -allowProvisioningUpdates \ -authenticationKeyPath $ASC_KEY_PATH \ -authenticationKeyID $ASC_KEY_ID \ -authenticationKeyIssuerID $ASC_KEY_ISSUER_ID - name: Publish to TestFlight if: matrix.if && matrix.name != 'macOS-standalone' && github.event_name == 'workflow_dispatch' && github.ref =='refs/heads/testing' run: |- go run -v ./cmd/internal/app_store_connect publish_testflight ${{ matrix.platform }} - name: Build image if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch' run: |- pushd clients/apple xcodebuild -exportArchive \ -archivePath "${{ matrix.archive }}" \ -exportOptionsPlist ${{ matrix.export }} \ -exportPath "${{ matrix.export_path }}" brew install create-dmg create-dmg \ --volname "sing-box" \ --volicon "${{ matrix.export_path }}/SFM.app/Contents/Resources/AppIcon.icns" \ --icon "SFM.app" 0 0 \ --hide-extension "SFM.app" \ --app-drop-link 0 0 \ --skip-jenkins \ SFM.dmg "${{ matrix.export_path }}/SFM.app" xcrun notarytool submit "SFM.dmg" --wait --keychain-profile "notarytool-password" cd "${{ matrix.archive }}" zip -r SFM.dSYMs.zip dSYMs popd mkdir -p dist cp clients/apple/SFM.dmg "dist/SFM-${VERSION}-universal.dmg" cp "clients/apple/${{ matrix.archive }}/SFM.dSYMs.zip" "dist/SFM-${VERSION}-universal.dSYMs.zip" - name: Upload image if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch' uses: actions/upload-artifact@v4 with: name: binary-macos-dmg path: 'dist' upload: name: Upload builds if: "!failure() && github.event_name == 'workflow_dispatch' && (inputs.build == 'All' || inputs.build == 'Binary' || inputs.build == 'Android' || inputs.build == 'Apple' || inputs.build == 'macOS-standalone')" runs-on: ubuntu-latest needs: - calculate_version - build - build_darwin - build_windows - build_android - build_apple steps: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 with: fetch-depth: 0 - name: Cache ghr uses: actions/cache@v4 id: cache-ghr with: path: | ~/go/bin/ghr key: ghr - name: Setup ghr if: steps.cache-ghr.outputs.cache-hit != 'true' run: |- cd $HOME git clone https://github.com/nekohasekai/ghr ghr cd ghr go install -v . - name: Set tag run: |- git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV" git tag v${{ needs.calculate_version.outputs.version }} -f echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV" - name: Download builds uses: actions/download-artifact@v5 with: path: dist merge-multiple: true - name: Upload builds if: ${{ env.PUBLISHED == 'false' }} run: |- export PATH="$PATH:$HOME/go/bin" ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Replace builds if: ${{ env.PUBLISHED != 'false' }} run: |- export PATH="$PATH:$HOME/go/bin" ghr --replace -p 5 "v${VERSION}" dist env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/docker.yml ================================================ name: Publish Docker Images on: #push: # branches: # - stable # - testing release: types: - published workflow_dispatch: inputs: tag: description: "The tag version you want to build" env: REGISTRY_IMAGE: ghcr.io/sagernet/sing-box jobs: build_binary: name: Build binary runs-on: ubuntu-latest strategy: fail-fast: true matrix: include: # Naive-enabled builds (musl) - { arch: amd64, naive: true, docker_platform: "linux/amd64" } - { arch: arm64, naive: true, docker_platform: "linux/arm64" } - { arch: "386", naive: true, docker_platform: "linux/386" } - { arch: arm, goarm: "7", naive: true, docker_platform: "linux/arm/v7" } - { arch: mipsle, gomips: softfloat, naive: true, docker_platform: "linux/mipsle" } - { arch: riscv64, naive: true, docker_platform: "linux/riscv64" } - { arch: loong64, naive: true, docker_platform: "linux/loong64" } # Non-naive builds - { arch: arm, goarm: "6", docker_platform: "linux/arm/v6" } - { arch: ppc64le, docker_platform: "linux/ppc64le" } - { arch: s390x, docker_platform: "linux/s390x" } steps: - name: Get commit to build id: ref run: |- if [[ -z "${{ github.event.inputs.tag }}" ]]; then ref="${{ github.ref_name }}" else ref="${{ github.event.inputs.tag }}" fi echo "ref=$ref" echo "ref=$ref" >> $GITHUB_OUTPUT - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 with: ref: ${{ steps.ref.outputs.ref }} fetch-depth: 0 - name: Setup Go uses: actions/setup-go@v5 with: go-version: ~1.25.9 - name: Clone cronet-go if: matrix.naive run: | set -xeuo pipefail CRONET_GO_VERSION=$(cat .github/CRONET_GO_VERSION) git init ~/cronet-go git -C ~/cronet-go remote add origin https://github.com/sagernet/cronet-go.git git -C ~/cronet-go fetch --depth=1 origin "$CRONET_GO_VERSION" git -C ~/cronet-go checkout FETCH_HEAD git -C ~/cronet-go submodule update --init --recursive --depth=1 - name: Regenerate Debian keyring if: matrix.naive run: | set -xeuo pipefail rm -f ~/cronet-go/naiveproxy/src/build/linux/sysroot_scripts/keyring.gpg cd ~/cronet-go GPG_TTY=/dev/null ./naiveproxy/src/build/linux/sysroot_scripts/generate_keyring.sh - name: Cache Chromium toolchain if: matrix.naive id: cache-chromium-toolchain uses: actions/cache@v4 with: path: | ~/cronet-go/naiveproxy/src/third_party/llvm-build/ ~/cronet-go/naiveproxy/src/gn/out/ ~/cronet-go/naiveproxy/src/chrome/build/pgo_profiles/ ~/cronet-go/naiveproxy/src/out/sysroot-build/ key: chromium-toolchain-${{ matrix.arch }}-musl-${{ hashFiles('.github/CRONET_GO_VERSION') }} - name: Download Chromium toolchain if: matrix.naive run: | set -xeuo pipefail cd ~/cronet-go go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl download-toolchain - name: Set version run: | set -xeuo pipefail VERSION=$(go run ./cmd/internal/read_tag) echo "VERSION=${VERSION}" >> "${GITHUB_ENV}" - name: Set Chromium toolchain environment if: matrix.naive run: | set -xeuo pipefail cd ~/cronet-go go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl env >> $GITHUB_ENV - name: Set build tags run: | set -xeuo pipefail if [[ "${{ matrix.naive }}" == "true" ]]; then TAGS="$(cat release/DEFAULT_BUILD_TAGS),with_musl" else TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS) fi echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}" - name: Set shared ldflags run: | echo "LDFLAGS_SHARED=$(cat release/LDFLAGS)" >> "${GITHUB_ENV}" - name: Build (naive) if: matrix.naive run: | set -xeuo pipefail go build -v -trimpath -o sing-box -tags "${BUILD_TAGS}" \ -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${VERSION}' ${LDFLAGS_SHARED} -s -w -buildid=" \ ./cmd/sing-box env: CGO_ENABLED: "1" GOOS: linux GOARCH: ${{ matrix.arch }} GOARM: ${{ matrix.goarm }} GOMIPS: ${{ matrix.gomips }} - name: Build (non-naive) if: ${{ ! matrix.naive }} run: | set -xeuo pipefail go build -v -trimpath -o sing-box -tags "${BUILD_TAGS}" \ -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${VERSION}' ${LDFLAGS_SHARED} -s -w -buildid=" \ ./cmd/sing-box env: CGO_ENABLED: "0" GOOS: linux GOARCH: ${{ matrix.arch }} GOARM: ${{ matrix.goarm }} - name: Prepare artifact run: | platform=${{ matrix.docker_platform }} echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV # Rename binary to include arch info for Dockerfile.binary BINARY_NAME="sing-box-${{ matrix.arch }}" if [[ -n "${{ matrix.goarm }}" ]]; then BINARY_NAME="${BINARY_NAME}v${{ matrix.goarm }}" fi mv sing-box "${BINARY_NAME}" echo "BINARY_NAME=${BINARY_NAME}" >> $GITHUB_ENV - name: Upload binary uses: actions/upload-artifact@v4 with: name: binary-${{ env.PLATFORM_PAIR }} path: ${{ env.BINARY_NAME }} if-no-files-found: error retention-days: 1 build_docker: name: Build Docker image runs-on: ubuntu-latest needs: - build_binary strategy: fail-fast: true matrix: include: - { platform: "linux/amd64" } - { platform: "linux/arm/v6" } - { platform: "linux/arm/v7" } - { platform: "linux/arm64" } - { platform: "linux/386" } # mipsle: no base Docker image available for this platform - { platform: "linux/ppc64le" } - { platform: "linux/riscv64" } - { platform: "linux/s390x" } - { platform: "linux/loong64", base_image: "ghcr.io/loong64/alpine:edge" } steps: - name: Get commit to build id: ref run: |- if [[ -z "${{ github.event.inputs.tag }}" ]]; then ref="${{ github.ref_name }}" else ref="${{ github.event.inputs.tag }}" fi echo "ref=$ref" echo "ref=$ref" >> $GITHUB_OUTPUT - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 with: ref: ${{ steps.ref.outputs.ref }} fetch-depth: 0 - name: Prepare run: | platform=${{ matrix.platform }} echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - name: Download binary uses: actions/download-artifact@v5 with: name: binary-${{ env.PLATFORM_PAIR }} path: . - name: Prepare binary run: | # Find and make the binary executable chmod +x sing-box-* ls -la sing-box-* - name: Setup QEMU uses: docker/setup-qemu-action@v3 - name: Setup Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Docker meta id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY_IMAGE }} - name: Build and push by digest id: build uses: docker/build-push-action@v6 with: platforms: ${{ matrix.platform }} context: . file: Dockerfile.binary build-args: | BASE_IMAGE=${{ matrix.base_image || 'alpine' }} labels: ${{ steps.meta.outputs.labels }} outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true - name: Export digest run: | mkdir -p /tmp/digests digest="${{ steps.build.outputs.digest }}" touch "/tmp/digests/${digest#sha256:}" - name: Upload digest uses: actions/upload-artifact@v4 with: name: digests-${{ env.PLATFORM_PAIR }} path: /tmp/digests/* if-no-files-found: error retention-days: 1 merge: if: github.event_name != 'push' runs-on: ubuntu-latest needs: - build_docker steps: - name: Get commit to build id: ref run: |- if [[ -z "${{ github.event.inputs.tag }}" ]]; then ref="${{ github.ref_name }}" else ref="${{ github.event.inputs.tag }}" fi echo "ref=$ref" echo "ref=$ref" >> $GITHUB_OUTPUT - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 with: ref: ${{ steps.ref.outputs.ref }} fetch-depth: 0 - name: Detect track run: bash .github/detect_track.sh - name: Download digests uses: actions/download-artifact@v5 with: path: /tmp/digests pattern: digests-* merge-multiple: true - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Create manifest list and push if: github.event_name != 'push' working-directory: /tmp/digests run: | docker buildx imagetools create \ -t "${{ env.REGISTRY_IMAGE }}:${{ env.DOCKER_TAG }}" \ -t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}" \ $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *) - name: Inspect image if: github.event_name != 'push' run: | docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ env.DOCKER_TAG }} docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }} ================================================ FILE: .github/workflows/lint.yml ================================================ name: Lint on: push: branches: - oldstable - stable - testing - unstable paths-ignore: - '**.md' - '.github/**' - '!.github/workflows/lint.yml' pull_request: branches: - oldstable - stable - testing - unstable concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}-${{ inputs.build }} cancel-in-progress: true jobs: build: name: Lint ${{ matrix.goos }}/${{ matrix.goarch }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - goos: windows goarch: amd64 - goos: windows goarch: '386' - goos: windows goarch: arm64 - goos: linux goarch: amd64 - goos: linux goarch: arm64 - goos: linux goarch: arm - goos: linux goarch: '386' - goos: darwin goarch: amd64 - goos: darwin goarch: arm64 - goos: android goarch: arm64 # - goos: freebsd # goarch: amd64 steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Go uses: actions/setup-go@v5 with: go-version: ^1.25 - name: Cache go module uses: actions/cache@v4 with: path: | ~/go/pkg/mod key: go-${{ hashFiles('**/go.sum') }} - name: golangci-lint uses: golangci/golangci-lint-action@v8 env: GOOS: ${{ matrix.goos }} GOARCH: ${{ matrix.goarch }} with: version: latest args: --timeout=30m install-mode: binary verify: false ================================================ FILE: .github/workflows/linux.yml ================================================ name: Build Linux Packages on: #push: # branches: # - stable # - testing workflow_dispatch: inputs: version: description: "Version name" required: true type: string release: types: - published jobs: calculate_version: name: Calculate version runs-on: ubuntu-latest outputs: version: ${{ steps.outputs.outputs.version }} steps: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 with: fetch-depth: 0 - name: Setup Go uses: actions/setup-go@v5 with: go-version: ~1.25.9 - name: Check input version if: github.event_name == 'workflow_dispatch' run: |- echo "version=${{ inputs.version }}" echo "version=${{ inputs.version }}" >> "$GITHUB_ENV" - name: Calculate version if: github.event_name != 'workflow_dispatch' run: |- go run -v ./cmd/internal/read_tag --ci --nightly - name: Set outputs id: outputs run: |- echo "version=$version" >> "$GITHUB_OUTPUT" build: name: Build binary runs-on: ubuntu-latest needs: - calculate_version strategy: matrix: include: # Naive-enabled builds (musl) - { os: linux, arch: amd64, naive: true, debian: amd64, rpm: x86_64, pacman: x86_64 } - { os: linux, arch: arm64, naive: true, debian: arm64, rpm: aarch64, pacman: aarch64 } - { os: linux, arch: "386", naive: true, debian: i386, rpm: i386 } - { os: linux, arch: arm, goarm: "7", naive: true, debian: armhf, rpm: armv7hl, pacman: armv7hl } - { os: linux, arch: mipsle, gomips: softfloat, naive: true, debian: mipsel, rpm: mipsel } - { os: linux, arch: riscv64, naive: true, debian: riscv64, rpm: riscv64 } - { os: linux, arch: loong64, naive: true, debian: loongarch64, rpm: loongarch64 } # Non-naive builds (unsupported architectures) - { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl } - { os: linux, arch: mips64le, debian: mips64el, rpm: mips64el } - { os: linux, arch: s390x, debian: s390x, rpm: s390x } - { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le } steps: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 with: fetch-depth: 0 - name: Setup Go uses: actions/setup-go@v5 with: go-version: ~1.25.9 - name: Clone cronet-go if: matrix.naive run: | set -xeuo pipefail CRONET_GO_VERSION=$(cat .github/CRONET_GO_VERSION) git init ~/cronet-go git -C ~/cronet-go remote add origin https://github.com/sagernet/cronet-go.git git -C ~/cronet-go fetch --depth=1 origin "$CRONET_GO_VERSION" git -C ~/cronet-go checkout FETCH_HEAD git -C ~/cronet-go submodule update --init --recursive --depth=1 - name: Regenerate Debian keyring if: matrix.naive run: | set -xeuo pipefail rm -f ~/cronet-go/naiveproxy/src/build/linux/sysroot_scripts/keyring.gpg cd ~/cronet-go GPG_TTY=/dev/null ./naiveproxy/src/build/linux/sysroot_scripts/generate_keyring.sh - name: Cache Chromium toolchain if: matrix.naive id: cache-chromium-toolchain uses: actions/cache@v4 with: path: | ~/cronet-go/naiveproxy/src/third_party/llvm-build/ ~/cronet-go/naiveproxy/src/gn/out/ ~/cronet-go/naiveproxy/src/chrome/build/pgo_profiles/ ~/cronet-go/naiveproxy/src/out/sysroot-build/ key: chromium-toolchain-${{ matrix.arch }}-musl-${{ hashFiles('.github/CRONET_GO_VERSION') }} - name: Download Chromium toolchain if: matrix.naive run: | set -xeuo pipefail cd ~/cronet-go go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl download-toolchain - name: Set Chromium toolchain environment if: matrix.naive run: | set -xeuo pipefail cd ~/cronet-go go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl env >> $GITHUB_ENV - name: Set tag run: |- git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV" git tag v${{ needs.calculate_version.outputs.version }} -f - name: Set build tags run: | set -xeuo pipefail if [[ "${{ matrix.naive }}" == "true" ]]; then TAGS="$(cat release/DEFAULT_BUILD_TAGS),with_musl" else TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS) fi echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}" - name: Set shared ldflags run: | echo "LDFLAGS_SHARED=$(cat release/LDFLAGS)" >> "${GITHUB_ENV}" - name: Build (naive) if: matrix.naive run: | set -xeuo pipefail mkdir -p dist go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \ -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \ ./cmd/sing-box env: CGO_ENABLED: "1" GOOS: linux GOARCH: ${{ matrix.arch }} GOARM: ${{ matrix.goarm }} GOMIPS: ${{ matrix.gomips }} GOMIPS64: ${{ matrix.gomips }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Build (non-naive) if: ${{ ! matrix.naive }} run: | set -xeuo pipefail mkdir -p dist go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \ -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \ ./cmd/sing-box env: CGO_ENABLED: "0" GOOS: ${{ matrix.os }} GOARCH: ${{ matrix.arch }} GOARM: ${{ matrix.goarm }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Set mtime run: |- TZ=UTC touch -t '197001010000' dist/sing-box - name: Detect track run: bash .github/detect_track.sh - name: Set version run: |- PKG_VERSION="${{ needs.calculate_version.outputs.version }}" PKG_VERSION="${PKG_VERSION//-/\~}" echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}" - name: Package DEB if: matrix.debian != '' run: | set -xeuo pipefail sudo gem install fpm sudo apt-get install -y debsigs cp .fpm_systemd .fpm fpm -t deb \ --name "${NAME}" \ -v "$PKG_VERSION" \ -p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.debian }}.deb" \ --architecture ${{ matrix.debian }} \ dist/sing-box=/usr/bin/sing-box curl -Lo '/tmp/debsigs.diff' 'https://gitlab.com/debsigs/debsigs/-/commit/160138f5de1ec110376d3c807b60a37388bc7c90.diff' sudo patch /usr/bin/debsigs < '/tmp/debsigs.diff' rm -rf $HOME/.gnupg gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import < $HOME/.rpmmacros <> "$GITHUB_ENV" git tag v${{ needs.calculate_version.outputs.version }} -f echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV" - name: Download builds uses: actions/download-artifact@v5 with: path: dist merge-multiple: true - name: Publish packages if: github.event_name != 'push' run: |- ls dist | xargs -I {} curl -F "package=@dist/{}" https://${{ secrets.FURY_TOKEN }}@push.fury.io/sagernet/ ================================================ FILE: .github/workflows/stale.yml ================================================ name: Mark stale issues and pull requests on: schedule: - cron: "30 1 * * *" jobs: stale: runs-on: ubuntu-latest steps: - uses: actions/stale@v9 with: stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 5 days' days-before-stale: 60 days-before-close: 5 exempt-issue-labels: 'bug,enhancement' ================================================ FILE: .github/workflows/test.yml ================================================ 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" ./... ================================================ FILE: .gitignore ================================================ /.idea/ /vendor/ /*.json /*.srs /*.db /site/ /bin/ /dist/ /sing-box /sing-box.exe /build/ /*.jar /*.aar /*.xcframework/ /experimental/libbox/*.aar /experimental/libbox/*.xcframework/ /experimental/libbox/*.nupkg .DS_Store /config.d/ /venv/ CLAUDE.md AGENTS.md /.claude/ ================================================ FILE: .gitmodules ================================================ [submodule "clients/apple"] path = clients/apple url = https://github.com/SagerNet/sing-box-for-apple.git [submodule "clients/android"] path = clients/android url = https://github.com/SagerNet/sing-box-for-android.git ================================================ FILE: .golangci.yml ================================================ version: "2" run: go: "1.24" build-tags: - with_gvisor - with_quic - with_dhcp - with_wireguard - with_utls - with_acme - with_clash_api - with_tailscale - with_ccm - with_ocm - badlinkname - tfogo_checklinkname0 linters: default: none enable: - ineffassign - staticcheck - unused - modernize settings: modernize: disable: - omitzero # nested struct omitempty -> omitzero changes JSON output semantics staticcheck: checks: - all - -QF1008 # could remove embedded field "" from selector - -ST1003 # should not use ALL_CAPS in Go names; use CamelCase instead - -QF1001 # could apply De Morgan's law exclusions: generated: lax presets: - comments - common-false-positives paths: - transport/simple-obfs - \.pb\.go$ - third_party$ - builtin$ - examples$ formatters: enable: - gci - gofumpt settings: gci: sections: - standard - prefix(github.com/sagernet/) - default custom-order: true ================================================ FILE: Dockerfile ================================================ FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder LABEL maintainer="nekohasekai " COPY . /go/src/github.com/sagernet/sing-box WORKDIR /go/src/github.com/sagernet/sing-box ARG TARGETOS TARGETARCH ARG GOPROXY="" ENV GOPROXY ${GOPROXY} ENV CGO_ENABLED=0 ENV GOOS=$TARGETOS ENV GOARCH=$TARGETARCH RUN set -ex \ && apk add git build-base \ && export COMMIT=$(git rev-parse --short HEAD) \ && export VERSION=$(go run ./cmd/internal/read_tag) \ && export TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS) \ && export LDFLAGS_SHARED=$(cat release/LDFLAGS) \ && go build -v -trimpath -tags "$TAGS" \ -o /go/bin/sing-box \ -ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" $LDFLAGS_SHARED -s -w -buildid=" \ ./cmd/sing-box FROM --platform=$TARGETPLATFORM alpine AS dist LABEL maintainer="nekohasekai " RUN set -ex \ && apk add --no-cache --upgrade bash tzdata ca-certificates nftables COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box ENTRYPOINT ["sing-box"] ================================================ FILE: Dockerfile.binary ================================================ ARG BASE_IMAGE=alpine FROM ${BASE_IMAGE} ARG TARGETARCH ARG TARGETVARIANT LABEL maintainer="nekohasekai " RUN set -ex \ && if command -v apk > /dev/null; then \ apk add --no-cache --upgrade bash tzdata ca-certificates nftables; \ else \ apt-get update && apt-get install -y --no-install-recommends bash tzdata ca-certificates nftables \ && rm -rf /var/lib/apt/lists/*; \ fi COPY sing-box-${TARGETARCH}${TARGETVARIANT} /usr/local/bin/sing-box ENTRYPOINT ["sing-box"] ================================================ FILE: LICENSE ================================================ Copyright (C) 2022 by nekohasekai 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 . In addition, no derivative work may use the name or imply association with this application without prior consent. ================================================ FILE: Makefile ================================================ NAME = sing-box COMMIT = $(shell git rev-parse --short HEAD) TAGS ?= $(shell cat release/DEFAULT_BUILD_TAGS_OTHERS) GOHOSTOS = $(shell go env GOHOSTOS) GOHOSTARCH = $(shell go env GOHOSTARCH) VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run github.com/sagernet/sing-box/cmd/internal/read_tag@latest) LDFLAGS_SHARED = $(shell cat release/LDFLAGS) PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' $(LDFLAGS_SHARED) -s -w -buildid=" MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)" MAIN = ./cmd/sing-box PREFIX ?= $(shell go env GOPATH) SING_FFI ?= sing-ffi LIBBOX_FFI_CONFIG ?= ./experimental/libbox/ffi.json .PHONY: test release docs build build: export GOTOOLCHAIN=local && \ go build $(MAIN_PARAMS) $(MAIN) race: export GOTOOLCHAIN=local && \ go build -race $(MAIN_PARAMS) $(MAIN) ci_build: export GOTOOLCHAIN=local && \ go build $(PARAMS) $(MAIN) && \ go build $(MAIN_PARAMS) $(MAIN) generate_completions: go run -v --tags "$(TAGS),generate,generate_completions" $(MAIN) install: go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN) fmt: @golangci-lint fmt fmt_docs: go run ./cmd/internal/format_docs lint: GOOS=linux golangci-lint run ./... GOOS=android golangci-lint run ./... GOOS=windows golangci-lint run ./... GOOS=darwin golangci-lint run ./... # GOOS=freebsd golangci-lint run ./... lint_install: go install -v github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest proto: @go run ./cmd/internal/protogen @gofumpt -l -w . @gofumpt -l -w . proto_install: go install -v google.golang.org/protobuf/cmd/protoc-gen-go@latest go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest update_certificates: go run ./cmd/internal/update_certificates release: go run ./cmd/internal/build goreleaser release --clean --skip publish mkdir dist/release mv dist/*.tar.gz \ dist/*.zip \ dist/*.deb \ dist/*.rpm \ dist/*_amd64.pkg.tar.zst \ dist/*_arm64.pkg.tar.zst \ dist/release ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release rm -r dist/release release_repo: go run ./cmd/internal/build goreleaser release -f .goreleaser.fury.yaml --clean release_install: go install -v github.com/tcnksm/ghr@latest update_android_version: go run ./cmd/internal/update_android_version build_android: cd ../sing-box-for-android && ./gradlew :app:clean :app:assembleOtherRelease :app:assembleOtherLegacyRelease && ./gradlew --stop upload_android: mkdir -p dist/release_android cp ../sing-box-for-android/app/build/outputs/apk/other/release/*.apk dist/release_android cp ../sing-box-for-android/app/build/outputs/apk/otherLegacy/release/*.apk dist/release_android ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_android rm -rf dist/release_android release_android: lib_android update_android_version build_android upload_android publish_android: cd ../sing-box-for-android && ./gradlew :app:publishPlayReleaseBundle && ./gradlew --stop # TODO: find why and remove `-destination 'generic/platform=iOS'` # TODO: remove xcode clean when fix control widget fixed build_ios: cd ../sing-box-for-apple && \ rm -rf build/SFI.xcarchive && \ xcodebuild clean -scheme SFI && \ xcodebuild archive -scheme SFI -configuration Release -destination 'generic/platform=iOS' -archivePath build/SFI.xcarchive -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌" upload_ios_app_store: cd ../sing-box-for-apple && \ xcodebuild -exportArchive -archivePath build/SFI.xcarchive -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates export_ios_ipa: cd ../sing-box-for-apple && \ xcodebuild -exportArchive -archivePath build/SFI.xcarchive -exportOptionsPlist SFI/Export.plist -allowProvisioningUpdates -exportPath build/SFI && \ cp build/SFI/sing-box.ipa dist/SFI.ipa upload_ios_ipa: cd dist && \ cp SFI.ipa "SFI-${VERSION}.ipa" && \ ghr --replace --draft --prerelease "v${VERSION}" "SFI-${VERSION}.ipa" release_ios: build_ios upload_ios_app_store build_macos: cd ../sing-box-for-apple && \ rm -rf build/SFM.xcarchive && \ xcodebuild archive -scheme SFM -configuration Release -archivePath build/SFM.xcarchive -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌" upload_macos_app_store: cd ../sing-box-for-apple && \ xcodebuild -exportArchive -archivePath build/SFM.xcarchive -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates release_macos: build_macos upload_macos_app_store build_macos_standalone: $(MAKE) -C ../sing-box-for-apple archive_macos_standalone build_macos_dmg: $(MAKE) -C ../sing-box-for-apple build_macos_dmg build_macos_pkg: $(MAKE) -C ../sing-box-for-apple build_macos_pkg notarize_macos_dmg: $(MAKE) -C ../sing-box-for-apple notarize_macos_dmg notarize_macos_pkg: $(MAKE) -C ../sing-box-for-apple notarize_macos_pkg upload_macos_dmg: mkdir -p dist/SFM cp ../sing-box-for-apple/build/SFM-Apple.dmg "dist/SFM/SFM-${VERSION}-Apple.dmg" cp ../sing-box-for-apple/build/SFM-Intel.dmg "dist/SFM/SFM-${VERSION}-Intel.dmg" cp ../sing-box-for-apple/build/SFM-Universal.dmg "dist/SFM/SFM-${VERSION}-Universal.dmg" ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Apple.dmg" ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Intel.dmg" ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Universal.dmg" upload_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 --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Apple.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 && \ xcodebuild archive -scheme SFT -configuration Release -archivePath build/SFT.xcarchive -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌" upload_tvos_app_store: cd ../sing-box-for-apple && \ xcodebuild -exportArchive -archivePath "build/SFT.xcarchive" -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates export_tvos_ipa: cd ../sing-box-for-apple && \ xcodebuild -exportArchive -archivePath "build/SFT.xcarchive" -exportOptionsPlist SFI/Export.plist -allowProvisioningUpdates -exportPath build/SFT && \ cp build/SFT/sing-box.ipa dist/SFT.ipa upload_tvos_ipa: cd dist && \ cp SFT.ipa "SFT-${VERSION}.ipa" && \ ghr --replace --draft --prerelease "v${VERSION}" "SFT-${VERSION}.ipa" release_tvos: build_tvos upload_tvos_app_store update_apple_version: go run ./cmd/internal/update_apple_version update_macos_version: MACOS_PROJECT_VERSION=$(shell go run -v ./cmd/internal/app_store_connect next_macos_project_version) go run ./cmd/internal/update_apple_version release_apple: lib_apple update_apple_version release_ios release_macos release_tvos release_macos_standalone release_apple_beta: update_apple_version release_ios release_macos release_tvos publish_testflight: go run -v ./cmd/internal/app_store_connect publish_testflight $(filter-out $@,$(MAKECMDGOALS)) prepare_app_store: go run -v ./cmd/internal/app_store_connect prepare_app_store publish_app_store: go run -v ./cmd/internal/app_store_connect publish_app_store test: @go test -v ./... && \ cd test && \ go mod tidy && \ go test -v -tags "$(TAGS_TEST)" . test_stdio: @go test -v ./... && \ cd test && \ go mod tidy && \ go test -v -tags "$(TAGS_TEST),force_stdio" . lib_android: go run ./cmd/internal/build_libbox -target android lib_apple: go run ./cmd/internal/build_libbox -target apple lib_windows: $(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type csharp lib_android_new: $(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type android lib_apple_new: $(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type apple lib_install: go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.12 go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.12 docs: venv/bin/mkdocs serve publish_docs: venv/bin/mkdocs gh-deploy -m "Update" --force --ignore-version --no-history docs_install: python3 -m venv venv source ./venv/bin/activate && pip install --force-reinstall mkdocs-material=="9.7.2" mkdocs-static-i18n=="1.2.*" clean: rm -rf bin dist sing-box rm -f $(shell go env GOPATH)/sing-box update: git fetch git reset FETCH_HEAD --hard git clean -fdx %: @: ================================================ FILE: README.md ================================================ # sing-box The universal proxy platform. [![Packaging status](https://repology.org/badge/vertical-allrepos/sing-box.svg)](https://repology.org/project/sing-box/versions) ## Documentation https://sing-box.sagernet.org ## License ``` Copyright (C) 2022 by nekohasekai 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 . In addition, no derivative work may use the name or imply association with this application without prior consent. ``` ================================================ FILE: adapter/certificate/adapter.go ================================================ 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 } ================================================ FILE: adapter/certificate/manager.go ================================================ 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 } ================================================ FILE: adapter/certificate/registry.go ================================================ 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 } ================================================ FILE: adapter/certificate.go ================================================ package adapter import ( "context" "crypto/x509" "github.com/sagernet/sing/service" ) type CertificateStore interface { LifecycleService Pool() *x509.CertPool ExclusiveAnchors() bool } func RootPoolFromContext(ctx context.Context) *x509.CertPool { store := service.FromContext[CertificateStore](ctx) if store == nil { return nil } return store.Pool() } ================================================ FILE: adapter/certificate_darwin.go ================================================ //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 } ================================================ FILE: adapter/certificate_provider.go ================================================ 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 } ================================================ FILE: adapter/connections.go ================================================ package adapter import ( "context" "net" N "github.com/sagernet/sing/common/network" ) type ConnectionManager interface { Lifecycle Count() int CloseAll() TrackConn(conn net.Conn) net.Conn TrackPacketConn(conn net.PacketConn) net.PacketConn NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc) NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc) } ================================================ FILE: adapter/dns.go ================================================ package adapter import ( "context" "net/netip" "time" 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" "github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/service" "github.com/miekg/dns" ) type DNSRouter interface { Lifecycle Exchange(ctx context.Context, message *dns.Msg, options DNSQueryOptions) (*dns.Msg, error) Lookup(ctx context.Context, domain string, options DNSQueryOptions) ([]netip.Addr, error) ClearCache() LookupReverseMapping(ip netip.Addr) (string, bool) ResetNetwork() } type DNSClient interface { Start() 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() } type DNSQueryOptions struct { Transport DNSTransport Strategy C.DomainStrategy LookupStrategy C.DomainStrategy DisableCache bool DisableOptimisticCache bool RewriteTTL *uint32 Timeout time.Duration ClientSubnet netip.Prefix } 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 DNSQueryOptions{}, E.New("domain resolver not found: " + options.Server) } return DNSQueryOptions{ Transport: transport, Strategy: C.DomainStrategy(options.Strategy), DisableCache: options.DisableCache, DisableOptimisticCache: options.DisableOptimisticCache, RewriteTTL: options.RewriteTTL, Timeout: time.Duration(options.Timeout), ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}), }, nil } type RDRCStore interface { LoadRDRC(transportName string, qName string, qType uint16) (rejected bool) SaveRDRC(transportName string, qName string, qType uint16) error 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 Tag() string Dependencies() []string // Reset closes the transport's existing connections so later requests use fresh connections. // Exchanges that are currently using those connections may fail. Reset() 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) } type DNSTransportManager interface { Lifecycle Transports() []DNSTransport Transport(tag string) (DNSTransport, bool) Default() DNSTransport FakeIP() FakeIPTransport Remove(tag string) error Create(ctx context.Context, logger log.ContextLogger, tag string, outboundType string, options any) error } ================================================ FILE: adapter/endpoint/adapter.go ================================================ package endpoint import "github.com/sagernet/sing-box/option" type Adapter struct { endpointType string endpointTag string network []string dependencies []string } func NewAdapter(endpointType string, endpointTag string, network []string, dependencies []string) Adapter { return Adapter{ endpointType: endpointType, endpointTag: endpointTag, network: network, dependencies: dependencies, } } func NewAdapterWithDialerOptions(endpointType string, endpointTag string, network []string, dialOptions option.DialerOptions) Adapter { var dependencies []string if dialOptions.Detour != "" { dependencies = []string{dialOptions.Detour} } return NewAdapter(endpointType, endpointTag, network, dependencies) } func (a *Adapter) Type() string { return a.endpointType } func (a *Adapter) Tag() string { return a.endpointTag } func (a *Adapter) Network() []string { return a.network } func (a *Adapter) Dependencies() []string { return a.dependencies } ================================================ FILE: adapter/endpoint/manager.go ================================================ package endpoint import ( "context" "os" "sync" "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" ) var _ adapter.EndpointManager = (*Manager)(nil) type Manager struct { logger log.ContextLogger registry adapter.EndpointRegistry access sync.Mutex started bool stage adapter.StartStage endpoints []adapter.Endpoint endpointByTag map[string]adapter.Endpoint } func NewManager(logger log.ContextLogger, registry adapter.EndpointRegistry) *Manager { return &Manager{ logger: logger, registry: registry, endpointByTag: make(map[string]adapter.Endpoint), } } func (m *Manager) Start(stage adapter.StartStage) error { m.access.Lock() defer m.access.Unlock() if m.started && m.stage >= stage { panic("already started") } m.started = true m.stage = stage if stage == adapter.StartStateStart { // started with outbound manager return nil } for _, endpoint := range m.endpoints { name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]" done := adapter.LogElapsed(m.logger, stage, " ", name) err := adapter.LegacyStart(endpoint, stage) done() if err != nil { return E.Cause(err, stage, " ", name) } } return nil } func (m *Manager) Close() error { m.access.Lock() defer m.access.Unlock() if !m.started { return nil } m.started = false endpoints := m.endpoints m.endpoints = nil monitor := taskmonitor.New(m.logger, C.StopTimeout) var err error for _, endpoint := range endpoints { name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]" done := adapter.LogElapsed(m.logger, "close ", name) monitor.Start("close ", name) err = E.Append(err, endpoint.Close(), func(err error) error { return E.Cause(err, "close ", name) }) monitor.Finish() done() } return nil } func (m *Manager) Endpoints() []adapter.Endpoint { m.access.Lock() defer m.access.Unlock() return m.endpoints } func (m *Manager) Get(tag string) (adapter.Endpoint, bool) { m.access.Lock() defer m.access.Unlock() endpoint, found := m.endpointByTag[tag] return endpoint, found } func (m *Manager) Remove(tag string) error { m.access.Lock() endpoint, found := m.endpointByTag[tag] if !found { m.access.Unlock() return os.ErrInvalid } delete(m.endpointByTag, tag) index := common.Index(m.endpoints, func(it adapter.Endpoint) bool { return it == endpoint }) if index == -1 { panic("invalid endpoint index") } m.endpoints = append(m.endpoints[:index], m.endpoints[index+1:]...) started := m.started m.access.Unlock() if started { return endpoint.Close() } return nil } func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) error { endpoint, err := m.registry.Create(ctx, router, logger, tag, outboundType, options) if err != nil { return err } m.access.Lock() defer m.access.Unlock() if m.started { name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]" for _, stage := range adapter.ListStartStages { done := adapter.LogElapsed(m.logger, stage, " ", name) err = adapter.LegacyStart(endpoint, stage) done() if err != nil { return E.Cause(err, stage, " ", name) } } } if existsEndpoint, loaded := m.endpointByTag[tag]; loaded { if m.started { err = existsEndpoint.Close() if err != nil { return E.Cause(err, "close endpoint/", existsEndpoint.Type(), "[", existsEndpoint.Tag(), "]") } } existsIndex := common.Index(m.endpoints, func(it adapter.Endpoint) bool { return it == existsEndpoint }) if existsIndex == -1 { panic("invalid endpoint index") } m.endpoints = append(m.endpoints[:existsIndex], m.endpoints[existsIndex+1:]...) } m.endpoints = append(m.endpoints, endpoint) m.endpointByTag[tag] = endpoint return nil } ================================================ FILE: adapter/endpoint/registry.go ================================================ package endpoint 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, router adapter.Router, logger log.ContextLogger, tag string, options T) (adapter.Endpoint, error) func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) { registry.register(outboundType, func() any { return new(Options) }, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, rawOptions any) (adapter.Endpoint, error) { var options *Options if rawOptions != nil { options = rawOptions.(*Options) } return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options)) }) } var _ adapter.EndpointRegistry = (*Registry)(nil) type ( optionsConstructorFunc func() any constructorFunc func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Endpoint, 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(outboundType string) (any, bool) { m.access.Lock() defer m.access.Unlock() optionsConstructor, loaded := m.optionsType[outboundType] if !loaded { return nil, false } return optionsConstructor(), true } func (m *Registry) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Endpoint, error) { m.access.Lock() defer m.access.Unlock() constructor, loaded := m.constructor[outboundType] if !loaded { return nil, E.New("outbound type not found: " + outboundType) } return constructor(ctx, router, logger, tag, options) } func (m *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) { m.access.Lock() defer m.access.Unlock() m.optionsType[outboundType] = optionsConstructor m.constructor[outboundType] = constructor } ================================================ FILE: adapter/endpoint.go ================================================ package adapter import ( "context" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" ) type Endpoint interface { Lifecycle Type() string Tag() string Outbound } type EndpointRegistry interface { option.EndpointOptionsRegistry Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, endpointType string, options any) (Endpoint, error) } type EndpointManager interface { Lifecycle Endpoints() []Endpoint Get(tag string) (Endpoint, bool) Remove(tag string) error Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, endpointType string, options any) error } ================================================ FILE: adapter/experimental.go ================================================ package adapter import ( "bytes" "context" "encoding/binary" "io" "time" "github.com/sagernet/sing/common/observable" "github.com/sagernet/sing/common/varbin" ) type ClashServer interface { LifecycleService ConnectionTracker Mode() string ModeList() []string SetModeUpdateHook(hook *observable.Subscriber[struct{}]) HistoryStorage() URLTestHistoryStorage } type URLTestHistory struct { Time time.Time `json:"time"` Delay uint16 `json:"delay"` } type URLTestHistoryStorage interface { SetHook(hook *observable.Subscriber[struct{}]) LoadURLTestHistory(tag string) *URLTestHistory DeleteURLTestHistory(tag string) StoreURLTestHistory(tag string, history *URLTestHistory) Close() error } type V2RayServer interface { LifecycleService StatsService() ConnectionTracker } type CacheFile interface { LifecycleService StoreFakeIP() bool FakeIPStorage StoreRDRC() bool RDRCStore StoreDNS() bool DNSCacheStore SetDisableExpire(disableExpire bool) SetOptimisticTimeout(timeout time.Duration) LoadMode() string StoreMode(mode string) error LoadSelected(group string) string StoreSelected(group string, selected string) error LoadGroupExpand(group string) (isExpand bool, loaded bool) StoreGroupExpand(group string, expand bool) error LoadRuleSet(tag string) *SavedBinary SaveRuleSet(tag string, set *SavedBinary) error } type SavedBinary struct { Content []byte LastUpdated time.Time LastEtag string } func (s *SavedBinary) MarshalBinary() ([]byte, error) { var buffer bytes.Buffer err := binary.Write(&buffer, binary.BigEndian, uint8(1)) if err != nil { return nil, err } _, err = varbin.WriteUvarint(&buffer, uint64(len(s.Content))) if err != nil { return nil, err } _, err = buffer.Write(s.Content) if err != nil { return nil, err } err = binary.Write(&buffer, binary.BigEndian, s.LastUpdated.Unix()) if err != nil { return nil, err } _, err = varbin.WriteUvarint(&buffer, uint64(len(s.LastEtag))) if err != nil { return nil, err } _, err = buffer.WriteString(s.LastEtag) if err != nil { return nil, err } return buffer.Bytes(), nil } func (s *SavedBinary) UnmarshalBinary(data []byte) error { reader := bytes.NewReader(data) var version uint8 err := binary.Read(reader, binary.BigEndian, &version) if err != nil { return err } contentLength, err := binary.ReadUvarint(reader) if err != nil { return err } s.Content = make([]byte, contentLength) _, err = io.ReadFull(reader, s.Content) if err != nil { return err } var lastUpdated int64 err = binary.Read(reader, binary.BigEndian, &lastUpdated) if err != nil { return err } s.LastUpdated = time.Unix(lastUpdated, 0) etagLength, err := binary.ReadUvarint(reader) if err != nil { return err } etagBytes := make([]byte, etagLength) _, err = io.ReadFull(reader, etagBytes) if err != nil { return err } s.LastEtag = string(etagBytes) return nil } type OutboundGroup interface { Outbound Now() string All() []string } type URLTestGroup interface { OutboundGroup URLTest(ctx context.Context) (map[string]uint16, error) } func OutboundTag(detour Outbound) string { if group, isGroup := detour.(OutboundGroup); isGroup { return group.Now() } return detour.Tag() } ================================================ FILE: adapter/fakeip.go ================================================ package adapter import ( "net/netip" "github.com/sagernet/sing/common/logger" ) type FakeIPStore interface { SimpleLifecycle Contains(address netip.Addr) bool Create(domain string, isIPv6 bool) (netip.Addr, error) Lookup(address netip.Addr) (string, bool) Reset() error } type FakeIPStorage interface { FakeIPMetadata() *FakeIPMetadata FakeIPSaveMetadata(metadata *FakeIPMetadata) error FakeIPSaveMetadataAsync(metadata *FakeIPMetadata) FakeIPStore(address netip.Addr, domain string) error FakeIPStoreAsync(address netip.Addr, domain string, logger logger.Logger) FakeIPLoad(address netip.Addr) (string, bool) FakeIPLoadDomain(domain string, isIPv6 bool) (netip.Addr, bool) FakeIPReset() error } type FakeIPTransport interface { DNSTransport Store() FakeIPStore } ================================================ FILE: adapter/fakeip_metadata.go ================================================ package adapter import ( "bytes" "encoding" "encoding/binary" "io" "net/netip" "github.com/sagernet/sing/common" ) type FakeIPMetadata struct { Inet4Range netip.Prefix Inet6Range netip.Prefix Inet4Current netip.Addr Inet6Current netip.Addr } func (m *FakeIPMetadata) MarshalBinary() (data []byte, err error) { var buffer bytes.Buffer for _, marshaler := range []encoding.BinaryMarshaler{m.Inet4Range, m.Inet6Range, m.Inet4Current, m.Inet6Current} { data, err = marshaler.MarshalBinary() if err != nil { return } common.Must(binary.Write(&buffer, binary.BigEndian, uint16(len(data)))) buffer.Write(data) } data = buffer.Bytes() return } func (m *FakeIPMetadata) UnmarshalBinary(data []byte) error { reader := bytes.NewReader(data) for _, unmarshaler := range []encoding.BinaryUnmarshaler{&m.Inet4Range, &m.Inet6Range, &m.Inet4Current, &m.Inet6Current} { var length uint16 common.Must(binary.Read(reader, binary.BigEndian, &length)) element := make([]byte, length) _, err := io.ReadFull(reader, element) if err != nil { return err } err = unmarshaler.UnmarshalBinary(element) if err != nil { return err } } return nil } ================================================ FILE: adapter/handler.go ================================================ package adapter import ( "context" "net" "github.com/sagernet/sing/common/buf" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) type ConnectionHandler interface { NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc) } type PacketHandler interface { NewPacket(buffer *buf.Buffer, source M.Socksaddr) } type PacketBatchHandler interface { NewPacketBatch(buffers []*buf.Buffer, sources []M.Socksaddr) } type OOBPacketHandler interface { NewPacket(buffer *buf.Buffer, oob []byte, source M.Socksaddr) } type PacketConnectionHandler interface { NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc) } type UpstreamHandlerAdapter interface { N.TCPConnectionHandlerEx N.UDPConnectionHandlerEx } ================================================ FILE: adapter/http.go ================================================ 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() } } ================================================ FILE: adapter/inbound/adapter.go ================================================ package inbound type Adapter struct { inboundType string inboundTag string } func NewAdapter(inboundType string, inboundTag string) Adapter { return Adapter{ inboundType: inboundType, inboundTag: inboundTag, } } func (a *Adapter) Type() string { return a.inboundType } func (a *Adapter) Tag() string { return a.inboundTag } ================================================ FILE: adapter/inbound/manager.go ================================================ package inbound import ( "context" "os" "sync" "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" ) var _ adapter.InboundManager = (*Manager)(nil) type Manager struct { logger log.ContextLogger registry adapter.InboundRegistry endpoint adapter.EndpointManager access sync.Mutex started bool stage adapter.StartStage inbounds []adapter.Inbound inboundByTag map[string]adapter.Inbound } func NewManager(logger log.ContextLogger, registry adapter.InboundRegistry, endpoint adapter.EndpointManager) *Manager { return &Manager{ logger: logger, registry: registry, endpoint: endpoint, inboundByTag: make(map[string]adapter.Inbound), } } 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 inbounds := m.inbounds m.access.Unlock() for _, inbound := range inbounds { name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]" done := adapter.LogElapsed(m.logger, stage, " ", name) err := adapter.LegacyStart(inbound, stage) done() if err != nil { return E.Cause(err, stage, " ", name) } } return nil } func (m *Manager) Close() error { m.access.Lock() defer m.access.Unlock() if !m.started { return nil } m.started = false inbounds := m.inbounds m.inbounds = nil monitor := taskmonitor.New(m.logger, C.StopTimeout) var err error for _, inbound := range inbounds { name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]" done := adapter.LogElapsed(m.logger, "close ", name) monitor.Start("close ", name) err = E.Append(err, inbound.Close(), func(err error) error { return E.Cause(err, "close ", name) }) monitor.Finish() done() } return nil } func (m *Manager) Inbounds() []adapter.Inbound { m.access.Lock() defer m.access.Unlock() return m.inbounds } func (m *Manager) Get(tag string) (adapter.Inbound, bool) { m.access.Lock() inbound, found := m.inboundByTag[tag] m.access.Unlock() if found { return inbound, true } return m.endpoint.Get(tag) } func (m *Manager) Remove(tag string) error { m.access.Lock() inbound, found := m.inboundByTag[tag] if !found { m.access.Unlock() return os.ErrInvalid } delete(m.inboundByTag, tag) index := common.Index(m.inbounds, func(it adapter.Inbound) bool { return it == inbound }) if index == -1 { panic("invalid inbound index") } m.inbounds = append(m.inbounds[:index], m.inbounds[index+1:]...) started := m.started m.access.Unlock() if started { return inbound.Close() } return nil } func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) error { inbound, err := m.registry.Create(ctx, router, logger, tag, outboundType, options) if err != nil { return err } m.access.Lock() defer m.access.Unlock() if m.started { name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]" for _, stage := range adapter.ListStartStages { done := adapter.LogElapsed(m.logger, stage, " ", name) err = adapter.LegacyStart(inbound, stage) done() if err != nil { return E.Cause(err, stage, " ", name) } } } if existsInbound, loaded := m.inboundByTag[tag]; loaded { if m.started { err = existsInbound.Close() if err != nil { return E.Cause(err, "close inbound/", existsInbound.Type(), "[", existsInbound.Tag(), "]") } } existsIndex := common.Index(m.inbounds, func(it adapter.Inbound) bool { return it == existsInbound }) if existsIndex == -1 { panic("invalid inbound index") } m.inbounds = append(m.inbounds[:existsIndex], m.inbounds[existsIndex+1:]...) } m.inbounds = append(m.inbounds, inbound) m.inboundByTag[tag] = inbound return nil } ================================================ FILE: adapter/inbound/registry.go ================================================ package inbound 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, router adapter.Router, logger log.ContextLogger, tag string, options T) (adapter.Inbound, error) func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) { registry.register(outboundType, func() any { return new(Options) }, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, rawOptions any) (adapter.Inbound, error) { var options *Options if rawOptions != nil { options = rawOptions.(*Options) } return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options)) }) } var _ adapter.InboundRegistry = (*Registry)(nil) type ( optionsConstructorFunc func() any constructorFunc func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Inbound, 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(outboundType string) (any, bool) { m.access.Lock() defer m.access.Unlock() optionsConstructor, loaded := m.optionsType[outboundType] if !loaded { return nil, false } return optionsConstructor(), true } func (m *Registry) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Inbound, error) { m.access.Lock() defer m.access.Unlock() constructor, loaded := m.constructor[outboundType] if !loaded { return nil, E.New("outbound type not found: " + outboundType) } return constructor(ctx, router, logger, tag, options) } func (m *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) { m.access.Lock() defer m.access.Unlock() m.optionsType[outboundType] = optionsConstructor m.constructor[outboundType] = constructor } ================================================ FILE: adapter/inbound.go ================================================ package adapter import ( "context" "net" "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" M "github.com/sagernet/sing/common/metadata" "github.com/miekg/dns" ) type Inbound interface { Lifecycle Type() string Tag() string } type TCPInjectableInbound interface { Inbound ConnectionHandler } type UDPInjectableInbound interface { Inbound PacketConnectionHandler } type InboundRegistry interface { option.InboundOptionsRegistry Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) (Inbound, error) } type InboundManager interface { Lifecycle Inbounds() []Inbound Get(tag string) (Inbound, bool) Remove(tag string) error Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) error } type InboundContext struct { Inbound string InboundType string IPVersion uint8 Network string Source M.Socksaddr Destination M.Socksaddr User string Outbound string // sniffer Protocol string Domain string Client string SniffContext any SnifferNames []string SniffError error // cache // Deprecated: implement in rule action InboundDetour string LastInbound string OriginDestination M.Socksaddr RouteOriginalDestination M.Socksaddr UDPDisableDomainUnmapping bool UDPConnect bool UDPTimeout time.Duration TLSFragment bool TLSFragmentFallbackDelay time.Duration TLSRecordFragment bool TLSSpoof string TLSSpoofMethod tlsspoof.Method NetworkStrategy *C.NetworkStrategy NetworkType []C.InterfaceType FallbackNetworkType []C.InterfaceType FallbackDelay time.Duration 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 IPCIDRMatchSource bool IPCIDRAcceptEmpty bool SourceAddressMatch bool SourcePortMatch bool DestinationAddressMatch bool DestinationPortMatch bool DidMatch bool IgnoreDestinationIPCIDRMatch bool } func (c *InboundContext) ResetRuleCache() { c.IPCIDRMatchSource = false c.IPCIDRAcceptEmpty = false c.ResetRuleMatchCache() } func (c *InboundContext) ResetRuleMatchCache() { c.SourceAddressMatch = false c.SourcePortMatch = false c.DestinationAddressMatch = false c.DestinationPortMatch = false 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 { return context.WithValue(ctx, (*inboundContextKey)(nil), inboundContext) } func ContextFrom(ctx context.Context) *InboundContext { metadata := ctx.Value((*inboundContextKey)(nil)) if metadata == nil { return nil } return metadata.(*InboundContext) } func ExtendContext(ctx context.Context) (context.Context, *InboundContext) { var newMetadata InboundContext if metadata := ContextFrom(ctx); metadata != nil { newMetadata = *metadata } return WithContext(ctx, &newMetadata), &newMetadata } func OverrideContext(ctx context.Context) context.Context { if metadata := ContextFrom(ctx); metadata != nil { newMetadata := *metadata return WithContext(ctx, &newMetadata) } return ctx } ================================================ FILE: adapter/inbound_test.go ================================================ 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()) } ================================================ FILE: adapter/lifecycle.go ================================================ package adapter import ( "reflect" "strings" "time" "github.com/sagernet/sing-box/log" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" ) type SimpleLifecycle interface { Start() error Close() error } type StartStage uint8 const ( StartStateInitialize StartStage = iota StartStateStart StartStatePostStart StartStateStarted ) var ListStartStages = []StartStage{ StartStateInitialize, StartStateStart, StartStatePostStart, StartStateStarted, } func (s StartStage) String() string { switch s { case StartStateInitialize: return "initialize" case StartStateStart: return "start" case StartStatePostStart: return "post-start" case StartStateStarted: return "finish-start" default: panic("unknown stage") } } type Lifecycle interface { Start(stage StartStage) error Close() error } type LifecycleService interface { Name() string Lifecycle } func getServiceName(service any) string { if named, ok := service.(interface { Type() string Tag() string }); ok { tag := named.Tag() if tag != "" { return named.Type() + "[" + tag + "]" } return named.Type() } t := reflect.TypeOf(service) if t.Kind() == reflect.Ptr { t = t.Elem() } return strings.ToLower(t.Name()) } func Start(logger log.ContextLogger, stage StartStage, services ...Lifecycle) error { for _, service := range services { name := getServiceName(service) done := LogElapsed(logger, stage, " ", name) err := service.Start(stage) done() if err != nil { return err } } return nil } func StartNamed(logger log.ContextLogger, stage StartStage, services []LifecycleService) error { for _, service := range services { done := LogElapsed(logger, stage, " ", service.Name()) err := service.Start(stage) done() if err != nil { return E.Cause(err, stage.String(), " ", service.Name()) } } return nil } func LogElapsed(logger log.ContextLogger, description ...any) func() { prefix := F.ToString(description...) startTime := time.Now() timer := time.AfterFunc(time.Second, func() { logger.Trace(prefix, "...") }) return func() { if timer.Stop() { return } logger.Trace(prefix, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") } } ================================================ FILE: adapter/lifecycle_legacy.go ================================================ package adapter func LegacyStart(starter any, stage StartStage) error { if lifecycle, isLifecycle := starter.(Lifecycle); isLifecycle { return lifecycle.Start(stage) } switch stage { case StartStateInitialize: if preStarter, isPreStarter := starter.(interface { PreStart() error }); isPreStarter { return preStarter.PreStart() } case StartStateStart: if starter, isStarter := starter.(interface { Start() error }); isStarter { return starter.Start() } case StartStateStarted: if postStarter, isPostStarter := starter.(interface { PostStart() error }); isPostStarter { return postStarter.PostStart() } } return nil } type lifecycleServiceWrapper struct { SimpleLifecycle name string } func NewLifecycleService(service SimpleLifecycle, name string) LifecycleService { return &lifecycleServiceWrapper{ SimpleLifecycle: service, name: name, } } func (l *lifecycleServiceWrapper) Name() string { return l.name } func (l *lifecycleServiceWrapper) Start(stage StartStage) error { return LegacyStart(l.SimpleLifecycle, stage) } func (l *lifecycleServiceWrapper) Close() error { return l.SimpleLifecycle.Close() } ================================================ FILE: adapter/neighbor.go ================================================ package adapter import ( "net" "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) LookupAddresses(hostname string) []netip.Addr Start() error Close() error } type NeighborUpdateListener interface { UpdateNeighborTable(entries []NeighborEntry) } ================================================ FILE: adapter/network.go ================================================ package adapter import ( "encoding/hex" "net" "strings" "time" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/control" ) type NetworkManager interface { Lifecycle Initialize(ruleSets []RuleSet) InterfaceFinder() control.InterfaceFinder UpdateInterfaces() error DefaultNetworkInterface() *NetworkInterface NetworkInterfaces() []NetworkInterface AutoDetectInterface() bool AutoDetectInterfaceFunc() control.Func ProtectFunc() control.Func DefaultOptions() NetworkOptions RegisterAutoRedirectOutputMark(mark uint32) error AutoRedirectOutputMark() uint32 AutoRedirectOutputMarkFunc() control.Func NetworkMonitor() tun.NetworkUpdateMonitor InterfaceMonitor() tun.DefaultInterfaceMonitor PackageManager() tun.PackageManager NeedWIFIState() bool WIFIState() WIFIState UpdateWIFIState() ResetNetwork() } type NetworkOptions struct { BindInterface string RoutingMark uint32 DomainResolver string DomainResolveOptions DNSQueryOptions NetworkStrategy *C.NetworkStrategy NetworkType []C.InterfaceType FallbackNetworkType []C.InterfaceType FallbackDelay time.Duration } type InterfaceUpdateListener interface { InterfaceUpdated() } type WIFIState struct { SSID string BSSID string } func NormalizeWIFIBSSID(bssid string) string { bssid = strings.TrimSpace(bssid) if bssid == "" { return "" } parsed, err := net.ParseMAC(bssid) if err == nil && len(parsed) == 6 { return parsed.String() } if len(bssid) == 12 { decoded, err := hex.DecodeString(bssid) if err == nil { return net.HardwareAddr(decoded).String() } } return bssid } type NetworkInterface struct { control.Interface Type C.InterfaceType DNSServers []string Expensive bool Constrained bool } ================================================ FILE: adapter/outbound/adapter.go ================================================ package outbound import ( "github.com/sagernet/sing-box/option" ) type Adapter struct { outboundType string outboundTag string network []string dependencies []string } func NewAdapter(outboundType string, outboundTag string, network []string, dependencies []string) Adapter { return Adapter{ outboundType: outboundType, outboundTag: outboundTag, network: network, dependencies: dependencies, } } func NewAdapterWithDialerOptions(outboundType string, outboundTag string, network []string, dialOptions option.DialerOptions) Adapter { var dependencies []string if dialOptions.Detour != "" { dependencies = []string{dialOptions.Detour} } return NewAdapter(outboundType, outboundTag, network, dependencies) } func (a *Adapter) Type() string { return a.outboundType } func (a *Adapter) Tag() string { return a.outboundTag } func (a *Adapter) Network() []string { return a.network } func (a *Adapter) Dependencies() []string { return a.dependencies } ================================================ FILE: adapter/outbound/manager.go ================================================ package outbound import ( "context" "io" "os" "strings" "sync" "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" "github.com/sagernet/sing/common/logger" ) var _ adapter.OutboundManager = (*Manager)(nil) type Manager struct { logger log.ContextLogger registry adapter.OutboundRegistry endpoint adapter.EndpointManager defaultTag string access sync.RWMutex started bool stage adapter.StartStage outbounds []adapter.Outbound outboundByTag map[string]adapter.Outbound dependByTag map[string][]string defaultOutbound adapter.Outbound defaultOutboundFallback func() (adapter.Outbound, error) } func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry, endpoint adapter.EndpointManager, defaultTag string) *Manager { return &Manager{ logger: logger, registry: registry, endpoint: endpoint, defaultTag: defaultTag, outboundByTag: make(map[string]adapter.Outbound), dependByTag: make(map[string][]string), } } func (m *Manager) Initialize(defaultOutboundFallback func() (adapter.Outbound, error)) { m.defaultOutboundFallback = defaultOutboundFallback } 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 if stage == adapter.StartStateStart { if m.defaultTag != "" && m.defaultOutbound == nil { defaultEndpoint, loaded := m.endpoint.Get(m.defaultTag) if !loaded { m.access.Unlock() return E.New("default outbound not found: ", m.defaultTag) } m.defaultOutbound = defaultEndpoint } if m.defaultOutbound == nil { directOutbound, err := m.defaultOutboundFallback() if err != nil { m.access.Unlock() return E.Cause(err, "create direct outbound for fallback") } m.outbounds = append(m.outbounds, directOutbound) m.outboundByTag[directOutbound.Tag()] = directOutbound m.defaultOutbound = directOutbound } outbounds := m.outbounds m.access.Unlock() return m.startOutbounds(append(outbounds, common.Map(m.endpoint.Endpoints(), func(it adapter.Endpoint) adapter.Outbound { return it })...)) } else { outbounds := m.outbounds m.access.Unlock() for _, outbound := range outbounds { name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]" done := adapter.LogElapsed(m.logger, stage, " ", name) err := adapter.LegacyStart(outbound, stage) done() if err != nil { return E.Cause(err, stage, " ", name) } } } return nil } func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error { monitor := taskmonitor.New(m.logger, C.StartTimeout) started := make(map[string]bool) for { canContinue := false startOne: for _, outboundToStart := range outbounds { outboundTag := outboundToStart.Tag() if started[outboundTag] { continue } dependencies := outboundToStart.Dependencies() for _, dependency := range dependencies { if !started[dependency] { continue startOne } } started[outboundTag] = true canContinue = true name := "outbound/" + outboundToStart.Type() + "[" + outboundTag + "]" if starter, isStarter := outboundToStart.(adapter.Lifecycle); isStarter { done := adapter.LogElapsed(m.logger, "start ", name) monitor.Start("start ", name) err := starter.Start(adapter.StartStateStart) monitor.Finish() done() if err != nil { return E.Cause(err, "start ", name) } } else if starter, isStarter := outboundToStart.(interface { Start() error }); isStarter { done := adapter.LogElapsed(m.logger, "start ", name) monitor.Start("start ", name) err := starter.Start() monitor.Finish() done() if err != nil { return E.Cause(err, "start ", name) } } } if len(started) == len(outbounds) { break } if canContinue { continue } currentOutbound := common.Find(outbounds, func(it adapter.Outbound) bool { return !started[it.Tag()] }) var lintOutbound func(oTree []string, oCurrent adapter.Outbound) error lintOutbound = func(oTree []string, oCurrent adapter.Outbound) error { problemOutboundTag := common.Find(oCurrent.Dependencies(), func(it string) bool { return !started[it] }) if common.Contains(oTree, problemOutboundTag) { return E.New("circular outbound dependency: ", strings.Join(oTree, " -> "), " -> ", problemOutboundTag) } m.access.Lock() problemOutbound := m.outboundByTag[problemOutboundTag] m.access.Unlock() if problemOutbound == nil { return E.New("dependency[", problemOutboundTag, "] not found for outbound[", oCurrent.Tag(), "]") } return lintOutbound(append(oTree, problemOutboundTag), problemOutbound) } return lintOutbound([]string{currentOutbound.Tag()}, currentOutbound) } return nil } func (m *Manager) Close() error { monitor := taskmonitor.New(m.logger, C.StopTimeout) m.access.Lock() if !m.started { m.access.Unlock() return nil } m.started = false outbounds := m.outbounds m.outbounds = nil m.access.Unlock() var err error for _, outbound := range outbounds { if closer, isCloser := outbound.(io.Closer); isCloser { name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]" done := adapter.LogElapsed(m.logger, "close ", name) monitor.Start("close ", name) err = E.Append(err, closer.Close(), func(err error) error { return E.Cause(err, "close ", name) }) monitor.Finish() done() } } return nil } func (m *Manager) Outbounds() []adapter.Outbound { m.access.RLock() defer m.access.RUnlock() return m.outbounds } func (m *Manager) Outbound(tag string) (adapter.Outbound, bool) { m.access.RLock() outbound, found := m.outboundByTag[tag] m.access.RUnlock() if found { return outbound, true } return m.endpoint.Get(tag) } func (m *Manager) Default() adapter.Outbound { m.access.RLock() defer m.access.RUnlock() return m.defaultOutbound } func (m *Manager) Remove(tag string) error { m.access.Lock() defer m.access.Unlock() outbound, found := m.outboundByTag[tag] if !found { return os.ErrInvalid } delete(m.outboundByTag, tag) index := common.Index(m.outbounds, func(it adapter.Outbound) bool { return it == outbound }) if index == -1 { panic("invalid inbound index") } m.outbounds = append(m.outbounds[:index], m.outbounds[index+1:]...) started := m.started if m.defaultOutbound == outbound { if len(m.outbounds) > 0 { m.defaultOutbound = m.outbounds[0] m.logger.Info("updated default outbound to ", m.defaultOutbound.Tag()) } else { m.defaultOutbound = nil } } dependBy := m.dependByTag[tag] if len(dependBy) > 0 { return E.New("outbound[", tag, "] is depended by ", strings.Join(dependBy, ", ")) } dependencies := outbound.Dependencies() for _, dependency := range dependencies { if len(m.dependByTag[dependency]) == 1 { delete(m.dependByTag, dependency) } else { m.dependByTag[dependency] = common.Filter(m.dependByTag[dependency], func(it string) bool { return it != tag }) } } if started { return common.Close(outbound) } return nil } func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, inboundType string, options any) error { if tag == "" { return os.ErrInvalid } outbound, err := m.registry.CreateOutbound(ctx, router, logger, tag, inboundType, options) if err != nil { return err } if m.started { name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]" for _, stage := range adapter.ListStartStages { done := adapter.LogElapsed(m.logger, stage, " ", name) err = adapter.LegacyStart(outbound, stage) done() if err != nil { return E.Cause(err, stage, " ", name) } } } m.access.Lock() defer m.access.Unlock() if existsOutbound, loaded := m.outboundByTag[tag]; loaded { if m.started { err = common.Close(existsOutbound) if err != nil { return E.Cause(err, "close outbound/", existsOutbound.Type(), "[", existsOutbound.Tag(), "]") } } existsIndex := common.Index(m.outbounds, func(it adapter.Outbound) bool { return it == existsOutbound }) if existsIndex == -1 { panic("invalid inbound index") } m.outbounds = append(m.outbounds[:existsIndex], m.outbounds[existsIndex+1:]...) } m.outbounds = append(m.outbounds, outbound) m.outboundByTag[tag] = outbound dependencies := outbound.Dependencies() for _, dependency := range dependencies { m.dependByTag[dependency] = append(m.dependByTag[dependency], tag) } if tag == m.defaultTag || (m.defaultTag == "" && m.defaultOutbound == nil) { m.defaultOutbound = outbound if m.started { m.logger.Info("updated default outbound to ", outbound.Tag()) } } return nil } ================================================ FILE: adapter/outbound/registry.go ================================================ package outbound 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, router adapter.Router, logger log.ContextLogger, tag string, options T) (adapter.Outbound, error) func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) { registry.register(outboundType, func() any { return new(Options) }, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, rawOptions any) (adapter.Outbound, error) { var options *Options if rawOptions != nil { options = rawOptions.(*Options) } return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options)) }) } var _ adapter.OutboundRegistry = (*Registry)(nil) type ( optionsConstructorFunc func() any constructorFunc func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Outbound, error) ) type Registry struct { access sync.Mutex optionsType map[string]optionsConstructorFunc constructors map[string]constructorFunc } func NewRegistry() *Registry { return &Registry{ optionsType: make(map[string]optionsConstructorFunc), constructors: make(map[string]constructorFunc), } } func (r *Registry) CreateOptions(outboundType string) (any, bool) { r.access.Lock() defer r.access.Unlock() optionsConstructor, loaded := r.optionsType[outboundType] if !loaded { return nil, false } return optionsConstructor(), true } func (r *Registry) CreateOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Outbound, error) { r.access.Lock() defer r.access.Unlock() constructor, loaded := r.constructors[outboundType] if !loaded { return nil, E.New("outbound type not found: " + outboundType) } return constructor(ctx, router, logger, tag, options) } func (r *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) { r.access.Lock() defer r.access.Unlock() r.optionsType[outboundType] = optionsConstructor r.constructors[outboundType] = constructor } ================================================ FILE: adapter/outbound.go ================================================ package adapter import ( "context" "net/netip" "time" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-tun" N "github.com/sagernet/sing/common/network" ) // Note: for proxy protocols, outbound creates early connections by default. type Outbound interface { Type() string Tag() string Network() []string Dependencies() []string N.Dialer } type OutboundWithPreferredRoutes interface { Outbound PreferredDomain(domain string) bool PreferredAddress(address netip.Addr) bool } type DirectRouteOutbound interface { Outbound NewDirectRouteConnection(metadata InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) } type OutboundRegistry interface { option.OutboundOptionsRegistry CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error) } type OutboundManager interface { Lifecycle Outbounds() []Outbound Outbound(tag string) (Outbound, bool) Default() Outbound Remove(tag string) error Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) error } ================================================ FILE: adapter/platform.go ================================================ package adapter import ( "net/netip" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/logger" ) type PlatformInterface interface { Initialize(networkManager NetworkManager) error UsePlatformAutoDetectInterfaceControl() bool AutoDetectInterfaceControl(fd int) error UsePlatformInterface() bool OpenInterface(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error) UsePlatformDefaultInterfaceMonitor() bool CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor UsePlatformNetworkInterfaces() bool NetworkInterfaces() ([]NetworkInterface, error) UnderNetworkExtension() bool NetworkExtensionIncludeAllNetworks() bool ClearDNSCache() RequestPermissionForWIFIState() error ReadWIFIState() WIFIState SystemCertificates() []string UsePlatformConnectionOwnerFinder() bool FindConnectionOwner(request *FindConnectionOwnerRequest) (*ConnectionOwner, error) UsePlatformWIFIMonitor() bool UsePlatformNotification() bool SendNotification(notification *Notification) error MyInterfaceAddress() []netip.Addr UsePlatformNeighborResolver() bool StartNeighborMonitor(listener NeighborUpdateListener) error CloseNeighborMonitor(listener NeighborUpdateListener) error } type FindConnectionOwnerRequest struct { IpProtocol int32 SourceAddress string SourcePort int32 DestinationAddress string DestinationPort int32 } type ConnectionOwner struct { ProcessID uint32 UserId int32 UserName string ProcessPath string AndroidPackageNames []string } type Notification struct { Identifier string TypeName string TypeID int32 Title string Subtitle string Body string OpenURL string } type SystemProxyStatus struct { Available bool Enabled bool } ================================================ FILE: adapter/prestart.go ================================================ package adapter ================================================ FILE: adapter/router.go ================================================ package adapter import ( "context" "net" "time" "github.com/sagernet/sing-tun" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/x/list" "go4.org/netipx" ) type Router interface { Lifecycle ConnectionRouter PreMatch(metadata InboundContext, context tun.DirectRouteContext, timeout time.Duration, supportBypass bool) (tun.DirectRouteDestination, error) ConnectionRouterEx RuleSet(tag string) (RuleSet, bool) Rules() []Rule NeedFindProcess() bool NeedFindNeighbor() bool NeighborResolver() NeighborResolver AppendTracker(tracker ConnectionTracker) ResetNetwork() } type ConnectionTracker interface { RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule, matchOutbound Outbound) net.Conn RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule, matchOutbound Outbound) N.PacketConn } // Deprecated: Use ConnectionRouterEx instead. type ConnectionRouter interface { RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error } type ConnectionRouterEx interface { ConnectionRouter RouteConnectionEx(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc) } type RuleSet interface { Name() string StartContext(ctx context.Context, startContext *HTTPStartContext) error PostStart() error Metadata() RuleSetMetadata ExtractIPSet() []*netipx.IPSet IncRef() DecRef() Cleanup() RegisterCallback(callback RuleSetUpdateCallback) *list.Element[RuleSetUpdateCallback] UnregisterCallback(element *list.Element[RuleSetUpdateCallback]) Close() error HeadlessRule } 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 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 } ================================================ FILE: adapter/rule.go ================================================ package adapter import ( C "github.com/sagernet/sing-box/constant" "github.com/miekg/dns" ) type HeadlessRule interface { Match(metadata *InboundContext) bool String() string } type Rule interface { HeadlessRule SimpleLifecycle Type() string Action() RuleAction } type DNSRule interface { Rule LegacyPreMatch(metadata *InboundContext) bool WithAddressLimit() bool MatchAddressLimit(metadata *InboundContext, response *dns.Msg) bool } type RuleAction interface { Type() string String() string } func IsFinalAction(action RuleAction) bool { switch action.Type() { case C.RuleActionTypeSniff, C.RuleActionTypeResolve, C.RuleActionTypeEvaluate: return false default: return true } } ================================================ FILE: adapter/service/adapter.go ================================================ package service type Adapter struct { serviceType string serviceTag string } func NewAdapter(serviceType string, serviceTag string) Adapter { return Adapter{ serviceType: serviceType, serviceTag: serviceTag, } } func (a *Adapter) Type() string { return a.serviceType } func (a *Adapter) Tag() string { return a.serviceTag } ================================================ FILE: adapter/service/manager.go ================================================ package service import ( "context" "os" "sync" "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" ) var _ adapter.ServiceManager = (*Manager)(nil) type Manager struct { logger log.ContextLogger registry adapter.ServiceRegistry access sync.Mutex started bool stage adapter.StartStage services []adapter.Service serviceByTag map[string]adapter.Service } func NewManager(logger log.ContextLogger, registry adapter.ServiceRegistry) *Manager { return &Manager{ logger: logger, registry: registry, serviceByTag: make(map[string]adapter.Service), } } 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 services := m.services m.access.Unlock() for _, service := range services { name := "service/" + service.Type() + "[" + service.Tag() + "]" done := adapter.LogElapsed(m.logger, stage, " ", name) err := adapter.LegacyStart(service, stage) done() if err != nil { return E.Cause(err, stage, " ", name) } } return nil } func (m *Manager) Close() error { m.access.Lock() defer m.access.Unlock() if !m.started { return nil } m.started = false services := m.services m.services = nil monitor := taskmonitor.New(m.logger, C.StopTimeout) var err error for _, service := range services { name := "service/" + service.Type() + "[" + service.Tag() + "]" done := adapter.LogElapsed(m.logger, "close ", name) monitor.Start("close ", name) err = E.Append(err, service.Close(), func(err error) error { return E.Cause(err, "close ", name) }) monitor.Finish() done() } return nil } func (m *Manager) Services() []adapter.Service { m.access.Lock() defer m.access.Unlock() return m.services } func (m *Manager) Get(tag string) (adapter.Service, bool) { m.access.Lock() service, found := m.serviceByTag[tag] m.access.Unlock() return service, found } func (m *Manager) Remove(tag string) error { m.access.Lock() service, found := m.serviceByTag[tag] if !found { m.access.Unlock() return os.ErrInvalid } delete(m.serviceByTag, tag) index := common.Index(m.services, func(it adapter.Service) bool { return it == service }) if index == -1 { panic("invalid service index") } m.services = append(m.services[:index], m.services[index+1:]...) started := m.started m.access.Unlock() if started { return service.Close() } return nil } func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error { service, err := m.registry.Create(ctx, logger, tag, serviceType, options) if err != nil { return err } m.access.Lock() defer m.access.Unlock() if m.started { name := "service/" + service.Type() + "[" + service.Tag() + "]" for _, stage := range adapter.ListStartStages { done := adapter.LogElapsed(m.logger, stage, " ", name) err = adapter.LegacyStart(service, stage) done() if err != nil { return E.Cause(err, stage, " ", name) } } } if existsService, loaded := m.serviceByTag[tag]; loaded { if m.started { err = existsService.Close() if err != nil { return E.Cause(err, "close service/", existsService.Type(), "[", existsService.Tag(), "]") } } existsIndex := common.Index(m.services, func(it adapter.Service) bool { return it == existsService }) if existsIndex == -1 { panic("invalid service index") } m.services = append(m.services[:existsIndex], m.services[existsIndex+1:]...) } m.services = append(m.services, service) m.serviceByTag[tag] = service return nil } ================================================ FILE: adapter/service/registry.go ================================================ package service 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.Service, error) func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) { registry.register(outboundType, func() any { return new(Options) }, func(ctx context.Context, logger log.ContextLogger, tag string, rawOptions any) (adapter.Service, error) { var options *Options if rawOptions != nil { options = rawOptions.(*Options) } return constructor(ctx, logger, tag, common.PtrValueOrDefault(options)) }) } var _ adapter.ServiceRegistry = (*Registry)(nil) type ( optionsConstructorFunc func() any constructorFunc func(ctx context.Context, logger log.ContextLogger, tag string, options any) (adapter.Service, 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(outboundType string) (any, bool) { m.access.Lock() defer m.access.Unlock() optionsConstructor, loaded := m.optionsType[outboundType] if !loaded { return nil, false } return optionsConstructor(), true } func (m *Registry) Create(ctx context.Context, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Service, error) { m.access.Lock() defer m.access.Unlock() constructor, loaded := m.constructor[outboundType] if !loaded { return nil, E.New("outbound type not found: " + outboundType) } return constructor(ctx, logger, tag, options) } func (m *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) { m.access.Lock() defer m.access.Unlock() m.optionsType[outboundType] = optionsConstructor m.constructor[outboundType] = constructor } ================================================ FILE: adapter/service.go ================================================ package adapter import ( "context" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" ) type Service interface { Lifecycle Type() string Tag() string } type ServiceRegistry interface { option.ServiceOptionsRegistry Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) (Service, error) } type ServiceManager interface { Lifecycle Services() []Service Get(tag string) (Service, bool) Remove(tag string) error Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error } ================================================ FILE: adapter/ssm.go ================================================ package adapter import ( "net" N "github.com/sagernet/sing/common/network" ) type ManagedSSMServer interface { Inbound SetTracker(tracker SSMTracker) UpdateUsers(users []string, uPSKs []string) error } type SSMTracker interface { TrackConnection(conn net.Conn, metadata InboundContext) net.Conn TrackPacketConnection(conn N.PacketConn, metadata InboundContext) N.PacketConn } ================================================ FILE: adapter/tailscale.go ================================================ package adapter import "context" 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 { BackendState string AuthURL string NetworkName string MagicDNSSuffix string Self *TailscalePeer UserGroups []*TailscaleUserGroup } type TailscaleUserGroup struct { UserID int64 LoginName string DisplayName string ProfilePicURL string 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 } ================================================ FILE: adapter/time.go ================================================ package adapter import "time" type TimeService interface { SimpleLifecycle TimeFunc() func() time.Time } ================================================ FILE: adapter/upstream.go ================================================ package adapter import ( "context" "net" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) type ( 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 NewUpstreamHandler( metadata InboundContext, connectionHandler ConnectionHandlerFunc, packetHandler PacketConnectionHandlerFunc, ) UpstreamHandlerAdapter { return &myUpstreamHandlerWrapper{ metadata: metadata, connectionHandler: connectionHandler, packetHandler: packetHandler, } } var _ UpstreamHandlerAdapter = (*myUpstreamHandlerWrapper)(nil) type myUpstreamHandlerWrapper struct { metadata InboundContext connectionHandler ConnectionHandlerFunc packetHandler PacketConnectionHandlerFunc } 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 } if destination.IsValid() { myMetadata.Destination = destination } w.connectionHandler(ctx, conn, myMetadata, onClose) } 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 } if destination.IsValid() { myMetadata.Destination = destination } w.packetHandler(ctx, conn, myMetadata, onClose) } var _ UpstreamHandlerAdapter = (*myUpstreamContextHandlerWrapper)(nil) type myUpstreamContextHandlerWrapper struct { connectionHandler ConnectionHandlerFunc packetHandler PacketConnectionHandlerFunc } func NewUpstreamContextHandler( connectionHandler ConnectionHandlerFunc, packetHandler PacketConnectionHandlerFunc, ) UpstreamHandlerAdapter { return &myUpstreamContextHandlerWrapper{ connectionHandler: connectionHandler, packetHandler: packetHandler, } } 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 } if destination.IsValid() { myMetadata.Destination = destination } w.connectionHandler(ctx, conn, *myMetadata, onClose) } 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 } if destination.IsValid() { myMetadata.Destination = destination } w.packetHandler(ctx, conn, *myMetadata, onClose) } func NewRouteHandler( metadata InboundContext, router ConnectionRouterEx, ) UpstreamHandlerAdapter { return &routeHandlerWrapper{ metadata: metadata, router: router, } } var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil) type routeHandlerWrapper struct { metadata InboundContext router ConnectionRouterEx } 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 } if destination.IsValid() { r.metadata.Destination = destination } r.router.RouteConnectionEx(ctx, conn, r.metadata, onClose) } 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 } if destination.IsValid() { r.metadata.Destination = destination } r.router.RoutePacketConnectionEx(ctx, conn, r.metadata, onClose) } func NewRouteContextHandler( router ConnectionRouterEx, ) UpstreamHandlerAdapter { return &routeContextHandlerWrapper{ router: router, } } var _ UpstreamHandlerAdapter = (*routeContextHandlerWrapper)(nil) type routeContextHandlerWrapper struct { router ConnectionRouterEx } 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 } if destination.IsValid() { metadata.Destination = destination } r.router.RouteConnectionEx(ctx, conn, *metadata, onClose) } 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 } if destination.IsValid() { metadata.Destination = destination } r.router.RoutePacketConnectionEx(ctx, conn, *metadata, onClose) } ================================================ FILE: adapter/upstream_legacy.go ================================================ package adapter import ( "context" "net" 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" ) type ( // Deprecated LegacyConnectionHandlerFunc = func(ctx context.Context, conn net.Conn, metadata InboundContext) error // Deprecated LegacyPacketConnectionHandlerFunc = func(ctx context.Context, conn N.PacketConn, metadata InboundContext) error ) // Deprecated // //nolint:staticcheck type LegacyUpstreamHandlerAdapter interface { N.TCPConnectionHandler N.UDPConnectionHandler E.Handler } // Deprecated // //nolint:staticcheck func NewLegacyUpstreamHandler( metadata InboundContext, connectionHandler LegacyConnectionHandlerFunc, packetHandler LegacyPacketConnectionHandlerFunc, errorHandler E.Handler, ) LegacyUpstreamHandlerAdapter { return &legacyUpstreamHandlerWrapper{ metadata: metadata, connectionHandler: connectionHandler, packetHandler: packetHandler, errorHandler: errorHandler, } } var _ LegacyUpstreamHandlerAdapter = (*legacyUpstreamHandlerWrapper)(nil) // Deprecated: use NewUpstreamHandler instead. // //nolint:staticcheck type legacyUpstreamHandlerWrapper struct { metadata InboundContext connectionHandler LegacyConnectionHandlerFunc packetHandler LegacyPacketConnectionHandlerFunc errorHandler E.Handler } // 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 } if metadata.Destination.IsValid() { myMetadata.Destination = metadata.Destination } return w.connectionHandler(ctx, conn, myMetadata) } // 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 } if metadata.Destination.IsValid() { myMetadata.Destination = metadata.Destination } return w.packetHandler(ctx, conn, myMetadata) } // Deprecated: use NewUpstreamHandler instead. func (w *legacyUpstreamHandlerWrapper) NewError(ctx context.Context, err error) { w.errorHandler.NewError(ctx, err) } // Deprecated: removed func UpstreamMetadata(metadata InboundContext) M.Metadata { return M.Metadata{ Source: metadata.Source.Unwrap(), Destination: metadata.Destination.Unwrap(), } } // Deprecated: Use NewUpstreamContextHandler instead. type legacyUpstreamContextHandlerWrapper struct { connectionHandler LegacyConnectionHandlerFunc packetHandler LegacyPacketConnectionHandlerFunc errorHandler E.Handler } // Deprecated: Use NewUpstreamContextHandler instead. func NewLegacyUpstreamContextHandler( connectionHandler LegacyConnectionHandlerFunc, packetHandler LegacyPacketConnectionHandlerFunc, errorHandler E.Handler, ) LegacyUpstreamHandlerAdapter { return &legacyUpstreamContextHandlerWrapper{ connectionHandler: connectionHandler, packetHandler: packetHandler, errorHandler: errorHandler, } } // 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 } if metadata.Destination.IsValid() { myMetadata.Destination = metadata.Destination } return w.connectionHandler(ctx, conn, *myMetadata) } // 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 } if metadata.Destination.IsValid() { myMetadata.Destination = metadata.Destination } return w.packetHandler(ctx, conn, *myMetadata) } // Deprecated: Use NewUpstreamContextHandler instead. func (w *legacyUpstreamContextHandlerWrapper) NewError(ctx context.Context, err error) { w.errorHandler.NewError(ctx, err) } // Deprecated: Use ConnectionRouterEx instead. func NewLegacyRouteHandler( metadata InboundContext, router ConnectionRouter, logger logger.ContextLogger, ) LegacyUpstreamHandlerAdapter { return &legacyRouteHandlerWrapper{ metadata: metadata, router: router, logger: logger, } } // Deprecated: Use ConnectionRouterEx instead. func NewLegacyRouteContextHandler( router ConnectionRouter, logger logger.ContextLogger, ) LegacyUpstreamHandlerAdapter { return &legacyRouteContextHandlerWrapper{ router: router, logger: logger, } } var _ LegacyUpstreamHandlerAdapter = (*legacyRouteHandlerWrapper)(nil) // Deprecated: Use ConnectionRouterEx instead. // //nolint:staticcheck type legacyRouteHandlerWrapper struct { metadata InboundContext router ConnectionRouter logger logger.ContextLogger } // Deprecated: Use ConnectionRouterEx instead. 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 } if metadata.Destination.IsValid() { myMetadata.Destination = metadata.Destination } return w.router.RouteConnection(ctx, conn, myMetadata) } // Deprecated: Use ConnectionRouterEx instead. 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 } if metadata.Destination.IsValid() { myMetadata.Destination = metadata.Destination } return w.router.RoutePacketConnection(ctx, conn, myMetadata) } // Deprecated: Use ConnectionRouterEx instead. func (w *legacyRouteHandlerWrapper) NewError(ctx context.Context, err error) { w.logger.ErrorContext(ctx, err) } var _ LegacyUpstreamHandlerAdapter = (*legacyRouteContextHandlerWrapper)(nil) // Deprecated: Use ConnectionRouterEx instead. type legacyRouteContextHandlerWrapper struct { router ConnectionRouter logger logger.ContextLogger } // Deprecated: Use ConnectionRouterEx instead. 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 } if metadata.Destination.IsValid() { myMetadata.Destination = metadata.Destination } return w.router.RouteConnection(ctx, conn, *myMetadata) } // Deprecated: Use ConnectionRouterEx instead. 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 } if metadata.Destination.IsValid() { myMetadata.Destination = metadata.Destination } return w.router.RoutePacketConnection(ctx, conn, *myMetadata) } // Deprecated: Use ConnectionRouterEx instead. func (w *legacyRouteContextHandlerWrapper) NewError(ctx context.Context, err error) { w.logger.ErrorContext(ctx, err) } ================================================ FILE: adapter/v2ray.go ================================================ package adapter import ( "context" "net" N "github.com/sagernet/sing/common/network" ) type V2RayServerTransport interface { Network() []string Serve(listener net.Listener) error ServePacket(listener net.PacketConn) error Close() error } type V2RayServerTransportHandler interface { N.TCPConnectionHandlerEx } type V2RayClientTransport interface { DialContext(ctx context.Context) (net.Conn, error) Close() error } ================================================ FILE: box.go ================================================ package box import ( "context" "fmt" "io" "os" "runtime/debug" "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" 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" "github.com/sagernet/sing-box/route" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/pause" ) 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 certificateProvider *boxCertificate.Manager dnsTransport *dns.TransportManager dnsRouter *dns.Router connection *route.ConnectionManager router *route.Router httpClientService adapter.LifecycleService internalService []adapter.LifecycleService done chan struct{} } type Options struct { option.Options Context context.Context PlatformLogWriter log.PlatformWriter } func Context( ctx context.Context, inboundRegistry adapter.InboundRegistry, outboundRegistry adapter.OutboundRegistry, 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 { ctx = service.ContextWith[option.InboundOptionsRegistry](ctx, inboundRegistry) ctx = service.ContextWith[adapter.InboundRegistry](ctx, inboundRegistry) } if service.FromContext[option.OutboundOptionsRegistry](ctx) == nil || service.FromContext[adapter.OutboundRegistry](ctx) == nil { ctx = service.ContextWith[option.OutboundOptionsRegistry](ctx, outboundRegistry) ctx = service.ContextWith[adapter.OutboundRegistry](ctx, outboundRegistry) } if service.FromContext[option.EndpointOptionsRegistry](ctx) == nil || service.FromContext[adapter.EndpointRegistry](ctx) == nil { ctx = service.ContextWith[option.EndpointOptionsRegistry](ctx, endpointRegistry) ctx = service.ContextWith[adapter.EndpointRegistry](ctx, endpointRegistry) } if service.FromContext[adapter.DNSTransportRegistry](ctx) == nil { ctx = service.ContextWith[option.DNSTransportOptionsRegistry](ctx, dnsTransportRegistry) ctx = service.ContextWith[adapter.DNSTransportRegistry](ctx, dnsTransportRegistry) } if service.FromContext[adapter.ServiceRegistry](ctx) == nil { 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 } func New(options Options) (*Box, error) { createdAt := time.Now() ctx := options.Context if ctx == nil { ctx = context.Background() } ctx = service.ContextWithDefaultRegistry(ctx) endpointRegistry := service.FromContext[adapter.EndpointRegistry](ctx) inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx) 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") } if inboundRegistry == nil { return nil, E.New("missing inbound registry in context") } if outboundRegistry == nil { return nil, E.New("missing outbound registry in context") } if dnsTransportRegistry == nil { return nil, E.New("missing DNS transport registry in context") } 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) err := applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug)) if err != nil { return nil, err } var needCacheFile bool var needClashAPI bool var needV2RayAPI bool if experimentalOptions.CacheFile != nil && experimentalOptions.CacheFile.Enabled || options.PlatformLogWriter != nil { needCacheFile = true } if experimentalOptions.ClashAPI != nil || options.PlatformLogWriter != nil { needClashAPI = true } if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" { needV2RayAPI = true } platformInterface := service.FromContext[adapter.PlatformInterface](ctx) var defaultLogWriter io.Writer if platformInterface != nil { defaultLogWriter = io.Discard } logFactory, err := log.New(log.Options{ Context: ctx, Options: common.PtrValueOrDefault(options.Log), Observable: needClashAPI, DefaultWriter: defaultLogWriter, BaseTime: createdAt, PlatformWriter: options.PlatformLogWriter, }) if err != nil { return nil, E.Cause(err, "create log factory") } 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 || len(certificateOptions.CertificatePath) > 0 || len(certificateOptions.CertificateDirectoryPath) > 0 { certificateStore, err := certificate.NewStore(ctx, logFactory.NewLogger("certificate"), certificateOptions) if err != nil { return nil, err } service.MustRegister[adapter.CertificateStore](ctx, certificateStore) internalServices = append(internalServices, certificateStore) } dnsOptions := common.PtrValueOrDefault(options.DNS) endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry) inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager) 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, 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) if err != nil { return nil, E.Cause(err, "initialize network manager") } 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) if err != nil { return nil, E.Cause(err, "initialize router") } ntpOptions := common.PtrValueOrDefault(options.NTP) var timeService *tls.TimeServiceWrapper if ntpOptions.Enabled { timeService = new(tls.TimeServiceWrapper) service.MustRegister[ntp.TimeService](ctx, timeService) } for i, transportOptions := range dnsOptions.Servers { var tag string if transportOptions.Tag != "" { tag = transportOptions.Tag } else { tag = F.ToString(i) } err = dnsTransportManager.Create( ctx, logFactory.NewLogger(F.ToString("dns/", transportOptions.Type, "[", tag, "]")), tag, transportOptions.Type, transportOptions.Options, ) if err != nil { return nil, E.Cause(err, "initialize DNS server[", i, "]") } } err = dnsRouter.Initialize(dnsOptions.Rules) if err != nil { return nil, E.Cause(err, "initialize dns router") } for i, endpointOptions := range options.Endpoints { var tag string if endpointOptions.Tag != "" { tag = endpointOptions.Tag } else { tag = F.ToString(i) } endpointCtx := ctx if tag != "" { // TODO: remove this endpointCtx = adapter.WithContext(endpointCtx, &adapter.InboundContext{ Outbound: tag, }) } err = endpointManager.Create( endpointCtx, router, logFactory.NewLogger(F.ToString("endpoint/", endpointOptions.Type, "[", tag, "]")), tag, endpointOptions.Type, endpointOptions.Options, ) if err != nil { return nil, E.Cause(err, "initialize endpoint[", i, "]") } } for i, inboundOptions := range options.Inbounds { var tag string if inboundOptions.Tag != "" { tag = inboundOptions.Tag } else { tag = F.ToString(i) } err = inboundManager.Create( ctx, router, logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")), tag, inboundOptions.Type, inboundOptions.Options, ) if err != nil { 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 != "" { tag = outboundOptions.Tag } else { tag = F.ToString(i) } outboundCtx := ctx if tag != "" { // TODO: remove this outboundCtx = adapter.WithContext(outboundCtx, &adapter.InboundContext{ Outbound: tag, }) } err = outboundManager.Create( outboundCtx, router, logFactory.NewLogger(F.ToString("outbound/", outboundOptions.Type, "[", tag, "]")), tag, outboundOptions.Type, outboundOptions.Options, ) if err != nil { return nil, E.Cause(err, "initialize outbound[", i, "]") } } for i, certificateProviderOptions := range options.CertificateProviders { var tag string if certificateProviderOptions.Tag != "" { tag = certificateProviderOptions.Tag } else { tag = F.ToString(i) } err = certificateProviderManager.Create( ctx, logFactory.NewLogger(F.ToString("certificate-provider/", certificateProviderOptions.Type, "[", tag, "]")), tag, certificateProviderOptions.Type, certificateProviderOptions.Options, ) if err != nil { return nil, E.Cause(err, "initialize certificate provider[", i, "]") } } outboundManager.Initialize(func() (adapter.Outbound, error) { return direct.NewOutbound( ctx, router, logFactory.NewLogger("outbound/direct"), "direct", option.DirectOutboundOptions{}, ) }) dnsTransportManager.Initialize(func() (adapter.DNSTransport, error) { return dnsTransportRegistry.CreateDNSTransport( ctx, logFactory.NewLogger("dns/local"), "local", C.DNSTypeLocal, &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 { return nil, E.Cause(err, "initialize platform interface") } } if needCacheFile { cacheFile := cachefile.New(ctx, logFactory.NewLogger("cache-file"), common.PtrValueOrDefault(experimentalOptions.CacheFile)) service.MustRegister[adapter.CacheFile](ctx, cacheFile) internalServices = append(internalServices, cacheFile) } if needClashAPI { clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI) clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options) clashServer, err := experimental.NewClashServer(ctx, logFactory.(log.ObservableFactory), clashAPIOptions) if err != nil { return nil, E.Cause(err, "create clash-server") } router.AppendTracker(clashServer) service.MustRegister[adapter.ClashServer](ctx, clashServer) internalServices = append(internalServices, clashServer) } if needV2RayAPI { v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI)) if err != nil { return nil, E.Cause(err, "create v2ray-server") } if v2rayServer.StatsService() != nil { router.AppendTracker(v2rayServer.StatsService()) internalServices = append(internalServices, v2rayServer) service.MustRegister[adapter.V2RayServer](ctx, v2rayServer) } } if ntpOptions.Enabled { ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions, ntpOptions.ServerIsDomain()) if err != nil { return nil, E.Cause(err, "create NTP service") } ntpService := ntp.NewService(ntp.Options{ Context: ctx, Dialer: ntpDialer, Logger: logFactory.NewLogger("ntp"), Server: ntpOptions.ServerOptions.Build(), Interval: time.Duration(ntpOptions.Interval), WriteToSystem: ntpOptions.WriteToSystem, }) timeService.TimeService = ntpService internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service")) } return &Box{ network: networkManager, endpoint: endpointManager, inbound: inboundManager, outbound: outboundManager, dnsTransport: dnsTransportManager, service: serviceManager, certificateProvider: certificateProviderManager, dnsRouter: dnsRouter, connection: connectionManager, router: router, httpClientService: httpClientService, createdAt: createdAt, logFactory: logFactory, logger: logFactory.Logger(), internalService: internalServices, done: make(chan struct{}), }, nil } func (s *Box) PreStart() error { err := s.preStart() if err != nil { // TODO: remove catch error defer func() { v := recover() if v != nil { println(err.Error()) debug.PrintStack() panic("panic on early close: " + fmt.Sprint(v)) } }() s.Close() return err } s.logger.Info("sing-box pre-started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)") return nil } func (s *Box) Start() error { err := s.start() if err != nil { // TODO: remove catch error defer func() { v := recover() if v != nil { println(err.Error()) debug.PrintStack() println("panic on early start: " + fmt.Sprint(v)) } }() s.Close() return err } s.logger.Info("sing-box started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)") return nil } func (s *Box) preStart() error { monitor := taskmonitor.New(s.logger, C.StartTimeout) monitor.Start("start logger") err := s.logFactory.Start() monitor.Finish() if err != nil { return E.Cause(err, "start logger") } err = adapter.StartNamed(s.logger, adapter.StartStateInitialize, s.internalService) // cache-file clash-api v2ray-api 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, s.certificateProvider) if err != nil { return err } 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 } return nil } func (s *Box) start() error { err := s.preStart() if err != nil { return err } err = adapter.StartNamed(s.logger, adapter.StartStateStart, s.internalService) if err != nil { return err } err = adapter.Start(s.logger, adapter.StartStateStart, s.endpoint) if err != nil { return err } 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 } err = adapter.StartNamed(s.logger, adapter.StartStatePostStart, s.internalService) 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.endpoint, s.certificateProvider, s.inbound, s.service) if err != nil { return err } err = adapter.StartNamed(s.logger, adapter.StartStateStarted, s.internalService) if err != nil { return err } return nil } func (s *Box) Close() error { select { case <-s.done: return os.ErrClosed default: close(s.done) } var err error for _, closeItem := range []struct { name string service adapter.Lifecycle }{ {"service", s.service}, {"inbound", s.inbound}, {"certificate-provider", s.certificateProvider}, {"endpoint", s.endpoint}, {"outbound", s.outbound}, {"router", s.router}, {"connection", s.connection}, {"dns-router", s.dnsRouter}, {"dns-transport", s.dnsTransport}, {"network", s.network}, } { done := adapter.LogElapsed(s.logger, "close ", closeItem.name) err = E.Append(err, closeItem.service.Close(), func(err error) error { return E.Cause(err, "close ", closeItem.name) }) 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 { return E.Cause(err, "close ", lifecycleService.Name()) }) done() } done := adapter.LogElapsed(s.logger, "close logger") err = E.Append(err, s.logFactory.Close(), func(err error) error { return E.Cause(err, "close logger") }) done() return err } func (s *Box) Network() adapter.NetworkManager { return s.network } func (s *Box) Router() adapter.Router { return s.router } func (s *Box) Inbound() adapter.InboundManager { return s.inbound } func (s *Box) Outbound() adapter.OutboundManager { return s.outbound } func (s *Box) Endpoint() adapter.EndpointManager { return s.endpoint } func (s *Box) LogFactory() log.Factory { return s.logFactory } ================================================ FILE: cmd/internal/app_store_connect/main.go ================================================ package main import ( "context" "net/http" "os" "strconv" "strings" "time" "github.com/sagernet/asc-go/asc" "github.com/sagernet/sing-box/cmd/internal/build_shared" "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" ) func main() { ctx := context.Background() switch os.Args[1] { case "next_macos_project_version": err := fetchMacOSVersion(ctx) if err != nil { log.Fatal(err) } case "publish_testflight": err := publishTestflight(ctx) if err != nil { log.Fatal(err) } case "cancel_app_store": err := cancelAppStore(ctx, os.Args[2]) if err != nil { log.Fatal(err) } case "prepare_app_store": err := prepareAppStore(ctx) if err != nil { log.Fatal(err) } case "publish_app_store": err := publishAppStore(ctx) if err != nil { log.Fatal(err) } default: log.Fatal("unknown action: ", os.Args[1]) } } const ( appID = "6673731168" groupID = "5c5f3b78-b7a0-40c0-bcad-e6ef87bbefda" ) func createClient(expireDuration time.Duration) *asc.Client { privateKey, err := os.ReadFile(os.Getenv("ASC_KEY_PATH")) if err != nil { log.Fatal(err) } tokenConfig, err := asc.NewTokenConfig(os.Getenv("ASC_KEY_ID"), os.Getenv("ASC_KEY_ISSUER_ID"), expireDuration, privateKey) if err != nil { log.Fatal(err) } return asc.NewClient(tokenConfig.Client()) } func fetchMacOSVersion(ctx context.Context) error { client := createClient(time.Minute) versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{ FilterPlatform: []string{"MAC_OS"}, }) if err != nil { return err } var versionID string findVersion: for _, version := range versions.Data { switch *version.Attributes.AppStoreState { case asc.AppStoreVersionStateReadyForSale, asc.AppStoreVersionStatePendingDeveloperRelease: versionID = version.ID break findVersion } } if versionID == "" { return E.New("no version found") } latestBuild, _, err := client.Builds.GetBuildForAppStoreVersion(ctx, versionID, &asc.GetBuildForAppStoreVersionQuery{}) if err != nil { return err } versionInt, err := strconv.Atoi(*latestBuild.Data.Attributes.Version) if err != nil { return E.Cause(err, "parse version code") } os.Stdout.WriteString(F.ToString(versionInt+1, "\n")) return nil } func publishTestflight(ctx context.Context) error { if len(os.Args) < 3 { return E.New("platform required: ios, macos, or tvos") } var platform asc.Platform switch os.Args[2] { case "ios": platform = asc.PlatformIOS case "macos": platform = asc.PlatformMACOS case "tvos": platform = asc.PlatformTVOS default: return E.New("unknown platform: ", os.Args[2]) } tagVersion, err := build_shared.ReadTagVersion() if err != nil { return err } tag := tagVersion.VersionString() releaseNotes := F.ToString("sing-box ", tagVersion.String()) if len(os.Args) >= 4 { releaseNotes = strings.Join(os.Args[3:], " ") } client := createClient(20 * time.Minute) log.Info(tag, " list build IDs") buildIDsResponse, _, err := client.TestFlight.ListBuildIDsForBetaGroup(ctx, groupID, nil) if err != nil { return err } buildIDs := common.Map(buildIDsResponse.Data, func(it asc.RelationshipData) string { return it.ID }) waitingForProcess := false log.Info(string(platform), " list builds") for { builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{ FilterApp: []string{appID}, FilterPreReleaseVersionPlatform: []string{string(platform)}, }) if err != nil { return err } build := builds.Data[0] log.Info(string(platform), " ", tag, " found build: ", build.ID, " (", *build.Attributes.Version, ")") if !waitingForProcess && (common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute) { log.Info(string(platform), " ", tag, " waiting for process") time.Sleep(15 * time.Second) continue } if *build.Attributes.ProcessingState != "VALID" { waitingForProcess = true log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState) time.Sleep(15 * time.Second) continue } log.Info(string(platform), " ", tag, " list localizations") localizations, _, err := client.TestFlight.ListBetaBuildLocalizationsForBuild(ctx, build.ID, nil) if err != nil { return err } localization := common.Find(localizations.Data, func(it asc.BetaBuildLocalization) bool { return *it.Attributes.Locale == "en-US" }) if localization.ID == "" { log.Fatal(string(platform), " ", tag, " no en-US localization found") } if localization.Attributes == nil || localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew == "" { log.Info(string(platform), " ", tag, " update localization") _, _, err = client.TestFlight.UpdateBetaBuildLocalization(ctx, localization.ID, common.Ptr(releaseNotes)) if err != nil { return err } } log.Info(string(platform), " ", tag, " publish") response, err := client.TestFlight.AddBuildsToBetaGroup(ctx, groupID, []string{build.ID}) if response != nil && (response.StatusCode == http.StatusUnprocessableEntity || response.StatusCode == http.StatusNotFound) { log.Info("waiting for process") time.Sleep(15 * time.Second) continue } else if err != nil { return err } log.Info(string(platform), " ", tag, " list submissions") betaSubmissions, _, err := client.TestFlight.ListBetaAppReviewSubmissions(ctx, &asc.ListBetaAppReviewSubmissionsQuery{ FilterBuild: []string{build.ID}, }) if err != nil { return err } if len(betaSubmissions.Data) == 0 { log.Info(string(platform), " ", tag, " create submission") _, _, err = client.TestFlight.CreateBetaAppReviewSubmission(ctx, build.ID) if err != nil { if strings.Contains(err.Error(), "ANOTHER_BUILD_IN_REVIEW") { log.Error(err) break } return err } } break } return nil } func cancelAppStore(ctx context.Context, platform string) error { switch platform { case "ios": platform = string(asc.PlatformIOS) case "macos": platform = string(asc.PlatformMACOS) case "tvos": platform = string(asc.PlatformTVOS) } tag, err := build_shared.ReadTag() if err != nil { return err } client := createClient(time.Minute) for { log.Info(platform, " list versions") versions, response, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{ FilterPlatform: []string{string(platform)}, }) if isRetryable(response) { continue } else if err != nil { return err } version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool { return *it.Attributes.VersionString == tag }) if version.ID == "" { return nil } log.Info(platform, " ", tag, " get submission") submission, response, err := client.Submission.GetAppStoreVersionSubmissionForAppStoreVersion(ctx, version.ID, nil) if response != nil && response.StatusCode == http.StatusNotFound { return nil } if isRetryable(response) { continue } else if err != nil { return err } log.Info(platform, " ", tag, " delete submission") _, err = client.Submission.DeleteSubmission(ctx, submission.Data.ID) if err != nil { return err } return nil } } func prepareAppStore(ctx context.Context) error { tag, err := build_shared.ReadTag() if err != nil { return err } client := createClient(time.Minute) for _, platform := range []asc.Platform{ asc.PlatformIOS, asc.PlatformMACOS, asc.PlatformTVOS, } { log.Info(string(platform), " list versions") versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{ FilterPlatform: []string{string(platform)}, }) if err != nil { return err } version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool { return *it.Attributes.VersionString == tag }) log.Info(string(platform), " ", tag, " list builds") builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{ FilterApp: []string{appID}, FilterPreReleaseVersionPlatform: []string{string(platform)}, }) if err != nil { return err } if len(builds.Data) == 0 { log.Fatal(platform, " ", tag, " no build found") } buildID := common.Ptr(builds.Data[0].ID) if version.ID == "" { log.Info(string(platform), " ", tag, " create version") newVersion, _, err := client.Apps.CreateAppStoreVersion(ctx, asc.AppStoreVersionCreateRequestAttributes{ Platform: platform, VersionString: tag, }, appID, buildID) if err != nil { return err } version = newVersion.Data } else { log.Info(string(platform), " ", tag, " check build") currentBuild, response, err := client.Apps.GetBuildIDForAppStoreVersion(ctx, version.ID) if err != nil { return err } if response.StatusCode != http.StatusOK || currentBuild.Data.ID != *buildID { switch *version.Attributes.AppStoreState { case asc.AppStoreVersionStatePrepareForSubmission, asc.AppStoreVersionStateRejected, asc.AppStoreVersionStateDeveloperRejected: case asc.AppStoreVersionStateWaitingForReview, asc.AppStoreVersionStateInReview, asc.AppStoreVersionStatePendingDeveloperRelease: submission, _, err := client.Submission.GetAppStoreVersionSubmissionForAppStoreVersion(ctx, version.ID, nil) if err != nil { return err } if submission != nil { log.Info(string(platform), " ", tag, " delete submission") _, err = client.Submission.DeleteSubmission(ctx, submission.Data.ID) if err != nil { return err } time.Sleep(5 * time.Second) } default: log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState)) } log.Info(string(platform), " ", tag, " update build") response, err = client.Apps.UpdateBuildForAppStoreVersion(ctx, version.ID, buildID) if err != nil { return err } if response.StatusCode != http.StatusNoContent { response.Write(os.Stderr) log.Fatal(string(platform), " ", tag, " unexpected response: ", response.Status) } } else { switch *version.Attributes.AppStoreState { case asc.AppStoreVersionStatePrepareForSubmission, asc.AppStoreVersionStateRejected, asc.AppStoreVersionStateDeveloperRejected: case asc.AppStoreVersionStateWaitingForReview, asc.AppStoreVersionStateInReview, asc.AppStoreVersionStatePendingDeveloperRelease: continue default: log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState)) } } } log.Info(string(platform), " ", tag, " list localization") localizations, _, err := client.Apps.ListLocalizationsForAppStoreVersion(ctx, version.ID, nil) if err != nil { return err } localization := common.Find(localizations.Data, func(it asc.AppStoreVersionLocalization) bool { return *it.Attributes.Locale == "en-US" }) if localization.ID == "" { log.Info(string(platform), " ", tag, " no en-US localization found") } if localization.Attributes == nil || localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew == "" { log.Info(string(platform), " ", tag, " update localization") _, _, err = client.Apps.UpdateAppStoreVersionLocalization(ctx, localization.ID, &asc.AppStoreVersionLocalizationUpdateRequestAttributes{ PromotionalText: common.Ptr("Yet another distribution for sing-box, the universal proxy platform."), WhatsNew: common.Ptr(F.ToString("sing-box ", tag, ": Fixes and improvements.")), }) if err != nil { return err } } log.Info(string(platform), " ", tag, " create submission") fixSubmit: for { _, response, err := client.Submission.CreateSubmission(ctx, version.ID) if err != nil { switch response.StatusCode { case http.StatusInternalServerError: continue default: return err } } switch response.StatusCode { case http.StatusCreated: break fixSubmit default: return err } } } return nil } func publishAppStore(ctx context.Context) error { tag, err := build_shared.ReadTag() if err != nil { return err } client := createClient(time.Minute) for _, platform := range []asc.Platform{ asc.PlatformIOS, asc.PlatformMACOS, asc.PlatformTVOS, } { log.Info(string(platform), " list versions") versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{ FilterPlatform: []string{string(platform)}, }) if err != nil { return err } version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool { return *it.Attributes.VersionString == tag }) switch *version.Attributes.AppStoreState { case asc.AppStoreVersionStatePrepareForSubmission, asc.AppStoreVersionStateDeveloperRejected: log.Fatal(string(platform), " ", tag, " not submitted") case asc.AppStoreVersionStateWaitingForReview, asc.AppStoreVersionStateInReview: log.Warn(string(platform), " ", tag, " waiting for review") continue case asc.AppStoreVersionStatePendingDeveloperRelease: default: log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState)) } _, _, err = client.Publishing.CreatePhasedRelease(ctx, common.Ptr(asc.PhasedReleaseStateComplete), version.ID) if err != nil { return err } } return nil } func isRetryable(response *asc.Response) bool { if response == nil { return false } switch response.StatusCode { case http.StatusInternalServerError, http.StatusUnprocessableEntity: return true default: return false } } ================================================ FILE: cmd/internal/build/main.go ================================================ package main import ( "go/build" "os" "os/exec" "github.com/sagernet/sing-box/cmd/internal/build_shared" "github.com/sagernet/sing-box/log" ) func main() { build_shared.FindSDK() if os.Getenv("GOPATH") == "" { os.Setenv("GOPATH", build.Default.GOPATH) } command := exec.Command(os.Args[1], os.Args[2:]...) command.Stdout = os.Stdout command.Stderr = os.Stderr err := command.Run() if err != nil { log.Fatal(err) } } ================================================ FILE: cmd/internal/build_libbox/main.go ================================================ package main import ( "flag" "os" "os/exec" "path/filepath" "strconv" "strings" _ "github.com/sagernet/gomobile" "github.com/sagernet/sing-box/cmd/internal/build_shared" "github.com/sagernet/sing-box/log" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/common/shell" ) var ( debugEnabled bool target string platform string // withTailscale bool ) func init() { flag.BoolVar(&debugEnabled, "debug", false, "enable debug") flag.StringVar(&target, "target", "android", "target platform") flag.StringVar(&platform, "platform", "", "specify platform") // flag.BoolVar(&withTailscale, "with-tailscale", false, "build tailscale for iOS and tvOS") } func main() { flag.Parse() build_shared.FindMobile() switch target { case "android": buildAndroid() case "apple": buildApple() } } var ( sharedFlags []string debugFlags []string sharedTags []string darwinTags []string // memcTags []string notMemcTags []string debugTags []string ) func init() { sharedFlags = append(sharedFlags, "-trimpath") sharedFlags = append(sharedFlags, "-buildvcs=false") currentTag, err := build_shared.ReadTag() if err != nil { currentTag = "unknown" } sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0") debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0") sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_naive_outbound", "with_clash_api", "badlinkname", "tfogo_checklinkname0") darwinTags = append(darwinTags, "with_dhcp", "grpcnotrace") // memcTags = append(memcTags, "with_tailscale") sharedTags = append(sharedTags, "with_tailscale", "ts_omit_logtail", "ts_omit_ssh", "ts_omit_drive", "ts_omit_taildrop", "ts_omit_webclient", "ts_omit_doctor", "ts_omit_capture", "ts_omit_kube", "ts_omit_aws", "ts_omit_synology", "ts_omit_bird") notMemcTags = append(notMemcTags, "with_low_memory") debugTags = append(debugTags, "debug") } type AndroidBuildConfig struct { AndroidAPI int OutputName string Tags []string } func filterTags(tags []string, exclude ...string) []string { excludeMap := make(map[string]bool) for _, tag := range exclude { excludeMap[tag] = true } var result []string for _, tag := range tags { if !excludeMap[tag] { result = append(result, tag) } } return result } func checkJavaVersion() { var javaPath string javaHome := os.Getenv("JAVA_HOME") if javaHome == "" { javaPath = "java" } else { javaPath = filepath.Join(javaHome, "bin", "java") } javaVersion, err := shell.Exec(javaPath, "--version").ReadOutput() if err != nil { log.Fatal(E.Cause(err, "check java version")) } if !strings.Contains(javaVersion, "openjdk 17") { log.Fatal("java version should be openjdk 17") } } func getAndroidBindTarget() string { if platform != "" { return platform } else if debugEnabled { return "android/arm64" } return "android" } func buildAndroidVariant(config AndroidBuildConfig, bindTarget string) { args := []string{ "bind", "-v", "-o", config.OutputName, "-target", bindTarget, "-androidapi", strconv.Itoa(config.AndroidAPI), "-javapkg=io.nekohasekai", "-libname=box", } if !debugEnabled { args = append(args, sharedFlags...) } else { args = append(args, debugFlags...) } args = append(args, "-tags", strings.Join(config.Tags, ",")) args = append(args, "./experimental/libbox") command := exec.Command(build_shared.GoBinPath+"/gomobile", args...) command.Stdout = os.Stdout command.Stderr = os.Stderr err := command.Run() if err != nil { log.Fatal(err) } copyPath := filepath.Join("..", "sing-box-for-android", "app", "libs") if rw.IsDir(copyPath) { copyPath, _ = filepath.Abs(copyPath) err = rw.CopyFile(config.OutputName, filepath.Join(copyPath, config.OutputName)) if err != nil { log.Fatal(err) } log.Info("copied ", config.OutputName, " to ", copyPath) } } func buildAndroid() { build_shared.FindSDK() checkJavaVersion() bindTarget := getAndroidBindTarget() // Build main variant (SDK 23) mainTags := append([]string{}, sharedTags...) // mainTags = append(mainTags, memcTags...) if debugEnabled { mainTags = append(mainTags, debugTags...) } buildAndroidVariant(AndroidBuildConfig{ AndroidAPI: 23, OutputName: "libbox.aar", Tags: mainTags, }, bindTarget) // Build legacy variant (SDK 21, no naive outbound) legacyTags := filterTags(sharedTags, "with_naive_outbound") // legacyTags = append(legacyTags, memcTags...) if debugEnabled { legacyTags = append(legacyTags, debugTags...) } buildAndroidVariant(AndroidBuildConfig{ AndroidAPI: 21, OutputName: "libbox-legacy.aar", Tags: legacyTags, }, bindTarget) } func buildApple() { var bindTarget string if platform != "" { bindTarget = platform } else if debugEnabled { bindTarget = "ios" } else { bindTarget = "ios,iossimulator,tvos,tvossimulator,macos" } args := []string{ "bind", "-v", "-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, ",")) //} if !debugEnabled { args = append(args, sharedFlags...) } else { args = append(args, debugFlags...) } tags := append(sharedTags, darwinTags...) //if withTailscale { // tags = append(tags, memcTags...) //} if debugEnabled { tags = append(tags, debugTags...) } args = append(args, "-tags", strings.Join(tags, ",")) args = append(args, "./experimental/libbox") command := exec.Command(build_shared.GoBinPath+"/gomobile", args...) command.Stdout = os.Stdout command.Stderr = os.Stderr err := command.Run() if err != nil { log.Fatal(err) } copyPath := filepath.Join("..", "sing-box-for-apple") if rw.IsDir(copyPath) { targetDir := filepath.Join(copyPath, "Libbox.xcframework") targetDir, _ = filepath.Abs(targetDir) os.RemoveAll(targetDir) os.Rename("Libbox.xcframework", targetDir) log.Info("copied to ", targetDir) } } ================================================ FILE: cmd/internal/build_shared/sdk.go ================================================ package build_shared import ( "go/build" "os" "path/filepath" "runtime" "sort" "strconv" "strings" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/rw" ) var ( androidSDKPath string androidNDKPath string ) func FindSDK() { searchPath := []string{ "$ANDROID_HOME", "$HOME/Android/Sdk", "$HOME/.local/lib/android/sdk", "$HOME/Library/Android/sdk", } for _, path := range searchPath { path = os.ExpandEnv(path) if rw.IsFile(filepath.Join(path, "licenses", "android-sdk-license")) { androidSDKPath = path break } } if androidSDKPath == "" { log.Fatal("android SDK not found") } if !findNDK() { log.Fatal("android NDK not found") } os.Setenv("ANDROID_HOME", androidSDKPath) os.Setenv("ANDROID_SDK_HOME", androidSDKPath) os.Setenv("ANDROID_NDK_HOME", androidNDKPath) os.Setenv("NDK", androidNDKPath) os.Setenv("PATH", os.Getenv("PATH")+":"+filepath.Join(androidNDKPath, "toolchains", "llvm", "prebuilt", runtime.GOOS+"-x86_64", "bin")) } func findNDK() bool { const fixedVersion = "28.0.13004108" const versionFile = "source.properties" if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.IsFile(filepath.Join(fixedPath, versionFile)) { androidNDKPath = fixedPath return true } if ndkHomeEnv := os.Getenv("ANDROID_NDK_HOME"); rw.IsFile(filepath.Join(ndkHomeEnv, versionFile)) { androidNDKPath = ndkHomeEnv return true } ndkVersions, err := os.ReadDir(filepath.Join(androidSDKPath, "ndk")) if err != nil { return false } versionNames := common.Map(ndkVersions, os.DirEntry.Name) if len(versionNames) == 0 { return false } sort.Slice(versionNames, func(i, j int) bool { iVersions := strings.Split(versionNames[i], ".") jVersions := strings.Split(versionNames[j], ".") for k := 0; k < len(iVersions) && k < len(jVersions); k++ { iVersion, _ := strconv.Atoi(iVersions[k]) jVersion, _ := strconv.Atoi(jVersions[k]) if iVersion != jVersion { return iVersion > jVersion } } return true }) for _, versionName := range versionNames { currentNDKPath := filepath.Join(androidSDKPath, "ndk", versionName) if rw.IsFile(filepath.Join(currentNDKPath, versionFile)) { androidNDKPath = currentNDKPath log.Warn("reproducibility warning: using NDK version " + versionName + " instead of " + fixedVersion) return true } } return false } var GoBinPath string func FindMobile() { goBin := filepath.Join(build.Default.GOPATH, "bin") if runtime.GOOS == "windows" { if !rw.IsFile(filepath.Join(goBin, "gobind.exe")) { log.Fatal("missing gomobile installation") } } else { if !rw.IsFile(filepath.Join(goBin, "gobind")) { log.Fatal("missing gomobile installation") } } GoBinPath = goBin } ================================================ FILE: cmd/internal/build_shared/tag.go ================================================ package build_shared import ( "github.com/sagernet/sing-box/common/badversion" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/shell" ) func ReadTag() (string, error) { currentTag, err := shell.Exec("git", "describe", "--tags").ReadOutput() if err != nil { return currentTag, err } currentTagRev, _ := shell.Exec("git", "describe", "--tags", "--abbrev=0").ReadOutput() if currentTagRev == currentTag { return currentTag[1:], nil } shortCommit, _ := shell.Exec("git", "rev-parse", "--short", "HEAD").ReadOutput() version := badversion.Parse(currentTagRev[1:]) return version.String() + "-" + shortCommit, nil } func ReadTagVersionRev() (badversion.Version, error) { currentTagRev := common.Must1(shell.Exec("git", "describe", "--tags", "--abbrev=0").ReadOutput()) return badversion.Parse(currentTagRev[1:]), nil } func ReadTagVersion() (badversion.Version, error) { currentTag := common.Must1(shell.Exec("git", "describe", "--tags").ReadOutput()) currentTagRev := common.Must1(shell.Exec("git", "describe", "--tags", "--abbrev=0").ReadOutput()) version := badversion.Parse(currentTagRev[1:]) if currentTagRev != currentTag { if version.PreReleaseIdentifier == "" { version.Patch++ } } return version, nil } ================================================ FILE: cmd/internal/format_docs/main.go ================================================ package main import ( "bytes" "os" "path/filepath" "strings" "github.com/sagernet/sing-box/log" ) func main() { err := filepath.Walk("docs", func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() { return nil } if !strings.HasSuffix(path, ".md") { return nil } return processFile(path) }) if err != nil { log.Fatal(err) } } func processFile(path string) error { content, err := os.ReadFile(path) if err != nil { return err } lines := strings.Split(string(content), "\n") modified := false result := make([]string, 0, len(lines)) inQuoteBlock := false materialLines := []int{} // indices of :material- lines in the block for _, line := range lines { // Check for quote block start if strings.HasPrefix(line, "!!! quote \"") && strings.Contains(line, "sing-box") { inQuoteBlock = true materialLines = nil result = append(result, line) continue } // Inside a quote block if inQuoteBlock { trimmed := strings.TrimPrefix(line, " ") isMaterialLine := strings.HasPrefix(trimmed, ":material-") isEmpty := strings.TrimSpace(line) == "" isIndented := strings.HasPrefix(line, " ") if isMaterialLine { materialLines = append(materialLines, len(result)) result = append(result, line) continue } // Block ends when: // - Empty line AFTER we've seen material lines, OR // - Non-indented, non-empty line blockEnds := (isEmpty && len(materialLines) > 0) || (!isEmpty && !isIndented) if blockEnds { // Process collected material lines if len(materialLines) > 0 { for j, idx := range materialLines { isLast := j == len(materialLines)-1 resultLine := strings.TrimRight(result[idx], " ") if !isLast { // Add trailing two spaces for non-last lines resultLine += " " } if result[idx] != resultLine { modified = true result[idx] = resultLine } } } inQuoteBlock = false materialLines = nil } } result = append(result, line) } // Handle case where file ends while still in a block if inQuoteBlock && len(materialLines) > 0 { for j, idx := range materialLines { isLast := j == len(materialLines)-1 resultLine := strings.TrimRight(result[idx], " ") if !isLast { resultLine += " " } if result[idx] != resultLine { modified = true result[idx] = resultLine } } } if modified { newContent := strings.Join(result, "\n") if !bytes.Equal(content, []byte(newContent)) { log.Info("formatted: ", path) return os.WriteFile(path, []byte(newContent), 0o644) } } return nil } ================================================ FILE: cmd/internal/protogen/main.go ================================================ package main import ( "bufio" "bytes" "fmt" "go/build" "io" "os" "os/exec" "path/filepath" "runtime" "strings" ) // envFile returns the name of the Go environment configuration file. // Copy from https://github.com/golang/go/blob/c4f2a9788a7be04daf931ac54382fbe2cb754938/src/cmd/go/internal/cfg/cfg.go#L150-L166 func envFile() (string, error) { if file := os.Getenv("GOENV"); file != "" { if file == "off" { return "", fmt.Errorf("GOENV=off") } return file, nil } dir, err := os.UserConfigDir() if err != nil { return "", err } if dir == "" { return "", fmt.Errorf("missing user-config dir") } return filepath.Join(dir, "go", "env"), nil } // GetRuntimeEnv returns the value of runtime environment variable, // that is set by running following command: `go env -w key=value`. func GetRuntimeEnv(key string) (string, error) { file, err := envFile() if err != nil { return "", err } if file == "" { return "", fmt.Errorf("missing runtime env file") } var data []byte var runtimeEnv string data, readErr := os.ReadFile(file) if readErr != nil { return "", readErr } envStrings := strings.SplitSeq(string(data), "\n") for envItem := range envStrings { envItem = strings.TrimSuffix(envItem, "\r") envKeyValue := strings.Split(envItem, "=") if strings.EqualFold(strings.TrimSpace(envKeyValue[0]), key) { runtimeEnv = strings.TrimSpace(envKeyValue[1]) } } return runtimeEnv, nil } // GetGOBIN returns GOBIN environment variable as a string. It will NOT be empty. func GetGOBIN() string { // The one set by user explicitly by `export GOBIN=/path` or `env GOBIN=/path command` GOBIN := os.Getenv("GOBIN") if GOBIN == "" { var err error // The one set by user by running `go env -w GOBIN=/path` GOBIN, err = GetRuntimeEnv("GOBIN") if err != nil { // The default one that Golang uses return filepath.Join(build.Default.GOPATH, "bin") } if GOBIN == "" { return filepath.Join(build.Default.GOPATH, "bin") } return GOBIN } return GOBIN } func main() { pwd, err := os.Getwd() if err != nil { fmt.Println("Can not get current working directory.") os.Exit(1) } GOBIN := GetGOBIN() binPath := os.Getenv("PATH") pathSlice := []string{pwd, GOBIN, binPath} binPath = strings.Join(pathSlice, string(os.PathListSeparator)) os.Setenv("PATH", binPath) suffix := "" if runtime.GOOS == "windows" { suffix = ".exe" } protoc := "protoc" if linkPath, err := os.Readlink(protoc); err == nil { protoc = linkPath } protoFilesMap := make(map[string][]string) walkErr := filepath.Walk("./", func(path string, info os.FileInfo, err error) error { if err != nil { fmt.Println(err) return err } if info.IsDir() { return nil } dir := filepath.Dir(path) filename := filepath.Base(path) if strings.HasSuffix(filename, ".proto") && filename != "typed_message.proto" && filename != "descriptor.proto" { protoFilesMap[dir] = append(protoFilesMap[dir], path) } return nil }) if walkErr != nil { fmt.Println(walkErr) os.Exit(1) } for _, files := range protoFilesMap { for _, relProtoFile := range files { args := []string{ "-I", ".", "--go_out", pwd, "--go_opt", "paths=source_relative", "--go-grpc_out", pwd, "--go-grpc_opt", "paths=source_relative", "--plugin", "protoc-gen-go=" + filepath.Join(GOBIN, "protoc-gen-go"+suffix), "--plugin", "protoc-gen-go-grpc=" + filepath.Join(GOBIN, "protoc-gen-go-grpc"+suffix), } args = append(args, relProtoFile) cmd := exec.Command(protoc, args...) cmd.Env = append(cmd.Env, os.Environ()...) output, cmdErr := cmd.CombinedOutput() if len(output) > 0 { fmt.Println(string(output)) } if cmdErr != nil { fmt.Println(cmdErr) os.Exit(1) } } } normalizeWalkErr := filepath.Walk("./", func(path string, info os.FileInfo, err error) error { if err != nil { fmt.Println(err) return err } if info.IsDir() { return nil } filename := filepath.Base(path) if strings.HasSuffix(filename, ".pb.go") && path != "config.pb.go" { if err := NormalizeGeneratedProtoFile(path); err != nil { fmt.Println(err) os.Exit(1) } } return nil }) if normalizeWalkErr != nil { fmt.Println(normalizeWalkErr) os.Exit(1) } } func NormalizeGeneratedProtoFile(path string) error { fd, err := os.OpenFile(path, os.O_RDWR, 0o644) if err != nil { return err } _, err = fd.Seek(0, io.SeekStart) if err != nil { return err } out := bytes.NewBuffer(nil) scanner := bufio.NewScanner(fd) valid := false for scanner.Scan() { if !valid && !strings.HasPrefix(scanner.Text(), "package ") { continue } valid = true out.Write(scanner.Bytes()) out.Write([]byte("\n")) } _, err = fd.Seek(0, io.SeekStart) if err != nil { return err } err = fd.Truncate(0) if err != nil { return err } _, err = io.Copy(fd, bytes.NewReader(out.Bytes())) if err != nil { return err } return nil } ================================================ FILE: cmd/internal/read_tag/main.go ================================================ package main import ( "flag" "os" "github.com/sagernet/sing-box/cmd/internal/build_shared" "github.com/sagernet/sing-box/common/badversion" "github.com/sagernet/sing-box/log" ) var ( flagRunInCI bool flagRunNightly bool ) func init() { flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI") flag.BoolVar(&flagRunNightly, "nightly", false, "Run nightly") } func main() { flag.Parse() var ( versionStr string err error ) if flagRunNightly { var version badversion.Version version, err = build_shared.ReadTagVersion() if err == nil { versionStr = version.String() } } else { versionStr, err = build_shared.ReadTag() } if flagRunInCI { if err != nil { log.Fatal(err) } err = setGitHubEnv("version", versionStr) if err != nil { log.Fatal(err) } } else { if err != nil { log.Error(err) os.Stdout.WriteString("unknown\n") } else { os.Stdout.WriteString(versionStr + "\n") } } } func setGitHubEnv(name string, value string) error { outputFile, err := os.OpenFile(os.Getenv("GITHUB_ENV"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) if err != nil { return err } _, err = outputFile.WriteString(name + "=" + value + "\n") if err != nil { outputFile.Close() return err } err = outputFile.Close() if err != nil { return err } os.Stderr.WriteString(name + "=" + value + "\n") return nil } ================================================ FILE: cmd/internal/tun_bench/main.go ================================================ package main import ( "context" "fmt" "io" "net/netip" "os" "os/exec" "strings" "syscall" "time" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/include" "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/json" "github.com/sagernet/sing/common/shell" ) var iperf3Path string func main() { err := main0() if err != nil { log.Fatal(err) } } func main0() error { err := shell.Exec("sudo", "ls").Run() if err != nil { return err } results, err := runTests() if err != nil { return err } encoder := json.NewEncoder(os.Stdout) encoder.SetIndent("", " ") return encoder.Encode(results) } func runTests() ([]TestResult, error) { boxPaths := []string{ os.ExpandEnv("$HOME/Downloads/sing-box-1.11.15-darwin-arm64/sing-box"), //"/Users/sekai/Downloads/sing-box-1.11.15-linux-arm64/sing-box", "./sing-box", } stacks := []string{ "gvisor", "system", } mtus := []int{ 1500, 4064, // 16384, // 32768, // 49152, 65535, } flagList := [][]string{ {}, } var results []TestResult for _, boxPath := range boxPaths { for _, stack := range stacks { for _, mtu := range mtus { if strings.HasPrefix(boxPath, ".") { for _, flags := range flagList { result, err := testOnce(boxPath, stack, mtu, false, flags) if err != nil { return nil, err } results = append(results, *result) } } else { result, err := testOnce(boxPath, stack, mtu, false, nil) if err != nil { return nil, err } results = append(results, *result) } } } } return results, nil } type TestResult struct { BoxPath string `json:"box_path"` Stack string `json:"stack"` MTU int `json:"mtu"` Flags []string `json:"flags"` MultiThread bool `json:"multi_thread"` UploadSpeed string `json:"upload_speed"` DownloadSpeed string `json:"download_speed"` } func testOnce(boxPath string, stackName string, mtu int, multiThread bool, flags []string) (result *TestResult, err error) { testAddress := netip.MustParseAddr("1.1.1.1") testConfig := option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeTun, Options: &option.TunInboundOptions{ Address: []netip.Prefix{netip.MustParsePrefix("172.18.0.1/30")}, AutoRoute: true, MTU: uint32(mtu), Stack: stackName, RouteAddress: []netip.Prefix{netip.PrefixFrom(testAddress, testAddress.BitLen())}, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ IPCIDR: []string{testAddress.String()}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRouteOptions, RouteOptionsOptions: option.RouteOptionsActionOptions{ OverrideAddress: "127.0.0.1", }, }, }, }, }, AutoDetectInterface: true, }, } ctx := include.Context(context.Background()) tempConfig, err := os.CreateTemp("", "tun-bench-*.json") if err != nil { return } defer os.Remove(tempConfig.Name()) encoder := json.NewEncoderContext(ctx, tempConfig) encoder.SetIndent("", " ") err = encoder.Encode(testConfig) if err != nil { return nil, E.Cause(err, "encode test config") } tempConfig.Close() var sudoArgs []string if len(flags) > 0 { sudoArgs = append(sudoArgs, "env") sudoArgs = append(sudoArgs, flags...) } sudoArgs = append(sudoArgs, boxPath, "run", "-c", tempConfig.Name()) boxProcess := shell.Exec("sudo", sudoArgs...) boxProcess.Stdout = &stderrWriter{} boxProcess.Stderr = io.Discard err = boxProcess.Start() if err != nil { return } if C.IsDarwin { iperf3Path, err = exec.LookPath("iperf3-darwin") } else { iperf3Path, err = exec.LookPath("iperf3") } if err != nil { return } serverProcess := shell.Exec(iperf3Path, "-s") serverProcess.Stdout = io.Discard serverProcess.Stderr = io.Discard err = serverProcess.Start() if err != nil { return nil, E.Cause(err, "start iperf3 server") } time.Sleep(time.Second) args := []string{"-c", testAddress.String()} if multiThread { args = append(args, "-P", "10") } uploadProcess := shell.Exec(iperf3Path, args...) output, err := uploadProcess.Read() if err != nil { boxProcess.Process.Signal(syscall.SIGKILL) serverProcess.Process.Signal(syscall.SIGKILL) println(output) return } uploadResult := common.SubstringBeforeLast(output, "iperf Done.") uploadResult = common.SubstringBeforeLast(uploadResult, "sender") uploadResult = common.SubstringBeforeLast(uploadResult, "bits/sec") uploadResult = common.SubstringAfterLast(uploadResult, "Bytes") uploadResult = strings.ReplaceAll(uploadResult, " ", "") result = &TestResult{ BoxPath: boxPath, Stack: stackName, MTU: mtu, Flags: flags, MultiThread: multiThread, UploadSpeed: uploadResult, } downloadProcess := shell.Exec(iperf3Path, append(args, "-R")...) output, err = downloadProcess.Read() if err != nil { boxProcess.Process.Signal(syscall.SIGKILL) serverProcess.Process.Signal(syscall.SIGKILL) println(output) return } downloadResult := common.SubstringBeforeLast(output, "iperf Done.") downloadResult = common.SubstringBeforeLast(downloadResult, "receiver") downloadResult = common.SubstringBeforeLast(downloadResult, "bits/sec") downloadResult = common.SubstringAfterLast(downloadResult, "Bytes") downloadResult = strings.ReplaceAll(downloadResult, " ", "") result.DownloadSpeed = downloadResult printArgs := []any{boxPath, stackName, mtu, "upload", uploadResult, "download", downloadResult} if len(flags) > 0 { printArgs = append(printArgs, "flags", strings.Join(flags, " ")) } if multiThread { printArgs = append(printArgs, "(-P 10)") } fmt.Println(printArgs...) err = boxProcess.Process.Signal(syscall.SIGTERM) if err != nil { return } err = serverProcess.Process.Signal(syscall.SIGTERM) if err != nil { return } boxDone := make(chan struct{}) go func() { boxProcess.Cmd.Wait() close(boxDone) }() serverDone := make(chan struct{}) go func() { serverProcess.Process.Wait() close(serverDone) }() select { case <-boxDone: case <-time.After(2 * time.Second): boxProcess.Process.Kill() case <-time.After(4 * time.Second): println("box process did not close!") os.Exit(1) } select { case <-serverDone: case <-time.After(2 * time.Second): serverProcess.Process.Kill() case <-time.After(4 * time.Second): println("server process did not close!") os.Exit(1) } return } type stderrWriter struct{} func (w *stderrWriter) Write(p []byte) (n int, err error) { return os.Stderr.Write(p) } ================================================ FILE: cmd/internal/update_android_version/main.go ================================================ package main import ( "flag" "os" "path/filepath" "runtime" "strconv" "strings" "github.com/sagernet/sing-box/cmd/internal/build_shared" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing/common" ) var ( flagRunInCI bool flagRunNightly bool ) func init() { flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI") flag.BoolVar(&flagRunNightly, "nightly", false, "Run nightly") } func main() { flag.Parse() newVersion := common.Must1(build_shared.ReadTag()) var androidPath string if flagRunInCI { androidPath = "clients/android" } else { androidPath = "../sing-box-for-android" } androidPath, err := filepath.Abs(androidPath) if err != nil { log.Fatal(err) } common.Must(os.Chdir(androidPath)) localProps := common.Must1(os.ReadFile("version.properties")) var propsList [][]string for propLine := range strings.SplitSeq(string(localProps), "\n") { propsList = append(propsList, strings.Split(propLine, "=")) } var ( versionUpdated bool goVersionUpdated bool ) for _, propPair := range propsList { switch propPair[0] { case "VERSION_NAME": if propPair[1] != newVersion { log.Info("updated version from ", propPair[1], " to ", newVersion) versionUpdated = true propPair[1] = newVersion } case "GO_VERSION": if propPair[1] != runtime.Version() { log.Info("updated Go version from ", propPair[1], " to ", runtime.Version()) goVersionUpdated = true propPair[1] = runtime.Version() } } } if !(versionUpdated || goVersionUpdated) { log.Info("version not changed") return } else if flagRunInCI && !flagRunNightly { log.Fatal("version changed, commit changes first.") } for _, propPair := range propsList { switch propPair[0] { case "VERSION_CODE": versionCode := common.Must1(strconv.ParseInt(propPair[1], 10, 64)) propPair[1] = strconv.Itoa(int(versionCode + 1)) log.Info("updated version code to ", propPair[1]) } } var newProps []string for _, propPair := range propsList { newProps = append(newProps, strings.Join(propPair, "=")) } common.Must(os.WriteFile("version.properties", []byte(strings.Join(newProps, "\n")), 0o644)) } ================================================ FILE: cmd/internal/update_apple_version/main.go ================================================ package main import ( "flag" "os" "path/filepath" "regexp" "strings" "github.com/sagernet/sing-box/cmd/internal/build_shared" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing/common" "howett.net/plist" ) var flagRunInCI bool func init() { flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI") } func main() { flag.Parse() newVersion := common.Must1(build_shared.ReadTagVersion()) var applePath string if flagRunInCI { applePath = "clients/apple" } else { applePath = "../sing-box-for-apple" } applePath, err := filepath.Abs(applePath) if err != nil { log.Fatal(err) } common.Must(os.Chdir(applePath)) projectFile := common.Must1(os.Open("sing-box.xcodeproj/project.pbxproj")) var project map[string]any decoder := plist.NewDecoder(projectFile) common.Must(decoder.Decode(&project)) objectsMap := project["objects"].(map[string]any) projectContent := string(common.Must1(os.ReadFile("sing-box.xcodeproj/project.pbxproj"))) newContent, updated0 := findAndReplace(objectsMap, projectContent, []string{"io.nekohasekai.sfavt"}, newVersion.VersionString()) newContent, updated1 := findAndReplace(objectsMap, newContent, []string{"io.nekohasekai.sfavt.standalone", "io.nekohasekai.sfavt.system"}, newVersion.String()) if updated0 || updated1 { log.Info("updated version to ", newVersion.VersionString(), " (", newVersion.String(), ")") } var updated2 bool if macProjectVersion := os.Getenv("MACOS_PROJECT_VERSION"); macProjectVersion != "" { newContent, updated2 = findAndReplaceProjectVersion(objectsMap, newContent, []string{"SFM"}, macProjectVersion) if updated2 { log.Info("updated macos project version to ", macProjectVersion) } } if updated0 || updated1 || updated2 { common.Must(os.WriteFile("sing-box.xcodeproj/project.pbxproj", []byte(newContent), 0o644)) } } func findAndReplace(objectsMap map[string]any, projectContent string, bundleIDList []string, newVersion string) (string, bool) { objectKeyList := findObjectKey(objectsMap, bundleIDList) var updated bool for _, objectKey := range objectKeyList { matchRegexp := common.Must1(regexp.Compile(objectKey + ".*= \\{")) indexes := matchRegexp.FindStringIndex(projectContent) if len(indexes) < 2 { println(projectContent) log.Fatal("failed to find object key ", objectKey, ": ", strings.Index(projectContent, objectKey)) } indexStart := indexes[1] indexEnd := indexStart + strings.Index(projectContent[indexStart:], "}") versionStart := indexStart + strings.Index(projectContent[indexStart:indexEnd], "MARKETING_VERSION = ") + 20 versionEnd := versionStart + strings.Index(projectContent[versionStart:indexEnd], ";") version := strings.Trim(projectContent[versionStart:versionEnd], "\"") if version == newVersion { continue } updated = true projectContent = projectContent[:versionStart] + "\"" + newVersion + "\"" + projectContent[versionEnd:] } return projectContent, updated } func findAndReplaceProjectVersion(objectsMap map[string]any, projectContent string, directoryList []string, newVersion string) (string, bool) { objectKeyList := findObjectKeyByDirectory(objectsMap, directoryList) var updated bool for _, objectKey := range objectKeyList { matchRegexp := common.Must1(regexp.Compile(objectKey + ".*= \\{")) indexes := matchRegexp.FindStringIndex(projectContent) if len(indexes) < 2 { println(projectContent) log.Fatal("failed to find object key ", objectKey, ": ", strings.Index(projectContent, objectKey)) } indexStart := indexes[1] indexEnd := indexStart + strings.Index(projectContent[indexStart:], "}") versionStart := indexStart + strings.Index(projectContent[indexStart:indexEnd], "CURRENT_PROJECT_VERSION = ") + 26 versionEnd := versionStart + strings.Index(projectContent[versionStart:indexEnd], ";") version := projectContent[versionStart:versionEnd] if version == newVersion { continue } updated = true projectContent = projectContent[:versionStart] + newVersion + projectContent[versionEnd:] } return projectContent, updated } func findObjectKey(objectsMap map[string]any, bundleIDList []string) []string { var objectKeyList []string for objectKey, object := range objectsMap { buildSettings := object.(map[string]any)["buildSettings"] if buildSettings == nil { continue } bundleIDObject := buildSettings.(map[string]any)["PRODUCT_BUNDLE_IDENTIFIER"] if bundleIDObject == nil { continue } if common.Contains(bundleIDList, bundleIDObject.(string)) { objectKeyList = append(objectKeyList, objectKey) } } return objectKeyList } func findObjectKeyByDirectory(objectsMap map[string]any, directoryList []string) []string { var objectKeyList []string for objectKey, object := range objectsMap { buildSettings := object.(map[string]any)["buildSettings"] if buildSettings == nil { continue } infoPListFile := buildSettings.(map[string]any)["INFOPLIST_FILE"] if infoPListFile == nil { continue } for _, searchDirectory := range directoryList { if strings.HasPrefix(infoPListFile.(string), searchDirectory+"/") { objectKeyList = append(objectKeyList, objectKey) } } } return objectKeyList } ================================================ FILE: cmd/internal/update_certificates/main.go ================================================ package main import ( "encoding/csv" "io" "net/http" "os" "strings" "github.com/sagernet/sing-box/log" "golang.org/x/exp/slices" ) func main() { err := updateMozillaIncludedRootCAs() if err != nil { log.Error(err) } err = updateChromeIncludedRootCAs() if err != nil { log.Error(err) } } func updateMozillaIncludedRootCAs() error { response, err := http.Get("https://ccadb.my.salesforce-sites.com/mozilla/IncludedCACertificateReportPEMCSV") if err != nil { return err } defer response.Body.Close() reader := csv.NewReader(response.Body) header, err := reader.Read() if err != nil { return err } geoIndex := slices.Index(header, "Geographic Focus") nameIndex := slices.Index(header, "Common Name or Certificate Name") certIndex := slices.Index(header, "PEM Info") generated := strings.Builder{} generated.WriteString(`// Code generated by 'make update_certificates'. DO NOT EDIT. package certificate func mozillaIncludedPEM() string { return ` + "`") for { record, err := reader.Read() if err == io.EOF { break } else if err != nil { return err } if record[geoIndex] == "China" { continue } cert := strings.Trim(record[certIndex], "'") generated.WriteString("\n// ") generated.WriteString(record[nameIndex]) generated.WriteString("\n") generated.WriteString(cert) generated.WriteString("\n") } generated.WriteString("`\n}\n") return os.WriteFile("common/certificate/mozilla.go", []byte(generated.String()), 0o644) } func fetchChinaFingerprints() (map[string]bool, error) { response, err := http.Get("https://ccadb.my.salesforce-sites.com/ccadb/AllCertificateRecordsCSVFormatv4") if err != nil { return nil, err } defer response.Body.Close() reader := csv.NewReader(response.Body) header, err := reader.Read() if err != nil { return nil, err } countryIndex := slices.Index(header, "Country") fingerprintIndex := slices.Index(header, "SHA-256 Fingerprint") chinaFingerprints := make(map[string]bool) for { record, err := reader.Read() if err == io.EOF { break } else if err != nil { return nil, err } if record[countryIndex] == "China" { chinaFingerprints[record[fingerprintIndex]] = true } } return chinaFingerprints, nil } func updateChromeIncludedRootCAs() error { chinaFingerprints, err := fetchChinaFingerprints() if err != nil { return err } response, err := http.Get("https://ccadb.my.salesforce-sites.com/ccadb/RootCACertificatesIncludedByRSReportCSV") if err != nil { return err } defer response.Body.Close() reader := csv.NewReader(response.Body) header, err := reader.Read() if err != nil { return err } subjectIndex := slices.Index(header, "Subject") statusIndex := slices.Index(header, "Google Chrome Status") certIndex := slices.Index(header, "X.509 Certificate (PEM)") fingerprintIndex := slices.Index(header, "SHA-256 Fingerprint") generated := strings.Builder{} generated.WriteString(`// Code generated by 'make update_certificates'. DO NOT EDIT. package certificate func chromeIncludedPEM() string { return ` + "`") for { record, err := reader.Read() if err == io.EOF { break } else if err != nil { return err } if record[statusIndex] != "Included" { continue } if chinaFingerprints[record[fingerprintIndex]] { continue } cert := strings.Trim(record[certIndex], "'") generated.WriteString("\n// ") generated.WriteString(record[subjectIndex]) generated.WriteString("\n") generated.WriteString(cert) generated.WriteString("\n") } generated.WriteString("`\n}\n") return os.WriteFile("common/certificate/chrome.go", []byte(generated.String()), 0o644) } ================================================ FILE: cmd/sing-box/cmd.go ================================================ package main import ( "context" "os" "os/user" "strconv" "time" "github.com/sagernet/sing-box/experimental/deprecated" "github.com/sagernet/sing-box/include" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/filemanager" "github.com/spf13/cobra" ) var ( globalCtx context.Context configPaths []string configDirectories []string workingDir string disableColor bool ) var mainCommand = &cobra.Command{ Use: "sing-box", PersistentPreRun: preRun, } func init() { mainCommand.PersistentFlags().StringArrayVarP(&configPaths, "config", "c", nil, "set configuration file path") mainCommand.PersistentFlags().StringArrayVarP(&configDirectories, "config-directory", "C", nil, "set configuration directory path") mainCommand.PersistentFlags().StringVarP(&workingDir, "directory", "D", "", "set working directory") mainCommand.PersistentFlags().BoolVarP(&disableColor, "disable-color", "", false, "disable color output") } func preRun(cmd *cobra.Command, args []string) { globalCtx = context.Background() sudoUser := os.Getenv("SUDO_USER") sudoUID, _ := strconv.Atoi(os.Getenv("SUDO_UID")) sudoGID, _ := strconv.Atoi(os.Getenv("SUDO_GID")) if sudoUID == 0 && sudoGID == 0 && sudoUser != "" { sudoUserObject, _ := user.Lookup(sudoUser) if sudoUserObject != nil { sudoUID, _ = strconv.Atoi(sudoUserObject.Uid) sudoGID, _ = strconv.Atoi(sudoUserObject.Gid) } } if sudoUID > 0 && sudoGID > 0 { globalCtx = filemanager.WithDefault(globalCtx, "", "", sudoUID, sudoGID) } if disableColor { log.SetStdLogger(log.NewDefaultFactory(context.Background(), log.Formatter{BaseTime: time.Now(), DisableColors: true}, os.Stderr, "", nil, false).Logger()) } if workingDir != "" { _, err := os.Stat(workingDir) if err != nil { filemanager.MkdirAll(globalCtx, workingDir, 0o777) } err = os.Chdir(workingDir) if err != nil { log.Fatal(err) } } if len(configPaths) == 0 && len(configDirectories) == 0 { configPaths = append(configPaths, "config.json") } globalCtx = include.Context(service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger()))) } ================================================ FILE: cmd/sing-box/cmd_check.go ================================================ package main import ( "context" "github.com/sagernet/sing-box" "github.com/sagernet/sing-box/log" "github.com/spf13/cobra" ) var commandCheck = &cobra.Command{ Use: "check", Short: "Check configuration", Run: func(cmd *cobra.Command, args []string) { err := check() if err != nil { log.Fatal(err) } }, Args: cobra.NoArgs, } func init() { mainCommand.AddCommand(commandCheck) } func check() error { options, err := readConfigAndMerge() if err != nil { return err } ctx, cancel := context.WithCancel(globalCtx) instance, err := box.New(box.Options{ Context: ctx, Options: options, }) if err == nil { instance.Close() } cancel() return err } ================================================ FILE: cmd/sing-box/cmd_format.go ================================================ package main import ( "bytes" "os" "path/filepath" "github.com/sagernet/sing-box/log" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json/badjson" "github.com/spf13/cobra" ) var commandFormatFlagWrite bool var commandFormat = &cobra.Command{ Use: "format", Short: "Format configuration", Run: func(cmd *cobra.Command, args []string) { err := format() if err != nil { log.Fatal(err) } }, Args: cobra.NoArgs, } func init() { commandFormat.Flags().BoolVarP(&commandFormatFlagWrite, "write", "w", false, "write result to (source) file instead of stdout") mainCommand.AddCommand(commandFormat) } func format() error { optionsList, err := readConfig() if err != nil { 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("", " ") err = encoder.Encode(optionsEntry.options) if err != nil { return E.Cause(err, "encode config") } outputPath, _ := filepath.Abs(optionsEntry.path) if !commandFormatFlagWrite { if len(optionsList) > 1 { os.Stdout.WriteString(outputPath + "\n") } os.Stdout.WriteString(buffer.String() + "\n") continue } if bytes.Equal(optionsEntry.content, buffer.Bytes()) { continue } output, err := os.Create(optionsEntry.path) if err != nil { return E.Cause(err, "open output") } _, err = output.Write(buffer.Bytes()) output.Close() if err != nil { return E.Cause(err, "write output") } os.Stderr.WriteString(outputPath + "\n") } return nil } ================================================ FILE: cmd/sing-box/cmd_generate.go ================================================ package main import ( "crypto/rand" "encoding/base64" "encoding/hex" "os" "strconv" "github.com/sagernet/sing-box/log" "github.com/gofrs/uuid/v5" "github.com/spf13/cobra" ) var commandGenerate = &cobra.Command{ Use: "generate", Short: "Generate things", } func init() { commandGenerate.AddCommand(commandGenerateUUID) commandGenerate.AddCommand(commandGenerateRandom) mainCommand.AddCommand(commandGenerate) } var ( outputBase64 bool outputHex bool ) var commandGenerateRandom = &cobra.Command{ Use: "rand ", Short: "Generate random bytes", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { err := generateRandom(args) if err != nil { log.Fatal(err) } }, } func init() { commandGenerateRandom.Flags().BoolVar(&outputBase64, "base64", false, "Generate base64 string") commandGenerateRandom.Flags().BoolVar(&outputHex, "hex", false, "Generate hex string") } func generateRandom(args []string) error { length, err := strconv.Atoi(args[0]) if err != nil { return err } randomBytes := make([]byte, length) _, err = rand.Read(randomBytes) if err != nil { return err } if outputBase64 { _, err = os.Stdout.WriteString(base64.StdEncoding.EncodeToString(randomBytes) + "\n") } else if outputHex { _, err = os.Stdout.WriteString(hex.EncodeToString(randomBytes) + "\n") } else { _, err = os.Stdout.Write(randomBytes) } return err } var commandGenerateUUID = &cobra.Command{ Use: "uuid", Short: "Generate UUID string", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { err := generateUUID() if err != nil { log.Fatal(err) } }, } func generateUUID() error { newUUID, err := uuid.NewV4() if err != nil { return err } _, err = os.Stdout.WriteString(newUUID.String() + "\n") return err } ================================================ FILE: cmd/sing-box/cmd_generate_ech.go ================================================ package main import ( "os" "github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/log" "github.com/spf13/cobra" ) var commandGenerateECHKeyPair = &cobra.Command{ Use: "ech-keypair ", Short: "Generate TLS ECH key pair", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { err := generateECHKeyPair(args[0]) if err != nil { log.Fatal(err) } }, } func init() { commandGenerate.AddCommand(commandGenerateECHKeyPair) } func generateECHKeyPair(serverName string) error { configPem, keyPem, err := tls.ECHKeygenDefault(serverName) if err != nil { return err } os.Stdout.WriteString(configPem) os.Stdout.WriteString(keyPem) return nil } ================================================ FILE: cmd/sing-box/cmd_generate_tls.go ================================================ package main import ( "os" "time" "github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/log" "github.com/spf13/cobra" ) var flagGenerateTLSKeyPairMonths int var commandGenerateTLSKeyPair = &cobra.Command{ Use: "tls-keypair ", Short: "Generate TLS self sign key pair", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { err := generateTLSKeyPair(args[0]) if err != nil { log.Fatal(err) } }, } func init() { commandGenerateTLSKeyPair.Flags().IntVarP(&flagGenerateTLSKeyPairMonths, "months", "m", 1, "Valid months") commandGenerate.AddCommand(commandGenerateTLSKeyPair) } func generateTLSKeyPair(serverName string) error { privateKeyPem, publicKeyPem, err := tls.GenerateCertificate(nil, nil, time.Now, serverName, time.Now().AddDate(0, flagGenerateTLSKeyPairMonths, 0)) if err != nil { return err } os.Stdout.WriteString(string(privateKeyPem) + "\n") os.Stdout.WriteString(string(publicKeyPem) + "\n") return nil } ================================================ FILE: cmd/sing-box/cmd_generate_vapid.go ================================================ //go:build go1.20 package main import ( "crypto/ecdh" "crypto/rand" "encoding/base64" "os" "github.com/sagernet/sing-box/log" "github.com/spf13/cobra" ) var commandGenerateVAPIDKeyPair = &cobra.Command{ Use: "vapid-keypair", Short: "Generate VAPID key pair", Run: func(cmd *cobra.Command, args []string) { err := generateVAPIDKeyPair() if err != nil { log.Fatal(err) } }, } func init() { commandGenerate.AddCommand(commandGenerateVAPIDKeyPair) } func generateVAPIDKeyPair() error { privateKey, err := ecdh.P256().GenerateKey(rand.Reader) if err != nil { return err } publicKey := privateKey.PublicKey() os.Stdout.WriteString("PrivateKey: " + base64.RawURLEncoding.EncodeToString(privateKey.Bytes()) + "\n") os.Stdout.WriteString("PublicKey: " + base64.RawURLEncoding.EncodeToString(publicKey.Bytes()) + "\n") return nil } ================================================ FILE: cmd/sing-box/cmd_generate_wireguard.go ================================================ package main import ( "encoding/base64" "os" "github.com/sagernet/sing-box/log" "github.com/spf13/cobra" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" ) func init() { commandGenerate.AddCommand(commandGenerateWireGuardKeyPair) commandGenerate.AddCommand(commandGenerateRealityKeyPair) } var commandGenerateWireGuardKeyPair = &cobra.Command{ Use: "wg-keypair", Short: "Generate WireGuard key pair", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { err := generateWireGuardKey() if err != nil { log.Fatal(err) } }, } func generateWireGuardKey() error { privateKey, err := wgtypes.GeneratePrivateKey() if err != nil { return err } os.Stdout.WriteString("PrivateKey: " + privateKey.String() + "\n") os.Stdout.WriteString("PublicKey: " + privateKey.PublicKey().String() + "\n") return nil } var commandGenerateRealityKeyPair = &cobra.Command{ Use: "reality-keypair", Short: "Generate reality key pair", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { err := generateRealityKey() if err != nil { log.Fatal(err) } }, } func generateRealityKey() error { privateKey, err := wgtypes.GeneratePrivateKey() if err != nil { return err } publicKey := privateKey.PublicKey() os.Stdout.WriteString("PrivateKey: " + base64.RawURLEncoding.EncodeToString(privateKey[:]) + "\n") os.Stdout.WriteString("PublicKey: " + base64.RawURLEncoding.EncodeToString(publicKey[:]) + "\n") return nil } ================================================ FILE: cmd/sing-box/cmd_geoip.go ================================================ package main import ( "github.com/sagernet/sing-box/log" E "github.com/sagernet/sing/common/exceptions" "github.com/oschwald/maxminddb-golang" "github.com/spf13/cobra" ) var ( geoipReader *maxminddb.Reader commandGeoIPFlagFile string ) var commandGeoip = &cobra.Command{ Use: "geoip", Short: "GeoIP tools", PersistentPreRun: func(cmd *cobra.Command, args []string) { err := geoipPreRun() if err != nil { log.Fatal(err) } }, } func init() { commandGeoip.PersistentFlags().StringVarP(&commandGeoIPFlagFile, "file", "f", "geoip.db", "geoip file") mainCommand.AddCommand(commandGeoip) } func geoipPreRun() error { reader, err := maxminddb.Open(commandGeoIPFlagFile) if err != nil { return err } if reader.Metadata.DatabaseType != "sing-geoip" { reader.Close() return E.New("incorrect database type, expected sing-geoip, got ", reader.Metadata.DatabaseType) } geoipReader = reader return nil } ================================================ FILE: cmd/sing-box/cmd_geoip_export.go ================================================ package main import ( "io" "net" "os" "strings" 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" "github.com/sagernet/sing/common/json" "github.com/oschwald/maxminddb-golang" "github.com/spf13/cobra" ) var flagGeoipExportOutput string const flagGeoipExportDefaultOutput = "geoip-.srs" var commandGeoipExport = &cobra.Command{ Use: "export ", Short: "Export geoip country as rule-set", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { err := geoipExport(args[0]) if err != nil { log.Fatal(err) } }, } func init() { commandGeoipExport.Flags().StringVarP(&flagGeoipExportOutput, "output", "o", flagGeoipExportDefaultOutput, "Output path") commandGeoip.AddCommand(commandGeoipExport) } func geoipExport(countryCode string) error { networks := geoipReader.Networks(maxminddb.SkipAliasedNetworks) countryMap := make(map[string][]*net.IPNet) var ( ipNet *net.IPNet nextCountryCode string err error ) for networks.Next() { ipNet, err = networks.Network(&nextCountryCode) if err != nil { return err } countryMap[nextCountryCode] = append(countryMap[nextCountryCode], ipNet) } ipNets := countryMap[strings.ToLower(countryCode)] if len(ipNets) == 0 { return E.New("country code not found: ", countryCode) } var ( outputFile *os.File outputWriter io.Writer ) switch flagGeoipExportOutput { case "stdout": outputWriter = os.Stdout case flagGeoipExportDefaultOutput: outputFile, err = os.Create("geoip-" + countryCode + ".json") if err != nil { return err } defer outputFile.Close() outputWriter = outputFile default: outputFile, err = os.Create(flagGeoipExportOutput) if err != nil { return err } defer outputFile.Close() outputWriter = outputFile } encoder := json.NewEncoder(outputWriter) encoder.SetIndent("", " ") var headlessRule option.DefaultHeadlessRule headlessRule.IPCIDR = make([]string, 0, len(ipNets)) for _, cidr := range ipNets { headlessRule.IPCIDR = append(headlessRule.IPCIDR, cidr.String()) } var plainRuleSet option.PlainRuleSetCompat plainRuleSet.Version = C.RuleSetVersion2 plainRuleSet.Options.Rules = []option.HeadlessRule{ { Type: C.RuleTypeDefault, DefaultOptions: headlessRule, }, } return encoder.Encode(plainRuleSet) } ================================================ FILE: cmd/sing-box/cmd_geoip_list.go ================================================ package main import ( "os" "github.com/sagernet/sing-box/log" "github.com/spf13/cobra" ) var commandGeoipList = &cobra.Command{ Use: "list", Short: "List geoip country codes", Run: func(cmd *cobra.Command, args []string) { err := listGeoip() if err != nil { log.Fatal(err) } }, } func init() { commandGeoip.AddCommand(commandGeoipList) } func listGeoip() error { for _, code := range geoipReader.Metadata.Languages { os.Stdout.WriteString(code + "\n") } return nil } ================================================ FILE: cmd/sing-box/cmd_geoip_lookup.go ================================================ package main import ( "net/netip" "os" "github.com/sagernet/sing-box/log" E "github.com/sagernet/sing/common/exceptions" N "github.com/sagernet/sing/common/network" "github.com/spf13/cobra" ) var commandGeoipLookup = &cobra.Command{ Use: "lookup
", Short: "Lookup if an IP address is contained in the GeoIP database", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { err := geoipLookup(args[0]) if err != nil { log.Fatal(err) } }, } func init() { commandGeoip.AddCommand(commandGeoipLookup) } func geoipLookup(address string) error { addr, err := netip.ParseAddr(address) if err != nil { return E.Cause(err, "parse address") } if !N.IsPublicAddr(addr) { os.Stdout.WriteString("private\n") return nil } var code string _ = geoipReader.Lookup(addr.AsSlice(), &code) if code != "" { os.Stdout.WriteString(code + "\n") return nil } os.Stdout.WriteString("unknown\n") return nil } ================================================ FILE: cmd/sing-box/cmd_geosite.go ================================================ package main import ( "github.com/sagernet/sing-box/common/geosite" "github.com/sagernet/sing-box/log" E "github.com/sagernet/sing/common/exceptions" "github.com/spf13/cobra" ) var ( commandGeoSiteFlagFile string geositeReader *geosite.Reader geositeCodeList []string ) var commandGeoSite = &cobra.Command{ Use: "geosite", Short: "Geosite tools", PersistentPreRun: func(cmd *cobra.Command, args []string) { err := geositePreRun() if err != nil { log.Fatal(err) } }, } func init() { commandGeoSite.PersistentFlags().StringVarP(&commandGeoSiteFlagFile, "file", "f", "geosite.db", "geosite file") mainCommand.AddCommand(commandGeoSite) } func geositePreRun() error { reader, codeList, err := geosite.Open(commandGeoSiteFlagFile) if err != nil { return E.Cause(err, "open geosite file") } geositeReader = reader geositeCodeList = codeList return nil } ================================================ FILE: cmd/sing-box/cmd_geosite_export.go ================================================ package main import ( "io" "os" "github.com/sagernet/sing-box/common/geosite" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common/json" "github.com/spf13/cobra" ) var commandGeositeExportOutput string const commandGeositeExportDefaultOutput = "geosite-.json" var commandGeositeExport = &cobra.Command{ Use: "export ", Short: "Export geosite category as rule-set", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { err := geositeExport(args[0]) if err != nil { log.Fatal(err) } }, } func init() { commandGeositeExport.Flags().StringVarP(&commandGeositeExportOutput, "output", "o", commandGeositeExportDefaultOutput, "Output path") commandGeoSite.AddCommand(commandGeositeExport) } func geositeExport(category string) error { sourceSet, err := geositeReader.Read(category) if err != nil { return err } var ( outputFile *os.File outputWriter io.Writer ) switch commandGeositeExportOutput { case "stdout": outputWriter = os.Stdout case commandGeositeExportDefaultOutput: outputFile, err = os.Create("geosite-" + category + ".json") if err != nil { return err } defer outputFile.Close() outputWriter = outputFile default: outputFile, err = os.Create(commandGeositeExportOutput) if err != nil { return err } defer outputFile.Close() outputWriter = outputFile } encoder := json.NewEncoder(outputWriter) encoder.SetIndent("", " ") var headlessRule option.DefaultHeadlessRule defaultRule := geosite.Compile(sourceSet) headlessRule.Domain = defaultRule.Domain headlessRule.DomainSuffix = defaultRule.DomainSuffix headlessRule.DomainKeyword = defaultRule.DomainKeyword headlessRule.DomainRegex = defaultRule.DomainRegex var plainRuleSet option.PlainRuleSetCompat plainRuleSet.Version = C.RuleSetVersion2 plainRuleSet.Options.Rules = []option.HeadlessRule{ { Type: C.RuleTypeDefault, DefaultOptions: headlessRule, }, } return encoder.Encode(plainRuleSet) } ================================================ FILE: cmd/sing-box/cmd_geosite_list.go ================================================ package main import ( "os" "sort" "github.com/sagernet/sing-box/log" F "github.com/sagernet/sing/common/format" "github.com/spf13/cobra" ) var commandGeositeList = &cobra.Command{ Use: "list ", Short: "List geosite categories", Run: func(cmd *cobra.Command, args []string) { err := geositeList() if err != nil { log.Fatal(err) } }, } func init() { commandGeoSite.AddCommand(commandGeositeList) } func geositeList() error { var geositeEntry []struct { category string items int } for _, category := range geositeCodeList { sourceSet, err := geositeReader.Read(category) if err != nil { return err } geositeEntry = append(geositeEntry, struct { category string items int }{category, len(sourceSet)}) } sort.SliceStable(geositeEntry, func(i, j int) bool { return geositeEntry[i].items < geositeEntry[j].items }) for _, entry := range geositeEntry { os.Stdout.WriteString(F.ToString(entry.category, " (", entry.items, ")\n")) } return nil } ================================================ FILE: cmd/sing-box/cmd_geosite_lookup.go ================================================ package main import ( "os" "sort" "github.com/sagernet/sing-box/log" E "github.com/sagernet/sing/common/exceptions" "github.com/spf13/cobra" ) var commandGeositeLookup = &cobra.Command{ Use: "lookup [category] ", Short: "Check if a domain is in the geosite", Args: cobra.RangeArgs(1, 2), Run: func(cmd *cobra.Command, args []string) { var ( source string target string ) switch len(args) { case 1: target = args[0] case 2: source = args[0] target = args[1] } err := geositeLookup(source, target) if err != nil { log.Fatal(err) } }, } func init() { commandGeoSite.AddCommand(commandGeositeLookup) } func geositeLookup(source string, target string) error { var sourceMatcherList []struct { code string matcher *searchGeositeMatcher } if source != "" { sourceSet, err := geositeReader.Read(source) if err != nil { return err } sourceMatcher, err := newSearchGeositeMatcher(sourceSet) if err != nil { return E.Cause(err, "compile code: "+source) } sourceMatcherList = []struct { code string matcher *searchGeositeMatcher }{ { code: source, matcher: sourceMatcher, }, } } else { for _, code := range geositeCodeList { sourceSet, err := geositeReader.Read(code) if err != nil { return err } sourceMatcher, err := newSearchGeositeMatcher(sourceSet) if err != nil { return E.Cause(err, "compile code: "+code) } sourceMatcherList = append(sourceMatcherList, struct { code string matcher *searchGeositeMatcher }{ code: code, matcher: sourceMatcher, }) } } sort.SliceStable(sourceMatcherList, func(i, j int) bool { return sourceMatcherList[i].code < sourceMatcherList[j].code }) for _, matcherItem := range sourceMatcherList { if matchRule := matcherItem.matcher.Match(target); matchRule != "" { os.Stdout.WriteString("Match code (") os.Stdout.WriteString(matcherItem.code) os.Stdout.WriteString(") ") os.Stdout.WriteString(matchRule) os.Stdout.WriteString("\n") } } return nil } ================================================ FILE: cmd/sing-box/cmd_geosite_matcher.go ================================================ package main import ( "regexp" "strings" "github.com/sagernet/sing-box/common/geosite" ) type searchGeositeMatcher struct { domainMap map[string]bool suffixList []string keywordList []string regexList []string } func newSearchGeositeMatcher(items []geosite.Item) (*searchGeositeMatcher, error) { options := geosite.Compile(items) domainMap := make(map[string]bool) for _, domain := range options.Domain { domainMap[domain] = true } rule := &searchGeositeMatcher{ domainMap: domainMap, suffixList: options.DomainSuffix, keywordList: options.DomainKeyword, regexList: options.DomainRegex, } return rule, nil } func (r *searchGeositeMatcher) Match(domain string) string { if r.domainMap[domain] { return "domain=" + domain } for _, suffix := range r.suffixList { if strings.HasSuffix(domain, suffix) { return "domain_suffix=" + suffix } } for _, keyword := range r.keywordList { if strings.Contains(domain, keyword) { return "domain_keyword=" + keyword } } for _, regexStr := range r.regexList { regex, err := regexp.Compile(regexStr) if err != nil { continue } if regex.MatchString(domain) { return "domain_regex=" + regexStr } } return "" } ================================================ FILE: cmd/sing-box/cmd_merge.go ================================================ package main import ( "bytes" "os" "path/filepath" "strings" 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" "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/rw" "github.com/spf13/cobra" ) var commandMerge = &cobra.Command{ Use: "merge ", Short: "Merge configurations", Run: func(cmd *cobra.Command, args []string) { err := merge(args[0]) if err != nil { log.Fatal(err) } }, Args: cobra.ExactArgs(1), } func init() { mainCommand.AddCommand(commandMerge) } func merge(outputPath string) error { mergedOptions, err := readConfigAndMerge() if err != nil { return err } err = mergePathResources(&mergedOptions) if err != nil { return err } buffer := new(bytes.Buffer) encoder := json.NewEncoder(buffer) encoder.SetIndent("", " ") err = encoder.Encode(mergedOptions) if err != nil { return E.Cause(err, "encode config") } if existsContent, err := os.ReadFile(outputPath); err != nil { if string(existsContent) == buffer.String() { return nil } } err = rw.MkdirParent(outputPath) if err != nil { return err } err = os.WriteFile(outputPath, buffer.Bytes(), 0o644) if err != nil { return err } outputPath, _ = filepath.Abs(outputPath) os.Stderr.WriteString(outputPath + "\n") return nil } func mergePathResources(options *option.Options) error { for _, inbound := range options.Inbounds { if tlsOptions, containsTLSOptions := inbound.Options.(option.InboundTLSOptionsWrapper); containsTLSOptions { tlsOptions.ReplaceInboundTLSOptions(mergeTLSInboundOptions(tlsOptions.TakeInboundTLSOptions())) } } for _, outbound := range options.Outbounds { switch outbound.Type { case C.TypeSSH: mergeSSHOutboundOptions(outbound.Options.(*option.SSHOutboundOptions)) } if tlsOptions, containsTLSOptions := outbound.Options.(option.OutboundTLSOptionsWrapper); containsTLSOptions { tlsOptions.ReplaceOutboundTLSOptions(mergeTLSOutboundOptions(tlsOptions.TakeOutboundTLSOptions())) } } return nil } func mergeTLSInboundOptions(options *option.InboundTLSOptions) *option.InboundTLSOptions { if options == nil { return nil } if options.CertificatePath != "" { if content, err := os.ReadFile(options.CertificatePath); err == nil { options.Certificate = trimStringArray(strings.Split(string(content), "\n")) } } if options.KeyPath != "" { if content, err := os.ReadFile(options.KeyPath); err == nil { options.Key = trimStringArray(strings.Split(string(content), "\n")) } } if options.ECH != nil { if options.ECH.KeyPath != "" { if content, err := os.ReadFile(options.ECH.KeyPath); err == nil { options.ECH.Key = trimStringArray(strings.Split(string(content), "\n")) } } } return options } func mergeTLSOutboundOptions(options *option.OutboundTLSOptions) *option.OutboundTLSOptions { if options == nil { return nil } if options.CertificatePath != "" { if content, err := os.ReadFile(options.CertificatePath); err == nil { options.Certificate = trimStringArray(strings.Split(string(content), "\n")) } } if options.ECH != nil { if options.ECH.ConfigPath != "" { if content, err := os.ReadFile(options.ECH.ConfigPath); err == nil { options.ECH.Config = trimStringArray(strings.Split(string(content), "\n")) } } } return options } func mergeSSHOutboundOptions(options *option.SSHOutboundOptions) { if options.PrivateKeyPath != "" { if content, err := os.ReadFile(os.ExpandEnv(options.PrivateKeyPath)); err == nil { options.PrivateKey = trimStringArray(strings.Split(string(content), "\n")) } } } func trimStringArray(array []string) []string { return common.Filter(array, func(it string) bool { return strings.TrimSpace(it) != "" }) } ================================================ FILE: cmd/sing-box/cmd_rule_set.go ================================================ package main import ( "github.com/spf13/cobra" ) var commandRuleSet = &cobra.Command{ Use: "rule-set", Short: "Manage rule-sets", } func init() { mainCommand.AddCommand(commandRuleSet) } ================================================ FILE: cmd/sing-box/cmd_rule_set_compile.go ================================================ package main import ( "io" "os" "strings" "github.com/sagernet/sing-box/common/srs" 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" "github.com/sagernet/sing/common/json" "github.com/spf13/cobra" ) var flagRuleSetCompileOutput string const flagRuleSetCompileDefaultOutput = ".srs" var commandRuleSetCompile = &cobra.Command{ Use: "compile [source-path]", Short: "Compile rule-set json to binary", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { err := compileRuleSet(args[0]) if err != nil { log.Fatal(err) } }, } func init() { commandRuleSet.AddCommand(commandRuleSetCompile) commandRuleSetCompile.Flags().StringVarP(&flagRuleSetCompileOutput, "output", "o", flagRuleSetCompileDefaultOutput, "Output file") } func compileRuleSet(sourcePath string) error { var ( reader io.Reader err error ) if sourcePath == "stdin" { reader = os.Stdin } else { reader, err = os.Open(sourcePath) if err != nil { return err } } content, err := io.ReadAll(reader) if err != nil { return err } plainRuleSet, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content) if err != nil { return err } var outputPath string if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput { if strings.HasSuffix(sourcePath, ".json") { outputPath = sourcePath[:len(sourcePath)-5] + ".srs" } else { outputPath = sourcePath + ".srs" } } else { outputPath = flagRuleSetCompileOutput } outputFile, err := os.Create(outputPath) if err != nil { return err } err = srs.Write(outputFile, plainRuleSet.Options, downgradeRuleSetVersion(plainRuleSet.Version, plainRuleSet.Options)) if err != nil { outputFile.Close() os.Remove(outputPath) return err } outputFile.Close() return nil } 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 }) { version = C.RuleSetVersion3 } if version == C.RuleSetVersion3 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool { return len(rule.NetworkType) > 0 || rule.NetworkIsExpensive || rule.NetworkIsConstrained }) { version = C.RuleSetVersion2 } return version } ================================================ FILE: cmd/sing-box/cmd_rule_set_convert.go ================================================ package main import ( "io" "os" "strings" "github.com/sagernet/sing-box/common/convertor/adguard" "github.com/sagernet/sing-box/common/srs" 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" "github.com/spf13/cobra" ) var ( flagRuleSetConvertType string flagRuleSetConvertOutput string ) var commandRuleSetConvert = &cobra.Command{ Use: "convert [source-path]", Short: "Convert adguard DNS filter to rule-set", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { err := convertRuleSet(args[0]) if err != nil { log.Fatal(err) } }, } func init() { commandRuleSet.AddCommand(commandRuleSetConvert) commandRuleSetConvert.Flags().StringVarP(&flagRuleSetConvertType, "type", "t", "", "Source type, available: adguard") commandRuleSetConvert.Flags().StringVarP(&flagRuleSetConvertOutput, "output", "o", flagRuleSetCompileDefaultOutput, "Output file") } func convertRuleSet(sourcePath string) error { var ( reader io.Reader err error ) if sourcePath == "stdin" { reader = os.Stdin } else { reader, err = os.Open(sourcePath) if err != nil { return err } } var rules []option.HeadlessRule switch flagRuleSetConvertType { case "adguard": rules, err = adguard.ToOptions(reader, log.StdLogger()) case "": return E.New("source type is required") default: return E.New("unsupported source type: ", flagRuleSetConvertType) } if err != nil { return err } var outputPath string if flagRuleSetConvertOutput == flagRuleSetCompileDefaultOutput { if strings.HasSuffix(sourcePath, ".txt") { outputPath = sourcePath[:len(sourcePath)-4] + ".srs" } else { outputPath = sourcePath + ".srs" } } else { outputPath = flagRuleSetConvertOutput } outputFile, err := os.Create(outputPath) if err != nil { return err } defer outputFile.Close() err = srs.Write(outputFile, option.PlainRuleSet{Rules: rules}, C.RuleSetVersion2) if err != nil { outputFile.Close() os.Remove(outputPath) return err } outputFile.Close() return nil } ================================================ FILE: cmd/sing-box/cmd_rule_set_decompile.go ================================================ package main import ( "io" "os" "strings" "github.com/sagernet/sing-box/common/srs" 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" "github.com/sagernet/sing/common/json" "github.com/spf13/cobra" ) var flagRuleSetDecompileOutput string const flagRuleSetDecompileDefaultOutput = ".json" var commandRuleSetDecompile = &cobra.Command{ Use: "decompile [binary-path]", Short: "Decompile rule-set binary to json", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { err := decompileRuleSet(args[0]) if err != nil { log.Fatal(err) } }, } func init() { commandRuleSet.AddCommand(commandRuleSetDecompile) commandRuleSetDecompile.Flags().StringVarP(&flagRuleSetDecompileOutput, "output", "o", flagRuleSetDecompileDefaultOutput, "Output file") } func decompileRuleSet(sourcePath string) error { var ( reader io.Reader err error ) if sourcePath == "stdin" { reader = os.Stdin } else { reader, err = os.Open(sourcePath) if err != nil { return err } } ruleSet, err := srs.Read(reader, true) if err != nil { return err } if hasRule(ruleSet.Options.Rules, func(rule option.DefaultHeadlessRule) bool { return len(rule.AdGuardDomain) > 0 }) { return E.New("unable to decompile binary AdGuard rules to rule-set.") } var outputPath string if flagRuleSetDecompileOutput == flagRuleSetDecompileDefaultOutput { if strings.HasSuffix(sourcePath, ".srs") { outputPath = sourcePath[:len(sourcePath)-4] + ".json" } else { outputPath = sourcePath + ".json" } } else { outputPath = flagRuleSetDecompileOutput } outputFile, err := os.Create(outputPath) if err != nil { return err } encoder := json.NewEncoder(outputFile) encoder.SetIndent("", " ") err = encoder.Encode(ruleSet) if err != nil { outputFile.Close() os.Remove(outputPath) return err } outputFile.Close() return nil } func hasRule(rules []option.HeadlessRule, cond func(rule option.DefaultHeadlessRule) bool) bool { for _, rule := range rules { switch rule.Type { case C.RuleTypeDefault: if cond(rule.DefaultOptions) { return true } case C.RuleTypeLogical: if hasRule(rule.LogicalOptions.Rules, cond) { return true } } } return false } ================================================ FILE: cmd/sing-box/cmd_rule_set_format.go ================================================ package main import ( "bytes" "io" "os" "path/filepath" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" "github.com/spf13/cobra" ) var commandRuleSetFormatFlagWrite bool var commandRuleSetFormat = &cobra.Command{ Use: "format ", Short: "Format rule-set json", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { err := formatRuleSet(args[0]) if err != nil { log.Fatal(err) } }, } func init() { commandRuleSetFormat.Flags().BoolVarP(&commandRuleSetFormatFlagWrite, "write", "w", false, "write result to (source) file instead of stdout") commandRuleSet.AddCommand(commandRuleSetFormat) } func formatRuleSet(sourcePath string) error { var ( reader io.Reader err error ) if sourcePath == "stdin" { reader = os.Stdin } else { reader, err = os.Open(sourcePath) if err != nil { return err } } content, err := io.ReadAll(reader) if err != nil { return err } plainRuleSet, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content) if err != nil { return err } buffer := new(bytes.Buffer) encoder := json.NewEncoder(buffer) encoder.SetIndent("", " ") err = encoder.Encode(plainRuleSet) if err != nil { return E.Cause(err, "encode config") } outputPath, _ := filepath.Abs(sourcePath) if !commandRuleSetFormatFlagWrite || sourcePath == "stdin" { os.Stdout.WriteString(buffer.String() + "\n") return nil } if bytes.Equal(content, buffer.Bytes()) { return nil } output, err := os.Create(sourcePath) if err != nil { return E.Cause(err, "open output") } _, err = output.Write(buffer.Bytes()) output.Close() if err != nil { return E.Cause(err, "write output") } os.Stderr.WriteString(outputPath + "\n") return nil } ================================================ FILE: cmd/sing-box/cmd_rule_set_match.go ================================================ package main import ( "bytes" "context" "io" "os" "path/filepath" "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/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/route/rule" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json" M "github.com/sagernet/sing/common/metadata" "github.com/spf13/cobra" ) var flagRuleSetMatchFormat string var commandRuleSetMatch = &cobra.Command{ Use: "match ", Short: "Check if an IP address or a domain matches the rule-set", Args: cobra.ExactArgs(2), Run: func(cmd *cobra.Command, args []string) { err := ruleSetMatch(args[0], args[1]) if err != nil { log.Fatal(err) } }, } func init() { commandRuleSetMatch.Flags().StringVarP(&flagRuleSetMatchFormat, "format", "f", "source", "rule-set format") commandRuleSet.AddCommand(commandRuleSetMatch) } func ruleSetMatch(sourcePath string, domain string) error { var ( reader io.Reader err error ) if sourcePath == "stdin" { reader = os.Stdin } else { reader, err = os.Open(sourcePath) if err != nil { return E.Cause(err, "read rule-set") } } content, err := io.ReadAll(reader) if err != nil { return E.Cause(err, "read rule-set") } if flagRuleSetMatchFormat == "" { switch filepath.Ext(sourcePath) { case ".json": flagRuleSetMatchFormat = C.RuleSetFormatSource case ".srs": flagRuleSetMatchFormat = C.RuleSetFormatBinary } } var ruleSet option.PlainRuleSetCompat switch flagRuleSetMatchFormat { case C.RuleSetFormatSource: ruleSet, err = json.UnmarshalExtended[option.PlainRuleSetCompat](content) if err != nil { return err } case C.RuleSetFormatBinary: ruleSet, err = srs.Read(bytes.NewReader(content), false) if err != nil { return err } default: return E.New("unknown rule-set format: ", flagRuleSetMatchFormat) } plainRuleSet, err := ruleSet.Upgrade() if err != nil { return err } ipAddress := M.ParseAddr(domain) var metadata adapter.InboundContext if ipAddress.IsValid() { metadata.Destination = M.SocksaddrFrom(ipAddress, 0) } else { metadata.Domain = domain } for i, ruleOptions := range plainRuleSet.Rules { var currentRule adapter.HeadlessRule currentRule, err = rule.NewHeadlessRule(context.Background(), ruleOptions) if err != nil { return E.Cause(err, "parse rule_set.rules.[", i, "]") } if currentRule.Match(&metadata) { println(F.ToString("match rules.[", i, "]: ", currentRule)) } } return nil } ================================================ FILE: cmd/sing-box/cmd_rule_set_merge.go ================================================ package main import ( "bytes" "io" "os" "path/filepath" "sort" "strings" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" 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/rw" "github.com/spf13/cobra" ) var ( ruleSetPaths []string ruleSetDirectories []string ) var commandRuleSetMerge = &cobra.Command{ Use: "merge ", Short: "Merge rule-set source files", Run: func(cmd *cobra.Command, args []string) { err := mergeRuleSet(args[0]) if err != nil { log.Fatal(err) } }, Args: cobra.ExactArgs(1), } func init() { commandRuleSetMerge.Flags().StringArrayVarP(&ruleSetPaths, "config", "c", nil, "set input rule-set file path") commandRuleSetMerge.Flags().StringArrayVarP(&ruleSetDirectories, "config-directory", "C", nil, "set input rule-set directory path") commandRuleSet.AddCommand(commandRuleSetMerge) } type RuleSetEntry struct { content []byte path string options option.PlainRuleSetCompat } func readRuleSetAt(path string) (*RuleSetEntry, error) { var ( configContent []byte err error ) if path == "stdin" { configContent, err = io.ReadAll(os.Stdin) } else { configContent, err = os.ReadFile(path) } if err != nil { return nil, E.Cause(err, "read config at ", path) } options, err := json.UnmarshalExtendedContext[option.PlainRuleSetCompat](globalCtx, configContent) if err != nil { return nil, E.Cause(err, "decode config at ", path) } return &RuleSetEntry{ content: configContent, path: path, options: options, }, nil } func readRuleSet() ([]*RuleSetEntry, error) { var optionsList []*RuleSetEntry for _, path := range ruleSetPaths { optionsEntry, err := readRuleSetAt(path) if err != nil { return nil, err } optionsList = append(optionsList, optionsEntry) } for _, directory := range ruleSetDirectories { entries, err := os.ReadDir(directory) if err != nil { return nil, E.Cause(err, "read rule-set directory at ", directory) } for _, entry := range entries { if !strings.HasSuffix(entry.Name(), ".json") || entry.IsDir() { continue } optionsEntry, err := readRuleSetAt(filepath.Join(directory, entry.Name())) if err != nil { return nil, err } optionsList = append(optionsList, optionsEntry) } } sort.Slice(optionsList, func(i, j int) bool { return optionsList[i].path < optionsList[j].path }) return optionsList, nil } func readRuleSetAndMerge() (option.PlainRuleSetCompat, error) { optionsList, err := readRuleSet() if err != nil { return option.PlainRuleSetCompat{}, err } if len(optionsList) == 1 { return optionsList[0].options, nil } var optionVersion uint8 for _, options := range optionsList { if optionVersion < options.options.Version { optionVersion = options.options.Version } } var mergedMessage json.RawMessage for _, options := range optionsList { mergedMessage, err = badjson.MergeJSON(globalCtx, options.options.RawMessage, mergedMessage, false) if err != nil { return option.PlainRuleSetCompat{}, E.Cause(err, "merge config at ", options.path) } } mergedOptions, err := json.UnmarshalExtendedContext[option.PlainRuleSetCompat](globalCtx, mergedMessage) if err != nil { return option.PlainRuleSetCompat{}, E.Cause(err, "unmarshal merged config") } mergedOptions.Version = optionVersion return mergedOptions, nil } func mergeRuleSet(outputPath string) error { mergedOptions, err := readRuleSetAndMerge() if err != nil { return err } buffer := new(bytes.Buffer) encoder := json.NewEncoder(buffer) encoder.SetIndent("", " ") err = encoder.Encode(mergedOptions) if err != nil { return E.Cause(err, "encode config") } if existsContent, err := os.ReadFile(outputPath); err != nil { if string(existsContent) == buffer.String() { return nil } } err = rw.MkdirParent(outputPath) if err != nil { return err } err = os.WriteFile(outputPath, buffer.Bytes(), 0o644) if err != nil { return err } outputPath, _ = filepath.Abs(outputPath) os.Stderr.WriteString(outputPath + "\n") return nil } ================================================ FILE: cmd/sing-box/cmd_rule_set_upgrade.go ================================================ package main import ( "bytes" "io" "os" "path/filepath" 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" "github.com/sagernet/sing/common/json" "github.com/spf13/cobra" ) var commandRuleSetUpgradeFlagWrite bool var commandRuleSetUpgrade = &cobra.Command{ Use: "upgrade ", Short: "Upgrade rule-set json", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { err := upgradeRuleSet(args[0]) if err != nil { log.Fatal(err) } }, } func init() { commandRuleSetUpgrade.Flags().BoolVarP(&commandRuleSetUpgradeFlagWrite, "write", "w", false, "write result to (source) file instead of stdout") commandRuleSet.AddCommand(commandRuleSetUpgrade) } func upgradeRuleSet(sourcePath string) error { var ( reader io.Reader err error ) if sourcePath == "stdin" { reader = os.Stdin } else { reader, err = os.Open(sourcePath) if err != nil { return err } } content, err := io.ReadAll(reader) if err != nil { return err } plainRuleSetCompat, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content) if err != nil { return err } switch plainRuleSetCompat.Version { case C.RuleSetVersion1: default: log.Info("already up-to-date") return nil } plainRuleSetCompat.Options, err = plainRuleSetCompat.Upgrade() if err != nil { return err } plainRuleSetCompat.Version = C.RuleSetVersionCurrent buffer := new(bytes.Buffer) encoder := json.NewEncoder(buffer) encoder.SetIndent("", " ") err = encoder.Encode(plainRuleSetCompat) if err != nil { return E.Cause(err, "encode config") } outputPath, _ := filepath.Abs(sourcePath) if !commandRuleSetUpgradeFlagWrite || sourcePath == "stdin" { os.Stdout.WriteString(buffer.String() + "\n") return nil } if bytes.Equal(content, buffer.Bytes()) { return nil } output, err := os.Create(sourcePath) if err != nil { return E.Cause(err, "open output") } _, err = output.Write(buffer.Bytes()) output.Close() if err != nil { return E.Cause(err, "write output") } os.Stderr.WriteString(outputPath + "\n") return nil } ================================================ FILE: cmd/sing-box/cmd_run.go ================================================ package main import ( "context" "io" "os" "os/signal" "path/filepath" runtimeDebug "runtime/debug" "sort" "strings" "syscall" "time" "github.com/sagernet/sing-box" 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" "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json/badjson" "github.com/spf13/cobra" ) var commandRun = &cobra.Command{ Use: "run", Short: "Run service", Run: func(cmd *cobra.Command, args []string) { err := run() if err != nil { log.Fatal(err) } }, } func init() { mainCommand.AddCommand(commandRun) } type OptionsEntry struct { content []byte path string options option.Options } func readConfigAt(path string) (*OptionsEntry, error) { var ( configContent []byte err error ) if path == "stdin" { configContent, err = io.ReadAll(os.Stdin) } else { configContent, err = os.ReadFile(path) } if err != nil { return nil, E.Cause(err, "read config at ", path) } options, err := json.UnmarshalExtendedContext[option.Options](globalCtx, configContent) if err != nil { return nil, E.Cause(err, "decode config at ", path) } return &OptionsEntry{ content: configContent, path: path, options: options, }, nil } func readConfig() ([]*OptionsEntry, error) { var optionsList []*OptionsEntry for _, path := range configPaths { optionsEntry, err := readConfigAt(path) if err != nil { return nil, err } optionsList = append(optionsList, optionsEntry) } for _, directory := range configDirectories { entries, err := os.ReadDir(directory) if err != nil { return nil, E.Cause(err, "read config directory at ", directory) } for _, entry := range entries { if !strings.HasSuffix(entry.Name(), ".json") || entry.IsDir() { continue } optionsEntry, err := readConfigAt(filepath.Join(directory, entry.Name())) if err != nil { return nil, err } optionsList = append(optionsList, optionsEntry) } } sort.Slice(optionsList, func(i, j int) bool { return optionsList[i].path < optionsList[j].path }) return optionsList, nil } func readConfigAndMerge() (option.Options, error) { optionsList, err := readConfig() if err != nil { return option.Options{}, err } if len(optionsList) == 1 { return optionsList[0].options, nil } var mergedMessage json.RawMessage for _, options := range optionsList { mergedMessage, err = badjson.MergeJSON(globalCtx, options.options.RawMessage, mergedMessage, false) if err != nil { return option.Options{}, E.Cause(err, "merge config at ", options.path) } } var mergedOptions option.Options err = mergedOptions.UnmarshalJSONContext(globalCtx, mergedMessage) if err != nil { return option.Options{}, E.Cause(err, "unmarshal merged config") } return mergedOptions, nil } func create() (*box.Box, context.CancelFunc, error) { options, err := readConfigAndMerge() if err != nil { return nil, nil, err } if disableColor { if options.Log == nil { options.Log = &option.LogOptions{} } options.Log.DisableColor = true } ctx, cancel := context.WithCancel(globalCtx) instance, err := box.New(box.Options{ Context: ctx, Options: options, }) if err != nil { cancel() return nil, nil, E.Cause(err, "create service") } osSignals := make(chan os.Signal, 1) signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) defer func() { signal.Stop(osSignals) close(osSignals) }() startCtx, finishStart := context.WithCancel(context.Background()) go func() { _, loaded := <-osSignals if loaded { cancel() closeMonitor(startCtx) } }() err = instance.Start() finishStart() if err != nil { cancel() return nil, nil, E.Cause(err, "start service") } return instance, cancel, nil } func run() error { osSignals := make(chan os.Signal, 1) signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) defer signal.Stop(osSignals) for { instance, cancel, err := create() if err != nil { return err } runtimeDebug.FreeOSMemory() for { osSignal := <-osSignals if osSignal == syscall.SIGHUP { err = check() if err != nil { log.Error(E.Cause(err, "reload service")) continue } } cancel() closeCtx, closed := context.WithCancel(context.Background()) go closeMonitor(closeCtx) err = instance.Close() closed() if osSignal != syscall.SIGHUP { if err != nil { log.Error(E.Cause(err, "sing-box did not closed properly")) } return nil } break } } } func closeMonitor(ctx context.Context) { time.Sleep(C.FatalStopTimeout) select { case <-ctx.Done(): return default: } log.Fatal("sing-box did not close!") } ================================================ FILE: cmd/sing-box/cmd_tools.go ================================================ package main import ( "errors" "os" "github.com/sagernet/sing-box" E "github.com/sagernet/sing/common/exceptions" N "github.com/sagernet/sing/common/network" "github.com/spf13/cobra" ) var commandToolsFlagOutbound string var commandTools = &cobra.Command{ Use: "tools", Short: "Experimental tools", } func init() { commandTools.PersistentFlags().StringVarP(&commandToolsFlagOutbound, "outbound", "o", "", "Use specified tag instead of default outbound") mainCommand.AddCommand(commandTools) } func createPreStartedClient() (*box.Box, error) { options, err := readConfigAndMerge() if err != nil { if !(errors.Is(err, os.ErrNotExist) && len(configDirectories) == 0 && len(configPaths) == 1) || configPaths[0] != "config.json" { return nil, err } } instance, err := box.New(box.Options{Context: globalCtx, Options: options}) if err != nil { return nil, E.Cause(err, "create service") } err = instance.PreStart() if err != nil { return nil, E.Cause(err, "start service") } return instance, nil } func createDialer(instance *box.Box, outboundTag string) (N.Dialer, error) { if outboundTag == "" { return instance.Outbound().Default(), nil } else { outbound, loaded := instance.Outbound().Outbound(outboundTag) if !loaded { return nil, E.New("outbound not found: ", outboundTag) } return outbound, nil } } ================================================ FILE: cmd/sing-box/cmd_tools_connect.go ================================================ package main import ( "context" "os" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing/common" "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/task" "github.com/spf13/cobra" ) var commandConnectFlagNetwork string var commandConnect = &cobra.Command{ Use: "connect
", Short: "Connect to an address", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { err := connect(args[0]) if err != nil { log.Fatal(err) } }, } func init() { commandConnect.Flags().StringVarP(&commandConnectFlagNetwork, "network", "n", "tcp", "network type") commandTools.AddCommand(commandConnect) } func connect(address string) error { switch N.NetworkName(commandConnectFlagNetwork) { case N.NetworkTCP, N.NetworkUDP: default: return E.Cause(N.ErrUnknownNetwork, commandConnectFlagNetwork) } instance, err := createPreStartedClient() if err != nil { return err } defer instance.Close() dialer, err := createDialer(instance, commandToolsFlagOutbound) if err != nil { return err } conn, err := dialer.DialContext(context.Background(), commandConnectFlagNetwork, M.ParseSocksaddr(address)) if err != nil { return E.Cause(err, "connect to server") } var group task.Group group.Append("upload", func(ctx context.Context) error { return common.Error(bufio.Copy(conn, os.Stdin)) }) group.Append("download", func(ctx context.Context) error { return common.Error(bufio.Copy(os.Stdout, conn)) }) group.Cleanup(func() { conn.Close() }) err = group.Run(context.Background()) if E.IsClosed(err) { log.Info(err) } else { log.Error(err) } return nil } ================================================ FILE: cmd/sing-box/cmd_tools_fetch.go ================================================ package main import ( "context" "errors" "io" "net" "net/http" "net/url" "os" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" "github.com/spf13/cobra" ) var commandFetch = &cobra.Command{ Use: "fetch", Short: "Fetch an URL", Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { err := fetch(args) if err != nil { log.Fatal(err) } }, } func init() { commandTools.AddCommand(commandFetch) } var ( httpClient *http.Client http3Client *http.Client ) func fetch(args []string) error { instance, err := createPreStartedClient() if err != nil { return err } defer instance.Close() httpClient = &http.Client{ Transport: &http.Transport{ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { dialer, err := createDialer(instance, commandToolsFlagOutbound) if err != nil { return nil, err } return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) }, ForceAttemptHTTP2: true, }, } defer httpClient.CloseIdleConnections() if C.WithQUIC { err = initializeHTTP3Client(instance) if err != nil { return err } defer http3Client.CloseIdleConnections() } for _, urlString := range args { var parsedURL *url.URL parsedURL, err = url.Parse(urlString) if err != nil { return err } switch parsedURL.Scheme { case "": parsedURL.Scheme = "http" fallthrough case "http", "https": err = fetchHTTP(httpClient, parsedURL) if err != nil { return err } case "http3": if !C.WithQUIC { return C.ErrQUICNotIncluded } parsedURL.Scheme = "https" err = fetchHTTP(http3Client, parsedURL) if err != nil { return err } default: return E.New("unsupported scheme: ", parsedURL.Scheme) } } return nil } func fetchHTTP(httpClient *http.Client, parsedURL *url.URL) error { request, err := http.NewRequest("GET", parsedURL.String(), nil) if err != nil { return err } request.Header.Add("User-Agent", "curl/7.88.0") response, err := httpClient.Do(request) if err != nil { return err } defer response.Body.Close() _, err = bufio.Copy(os.Stdout, response.Body) if errors.Is(err, io.EOF) { return nil } return err } ================================================ FILE: cmd/sing-box/cmd_tools_fetch_http3.go ================================================ //go:build with_quic package main import ( "context" "crypto/tls" "net/http" "github.com/sagernet/quic-go" "github.com/sagernet/quic-go/http3" box "github.com/sagernet/sing-box" "github.com/sagernet/sing/common/bufio" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) func initializeHTTP3Client(instance *box.Box) error { dialer, err := createDialer(instance, commandToolsFlagOutbound) if err != nil { return err } http3Client = &http.Client{ Transport: &http3.Transport{ Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) { destination := M.ParseSocksaddr(addr) udpConn, dErr := dialer.DialContext(ctx, N.NetworkUDP, destination) if dErr != nil { return nil, dErr } return quic.DialEarly(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), tlsCfg, cfg) }, }, } return nil } ================================================ FILE: cmd/sing-box/cmd_tools_fetch_http3_stub.go ================================================ //go:build !with_quic package main import ( "net/url" "os" box "github.com/sagernet/sing-box" ) func initializeHTTP3Client(instance *box.Box) error { return os.ErrInvalid } func fetchHTTP3(parsedURL *url.URL) error { return os.ErrInvalid } ================================================ FILE: cmd/sing-box/cmd_tools_networkquality.go ================================================ 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 } ================================================ FILE: cmd/sing-box/cmd_tools_stun.go ================================================ 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 } ================================================ FILE: cmd/sing-box/cmd_tools_synctime.go ================================================ package main import ( "context" "os" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/ntp" "github.com/spf13/cobra" ) var ( commandSyncTimeFlagServer string commandSyncTimeOutputFormat string commandSyncTimeWrite bool ) var commandSyncTime = &cobra.Command{ Use: "synctime", Short: "Sync time using the NTP protocol", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { err := syncTime() if err != nil { log.Fatal(err) } }, } func init() { commandSyncTime.Flags().StringVarP(&commandSyncTimeFlagServer, "server", "s", "time.apple.com", "Set NTP server") commandSyncTime.Flags().StringVarP(&commandSyncTimeOutputFormat, "format", "f", C.TimeLayout, "Set output format") commandSyncTime.Flags().BoolVarP(&commandSyncTimeWrite, "write", "w", false, "Write time to system") commandTools.AddCommand(commandSyncTime) } func syncTime() error { instance, err := createPreStartedClient() if err != nil { return err } dialer, err := createDialer(instance, commandToolsFlagOutbound) if err != nil { return err } defer instance.Close() serverAddress := M.ParseSocksaddr(commandSyncTimeFlagServer) if serverAddress.Port == 0 { serverAddress.Port = 123 } response, err := ntp.Exchange(context.Background(), dialer, serverAddress) if err != nil { return err } if commandSyncTimeWrite { err = ntp.SetSystemTime(response.Time) if err != nil { return E.Cause(err, "write time to system") } } os.Stdout.WriteString(response.Time.Local().Format(commandSyncTimeOutputFormat)) return nil } ================================================ FILE: cmd/sing-box/cmd_version.go ================================================ package main import ( "os" "runtime" "runtime/debug" C "github.com/sagernet/sing-box/constant" "github.com/spf13/cobra" ) var commandVersion = &cobra.Command{ Use: "version", Short: "Print current version of sing-box", Run: printVersion, Args: cobra.NoArgs, } var nameOnly bool func init() { commandVersion.Flags().BoolVarP(&nameOnly, "name", "n", false, "print version name only") mainCommand.AddCommand(commandVersion) } func printVersion(cmd *cobra.Command, args []string) { if nameOnly { os.Stdout.WriteString(C.Version + "\n") return } version := "sing-box version " + C.Version + "\n\n" version += "Environment: " + runtime.Version() + " " + runtime.GOOS + "/" + runtime.GOARCH + "\n" var tags string var revision string debugInfo, loaded := debug.ReadBuildInfo() if loaded { for _, setting := range debugInfo.Settings { switch setting.Key { case "-tags": tags = setting.Value case "vcs.revision": revision = setting.Value } } } if tags != "" { version += "Tags: " + tags + "\n" } if revision != "" { version += "Revision: " + revision + "\n" } if C.CGO_ENABLED { version += "CGO: enabled\n" } else { version += "CGO: disabled\n" } os.Stdout.WriteString(version) } ================================================ FILE: cmd/sing-box/generate_completions.go ================================================ //go:build generate && generate_completions package main import "github.com/sagernet/sing-box/log" func main() { err := generateCompletions() if err != nil { log.Fatal(err) } } func generateCompletions() error { err := mainCommand.GenBashCompletionFile("release/completions/sing-box.bash") if err != nil { return err } err = mainCommand.GenFishCompletionFile("release/completions/sing-box.fish", true) if err != nil { return err } err = mainCommand.GenZshCompletionFile("release/completions/sing-box.zsh") if err != nil { return err } return nil } ================================================ FILE: cmd/sing-box/main.go ================================================ //go:build !generate package main import "github.com/sagernet/sing-box/log" func main() { if err := mainCommand.Execute(); err != nil { log.Fatal(err) } } ================================================ FILE: common/badtls/raw_conn.go ================================================ //go:build go1.25 && badlinkname package badtls import ( "bytes" "os" "reflect" "sync/atomic" "unsafe" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/tls" ) type RawConn struct { pointer unsafe.Pointer methods *Methods IsClient *bool IsHandshakeComplete *atomic.Bool Vers *uint16 CipherSuite *uint16 RawInput *bytes.Buffer Input *bytes.Reader Hand *bytes.Buffer CloseNotifySent *bool CloseNotifyErr *error In *RawHalfConn Out *RawHalfConn BytesSent *int64 PacketsSent *int64 ActiveCall *atomic.Int32 Tmp *[16]byte } func NewRawConn(rawTLSConn tls.Conn) (*RawConn, error) { var ( pointer unsafe.Pointer methods *Methods loaded bool ) for _, tlsCreator := range methodRegistry { pointer, methods, loaded = tlsCreator(rawTLSConn) if loaded { break } } if !loaded { return nil, os.ErrInvalid } conn := &RawConn{ pointer: pointer, methods: methods, } rawConn := reflect.Indirect(reflect.ValueOf(rawTLSConn)) rawIsClient := rawConn.FieldByName("isClient") if !rawIsClient.IsValid() || rawIsClient.Kind() != reflect.Bool { return nil, E.New("invalid Conn.isClient") } conn.IsClient = (*bool)(unsafe.Pointer(rawIsClient.UnsafeAddr())) rawIsHandshakeComplete := rawConn.FieldByName("isHandshakeComplete") if !rawIsHandshakeComplete.IsValid() || rawIsHandshakeComplete.Kind() != reflect.Struct { return nil, E.New("invalid Conn.isHandshakeComplete") } conn.IsHandshakeComplete = (*atomic.Bool)(unsafe.Pointer(rawIsHandshakeComplete.UnsafeAddr())) rawVers := rawConn.FieldByName("vers") if !rawVers.IsValid() || rawVers.Kind() != reflect.Uint16 { return nil, E.New("invalid Conn.vers") } conn.Vers = (*uint16)(unsafe.Pointer(rawVers.UnsafeAddr())) rawCipherSuite := rawConn.FieldByName("cipherSuite") if !rawCipherSuite.IsValid() || rawCipherSuite.Kind() != reflect.Uint16 { return nil, E.New("invalid Conn.cipherSuite") } conn.CipherSuite = (*uint16)(unsafe.Pointer(rawCipherSuite.UnsafeAddr())) rawRawInput := rawConn.FieldByName("rawInput") if !rawRawInput.IsValid() || rawRawInput.Kind() != reflect.Struct { return nil, E.New("invalid Conn.rawInput") } conn.RawInput = (*bytes.Buffer)(unsafe.Pointer(rawRawInput.UnsafeAddr())) rawInput := rawConn.FieldByName("input") if !rawInput.IsValid() || rawInput.Kind() != reflect.Struct { return nil, E.New("invalid Conn.input") } conn.Input = (*bytes.Reader)(unsafe.Pointer(rawInput.UnsafeAddr())) rawHand := rawConn.FieldByName("hand") if !rawHand.IsValid() || rawHand.Kind() != reflect.Struct { return nil, E.New("invalid Conn.hand") } conn.Hand = (*bytes.Buffer)(unsafe.Pointer(rawHand.UnsafeAddr())) rawCloseNotifySent := rawConn.FieldByName("closeNotifySent") if !rawCloseNotifySent.IsValid() || rawCloseNotifySent.Kind() != reflect.Bool { return nil, E.New("invalid Conn.closeNotifySent") } conn.CloseNotifySent = (*bool)(unsafe.Pointer(rawCloseNotifySent.UnsafeAddr())) rawCloseNotifyErr := rawConn.FieldByName("closeNotifyErr") if !rawCloseNotifyErr.IsValid() || rawCloseNotifyErr.Kind() != reflect.Interface { return nil, E.New("invalid Conn.closeNotifyErr") } conn.CloseNotifyErr = (*error)(unsafe.Pointer(rawCloseNotifyErr.UnsafeAddr())) rawIn := rawConn.FieldByName("in") if !rawIn.IsValid() || rawIn.Kind() != reflect.Struct { return nil, E.New("invalid Conn.in") } halfIn, err := NewRawHalfConn(rawIn, methods) if err != nil { return nil, E.Cause(err, "invalid Conn.in") } conn.In = halfIn rawOut := rawConn.FieldByName("out") if !rawOut.IsValid() || rawOut.Kind() != reflect.Struct { return nil, E.New("invalid Conn.out") } halfOut, err := NewRawHalfConn(rawOut, methods) if err != nil { return nil, E.Cause(err, "invalid Conn.out") } conn.Out = halfOut rawBytesSent := rawConn.FieldByName("bytesSent") if !rawBytesSent.IsValid() || rawBytesSent.Kind() != reflect.Int64 { return nil, E.New("invalid Conn.bytesSent") } conn.BytesSent = (*int64)(unsafe.Pointer(rawBytesSent.UnsafeAddr())) rawPacketsSent := rawConn.FieldByName("packetsSent") if !rawPacketsSent.IsValid() || rawPacketsSent.Kind() != reflect.Int64 { return nil, E.New("invalid Conn.packetsSent") } conn.PacketsSent = (*int64)(unsafe.Pointer(rawPacketsSent.UnsafeAddr())) rawActiveCall := rawConn.FieldByName("activeCall") if !rawActiveCall.IsValid() || rawActiveCall.Kind() != reflect.Struct { return nil, E.New("invalid Conn.activeCall") } conn.ActiveCall = (*atomic.Int32)(unsafe.Pointer(rawActiveCall.UnsafeAddr())) rawTmp := rawConn.FieldByName("tmp") if !rawTmp.IsValid() || rawTmp.Kind() != reflect.Array || rawTmp.Len() != 16 || rawTmp.Type().Elem().Kind() != reflect.Uint8 { return nil, E.New("invalid Conn.tmp") } conn.Tmp = (*[16]byte)(unsafe.Pointer(rawTmp.UnsafeAddr())) return conn, nil } func (c *RawConn) ReadRecord() error { return c.methods.readRecord(c.pointer) } func (c *RawConn) HandlePostHandshakeMessage() error { return c.methods.handlePostHandshakeMessage(c.pointer) } func (c *RawConn) WriteRecordLocked(typ uint16, data []byte) (int, error) { return c.methods.writeRecordLocked(c.pointer, typ, data) } ================================================ FILE: common/badtls/raw_half_conn.go ================================================ //go:build go1.25 && badlinkname package badtls import ( "hash" "reflect" "sync" "unsafe" E "github.com/sagernet/sing/common/exceptions" ) type RawHalfConn struct { pointer unsafe.Pointer methods *Methods *sync.Mutex Err *error Version *uint16 Cipher *any Seq *[8]byte ScratchBuf *[13]byte TrafficSecret *[]byte Mac *hash.Hash RawKey *[]byte RawIV *[]byte RawMac *[]byte } func NewRawHalfConn(rawHalfConn reflect.Value, methods *Methods) (*RawHalfConn, error) { halfConn := &RawHalfConn{ pointer: (unsafe.Pointer)(rawHalfConn.UnsafeAddr()), methods: methods, } rawMutex := rawHalfConn.FieldByName("Mutex") if !rawMutex.IsValid() || rawMutex.Kind() != reflect.Struct { return nil, E.New("badtls: invalid halfConn.Mutex") } halfConn.Mutex = (*sync.Mutex)(unsafe.Pointer(rawMutex.UnsafeAddr())) rawErr := rawHalfConn.FieldByName("err") if !rawErr.IsValid() || rawErr.Kind() != reflect.Interface { return nil, E.New("badtls: invalid halfConn.err") } halfConn.Err = (*error)(unsafe.Pointer(rawErr.UnsafeAddr())) rawVersion := rawHalfConn.FieldByName("version") if !rawVersion.IsValid() || rawVersion.Kind() != reflect.Uint16 { return nil, E.New("badtls: invalid halfConn.version") } halfConn.Version = (*uint16)(unsafe.Pointer(rawVersion.UnsafeAddr())) rawCipher := rawHalfConn.FieldByName("cipher") if !rawCipher.IsValid() || rawCipher.Kind() != reflect.Interface { return nil, E.New("badtls: invalid halfConn.cipher") } halfConn.Cipher = (*any)(unsafe.Pointer(rawCipher.UnsafeAddr())) rawSeq := rawHalfConn.FieldByName("seq") if !rawSeq.IsValid() || rawSeq.Kind() != reflect.Array || rawSeq.Len() != 8 || rawSeq.Type().Elem().Kind() != reflect.Uint8 { return nil, E.New("badtls: invalid halfConn.seq") } halfConn.Seq = (*[8]byte)(unsafe.Pointer(rawSeq.UnsafeAddr())) rawScratchBuf := rawHalfConn.FieldByName("scratchBuf") if !rawScratchBuf.IsValid() || rawScratchBuf.Kind() != reflect.Array || rawScratchBuf.Len() != 13 || rawScratchBuf.Type().Elem().Kind() != reflect.Uint8 { return nil, E.New("badtls: invalid halfConn.scratchBuf") } halfConn.ScratchBuf = (*[13]byte)(unsafe.Pointer(rawScratchBuf.UnsafeAddr())) rawTrafficSecret := rawHalfConn.FieldByName("trafficSecret") if !rawTrafficSecret.IsValid() || rawTrafficSecret.Kind() != reflect.Slice || rawTrafficSecret.Type().Elem().Kind() != reflect.Uint8 { return nil, E.New("badtls: invalid halfConn.trafficSecret") } halfConn.TrafficSecret = (*[]byte)(unsafe.Pointer(rawTrafficSecret.UnsafeAddr())) rawMac := rawHalfConn.FieldByName("mac") if !rawMac.IsValid() || rawMac.Kind() != reflect.Interface { return nil, E.New("badtls: invalid halfConn.mac") } halfConn.Mac = (*hash.Hash)(unsafe.Pointer(rawMac.UnsafeAddr())) rawKey := rawHalfConn.FieldByName("rawKey") if rawKey.IsValid() { if /*!rawKey.IsValid() || */ rawKey.Kind() != reflect.Slice || rawKey.Type().Elem().Kind() != reflect.Uint8 { return nil, E.New("badtls: invalid halfConn.rawKey") } halfConn.RawKey = (*[]byte)(unsafe.Pointer(rawKey.UnsafeAddr())) rawIV := rawHalfConn.FieldByName("rawIV") if !rawIV.IsValid() || rawIV.Kind() != reflect.Slice || rawIV.Type().Elem().Kind() != reflect.Uint8 { return nil, E.New("badtls: invalid halfConn.rawIV") } halfConn.RawIV = (*[]byte)(unsafe.Pointer(rawIV.UnsafeAddr())) rawMAC := rawHalfConn.FieldByName("rawMac") if !rawMAC.IsValid() || rawMAC.Kind() != reflect.Slice || rawMAC.Type().Elem().Kind() != reflect.Uint8 { return nil, E.New("badtls: invalid halfConn.rawMac") } halfConn.RawMac = (*[]byte)(unsafe.Pointer(rawMAC.UnsafeAddr())) } return halfConn, nil } func (hc *RawHalfConn) Decrypt(record []byte) ([]byte, uint8, error) { return hc.methods.decrypt(hc.pointer, record) } func (hc *RawHalfConn) SetErrorLocked(err error) error { return hc.methods.setErrorLocked(hc.pointer, err) } func (hc *RawHalfConn) SetTrafficSecret(suite unsafe.Pointer, level int, secret []byte) { hc.methods.setTrafficSecret(hc.pointer, suite, level, secret) } func (hc *RawHalfConn) ExplicitNonceLen() int { return hc.methods.explicitNonceLen(hc.pointer) } ================================================ FILE: common/badtls/read_wait.go ================================================ //go:build go1.25 && badlinkname package badtls import ( "github.com/sagernet/sing/common/buf" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/tls" ) var _ N.ReadWaiter = (*ReadWaitConn)(nil) type ReadWaitConn struct { tls.Conn rawConn *RawConn readWaitOptions N.ReadWaitOptions } func NewReadWaitConn(conn tls.Conn) (tls.Conn, error) { if _, isReadWaitConn := conn.(N.ReadWaiter); isReadWaitConn { return conn, nil } rawConn, err := NewRawConn(conn) if err != nil { return nil, err } return &ReadWaitConn{ Conn: conn, rawConn: rawConn, }, nil } func (c *ReadWaitConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { c.readWaitOptions = options return false } func (c *ReadWaitConn) WaitReadBuffer() (buffer *buf.Buffer, err error) { //err = c.HandshakeContext(context.Background()) //if err != nil { // return //} c.rawConn.In.Lock() defer c.rawConn.In.Unlock() for c.rawConn.Input.Len() == 0 { err = c.rawConn.ReadRecord() if err != nil { return } for c.rawConn.Hand.Len() > 0 { err = c.rawConn.HandlePostHandshakeMessage() if err != nil { return } } } buffer = c.readWaitOptions.NewBuffer() n, err := c.rawConn.Input.Read(buffer.FreeBytes()) if err != nil { buffer.Release() return } buffer.Truncate(n) if n != 0 && c.rawConn.Input.Len() == 0 && c.rawConn.Input.Len() > 0 && // recordType(c.RawInput.Bytes()[0]) == recordTypeAlert { c.rawConn.RawInput.Bytes()[0] == 21 { _ = c.rawConn.ReadRecord() // return n, err // will be io.EOF on closeNotify } c.readWaitOptions.PostReturn(buffer) return } func (c *ReadWaitConn) Upstream() any { return c.Conn } func (c *ReadWaitConn) ReaderReplaceable() bool { return true } ================================================ FILE: common/badtls/read_wait_stub.go ================================================ //go:build !go1.25 || !badlinkname package badtls import ( "os" "github.com/sagernet/sing/common/tls" ) func NewReadWaitConn(conn tls.Conn) (tls.Conn, error) { return nil, os.ErrInvalid } ================================================ FILE: common/badtls/registry.go ================================================ //go:build go1.25 && badlinkname package badtls import ( "crypto/tls" "net" "unsafe" ) type Methods struct { readRecord func(c unsafe.Pointer) error handlePostHandshakeMessage func(c unsafe.Pointer) error writeRecordLocked func(c unsafe.Pointer, typ uint16, data []byte) (int, error) setErrorLocked func(hc unsafe.Pointer, err error) error decrypt func(hc unsafe.Pointer, record []byte) ([]byte, uint8, error) setTrafficSecret func(hc unsafe.Pointer, suite unsafe.Pointer, level int, secret []byte) explicitNonceLen func(hc unsafe.Pointer) int } var methodRegistry []func(conn net.Conn) (unsafe.Pointer, *Methods, bool) func init() { methodRegistry = append(methodRegistry, func(conn net.Conn) (unsafe.Pointer, *Methods, bool) { tlsConn, loaded := conn.(*tls.Conn) if !loaded { return nil, nil, false } return unsafe.Pointer(tlsConn), &Methods{ readRecord: stdTLSReadRecord, handlePostHandshakeMessage: stdTLSHandlePostHandshakeMessage, writeRecordLocked: stdWriteRecordLocked, setErrorLocked: stdSetErrorLocked, decrypt: stdDecrypt, setTrafficSecret: stdSetTrafficSecret, explicitNonceLen: stdExplicitNonceLen, }, true }) } //go:linkname stdTLSReadRecord crypto/tls.(*Conn).readRecord func stdTLSReadRecord(c unsafe.Pointer) error //go:linkname stdTLSHandlePostHandshakeMessage crypto/tls.(*Conn).handlePostHandshakeMessage func stdTLSHandlePostHandshakeMessage(c unsafe.Pointer) error //go:linkname stdWriteRecordLocked crypto/tls.(*Conn).writeRecordLocked func stdWriteRecordLocked(c unsafe.Pointer, typ uint16, data []byte) (int, error) //go:linkname stdSetErrorLocked crypto/tls.(*halfConn).setErrorLocked func stdSetErrorLocked(hc unsafe.Pointer, err error) error //go:linkname stdDecrypt crypto/tls.(*halfConn).decrypt func stdDecrypt(hc unsafe.Pointer, record []byte) ([]byte, uint8, error) //go:linkname stdSetTrafficSecret crypto/tls.(*halfConn).setTrafficSecret func stdSetTrafficSecret(hc unsafe.Pointer, suite unsafe.Pointer, level int, secret []byte) //go:linkname stdExplicitNonceLen crypto/tls.(*halfConn).explicitNonceLen func stdExplicitNonceLen(hc unsafe.Pointer) int ================================================ FILE: common/badtls/registry_utls.go ================================================ //go:build go1.25 && badlinkname package badtls import ( "net" "unsafe" N "github.com/sagernet/sing/common/network" "github.com/metacubex/utls" ) func init() { methodRegistry = append(methodRegistry, func(conn net.Conn) (unsafe.Pointer, *Methods, bool) { var pointer unsafe.Pointer if uConn, loaded := N.CastReader[*tls.Conn](conn); loaded { pointer = unsafe.Pointer(uConn) } else if uConn, loaded := N.CastReader[*tls.UConn](conn); loaded { pointer = unsafe.Pointer(uConn.Conn) } else { return nil, nil, false } return pointer, &Methods{ readRecord: utlsReadRecord, handlePostHandshakeMessage: utlsHandlePostHandshakeMessage, writeRecordLocked: utlsWriteRecordLocked, setErrorLocked: utlsSetErrorLocked, decrypt: utlsDecrypt, setTrafficSecret: utlsSetTrafficSecret, explicitNonceLen: utlsExplicitNonceLen, }, true }) } //go:linkname utlsReadRecord github.com/metacubex/utls.(*Conn).readRecord func utlsReadRecord(c unsafe.Pointer) error //go:linkname utlsHandlePostHandshakeMessage github.com/metacubex/utls.(*Conn).handlePostHandshakeMessage func utlsHandlePostHandshakeMessage(c unsafe.Pointer) error //go:linkname utlsWriteRecordLocked github.com/metacubex/utls.(*Conn).writeRecordLocked func utlsWriteRecordLocked(hc unsafe.Pointer, typ uint16, data []byte) (int, error) //go:linkname utlsSetErrorLocked github.com/metacubex/utls.(*halfConn).setErrorLocked func utlsSetErrorLocked(hc unsafe.Pointer, err error) error //go:linkname utlsDecrypt github.com/metacubex/utls.(*halfConn).decrypt func utlsDecrypt(hc unsafe.Pointer, record []byte) ([]byte, uint8, error) //go:linkname utlsSetTrafficSecret github.com/metacubex/utls.(*halfConn).setTrafficSecret func utlsSetTrafficSecret(hc unsafe.Pointer, suite unsafe.Pointer, level int, secret []byte) //go:linkname utlsExplicitNonceLen github.com/metacubex/utls.(*halfConn).explicitNonceLen func utlsExplicitNonceLen(hc unsafe.Pointer) int ================================================ FILE: common/badversion/version.go ================================================ package badversion import ( "strconv" "strings" F "github.com/sagernet/sing/common/format" "golang.org/x/mod/semver" ) type Version struct { Major int Minor int Patch int Commit string PreReleaseIdentifier string PreReleaseVersion int } func (v Version) LessThan(anotherVersion Version) bool { return !v.GreaterThanOrEqual(anotherVersion) } func (v Version) LessThanOrEqual(anotherVersion Version) bool { return v == anotherVersion || anotherVersion.GreaterThan(v) } func (v Version) GreaterThanOrEqual(anotherVersion Version) bool { return v == anotherVersion || v.GreaterThan(anotherVersion) } func (v Version) GreaterThan(anotherVersion Version) bool { if v.Major > anotherVersion.Major { return true } else if v.Major < anotherVersion.Major { return false } if v.Minor > anotherVersion.Minor { return true } else if v.Minor < anotherVersion.Minor { return false } if v.Patch > anotherVersion.Patch { return true } else if v.Patch < anotherVersion.Patch { return false } if v.PreReleaseIdentifier == "" && anotherVersion.PreReleaseIdentifier != "" { return true } else if v.PreReleaseIdentifier != "" && anotherVersion.PreReleaseIdentifier == "" { return false } if v.PreReleaseIdentifier != "" && anotherVersion.PreReleaseIdentifier != "" { if v.PreReleaseIdentifier == anotherVersion.PreReleaseIdentifier { if v.PreReleaseVersion > anotherVersion.PreReleaseVersion { return true } else if v.PreReleaseVersion < anotherVersion.PreReleaseVersion { return false } } preReleaseIdentifier := parsePreReleaseIdentifier(v.PreReleaseIdentifier) anotherPreReleaseIdentifier := parsePreReleaseIdentifier(anotherVersion.PreReleaseIdentifier) if preReleaseIdentifier < anotherPreReleaseIdentifier { return true } else if preReleaseIdentifier > anotherPreReleaseIdentifier { return false } } return false } func parsePreReleaseIdentifier(identifier string) int { if strings.HasPrefix(identifier, "rc") { return 1 } else if strings.HasPrefix(identifier, "beta") { return 2 } else if strings.HasPrefix(identifier, "alpha") { return 3 } return 0 } func (v Version) VersionString() string { return F.ToString(v.Major, ".", v.Minor, ".", v.Patch) } func (v Version) String() string { version := F.ToString(v.Major, ".", v.Minor, ".", v.Patch) if v.PreReleaseIdentifier != "" { version = F.ToString(version, "-", v.PreReleaseIdentifier, ".", v.PreReleaseVersion) } return version } func (v Version) BadString() string { version := F.ToString(v.Major, ".", v.Minor) if v.Patch > 0 { version = F.ToString(version, ".", v.Patch) } if v.PreReleaseIdentifier != "" { version = F.ToString(version, "-", v.PreReleaseIdentifier) if v.PreReleaseVersion > 0 { version = F.ToString(version, v.PreReleaseVersion) } } return version } func IsValid(versionName string) bool { return semver.IsValid("v" + versionName) } func Parse(versionName string) (version Version) { versionName = strings.TrimPrefix(versionName, "v") if strings.Contains(versionName, "-") { parts := strings.Split(versionName, "-") versionName = parts[0] identifier := parts[1] if strings.Contains(identifier, ".") { identifierParts := strings.Split(identifier, ".") version.PreReleaseIdentifier = identifierParts[0] if len(identifierParts) >= 2 { version.PreReleaseVersion, _ = strconv.Atoi(identifierParts[1]) } } else { if strings.HasPrefix(identifier, "alpha") { version.PreReleaseIdentifier = "alpha" version.PreReleaseVersion, _ = strconv.Atoi(identifier[5:]) } else if strings.HasPrefix(identifier, "beta") { version.PreReleaseIdentifier = "beta" version.PreReleaseVersion, _ = strconv.Atoi(identifier[4:]) } else { version.Commit = identifier } } } versionElements := strings.Split(versionName, ".") versionLen := len(versionElements) if versionLen >= 1 { version.Major, _ = strconv.Atoi(versionElements[0]) } if versionLen >= 2 { version.Minor, _ = strconv.Atoi(versionElements[1]) } if versionLen >= 3 { version.Patch, _ = strconv.Atoi(versionElements[2]) } return } ================================================ FILE: common/badversion/version_json.go ================================================ package badversion import "github.com/sagernet/sing/common/json" func (v Version) MarshalJSON() ([]byte, error) { return json.Marshal(v.String()) } func (v *Version) UnmarshalJSON(data []byte) error { var version string err := json.Unmarshal(data, &version) if err != nil { return err } *v = Parse(version) return nil } ================================================ FILE: common/badversion/version_test.go ================================================ package badversion import ( "testing" "github.com/stretchr/testify/require" ) func TestCompareVersion(t *testing.T) { t.Parallel() require.Equal(t, "1.3.0-beta.1", Parse("v1.3.0-beta1").String()) require.Equal(t, "1.3-beta1", Parse("v1.3.0-beta.1").BadString()) require.True(t, Parse("1.3.0").GreaterThan(Parse("1.3-beta1"))) require.True(t, Parse("1.3.0").GreaterThan(Parse("1.3.0-beta1"))) require.True(t, Parse("1.3.0-beta1").GreaterThan(Parse("1.3.0-alpha1"))) require.True(t, Parse("1.3.1").GreaterThan(Parse("1.3.0"))) require.True(t, Parse("1.4").GreaterThan(Parse("1.3"))) } ================================================ FILE: common/certificate/anchors_darwin.h ================================================ #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 ================================================ FILE: common/certificate/anchors_darwin.m ================================================ #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); } ================================================ FILE: common/certificate/chrome.go ================================================ // Code generated by 'make update_certificates'. DO NOT EDIT. package 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 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----- // CN=TunTrust Root CA; O=Agence Nationale de Certification Electronique; C=TN -----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----- // 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 -----BEGIN CERTIFICATE----- MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5 MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi 9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB /zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW 1KyLa2tJElMzrdfkviT8tQp21KW8EA== -----END CERTIFICATE----- // CN=Amazon Root CA 1; O=Amazon; C=US -----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----- // CN=Amazon Root CA 3; O=Amazon; C=US -----BEGIN CERTIFICATE----- MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5 MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM YyRIHN8wfdVoOw== -----END CERTIFICATE----- // CN=Certum Trusted Network CA; OU=Certum Certification Authority; O=Unizeto Technologies S.A.; C=PL -----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----- // CN=Certum EC-384 CA; OU=Certum Certification Authority; O=Asseco Data Systems S.A.; C=PL -----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----- // CN=Certum Trusted Root CA; OU=Certum Certification Authority; O=Asseco Data Systems S.A.; C=PL -----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----- // CN=Certum Trusted Network CA 2; OU=Certum Certification Authority; O=Unizeto Technologies S.A.; C=PL -----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----- // CN=Autoridad de Certificacion Firmaprofesional CIF A62634068; C=ES -----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----- // CN=ANF Secure Server Root CA; OU=ANF CA Raiz; O=ANF Autoridad de Certificacion; C=ES; SerialNumber=G63287510 -----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----- // CN=Buypass Class 2 Root CA; O=Buypass AS-983163327; C=NO -----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----- // CN=Buypass Class 3 Root CA; O=Buypass AS-983163327; C=NO -----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----- // CN=Certainly Root R1; O=Certainly; C=US -----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----- // CN=Certainly Root E1; O=Certainly; C=US -----BEGIN CERTIFICATE----- MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQsw CQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlu bHkgUm9vdCBFMTAeFw0yMTA0MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJ BgNVBAYTAlVTMRIwEAYDVQQKEwlDZXJ0YWlubHkxGjAYBgNVBAMTEUNlcnRhaW5s eSBSb290IEUxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3m/4fxzf7flHh4axpMCK +IKXgOqPyEpeKn2IaKcBYhSRJHpcnqMXfYqGITQYUBsQ3tA3SybHGWCA6TS9YBk2 QNYphwk8kXr2vBMj3VlOBF7PyAIcGFPBMdjaIOlEjeR2o0IwQDAOBgNVHQ8BAf8E BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4 hevIIgcwCgYIKoZIzj0EAwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozm ut6Dacpps6kFtZaSF4fC0urQe87YQVt8rgIwRt7qy12a7DLCZRawTDBcMPPaTnOG BtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR -----END CERTIFICATE----- // CN=Certigna Root CA; OU=0002 48146308100036; O=Dhimyotis; C=FR -----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----- // OU=certSIGN ROOT CA G2; O=CERTSIGN SA; C=RO -----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----- // CN=HiPKI Root CA - G1; O=Chunghwa Telecom Co., Ltd.; C=TW -----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----- // OU=ePKI Root Certification Authority; O=Chunghwa Telecom Co., Ltd.; C=TW -----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----- // 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 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----- // CN=D-TRUST EV Root CA 1 2020; O=D-Trust GmbH; C=DE -----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----- // 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 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----- // 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 -----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----- // CN=DigiCert TLS ECC P384 Root G5; O=DigiCert, Inc.; C=US -----BEGIN CERTIFICATE----- MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS 7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp 0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 DXZDjC5Ty3zfDBeWUA== -----END 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 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----- // CN=DigiCert Assured ID Root G3; OU=www.digicert.com; O=DigiCert Inc; C=US -----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----- // CN=DigiCert Global Root G2; OU=www.digicert.com; O=DigiCert Inc; C=US -----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----- // CN=DigiCert Global Root G3; OU=www.digicert.com; O=DigiCert Inc; C=US -----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----- // CN=DigiCert Trusted Root G4; OU=www.digicert.com; O=DigiCert Inc; C=US -----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----- // CN=CA Disig Root R2; O=Disig a.s.; L=Bratislava; C=SK -----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----- // CN=emSign Root CA - G1; OU=emSign PKI; O=eMudhra Technologies Limited; C=IN -----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----- // 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 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----- // CN=Atos TrustedRoot Root CA RSA TLS 2021; O=Atos; C=DE -----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----- // CN=GlobalSign Root R46; O=GlobalSign nv-sa; C=BE -----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----- // 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 DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc 8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg 515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO xwy8p2Fp8fc74SrL+SvzZpA3 -----END CERTIFICATE----- // CN=GlobalSign; OU=GlobalSign Root CA - R3; O=GlobalSign -----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----- // 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 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----- // CN=Go Daddy Root Certificate Authority - G2; O=GoDaddy.com, Inc.; L=Scottsdale; ST=Arizona; C=US -----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----- // CN=GTS Root R4; O=Google Trust Services LLC; C=US -----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----- // CN=GTS Root R2; O=Google Trust Services LLC; C=US -----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----- // CN=GTS Root R1; O=Google Trust Services LLC; C=US -----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----- // CN=GTS Root R3; O=Google Trust Services LLC; C=US -----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----- // 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 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----- // 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 -----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----- // 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 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----- // CN=HARICA TLS RSA Root CA 2021; O=Hellenic Academic and Research Institutions CA; C=GR -----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----- // CN=HARICA TLS ECC Root CA 2021; O=Hellenic Academic and Research Institutions CA; C=GR -----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----- // CN=IdenTrust Commercial Root CA 1; O=IdenTrust; C=US -----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----- // CN=ISRG Root X1; O=Internet Security Research Group; C=US -----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----- // CN=ISRG Root X2; O=Internet Security Research Group; C=US -----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----- // CN=Izenpe.com; O=IZENPE S.A.; C=ES -----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----- // CN=SZAFIR ROOT CA2; O=Krajowa Izba Rozliczeniowa S.A.; C=PL -----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----- // 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 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----- // 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 -----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----- // CN=Microsoft RSA Root Certificate Authority 2017; O=Microsoft Corporation; C=US -----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----- // CN=NAVER Global Root Certification Authority; O=NAVER BUSINESS PLATFORM Corp.; C=KR -----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----- // 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 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----- // CN=OISTE WISeKey Global Root GC CA; OU=OISTE Foundation Endorsed; O=WISeKey; C=CH -----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----- // CN=OISTE WISeKey Global Root GB CA; OU=OISTE Foundation Endorsed; O=WISeKey; C=CH -----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----- // CN=Security Communication ECC RootCA1; O=SECOM Trust Systems CO.,LTD.; C=JP -----BEGIN CERTIFICATE----- MIICODCCAb6gAwIBAgIJANZdm7N4gS7rMAoGCCqGSM49BAMDMGExCzAJBgNVBAYT AkpQMSUwIwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMSswKQYD VQQDEyJTZWN1cml0eSBDb21tdW5pY2F0aW9uIEVDQyBSb290Q0ExMB4XDTE2MDYx NjA1MTUyOFoXDTM4MDExODA1MTUyOFowYTELMAkGA1UEBhMCSlAxJTAjBgNVBAoT HFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKzApBgNVBAMTIlNlY3VyaXR5 IENvbW11bmljYXRpb24gRUNDIFJvb3RDQTEwdjAQBgcqhkjOPQIBBgUrgQQAIgNi AASkpW9gAwPDvTH00xecK4R1rOX9PVdu12O/5gSJko6BnOPpR27KkBLIE+Cnnfdl dB9sELLo5OnvbYUymUSxXv3MdhDYW72ixvnWQuRXdtyQwjWpS4g8EkdtXP9JTxpK ULGjQjBAMB0GA1UdDgQWBBSGHOf+LaVKiwj+KBH6vqNm+GBZLzAOBgNVHQ8BAf8E BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjAVXUI9/Lbu 9zuxNuie9sRGKEkz0FhDKmMpzE2xtHqiuQ04pV1IKv3LsnNdo4gIxwwCMQDAqy0O be0YottT6SXbVQjgUMzfRGEWgqtJsLKB7HOHeLRMsmIbEvoWTSVLY70eN9k= -----END CERTIFICATE----- // OU=Security Communication RootCA2; O=SECOM Trust Systems CO.,LTD.; C=JP -----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----- // CN=COMODO Certification Authority; O=COMODO CA Limited; L=Salford; ST=Greater Manchester; C=GB -----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----- // CN=COMODO RSA Certification Authority; O=COMODO CA Limited; L=Salford; ST=Greater Manchester; C=GB -----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----- // CN=USERTrust RSA Certification Authority; O=The USERTRUST Network; L=Jersey City; ST=New Jersey; C=US -----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----- // CN=USERTrust ECC Certification Authority; O=The USERTRUST Network; L=Jersey City; ST=New Jersey; C=US -----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----- // 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 -----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----- // CN=SSL.com TLS ECC Root CA 2022; O=SSL Corporation; C=US -----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----- // 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 -----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----- // CN=SSL.com Root Certification Authority ECC; O=SSL Corporation; L=Houston; ST=Texas; C=US -----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----- // CN=SSL.com EV Root Certification Authority RSA R2; O=SSL Corporation; L=Houston; ST=Texas; C=US -----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----- // 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 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----- // CN=Telia Root CA v2; O=Telia Finland Oyj; C=FI -----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----- ` } ================================================ FILE: common/certificate/chrome.pem ================================================ -----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----- ================================================ FILE: common/certificate/mozilla.go ================================================ // Code generated by 'make update_certificates'. DO NOT EDIT. package certificate func mozillaIncludedPEM() string { return ` // Actalis Authentication Root CA -----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----- // TunTrust Root CA -----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----- // Amazon Root CA 1 -----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----- // Amazon Root CA 2 -----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----- // Amazon Root CA 3 -----BEGIN CERTIFICATE----- MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5 MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM YyRIHN8wfdVoOw== -----END CERTIFICATE----- // Amazon Root CA 4 -----BEGIN CERTIFICATE----- MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5 MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi 9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB /zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW 1KyLa2tJElMzrdfkviT8tQp21KW8EA== -----END CERTIFICATE----- // Starfield Services Root Certificate Authority - G2 -----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----- // Certum CA -----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----- // Certum EC-384 CA -----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----- // Certum Trusted Network CA -----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----- // Certum Trusted Network CA 2 -----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----- // Certum Trusted Root CA -----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----- // Autoridad de Certificacion Firmaprofesional CIF A62634068 -----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----- // ANF Secure Server Root CA -----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----- // Buypass Class 2 Root CA -----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----- // Buypass Class 3 Root CA -----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----- // Certainly Root E1 -----BEGIN CERTIFICATE----- MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQsw CQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlu bHkgUm9vdCBFMTAeFw0yMTA0MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJ BgNVBAYTAlVTMRIwEAYDVQQKEwlDZXJ0YWlubHkxGjAYBgNVBAMTEUNlcnRhaW5s eSBSb290IEUxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3m/4fxzf7flHh4axpMCK +IKXgOqPyEpeKn2IaKcBYhSRJHpcnqMXfYqGITQYUBsQ3tA3SybHGWCA6TS9YBk2 QNYphwk8kXr2vBMj3VlOBF7PyAIcGFPBMdjaIOlEjeR2o0IwQDAOBgNVHQ8BAf8E BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4 hevIIgcwCgYIKoZIzj0EAwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozm ut6Dacpps6kFtZaSF4fC0urQe87YQVt8rgIwRt7qy12a7DLCZRawTDBcMPPaTnOG BtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR -----END CERTIFICATE----- // Certainly Root R1 -----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----- // Certigna -----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----- // Certigna Root CA -----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----- // certSIGN ROOT CA G2 -----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----- // ePKI Root Certification Authority -----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----- // HiPKI Root CA - G1 -----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----- // SecureSign Root CA12 -----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----- // SecureSign Root CA14 -----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----- // SecureSign Root CA15 -----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----- // D-TRUST BR Root CA 1 2020 -----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----- // D-TRUST BR Root CA 2 2023 -----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----- // D-TRUST EV Root CA 1 2020 -----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----- // D-TRUST EV Root CA 2 2023 -----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----- // D-TRUST Root CA 3 2013 -----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----- // D-TRUST Root Class 3 CA 2 2009 -----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----- // D-TRUST Root Class 3 CA 2 EV 2009 -----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----- // D-Trust SBR Root CA 1 2022 -----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----- // D-Trust SBR Root CA 2 2022 -----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----- // T-TeleSec GlobalRoot Class 2 -----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----- // T-TeleSec GlobalRoot Class 3 -----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----- // Telekom Security SMIME ECC Root 2021 -----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----- // Telekom Security SMIME RSA Root 2023 -----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----- // Telekom Security TLS ECC Root 2020 -----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----- // Telekom Security TLS RSA Root 2023 -----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----- // DigiCert Assured ID Root CA -----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----- // DigiCert Assured ID Root G2 -----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----- // DigiCert Assured ID Root G3 -----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----- // DigiCert Global Root CA -----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----- // DigiCert Global Root G2 -----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----- // DigiCert Global Root G3 -----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----- // DigiCert High Assurance EV Root CA -----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----- // DigiCert SMIME ECC P384 Root G5 -----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----- // DigiCert SMIME RSA4096 Root G5 -----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----- // DigiCert TLS ECC P384 Root G5 -----BEGIN CERTIFICATE----- MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS 7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp 0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 DXZDjC5Ty3zfDBeWUA== -----END CERTIFICATE----- // DigiCert TLS RSA4096 Root G5 -----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----- // DigiCert Trusted Root G4 -----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----- // QuoVadis Root CA 1 G3 -----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----- // QuoVadis Root CA 2 -----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----- // QuoVadis Root CA 2 G3 -----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----- // QuoVadis Root CA 3 -----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----- // QuoVadis Root CA 3 G3 -----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----- // DIGITALSIGN GLOBAL ROOT ECDSA CA -----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----- // DIGITALSIGN GLOBAL ROOT RSA CA -----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----- // CA Disig Root R2 -----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----- // emSign ECC Root CA - C3 -----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----- // emSign ECC Root CA - G3 -----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----- // emSign Root CA - C1 -----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----- // emSign Root CA - G1 -----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----- // Atos TrustedRoot 2011 -----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----- // Atos TrustedRoot Root CA ECC G2 2020 -----BEGIN CERTIFICATE----- MIICMTCCAbagAwIBAgIMC3MoERh0MBzvbwiEMAoGCCqGSM49BAMDMEsxCzAJBgNV BAYTAkRFMQ0wCwYDVQQKDARBdG9zMS0wKwYDVQQDDCRBdG9zIFRydXN0ZWRSb290 IFJvb3QgQ0EgRUNDIEcyIDIwMjAwHhcNMjAxMjE1MDgzOTEwWhcNNDAxMjEwMDgz OTA5WjBLMQswCQYDVQQGEwJERTENMAsGA1UECgwEQXRvczEtMCsGA1UEAwwkQXRv cyBUcnVzdGVkUm9vdCBSb290IENBIEVDQyBHMiAyMDIwMHYwEAYHKoZIzj0CAQYF K4EEACIDYgAEyFyAyk7CKB9XvzjmYSP80KlblhYWwwxeFaWQCf84KLR6HgrWUyrB u5BAdDfpgeiNL2gBNXxSLtj0WLMRHFvZhxiTkS3sndpsnm2ESPzCiQXrmBMCAWxT Hg5JY1hHsa/Co2MwYTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFFsfxHFs shufvlwfjP2ztvuzDgmHMB0GA1UdDgQWBBRbH8RxbLIbn75cH4z9s7b7sw4JhzAO BgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMDaQAwZgIxAOzgmf3d5FTByx/oPijX FVlKgspTMOzrNqW5yM6TR1bIYabhbZJTlY/241VT8N165wIxALCH1RuzYPyRjYDK ohtRSzhUy6oee9flRJUWLzxEeC4luuqQ5OxS7lfsA4TzXtsWDQ== -----END CERTIFICATE----- // Atos TrustedRoot Root CA ECC TLS 2021 -----BEGIN CERTIFICATE----- MIICFTCCAZugAwIBAgIQPZg7pmY9kGP3fiZXOATvADAKBggqhkjOPQQDAzBMMS4w LAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgRUNDIFRMUyAyMDIxMQ0w CwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTI2MjNaFw00MTA0 MTcwOTI2MjJaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBDQSBF Q0MgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMHYwEAYHKoZI zj0CAQYFK4EEACIDYgAEloZYKDcKZ9Cg3iQZGeHkBQcfl+3oZIK59sRxUM6KDP/X tXa7oWyTbIOiaG6l2b4siJVBzV3dscqDY4PMwL502eCdpO5KTlbgmClBk1IQ1SQ4 AjJn8ZQSb+/Xxd4u/RmAo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR2 KCXWfeBmmnoJsmo7jjPXNtNPojAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMD aAAwZQIwW5kp85wxtolrbNa9d+F851F+uDrNozZffPc8dz7kUK2o59JZDCaOMDtu CCrCp1rIAjEAmeMM56PDr9NJLkaCI2ZdyQAUEv049OGYa3cpetskz2VAv9LcjBHo 9H1/IISpQuQo -----END CERTIFICATE----- // Atos TrustedRoot Root CA RSA G2 2020 -----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----- // Atos TrustedRoot Root CA RSA TLS 2021 -----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----- // 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----- // 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 -----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----- // GlobalSign Root CA -----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----- // GlobalSign Root E46 -----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----- // GlobalSign Root R46 -----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----- // GlobalSign Secure Mail Root E45 -----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----- // GlobalSign Secure Mail Root R45 -----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----- // Go Daddy Class 2 Certification Authority -----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----- // Go Daddy Root Certificate Authority - G2 -----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----- // Starfield Class 2 Certification Authority -----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----- // Starfield Root Certificate Authority - G2 -----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----- // 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----- // GTS Root R1 -----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----- // GTS Root R2 -----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----- // GTS Root R3 -----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----- // GTS Root R4 -----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----- // Hongkong Post Root CA 3 -----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----- // ACCVRAIZ1 -----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----- // AC RAIZ FNMT-RCM -----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----- // AC RAIZ FNMT-RCM SERVIDORES SEGUROS -----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----- // Staat der Nederlanden Root CA - G3 -----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----- // TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 -----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----- // HARICA Client ECC Root CA 2021 -----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----- // HARICA Client RSA Root CA 2021 -----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----- // HARICA TLS ECC Root CA 2021 -----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----- // HARICA TLS RSA Root CA 2021 -----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----- // Hellenic Academic and Research Institutions ECC RootCA 2015 -----BEGIN CERTIFICATE----- MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzAN BgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl c2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hl bGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgRUNDIFJv b3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEwMzcxMlowgaoxCzAJ BgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmljIEFj YWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5 MUQwQgYDVQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0 dXRpb25zIEVDQyBSb290Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKg QehLgoRc4vgxEZmGZE4JJS+dQS8KrjVPdJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJa jq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoKVlp8aQuqgAkkbH7BRqNC MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLQi C4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaep lSTAGiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7Sof TUwJCA3sS61kFyjndc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR -----END CERTIFICATE----- // Hellenic Academic and Research Institutions RootCA 2015 -----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----- // IdenTrust Commercial Root CA 1 -----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----- // IdenTrust Public Sector Root CA 1 -----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----- // ISRG Root X1 -----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----- // ISRG Root X2 -----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----- // Izenpe.com -----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----- // SZAFIR ROOT CA2 -----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----- // LAWtrust Root CA2 (4096) -----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----- // e-Szigno Root CA 2017 -----BEGIN CERTIFICATE----- MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNV BAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRk LjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJv b3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZaFw00MjA4MjIxMjA3MDZaMHExCzAJ BgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMg THRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25v IFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtv xie+RJCxs1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+H Wyx7xf58etqjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G A1UdDgQWBBSHERUI0arBeAyxr87GyZDvvzAEwDAfBgNVHSMEGDAWgBSHERUI0arB eAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEAtVfd14pVCzbhhkT61Nlo jbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxOsvxyqltZ +efcMQ== -----END 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 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----- // Microsoft ECC Root Certificate Authority 2017 -----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----- // Microsoft RSA Root Certificate Authority 2017 -----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----- // NAVER Global Root Certification Authority -----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----- // NetLock Arany (Class Gold) Főtanúsítvány -----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----- // OISTE Client Root ECC G1 -----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----- // OISTE Client Root RSA G1 -----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----- // OISTE Server Root ECC G1 -----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----- // OISTE Server Root RSA G1 -----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----- // OISTE WISeKey Global Root GA CA -----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----- // OISTE WISeKey Global Root GB CA -----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----- // OISTE WISeKey Global Root GC CA -----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----- // Security Communication ECC RootCA1 -----BEGIN CERTIFICATE----- MIICODCCAb6gAwIBAgIJANZdm7N4gS7rMAoGCCqGSM49BAMDMGExCzAJBgNVBAYT AkpQMSUwIwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMSswKQYD VQQDEyJTZWN1cml0eSBDb21tdW5pY2F0aW9uIEVDQyBSb290Q0ExMB4XDTE2MDYx NjA1MTUyOFoXDTM4MDExODA1MTUyOFowYTELMAkGA1UEBhMCSlAxJTAjBgNVBAoT HFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKzApBgNVBAMTIlNlY3VyaXR5 IENvbW11bmljYXRpb24gRUNDIFJvb3RDQTEwdjAQBgcqhkjOPQIBBgUrgQQAIgNi AASkpW9gAwPDvTH00xecK4R1rOX9PVdu12O/5gSJko6BnOPpR27KkBLIE+Cnnfdl dB9sELLo5OnvbYUymUSxXv3MdhDYW72ixvnWQuRXdtyQwjWpS4g8EkdtXP9JTxpK ULGjQjBAMB0GA1UdDgQWBBSGHOf+LaVKiwj+KBH6vqNm+GBZLzAOBgNVHQ8BAf8E BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjAVXUI9/Lbu 9zuxNuie9sRGKEkz0FhDKmMpzE2xtHqiuQ04pV1IKv3LsnNdo4gIxwwCMQDAqy0O be0YottT6SXbVQjgUMzfRGEWgqtJsLKB7HOHeLRMsmIbEvoWTSVLY70eN9k= -----END CERTIFICATE----- // Security Communication RootCA2 -----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----- // AAA Certificate Services -----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----- // COMODO Certification Authority -----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----- // COMODO ECC Certification Authority -----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----- // COMODO RSA Certification Authority -----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----- // Entrust Root Certification Authority -----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----- // Entrust Root Certification Authority - EC1 -----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----- // Entrust Root Certification Authority - G2 -----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----- // Entrust Root Certification Authority - G4 -----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----- // Entrust.net Certification Authority (2048) -----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----- // Sectigo Public Email Protection Root E46 -----BEGIN CERTIFICATE----- MIICMTCCAbegAwIBAgIQbvXTp0GOoFlApzBr0kBlVjAKBggqhkjOPQQDAzBaMQsw CQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTEwLwYDVQQDEyhT ZWN0aWdvIFB1YmxpYyBFbWFpbCBQcm90ZWN0aW9uIFJvb3QgRTQ2MB4XDTIxMDMy MjAwMDAwMFoXDTQ2MDMyMTIzNTk1OVowWjELMAkGA1UEBhMCR0IxGDAWBgNVBAoT D1NlY3RpZ28gTGltaXRlZDExMC8GA1UEAxMoU2VjdGlnbyBQdWJsaWMgRW1haWwg UHJvdGVjdGlvbiBSb290IEU0NjB2MBAGByqGSM49AgEGBSuBBAAiA2IABLinUpT1 PgWwG/YfsdN+ueQFZlSAzmylaH3kU1LbgvrEht9DePfIrRa8P3gyy2vTSdZE5bN+ n3umxizy4rbTibCaPEvOiUvGxss6SWAPRrxtTnqcyZuFewq2sEfCiOPU0aNCMEAw HQYDVR0OBBYEFC1OjKfCI7JXqQZrPmsrifPDXkfOMA4GA1UdDwEB/wQEAwIBhjAP BgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQCSnRpZY0VYjhsW5H16 bDZIMB8rcueQMzT9JKLGBoxvOzJXWvj+xkkSU5rZELKZUXICMAUlKjMh/JPmIqLM cFUoNVaiB8QhhCMaTEyZUJmSFMtK3Fb79dOPaiz1cTr4izsDng== -----END CERTIFICATE----- // Sectigo Public Email Protection Root R46 -----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----- // Sectigo Public Server Authentication Root E46 -----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----- // Sectigo Public Server Authentication Root R46 -----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----- // USERTrust ECC Certification Authority -----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----- // USERTrust RSA Certification Authority -----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----- // SSL.com Client ECC Root CA 2022 -----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----- // SSL.com Client RSA Root CA 2022 -----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----- // SSL.com EV Root Certification Authority ECC -----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----- // SSL.com EV Root Certification Authority RSA R2 -----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----- // SSL.com Root Certification Authority ECC -----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----- // SSL.com Root Certification Authority RSA -----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----- // SSL.com TLS ECC Root CA 2022 -----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----- // SSL.com TLS RSA Root CA 2022 -----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----- // SwissSign Gold CA - G2 -----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----- // SwissSign RSA SMIME Root CA 2022 - 1 -----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----- // SwissSign RSA TLS Root CA 2022 - 1 -----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----- // TWCA CYBER Root CA -----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----- // TWCA Global Root CA -----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----- // TWCA Global Root CA G2 -----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----- // TWCA Root Certification Authority -----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----- // Telia Root CA v2 -----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----- // TrustAsia Global Root CA G3 -----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----- // TrustAsia Global Root CA G4 -----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----- // TrustAsia SMIME ECC Root CA -----BEGIN CERTIFICATE----- MIICNjCCAbugAwIBAgIUWsL4KU/jfcVeHRhvO5MgH/97ui0wCgYIKoZIzj0EAwMw WjELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs IEluYy4xJDAiBgNVBAMTG1RydXN0QXNpYSBTTUlNRSBFQ0MgUm9vdCBDQTAeFw0y NDA1MTUwNTQxNTlaFw00NDA1MTUwNTQxNThaMFoxCzAJBgNVBAYTAkNOMSUwIwYD VQQKExxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDExtUcnVz dEFzaWEgU01JTUUgRUNDIFJvb3QgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATN 2fsnvWnshsmQQ7FwF5SnyXcjOj8jZdMcox0eQlQg69BCu1m5i6zyho1Ljh2qliIj OXZtkpvrIst6Q6Jz/XNLwiUPKrFpxv9F36k8lYC7qR5Kky/sHB2I9BGSN583mHKj QjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFDFn5nKyDeYioKzPfiKnWTLj ZiOlMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNpADBmAjEA3TpMjaTGf+29 pcZPPv0xSyjWilbfZRZ3h037ujIIgeCeM0iLn5SG7wErlOaM1tSOAjEAn4GcsCb9 K9by9XGEnqjHiozWWBFStbgEy8xxdWPixhk42W1sGXGkFhkhk7oGRChs -----END CERTIFICATE----- // TrustAsia SMIME RSA Root CA -----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----- // TrustAsia TLS ECC Root CA -----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----- // TrustAsia TLS RSA Root CA -----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----- ` } ================================================ FILE: common/certificate/mozilla.pem ================================================ -----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----- ================================================ FILE: common/certificate/store.go ================================================ package certificate import ( "bytes" "context" "crypto/x509" "io/fs" "os" "path/filepath" "strings" "sync" "github.com/sagernet/fswatch" "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/logger" "github.com/sagernet/sing/service" ) var _ adapter.CertificateStore = (*Store)(nil) type Store struct { access sync.RWMutex storeType string systemPool *x509.CertPool currentPool *x509.CertPool certificate string certificatePaths []string certificateDirectoryPaths []string watcher *fswatch.Watcher //nolint:unused // populated only on darwin && cgo via the storePlatform embed. platform storePlatform } func NewStore(ctx context.Context, logger logger.Logger, options option.CertificateOptions) (*Store, error) { storeType := options.Store if storeType == "" { storeType = C.CertificateStoreSystem } var systemPool *x509.CertPool switch storeType { case C.CertificateStoreSystem: systemPool = x509.NewCertPool() platformInterface := service.FromContext[adapter.PlatformInterface](ctx) var systemValid bool if platformInterface != nil { for _, cert := range platformInterface.SystemCertificates() { if systemPool.AppendCertsFromPEM([]byte(cert)) { systemValid = true } } } if !systemValid { certPool, err := x509.SystemCertPool() if err != nil { return nil, err } systemPool = certPool } case C.CertificateStoreMozilla, C.CertificateStoreChrome: case C.CertificateStoreNone: default: return nil, E.New("unknown certificate store: ", options.Store) } store := &Store{ storeType: storeType, systemPool: systemPool, certificate: strings.Join(options.Certificate, "\n"), certificatePaths: options.CertificatePath, certificateDirectoryPaths: options.CertificateDirectoryPath, } var watchPaths []string for _, target := range options.CertificatePath { watchPaths = append(watchPaths, target) } for _, target := range options.CertificateDirectoryPath { watchPaths = append(watchPaths, target) } if len(watchPaths) > 0 { watcher, err := fswatch.NewWatcher(fswatch.Options{ Path: watchPaths, Logger: logger, Callback: func(_ string) { err := store.update() if err != nil { logger.Error(E.Cause(err, "reload certificates")) } }, }) if err != nil { return nil, E.Cause(err, "fswatch: create fsnotify watcher") } store.watcher = watcher } err := store.update() if err != nil { return nil, E.Cause(err, "initializing certificate store") } return store, nil } func (s *Store) Name() string { return "certificate" } func (s *Store) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } if s.watcher != nil { return s.watcher.Start() } return nil } func (s *Store) Close() error { watcher := s.watcher s.watcher = nil var closeErr error if watcher != nil { closeErr = watcher.Close() } platformErr := s.closePlatform() if platformErr != nil { closeErr = platformErr } return closeErr } func (s *Store) Pool() *x509.CertPool { s.access.RLock() defer s.access.RUnlock() return s.currentPool } func (s *Store) StoreKind() string { return s.storeType } func (s *Store) ExclusiveAnchors() bool { return s.storeType != C.CertificateStoreSystem } func (s *Store) update() error { currentPool, err := s.newBasePool() 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") } appendPEMBlock(pemBuffer, string(pemContent)) case C.CertificateStoreChrome: pemContent := chromeIncludedPEM() if !currentPool.AppendCertsFromPEM([]byte(pemContent)) { return E.New("invalid Chrome included certificate PEM") } appendPEMBlock(pemBuffer, string(pemContent)) } if s.certificate != "" { if !currentPool.AppendCertsFromPEM([]byte(s.certificate)) { return E.New("invalid certificate PEM strings") } appendPEMBlock(pemBuffer, s.certificate) } for _, path := range s.certificatePaths { pemContent, err := os.ReadFile(path) if err != nil { return err } if !currentPool.AppendCertsFromPEM(pemContent) { return E.New("invalid certificate PEM file: ", path) } appendPEMBlock(pemBuffer, string(pemContent)) } var firstErr error for _, directoryPath := range s.certificateDirectoryPaths { directoryEntries, err := readUniqueDirectoryEntries(directoryPath) if err != nil { if firstErr == nil && !os.IsNotExist(err) { firstErr = E.Cause(err, "invalid certificate directory: ", directoryPath) } continue } for _, directoryEntry := range directoryEntries { pemContent, err := os.ReadFile(filepath.Join(directoryPath, directoryEntry.Name())) if err == nil && currentPool.AppendCertsFromPEM(pemContent) { appendPEMBlock(pemBuffer, string(pemContent)) } } } if firstErr != nil { return firstErr } s.access.Lock() defer s.access.Unlock() s.currentPool = currentPool 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) { switch s.storeType { case C.CertificateStoreSystem: if s.systemPool == nil { return x509.NewCertPool(), nil } return s.systemPool.Clone(), nil case C.CertificateStoreMozilla, C.CertificateStoreChrome: return x509.NewCertPool(), nil case C.CertificateStoreNone: return x509.NewCertPool(), nil default: return nil, E.New("unknown certificate store: ", s.storeType) } } func readUniqueDirectoryEntries(dir string) ([]fs.DirEntry, error) { files, err := os.ReadDir(dir) if err != nil { return nil, err } uniq := files[:0] for _, f := range files { if !isSameDirSymlink(f, dir) { uniq = append(uniq, f) } } return uniq, nil } func isSameDirSymlink(f fs.DirEntry, dir string) bool { if f.Type()&fs.ModeSymlink == 0 { return false } target, err := os.Readlink(filepath.Join(dir, f.Name())) return err == nil && !strings.Contains(target, "/") } ================================================ FILE: common/certificate/store_darwin.go ================================================ //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 } ================================================ FILE: common/certificate/store_other.go ================================================ //go:build !(darwin && cgo) package certificate //nolint:unused // referenced by Store.platform; populated only in store_darwin.go. type storePlatform struct{} func (s *Store) updatePlatformLocked(_ []byte) error { return nil } func (s *Store) closePlatform() error { return nil } ================================================ FILE: common/compatible/map.go ================================================ package compatible import "sync" // Map is a generics sync.Map type Map[K comparable, V any] struct { m sync.Map } func (m *Map[K, V]) Len() int { var count int m.m.Range(func(key, value any) bool { count++ return true }) return count } func (m *Map[K, V]) Load(key K) (V, bool) { v, ok := m.m.Load(key) if !ok { return *new(V), false } return v.(V), ok } func (m *Map[K, V]) Store(key K, value V) { m.m.Store(key, value) } func (m *Map[K, V]) Delete(key K) { m.m.Delete(key) } func (m *Map[K, V]) Range(f func(key K, value V) bool) { m.m.Range(func(key, value any) bool { return f(key.(K), value.(V)) }) } func (m *Map[K, V]) LoadOrStore(key K, value V) (V, bool) { v, ok := m.m.LoadOrStore(key, value) return v.(V), ok } func (m *Map[K, V]) LoadAndDelete(key K) (V, bool) { v, ok := m.m.LoadAndDelete(key) if !ok { return *new(V), false } return v.(V), ok } func New[K comparable, V any]() *Map[K, V] { return &Map[K, V]{m: sync.Map{}} } ================================================ FILE: common/convertor/adguard/convertor.go ================================================ package adguard import ( "bufio" "bytes" "io" "net/netip" "os" "strconv" "strings" 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" M "github.com/sagernet/sing/common/metadata" ) type agdguardRuleLine struct { ruleLine string isRawDomain bool isExclude bool isSuffix bool hasStart bool hasEnd bool isRegexp bool isImportant bool } func ToOptions(reader io.Reader, logger logger.Logger) ([]option.HeadlessRule, error) { scanner := bufio.NewScanner(reader) var ( ruleLines []agdguardRuleLine ignoredLines int ) parseLine: for scanner.Scan() { ruleLine := scanner.Text() if ruleLine == "" { continue } if strings.HasPrefix(ruleLine, "!") || strings.HasPrefix(ruleLine, "#") { continue } originRuleLine := ruleLine if M.IsDomainName(ruleLine) { ruleLines = append(ruleLines, agdguardRuleLine{ ruleLine: ruleLine, isRawDomain: true, }) continue } hostLine, err := parseAdGuardHostLine(ruleLine) if err == nil { if hostLine != "" { ruleLines = append(ruleLines, agdguardRuleLine{ ruleLine: hostLine, isRawDomain: true, hasStart: true, hasEnd: true, }) } continue } ruleLine = strings.TrimSuffix(ruleLine, "|") var ( isExclude bool isSuffix bool hasStart bool hasEnd bool isRegexp bool isImportant bool ) if !strings.HasPrefix(ruleLine, "/") && strings.Contains(ruleLine, "$") { params := common.SubstringAfter(ruleLine, "$") for param := range strings.SplitSeq(params, ",") { paramParts := strings.Split(param, "=") var ignored bool if len(paramParts) > 0 && len(paramParts) <= 2 { switch paramParts[0] { case "app", "network": // maybe support by package_name/process_name case "dnstype": // maybe support by query_type case "important": ignored = true isImportant = true case "dnsrewrite": if len(paramParts) == 2 && M.ParseAddr(paramParts[1]).IsUnspecified() { ignored = true } } } if !ignored { ignoredLines++ logger.Debug("ignored unsupported rule with modifier: ", paramParts[0], ": ", originRuleLine) continue parseLine } } ruleLine = common.SubstringBefore(ruleLine, "$") } if strings.HasPrefix(ruleLine, "@@") { ruleLine = ruleLine[2:] isExclude = true } ruleLine = strings.TrimSuffix(ruleLine, "|") if strings.HasPrefix(ruleLine, "||") { ruleLine = ruleLine[2:] isSuffix = true } else if strings.HasPrefix(ruleLine, "|") { ruleLine = ruleLine[1:] hasStart = true } if strings.HasSuffix(ruleLine, "^") { ruleLine = ruleLine[:len(ruleLine)-1] hasEnd = true } if strings.HasPrefix(ruleLine, "/") && strings.HasSuffix(ruleLine, "/") { ruleLine = ruleLine[1 : len(ruleLine)-1] if ignoreIPCIDRRegexp(ruleLine) { ignoredLines++ logger.Debug("ignored unsupported rule with IPCIDR regexp: ", originRuleLine) continue } isRegexp = true } else { if strings.Contains(ruleLine, "://") { ruleLine = common.SubstringAfter(ruleLine, "://") isSuffix = true } if strings.Contains(ruleLine, "/") { ignoredLines++ logger.Debug("ignored unsupported rule with path: ", originRuleLine) continue } if strings.Contains(ruleLine, "?") || strings.Contains(ruleLine, "&") { ignoredLines++ logger.Debug("ignored unsupported rule with query: ", originRuleLine) continue } if strings.Contains(ruleLine, "[") || strings.Contains(ruleLine, "]") || strings.Contains(ruleLine, "(") || strings.Contains(ruleLine, ")") || strings.Contains(ruleLine, "!") || strings.Contains(ruleLine, "#") { ignoredLines++ logger.Debug("ignored unsupported cosmetic filter: ", originRuleLine) continue } if strings.Contains(ruleLine, "~") { ignoredLines++ logger.Debug("ignored unsupported rule modifier: ", originRuleLine) continue } var domainCheck string if strings.HasPrefix(ruleLine, ".") || strings.HasPrefix(ruleLine, "-") { domainCheck = "r" + ruleLine } else { domainCheck = ruleLine } if ruleLine == "" { ignoredLines++ logger.Debug("ignored unsupported rule with empty domain", originRuleLine) continue } else { domainCheck = strings.ReplaceAll(domainCheck, "*", "x") if !M.IsDomainName(domainCheck) { _, ipErr := parseADGuardIPCIDRLine(ruleLine) if ipErr == nil { ignoredLines++ logger.Debug("ignored unsupported rule with IPCIDR: ", originRuleLine) continue } if M.ParseSocksaddr(domainCheck).Port != 0 { logger.Debug("ignored unsupported rule with port: ", originRuleLine) } else { logger.Debug("ignored unsupported rule with invalid domain: ", originRuleLine) } ignoredLines++ continue } } } ruleLines = append(ruleLines, agdguardRuleLine{ ruleLine: ruleLine, isExclude: isExclude, isSuffix: isSuffix, hasStart: hasStart, hasEnd: hasEnd, isRegexp: isRegexp, isImportant: isImportant, }) } if len(ruleLines) == 0 { return nil, E.New("AdGuard rule-set is empty or all rules are unsupported") } if common.All(ruleLines, func(it agdguardRuleLine) bool { return it.isRawDomain }) { return []option.HeadlessRule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultHeadlessRule{ Domain: common.Map(ruleLines, func(it agdguardRuleLine) string { return it.ruleLine }), }, }, }, nil } mapDomain := func(it agdguardRuleLine) string { ruleLine := it.ruleLine if it.isSuffix { ruleLine = "||" + ruleLine } else if it.hasStart { ruleLine = "|" + ruleLine } if it.hasEnd { ruleLine += "^" } return ruleLine } importantDomain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && !it.isRegexp && !it.isExclude }), mapDomain) importantDomainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && it.isRegexp && !it.isExclude }), mapDomain) importantExcludeDomain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && !it.isRegexp && it.isExclude }), mapDomain) importantExcludeDomainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && it.isRegexp && it.isExclude }), mapDomain) domain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && !it.isRegexp && !it.isExclude }), mapDomain) domainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && it.isRegexp && !it.isExclude }), mapDomain) excludeDomain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && !it.isRegexp && it.isExclude }), mapDomain) excludeDomainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && it.isRegexp && it.isExclude }), mapDomain) currentRule := option.HeadlessRule{ Type: C.RuleTypeDefault, DefaultOptions: option.DefaultHeadlessRule{ AdGuardDomain: domain, DomainRegex: domainRegex, }, } if len(excludeDomain) > 0 || len(excludeDomainRegex) > 0 { currentRule = option.HeadlessRule{ Type: C.RuleTypeLogical, LogicalOptions: option.LogicalHeadlessRule{ Mode: C.LogicalTypeAnd, Rules: []option.HeadlessRule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultHeadlessRule{ AdGuardDomain: excludeDomain, DomainRegex: excludeDomainRegex, Invert: true, }, }, currentRule, }, }, } } if len(importantDomain) > 0 || len(importantDomainRegex) > 0 { currentRule = option.HeadlessRule{ Type: C.RuleTypeLogical, LogicalOptions: option.LogicalHeadlessRule{ Mode: C.LogicalTypeOr, Rules: []option.HeadlessRule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultHeadlessRule{ AdGuardDomain: importantDomain, DomainRegex: importantDomainRegex, }, }, currentRule, }, }, } } if len(importantExcludeDomain) > 0 || len(importantExcludeDomainRegex) > 0 { currentRule = option.HeadlessRule{ Type: C.RuleTypeLogical, LogicalOptions: option.LogicalHeadlessRule{ Mode: C.LogicalTypeAnd, Rules: []option.HeadlessRule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultHeadlessRule{ AdGuardDomain: importantExcludeDomain, DomainRegex: importantExcludeDomainRegex, Invert: true, }, }, currentRule, }, }, } } if ignoredLines > 0 { logger.Info("parsed rules: ", len(ruleLines), "/", len(ruleLines)+ignoredLines) } return []option.HeadlessRule{currentRule}, nil } var ErrInvalid = E.New("invalid binary AdGuard rule-set") func FromOptions(rules []option.HeadlessRule) ([]byte, error) { if len(rules) != 1 { return nil, ErrInvalid } rule := rules[0] var ( importantDomain []string importantDomainRegex []string importantExcludeDomain []string importantExcludeDomainRegex []string domain []string domainRegex []string excludeDomain []string excludeDomainRegex []string ) parse: for { switch rule.Type { case C.RuleTypeLogical: if !(len(rule.LogicalOptions.Rules) == 2 && rule.LogicalOptions.Rules[0].Type == C.RuleTypeDefault) { return nil, ErrInvalid } if rule.LogicalOptions.Mode == C.LogicalTypeAnd && rule.LogicalOptions.Rules[0].DefaultOptions.Invert { if len(importantExcludeDomain) == 0 && len(importantExcludeDomainRegex) == 0 { importantExcludeDomain = rule.LogicalOptions.Rules[0].DefaultOptions.AdGuardDomain importantExcludeDomainRegex = rule.LogicalOptions.Rules[0].DefaultOptions.DomainRegex if len(importantExcludeDomain)+len(importantExcludeDomainRegex) == 0 { return nil, ErrInvalid } } else { excludeDomain = rule.LogicalOptions.Rules[0].DefaultOptions.AdGuardDomain excludeDomainRegex = rule.LogicalOptions.Rules[0].DefaultOptions.DomainRegex if len(excludeDomain)+len(excludeDomainRegex) == 0 { return nil, ErrInvalid } } } else if rule.LogicalOptions.Mode == C.LogicalTypeOr && !rule.LogicalOptions.Rules[0].DefaultOptions.Invert { importantDomain = rule.LogicalOptions.Rules[0].DefaultOptions.AdGuardDomain importantDomainRegex = rule.LogicalOptions.Rules[0].DefaultOptions.DomainRegex if len(importantDomain)+len(importantDomainRegex) == 0 { return nil, ErrInvalid } } else { return nil, ErrInvalid } rule = rule.LogicalOptions.Rules[1] case C.RuleTypeDefault: domain = rule.DefaultOptions.AdGuardDomain domainRegex = rule.DefaultOptions.DomainRegex if len(domain)+len(domainRegex) == 0 { return nil, ErrInvalid } break parse } } var output bytes.Buffer for _, ruleLine := range importantDomain { output.WriteString(ruleLine) output.WriteString("$important\n") } for _, ruleLine := range importantDomainRegex { output.WriteString("/") output.WriteString(ruleLine) output.WriteString("/$important\n") } for _, ruleLine := range importantExcludeDomain { output.WriteString("@@") output.WriteString(ruleLine) output.WriteString("$important\n") } for _, ruleLine := range importantExcludeDomainRegex { output.WriteString("@@/") output.WriteString(ruleLine) output.WriteString("/$important\n") } for _, ruleLine := range domain { output.WriteString(ruleLine) output.WriteString("\n") } for _, ruleLine := range domainRegex { output.WriteString("/") output.WriteString(ruleLine) output.WriteString("/\n") } for _, ruleLine := range excludeDomain { output.WriteString("@@") output.WriteString(ruleLine) output.WriteString("\n") } for _, ruleLine := range excludeDomainRegex { output.WriteString("@@/") output.WriteString(ruleLine) output.WriteString("/\n") } return output.Bytes(), nil } func ignoreIPCIDRRegexp(ruleLine string) bool { if strings.HasPrefix(ruleLine, "(http?:\\/\\/)") { ruleLine = ruleLine[12:] } else if strings.HasPrefix(ruleLine, "(https?:\\/\\/)") { ruleLine = ruleLine[13:] } else if strings.HasPrefix(ruleLine, "^") { ruleLine = ruleLine[1:] } return common.Error(strconv.ParseUint(common.SubstringBefore(ruleLine, "\\."), 10, 8)) == nil || common.Error(strconv.ParseUint(common.SubstringBefore(ruleLine, "."), 10, 8)) == nil } func parseAdGuardHostLine(ruleLine string) (string, error) { before, after, ok := strings.Cut(ruleLine, " ") if !ok { return "", os.ErrInvalid } address, err := netip.ParseAddr(before) if err != nil { return "", err } if !address.IsUnspecified() { return "", nil } domain := after if !M.IsDomainName(domain) { return "", E.New("invalid domain name: ", domain) } return domain, nil } func parseADGuardIPCIDRLine(ruleLine string) (netip.Prefix, error) { var isPrefix bool if strings.HasSuffix(ruleLine, ".") { isPrefix = true ruleLine = ruleLine[:len(ruleLine)-1] } ruleStringParts := strings.Split(ruleLine, ".") if len(ruleStringParts) > 4 || len(ruleStringParts) < 4 && !isPrefix { return netip.Prefix{}, os.ErrInvalid } ruleParts := make([]uint8, 0, len(ruleStringParts)) for _, part := range ruleStringParts { rulePart, err := strconv.ParseUint(part, 10, 8) if err != nil { return netip.Prefix{}, err } ruleParts = append(ruleParts, uint8(rulePart)) } bitLen := len(ruleParts) * 8 for len(ruleParts) < 4 { ruleParts = append(ruleParts, 0) } return netip.PrefixFrom(netip.AddrFrom4([4]byte(ruleParts)), bitLen), nil } ================================================ FILE: common/convertor/adguard/convertor_test.go ================================================ package adguard import ( "context" "strings" "testing" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/route/rule" "github.com/sagernet/sing/common/logger" "github.com/stretchr/testify/require" ) func TestConverter(t *testing.T) { t.Parallel() ruleString := `||sagernet.org^$important @@|sing-box.sagernet.org^$important ||example.org^ |example.com^ example.net^ ||example.edu ||example.edu.tw^ |example.gov example.arpa @@|sagernet.example.org^ ` rules, err := ToOptions(strings.NewReader(ruleString), logger.NOP()) require.NoError(t, err) require.Len(t, rules, 1) rule, err := rule.NewHeadlessRule(context.Background(), rules[0]) require.NoError(t, err) matchDomain := []string{ "example.org", "www.example.org", "example.com", "example.net", "isexample.net", "www.example.net", "example.edu", "example.edu.cn", "example.edu.tw", "www.example.edu", "www.example.edu.cn", "example.gov", "example.gov.cn", "example.arpa", "www.example.arpa", "isexample.arpa", "example.arpa.cn", "www.example.arpa.cn", "isexample.arpa.cn", "sagernet.org", "www.sagernet.org", } notMatchDomain := []string{ "example.org.cn", "notexample.org", "example.com.cn", "www.example.com.cn", "example.net.cn", "notexample.edu", "notexample.edu.cn", "www.example.gov", "notexample.gov", "sagernet.example.org", "sing-box.sagernet.org", } for _, domain := range matchDomain { require.True(t, rule.Match(&adapter.InboundContext{ Domain: domain, }), domain) } for _, domain := range notMatchDomain { require.False(t, rule.Match(&adapter.InboundContext{ Domain: domain, }), domain) } ruleFromOptions, err := FromOptions(rules) require.NoError(t, err) require.Equal(t, ruleString, string(ruleFromOptions)) } func TestHosts(t *testing.T) { t.Parallel() rules, err := ToOptions(strings.NewReader(` 127.0.0.1 localhost ::1 localhost #[IPv6] 0.0.0.0 google.com `), logger.NOP()) require.NoError(t, err) require.Len(t, rules, 1) rule, err := rule.NewHeadlessRule(context.Background(), rules[0]) require.NoError(t, err) matchDomain := []string{ "google.com", } notMatchDomain := []string{ "www.google.com", "notgoogle.com", "localhost", } for _, domain := range matchDomain { require.True(t, rule.Match(&adapter.InboundContext{ Domain: domain, }), domain) } for _, domain := range notMatchDomain { require.False(t, rule.Match(&adapter.InboundContext{ Domain: domain, }), domain) } } func TestSimpleHosts(t *testing.T) { t.Parallel() rules, err := ToOptions(strings.NewReader(` example.com www.example.org `), logger.NOP()) require.NoError(t, err) require.Len(t, rules, 1) rule, err := rule.NewHeadlessRule(context.Background(), rules[0]) require.NoError(t, err) matchDomain := []string{ "example.com", "www.example.org", } notMatchDomain := []string{ "example.com.cn", "www.example.com", "notexample.com", "example.org", } for _, domain := range matchDomain { require.True(t, rule.Match(&adapter.InboundContext{ Domain: domain, }), domain) } for _, domain := range notMatchDomain { require.False(t, rule.Match(&adapter.InboundContext{ Domain: domain, }), domain) } } ================================================ FILE: common/dialer/default.go ================================================ package dialer import ( "context" "errors" "net" "net/netip" "syscall" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/listener" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/control" 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/database64128/tfo-go/v2" ) var ( _ ParallelInterfaceDialer = (*DefaultDialer)(nil) _ WireGuardListener = (*DefaultDialer)(nil) ) type DefaultDialer struct { dialer4 tfo.Dialer dialer6 tfo.Dialer udpDialer4 net.Dialer udpDialer6 net.Dialer udpListener net.ListenConfig udpAddr4 string udpAddr6 string netns string connectionManager adapter.ConnectionManager networkManager adapter.NetworkManager networkStrategy *C.NetworkStrategy defaultNetworkStrategy bool networkType []C.InterfaceType fallbackNetworkType []C.InterfaceType networkFallbackDelay time.Duration networkLastFallback common.TypedValue[time.Time] } func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDialer, error) { connectionManager := service.FromContext[adapter.ConnectionManager](ctx) networkManager := service.FromContext[adapter.NetworkManager](ctx) platformInterface := service.FromContext[adapter.PlatformInterface](ctx) var ( dialer net.Dialer listener net.ListenConfig interfaceFinder control.InterfaceFinder networkStrategy *C.NetworkStrategy defaultNetworkStrategy bool networkType []C.InterfaceType fallbackNetworkType []C.InterfaceType networkFallbackDelay time.Duration ) if networkManager != nil { interfaceFinder = networkManager.InterfaceFinder() } else { interfaceFinder = control.NewDefaultInterfaceFinder() } if options.BindInterface != "" { if !(C.IsLinux || C.IsDarwin || C.IsWindows) { return nil, E.New("`bind_interface` is only supported on Linux, macOS and Windows") } bindFunc := control.BindToInterface(interfaceFinder, options.BindInterface, -1) dialer.Control = control.Append(dialer.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc) } if options.RoutingMark > 0 { if !C.IsLinux { return nil, E.New("`routing_mark` is only supported on Linux") } dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false)) listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false)) } disableDefaultBind := options.BindInterface != "" || options.Inet4BindAddress != nil || options.Inet6BindAddress != nil if disableDefaultBind || options.TCPFastOpen { if options.NetworkStrategy != nil || len(options.NetworkType) > 0 && options.FallbackNetworkType == nil && options.FallbackDelay == 0 { return nil, E.New("`network_strategy` is conflict with `bind_interface`, `inet4_bind_address`, `inet6_bind_address` and `tcp_fast_open`") } } if networkManager != nil { defaultOptions := networkManager.DefaultOptions() if defaultOptions.BindInterface != "" && !disableDefaultBind { bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1) dialer.Control = control.Append(dialer.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc) } else if networkManager.AutoDetectInterface() && !disableDefaultBind { if platformInterface != nil { networkStrategy = (*C.NetworkStrategy)(options.NetworkStrategy) networkType = common.Map(options.NetworkType, option.InterfaceType.Build) fallbackNetworkType = common.Map(options.FallbackNetworkType, option.InterfaceType.Build) if networkStrategy == nil && len(networkType) == 0 && len(fallbackNetworkType) == 0 { networkStrategy = defaultOptions.NetworkStrategy networkType = defaultOptions.NetworkType fallbackNetworkType = defaultOptions.FallbackNetworkType } networkFallbackDelay = time.Duration(options.FallbackDelay) if networkFallbackDelay == 0 && defaultOptions.FallbackDelay != 0 { networkFallbackDelay = defaultOptions.FallbackDelay } if networkStrategy == nil { networkStrategy = common.Ptr(C.NetworkStrategyDefault) defaultNetworkStrategy = true } bindFunc := networkManager.ProtectFunc() dialer.Control = control.Append(dialer.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc) } else { bindFunc := networkManager.AutoDetectInterfaceFunc() dialer.Control = control.Append(dialer.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc) } } if options.RoutingMark == 0 && defaultOptions.RoutingMark != 0 { dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true)) listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true)) } } if networkManager != nil { markFunc := networkManager.AutoRedirectOutputMarkFunc() dialer.Control = control.Append(dialer.Control, markFunc) listener.Control = control.Append(listener.Control, markFunc) } if options.ReuseAddr { listener.Control = control.Append(listener.Control, control.ReuseAddr()) } if options.ProtectPath != "" { dialer.Control = control.Append(dialer.Control, control.ProtectPath(options.ProtectPath)) listener.Control = control.Append(listener.Control, control.ProtectPath(options.ProtectPath)) } if options.BindAddressNoPort { if !C.IsLinux { return nil, E.New("`bind_address_no_port` is only supported on Linux") } dialer.Control = control.Append(dialer.Control, control.BindAddressNoPort()) } if options.ConnectTimeout != 0 { dialer.Timeout = time.Duration(options.ConnectTimeout) } else { dialer.Timeout = C.TCPConnectTimeout } if options.DisableTCPKeepAlive { dialer.KeepAlive = -1 dialer.KeepAliveConfig.Enable = false } else { keepIdle := time.Duration(options.TCPKeepAlive) if keepIdle == 0 { keepIdle = C.TCPKeepAliveInitial } keepInterval := time.Duration(options.TCPKeepAliveInterval) if keepInterval == 0 { keepInterval = C.TCPKeepAliveInterval } dialer.KeepAliveConfig = net.KeepAliveConfig{ Enable: true, Idle: keepIdle, Interval: keepInterval, } } var udpFragment bool if options.UDPFragment != nil { udpFragment = *options.UDPFragment } else { udpFragment = options.UDPFragmentDefault } if !udpFragment { dialer.Control = control.Append(dialer.Control, control.DisableUDPFragment()) listener.Control = control.Append(listener.Control, control.DisableUDPFragment()) } var ( dialer4 = dialer udpDialer4 = dialer udpAddr4 string ) if options.Inet4BindAddress != nil { bindAddr := options.Inet4BindAddress.Build(netip.IPv4Unspecified()) dialer4.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()} udpDialer4.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()} udpAddr4 = M.SocksaddrFrom(bindAddr, 0).String() } var ( dialer6 = dialer udpDialer6 = dialer udpAddr6 string ) if options.Inet6BindAddress != nil { bindAddr := options.Inet6BindAddress.Build(netip.IPv6Unspecified()) dialer6.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()} udpDialer6.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()} udpAddr6 = M.SocksaddrFrom(bindAddr, 0).String() } if options.TCPMultiPath { dialer4.SetMultipathTCP(true) } tcpDialer4 := tfo.Dialer{Dialer: dialer4, DisableTFO: !options.TCPFastOpen} tcpDialer6 := tfo.Dialer{Dialer: dialer6, DisableTFO: !options.TCPFastOpen} return &DefaultDialer{ dialer4: tcpDialer4, dialer6: tcpDialer6, udpDialer4: udpDialer4, udpDialer6: udpDialer6, udpListener: listener, udpAddr4: udpAddr4, udpAddr6: udpAddr6, netns: options.NetNs, connectionManager: connectionManager, networkManager: networkManager, networkStrategy: networkStrategy, defaultNetworkStrategy: defaultNetworkStrategy, networkType: networkType, fallbackNetworkType: fallbackNetworkType, networkFallbackDelay: networkFallbackDelay, }, nil } func setMarkWrapper(networkManager adapter.NetworkManager, mark uint32, isDefault bool) control.Func { if networkManager == nil { return control.RoutingMark(mark) } return func(network, address string, conn syscall.RawConn) error { if networkManager.AutoRedirectOutputMark() != 0 { if isDefault { return E.New("`route.default_mark` is conflict with `tun.auto_redirect`") } else { return E.New("`routing_mark` is conflict with `tun.auto_redirect`") } } return control.RoutingMark(mark)(network, address, conn) } } func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) { if !address.IsValid() { return nil, E.New("invalid address") } else if address.IsDomain() { return nil, E.New("domain not resolved") } if d.networkStrategy == nil { return d.trackConn(listener.ListenNetworkNamespace[net.Conn](d.netns, func() (net.Conn, error) { switch N.NetworkName(network) { case N.NetworkUDP: if !address.IsIPv6() { return d.udpDialer4.DialContext(ctx, network, address.String()) } else { return d.udpDialer6.DialContext(ctx, network, address.String()) } } if !address.IsIPv6() { return DialSlowContext(&d.dialer4, ctx, network, address) } else { return DialSlowContext(&d.dialer6, ctx, network, address) } })) } else { return d.DialParallelInterface(ctx, network, address, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay) } } func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network string, address M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) { if strategy == nil { strategy = d.networkStrategy } if strategy == nil { return d.DialContext(ctx, network, address) } if len(interfaceType) == 0 { interfaceType = d.networkType } if len(fallbackInterfaceType) == 0 { fallbackInterfaceType = d.fallbackNetworkType } if fallbackDelay == 0 { fallbackDelay = d.networkFallbackDelay } var dialer net.Dialer if N.NetworkName(network) == N.NetworkTCP { dialer = d.dialer4.Dialer } else { dialer = d.udpDialer4 } fastFallback := time.Since(d.networkLastFallback.Load()) < C.TCPTimeout var ( conn net.Conn isPrimary bool err error ) if !fastFallback { conn, isPrimary, err = d.dialParallelInterface(ctx, dialer, network, address.String(), *strategy, interfaceType, fallbackInterfaceType, fallbackDelay) } else { conn, isPrimary, err = d.dialParallelInterfaceFastFallback(ctx, dialer, network, address.String(), *strategy, interfaceType, fallbackInterfaceType, fallbackDelay, d.networkLastFallback.Store) } if err != nil { // bind interface failed on legacy xiaomi systems if d.defaultNetworkStrategy && errors.Is(err, syscall.EPERM) { d.networkStrategy = nil return d.DialContext(ctx, network, address) } else { return nil, err } } if !fastFallback && !isPrimary { d.networkLastFallback.Store(time.Now()) } return d.trackConn(conn, nil) } func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { if d.networkStrategy == nil { return d.trackPacketConn(listener.ListenNetworkNamespace[net.PacketConn](d.netns, func() (net.PacketConn, error) { if destination.IsIPv6() { return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6) } else if destination.IsIPv4() && !destination.Addr.IsUnspecified() { return d.udpListener.ListenPacket(ctx, N.NetworkUDP+"4", d.udpAddr4) } else { return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr4) } })) } else { return d.ListenSerialInterfacePacket(ctx, destination, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay) } } func (d *DefaultDialer) DialerForICMPDestination(destination netip.Addr) net.Dialer { if !destination.Is6() { return d.dialer4.Dialer } else { return d.dialer6.Dialer } } func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) { if strategy == nil { strategy = d.networkStrategy } if strategy == nil { return d.ListenPacket(ctx, destination) } if len(interfaceType) == 0 { interfaceType = d.networkType } if len(fallbackInterfaceType) == 0 { fallbackInterfaceType = d.fallbackNetworkType } if fallbackDelay == 0 { fallbackDelay = d.networkFallbackDelay } network := N.NetworkUDP if destination.IsIPv4() && !destination.Addr.IsUnspecified() { network += "4" } packetConn, err := d.listenSerialInterfacePacket(ctx, d.udpListener, network, "", *strategy, interfaceType, fallbackInterfaceType, fallbackDelay) if err != nil { // bind interface failed on legacy xiaomi systems if d.defaultNetworkStrategy && errors.Is(err, syscall.EPERM) { d.networkStrategy = nil return d.ListenPacket(ctx, destination) } else { return nil, err } } return d.trackPacketConn(packetConn, nil) } func (d *DefaultDialer) WireGuardControl() control.Func { return d.udpListener.Control } func (d *DefaultDialer) trackConn(conn net.Conn, err error) (net.Conn, error) { if d.connectionManager == nil || err != nil { return conn, err } return d.connectionManager.TrackConn(conn), nil } func (d *DefaultDialer) trackPacketConn(conn net.PacketConn, err error) (net.PacketConn, error) { if d.connectionManager == nil || err != nil { return conn, err } return d.connectionManager.TrackPacketConn(conn), nil } ================================================ FILE: common/dialer/default_parallel_interface.go ================================================ package dialer import ( "context" "net" "time" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" N "github.com/sagernet/sing/common/network" ) func (d *DefaultDialer) dialParallelInterface(ctx context.Context, dialer net.Dialer, network string, addr string, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, bool, error) { primaryInterfaces, fallbackInterfaces := selectInterfaces(d.networkManager, strategy, interfaceType, fallbackInterfaceType) if len(primaryInterfaces)+len(fallbackInterfaces) == 0 { return nil, false, E.New("no available network interface") } defaultInterface := d.networkManager.InterfaceMonitor().DefaultInterface() if fallbackDelay == 0 { fallbackDelay = N.DefaultFallbackDelay } returned := make(chan struct{}) defer close(returned) type dialResult struct { net.Conn error primary bool } results := make(chan dialResult) // unbuffered startRacer := func(ctx context.Context, primary bool, iif adapter.NetworkInterface) { perNetDialer := dialer if defaultInterface == nil || iif.Index != defaultInterface.Index { perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index)) } conn, err := perNetDialer.DialContext(ctx, network, addr) if err != nil { select { case results <- dialResult{error: E.Cause(err, "dial ", iif.Name, " (", iif.Index, ")"), primary: primary}: case <-returned: } } else { select { case results <- dialResult{Conn: conn, primary: primary}: case <-returned: conn.Close() } } } primaryCtx, primaryCancel := context.WithCancel(ctx) defer primaryCancel() for _, iif := range primaryInterfaces { go startRacer(primaryCtx, true, iif) } var ( fallbackTimer *time.Timer fallbackChan <-chan time.Time ) if len(fallbackInterfaces) > 0 { fallbackTimer = time.NewTimer(fallbackDelay) defer fallbackTimer.Stop() fallbackChan = fallbackTimer.C } var errors []error for { select { case <-fallbackChan: fallbackCtx, fallbackCancel := context.WithCancel(ctx) defer fallbackCancel() for _, iif := range fallbackInterfaces { go startRacer(fallbackCtx, false, iif) } case res := <-results: if res.error == nil { return res.Conn, res.primary, nil } errors = append(errors, res.error) if len(errors) == len(primaryInterfaces)+len(fallbackInterfaces) { return nil, false, E.Errors(errors...) } if res.primary && fallbackTimer != nil && fallbackTimer.Stop() { fallbackTimer.Reset(0) } } } } func (d *DefaultDialer) dialParallelInterfaceFastFallback(ctx context.Context, dialer net.Dialer, network string, addr string, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration, resetFastFallback func(time.Time)) (net.Conn, bool, error) { primaryInterfaces, fallbackInterfaces := selectInterfaces(d.networkManager, strategy, interfaceType, fallbackInterfaceType) if len(primaryInterfaces)+len(fallbackInterfaces) == 0 { return nil, false, E.New("no available network interface") } defaultInterface := d.networkManager.InterfaceMonitor().DefaultInterface() if fallbackDelay == 0 { fallbackDelay = N.DefaultFallbackDelay } returned := make(chan struct{}) defer close(returned) type dialResult struct { net.Conn error primary bool } startAt := time.Now() results := make(chan dialResult) // unbuffered startRacer := func(ctx context.Context, primary bool, iif adapter.NetworkInterface) { perNetDialer := dialer if defaultInterface == nil || iif.Index != defaultInterface.Index { perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index)) } conn, err := perNetDialer.DialContext(ctx, network, addr) if err != nil { select { case results <- dialResult{error: E.Cause(err, "dial ", iif.Name, " (", iif.Index, ")"), primary: primary}: case <-returned: } } else { select { case results <- dialResult{Conn: conn, primary: primary}: case <-returned: if primary && time.Since(startAt) <= fallbackDelay { resetFastFallback(time.Time{}) } conn.Close() } } } for _, iif := range primaryInterfaces { go startRacer(ctx, true, iif) } fallbackCtx, fallbackCancel := context.WithCancel(ctx) defer fallbackCancel() for _, iif := range fallbackInterfaces { go startRacer(fallbackCtx, false, iif) } var errors []error for res := range results { if res.error == nil { return res.Conn, res.primary, nil } errors = append(errors, res.error) if len(errors) == len(primaryInterfaces)+len(fallbackInterfaces) { return nil, false, E.Errors(errors...) } } return nil, false, E.Errors(errors...) } func (d *DefaultDialer) listenSerialInterfacePacket(ctx context.Context, listener net.ListenConfig, network string, addr string, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) { primaryInterfaces, fallbackInterfaces := selectInterfaces(d.networkManager, strategy, interfaceType, fallbackInterfaceType) if len(primaryInterfaces)+len(fallbackInterfaces) == 0 { return nil, E.New("no available network interface") } defaultInterface := d.networkManager.InterfaceMonitor().DefaultInterface() var errors []error for _, primaryInterface := range primaryInterfaces { perNetListener := listener if defaultInterface == nil || primaryInterface.Index != defaultInterface.Index { perNetListener.Control = control.Append(perNetListener.Control, control.BindToInterface(nil, primaryInterface.Name, primaryInterface.Index)) } conn, err := perNetListener.ListenPacket(ctx, network, addr) if err == nil { return conn, nil } errors = append(errors, E.Cause(err, "listen ", primaryInterface.Name, " (", primaryInterface.Index, ")")) } for _, fallbackInterface := range fallbackInterfaces { perNetListener := listener if defaultInterface == nil || fallbackInterface.Index != defaultInterface.Index { perNetListener.Control = control.Append(perNetListener.Control, control.BindToInterface(nil, fallbackInterface.Name, fallbackInterface.Index)) } conn, err := perNetListener.ListenPacket(ctx, network, addr) if err == nil { return conn, nil } errors = append(errors, E.Cause(err, "listen ", fallbackInterface.Name, " (", fallbackInterface.Index, ")")) } return nil, E.Errors(errors...) } func selectInterfaces(networkManager adapter.NetworkManager, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType) (primaryInterfaces []adapter.NetworkInterface, fallbackInterfaces []adapter.NetworkInterface) { interfaces := networkManager.NetworkInterfaces() myInterface := networkManager.InterfaceMonitor().MyInterface() if myInterface != "" { interfaces = common.Filter(interfaces, func(it adapter.NetworkInterface) bool { return it.Name != myInterface }) } switch strategy { case C.NetworkStrategyDefault: if len(interfaceType) == 0 { defaultIf := networkManager.InterfaceMonitor().DefaultInterface() if defaultIf != nil { for _, iif := range interfaces { if iif.Index == defaultIf.Index { primaryInterfaces = append(primaryInterfaces, iif) } } } else { primaryInterfaces = interfaces } } else { primaryInterfaces = common.Filter(interfaces, func(it adapter.NetworkInterface) bool { return common.Contains(interfaceType, it.Type) }) } case C.NetworkStrategyHybrid: if len(interfaceType) == 0 { primaryInterfaces = interfaces } else { primaryInterfaces = common.Filter(interfaces, func(it adapter.NetworkInterface) bool { return common.Contains(interfaceType, it.Type) }) } case C.NetworkStrategyFallback: if len(interfaceType) == 0 { defaultIf := networkManager.InterfaceMonitor().DefaultInterface() if defaultIf != nil { for _, iif := range interfaces { if iif.Index == defaultIf.Index { primaryInterfaces = append(primaryInterfaces, iif) break } } } else { primaryInterfaces = interfaces } } else { primaryInterfaces = common.Filter(interfaces, func(it adapter.NetworkInterface) bool { return common.Contains(interfaceType, it.Type) }) } if len(fallbackInterfaceType) == 0 { fallbackInterfaces = common.Filter(interfaces, func(it adapter.NetworkInterface) bool { return !common.Any(primaryInterfaces, func(iif adapter.NetworkInterface) bool { return it.Index == iif.Index }) }) } else { fallbackInterfaces = common.Filter(interfaces, func(iif adapter.NetworkInterface) bool { return common.Contains(fallbackInterfaceType, iif.Type) }) } } return primaryInterfaces, fallbackInterfaces } ================================================ FILE: common/dialer/default_parallel_network.go ================================================ package dialer import ( "context" "net" "net/netip" "time" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) func DialSerialNetwork(ctx context.Context, dialer N.Dialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) { if len(destinationAddresses) == 0 { if !destination.IsIP() { panic("invalid usage") } destinationAddresses = []netip.Addr{destination.Addr} } if parallelDialer, isParallel := dialer.(ParallelNetworkDialer); isParallel { return parallelDialer.DialParallelNetwork(ctx, network, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay) } var errors []error if parallelDialer, isParallel := dialer.(ParallelInterfaceDialer); isParallel { for _, address := range destinationAddresses { conn, err := parallelDialer.DialParallelInterface(ctx, network, M.SocksaddrFrom(address, destination.Port), strategy, interfaceType, fallbackInterfaceType, fallbackDelay) if err == nil { return conn, nil } errors = append(errors, err) } } else { for _, address := range destinationAddresses { conn, err := dialer.DialContext(ctx, network, M.SocksaddrFrom(address, destination.Port)) if err == nil { return conn, nil } errors = append(errors, err) } } return nil, E.Errors(errors...) } func DialParallelNetwork(ctx context.Context, dialer ParallelInterfaceDialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, preferIPv6 bool, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) { if len(destinationAddresses) == 0 { if !destination.IsIP() { panic("invalid usage") } destinationAddresses = []netip.Addr{destination.Addr} } if fallbackDelay == 0 { fallbackDelay = N.DefaultFallbackDelay } returned := make(chan struct{}) defer close(returned) addresses4 := common.Filter(destinationAddresses, func(address netip.Addr) bool { return address.Is4() || address.Is4In6() }) addresses6 := common.Filter(destinationAddresses, func(address netip.Addr) bool { return address.Is6() && !address.Is4In6() }) if len(addresses4) == 0 || len(addresses6) == 0 { return DialSerialNetwork(ctx, dialer, network, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay) } var primaries, fallbacks []netip.Addr if preferIPv6 { primaries = addresses6 fallbacks = addresses4 } else { primaries = addresses4 fallbacks = addresses6 } type dialResult struct { net.Conn error primary bool done bool } results := make(chan dialResult) // unbuffered startRacer := func(ctx context.Context, primary bool) { ras := primaries if !primary { ras = fallbacks } c, err := DialSerialNetwork(ctx, dialer, network, destination, ras, strategy, interfaceType, fallbackInterfaceType, fallbackDelay) select { case results <- dialResult{Conn: c, error: err, primary: primary, done: true}: case <-returned: if c != nil { c.Close() } } } var primary, fallback dialResult primaryCtx, primaryCancel := context.WithCancel(ctx) defer primaryCancel() go startRacer(primaryCtx, true) fallbackTimer := time.NewTimer(fallbackDelay) defer fallbackTimer.Stop() for { select { case <-fallbackTimer.C: fallbackCtx, fallbackCancel := context.WithCancel(ctx) defer fallbackCancel() go startRacer(fallbackCtx, false) case res := <-results: if res.error == nil { return res.Conn, nil } if res.primary { primary = res } else { fallback = res } if primary.done && fallback.done { return nil, primary.error } if res.primary && fallbackTimer.Stop() { fallbackTimer.Reset(0) } } } } func ListenSerialNetworkPacket(ctx context.Context, dialer N.Dialer, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) { if len(destinationAddresses) == 0 { if !destination.IsIP() { panic("invalid usage") } destinationAddresses = []netip.Addr{destination.Addr} } if parallelDialer, isParallel := dialer.(ParallelNetworkDialer); isParallel { return parallelDialer.ListenSerialNetworkPacket(ctx, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay) } var errors []error if parallelDialer, isParallel := dialer.(ParallelInterfaceDialer); isParallel { for _, address := range destinationAddresses { conn, err := parallelDialer.ListenSerialInterfacePacket(ctx, M.SocksaddrFrom(address, destination.Port), strategy, interfaceType, fallbackInterfaceType, fallbackDelay) if err == nil { return conn, address, nil } errors = append(errors, err) } } else { for _, address := range destinationAddresses { conn, err := dialer.ListenPacket(ctx, M.SocksaddrFrom(address, destination.Port)) if err == nil { return conn, address, nil } errors = append(errors, err) } } return nil, netip.Addr{}, E.Errors(errors...) } ================================================ FILE: common/dialer/detour.go ================================================ package dialer import ( "context" "net" "sync" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) type DirectDialer interface { IsEmpty() bool } type DetourDialer struct { outboundManager adapter.OutboundManager detour string defaultOutbound bool disableEmptyDirectCheck bool dialer N.Dialer initOnce sync.Once initErr error } func NewDetour(outboundManager adapter.OutboundManager, detour string, disableEmptyDirectCheck bool) N.Dialer { return &DetourDialer{ outboundManager: outboundManager, detour: detour, disableEmptyDirectCheck: disableEmptyDirectCheck, } } 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 { return nil } return common.Error(detourDialer.Dialer()) } func (d *DetourDialer) Dialer() (N.Dialer, error) { d.initOnce.Do(d.init) return d.dialer, d.initErr } func (d *DetourDialer) init() { 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.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") return } } } d.dialer = dialer } func (d *DetourDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { dialer, err := d.Dialer() if err != nil { return nil, err } return dialer.DialContext(ctx, network, destination) } func (d *DetourDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { dialer, err := d.Dialer() if err != nil { return nil, err } return dialer.ListenPacket(ctx, destination) } func (d *DetourDialer) Upstream() any { detour, _ := d.Dialer() return detour } ================================================ FILE: common/dialer/dialer.go ================================================ package dialer import ( "context" "net" "net/netip" "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/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" ) type Options struct { Context context.Context Options option.DialerOptions RemoteIsDomain bool DirectResolver bool ResolverOnDetour bool NewDialer bool DisableEmptyDirectCheck bool DirectOutbound bool DefaultOutbound bool } // TODO: merge with NewWithOptions func New(ctx context.Context, options option.DialerOptions, remoteIsDomain bool) (N.Dialer, error) { return NewWithOptions(Options{ Context: ctx, Options: options, RemoteIsDomain: remoteIsDomain, }) } func NewWithOptions(options Options) (N.Dialer, error) { dialOptions := options.Options var ( 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.DisableEmptyDirectCheck) } 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 && (!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 if networkManager != nil { defaultOptions = networkManager.DefaultOptions() } var ( server string dnsQueryOptions adapter.DNSQueryOptions resolveFallbackDelay time.Duration ) if dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "" { var transport adapter.DNSTransport if !options.DirectResolver { var loaded bool transport, loaded = dnsTransport.Transport(dialOptions.DomainResolver.Server) if !loaded { return nil, E.New("domain resolver not found: " + dialOptions.DomainResolver.Server) } } var strategy C.DomainStrategy if dialOptions.DomainResolver.Strategy != option.DomainStrategy(C.DomainStrategyAsIS) { strategy = C.DomainStrategy(dialOptions.DomainResolver.Strategy) } else if //nolint:staticcheck dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) { //nolint:staticcheck strategy = C.DomainStrategy(dialOptions.DomainStrategy) deprecated.Report(options.Context, deprecated.OptionLegacyDomainStrategyOptions) } server = dialOptions.DomainResolver.Server 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, ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}), } resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay) } else if options.DirectResolver { return nil, E.New("missing domain resolver for domain server address") } else { if defaultOptions.DomainResolver != "" { dnsQueryOptions = defaultOptions.DomainResolveOptions transport, loaded := dnsTransport.Transport(defaultOptions.DomainResolver) if !loaded { return nil, E.New("default domain resolver not found: " + defaultOptions.DomainResolver) } dnsQueryOptions.Transport = transport resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay) } else { transports := dnsTransport.Transports() if len(transports) < 2 { dnsQueryOptions.Transport = dnsTransport.Default() } else if options.NewDialer { return nil, E.New("missing domain resolver for domain server address") } else { deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver) } } if //nolint:staticcheck dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) { //nolint:staticcheck dnsQueryOptions.Strategy = C.DomainStrategy(dialOptions.DomainStrategy) deprecated.Report(options.Context, deprecated.OptionLegacyDomainStrategyOptions) } } dialer = NewResolveDialer( options.Context, dialer, dialOptions.Detour == "" && !dialOptions.TCPFastOpen, server, dnsQueryOptions, resolveFallbackDelay, ) } return dialer, nil } type ParallelInterfaceDialer interface { N.Dialer DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) } type ParallelNetworkDialer interface { DialParallelNetwork(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) ListenSerialNetworkPacket(ctx context.Context, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) } type PacketDialerWithDestination interface { ListenPacketWithDestination(ctx context.Context, destination M.Socksaddr) (net.PacketConn, netip.Addr, error) } ================================================ FILE: common/dialer/resolve.go ================================================ package dialer import ( "context" "net" "sync" "time" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/service" ) var ( _ N.Dialer = (*resolveDialer)(nil) _ ParallelInterfaceDialer = (*resolveParallelNetworkDialer)(nil) ) type ResolveDialer interface { N.Dialer QueryOptions() adapter.DNSQueryOptions } type ParallelInterfaceResolveDialer interface { ParallelInterfaceDialer QueryOptions() adapter.DNSQueryOptions } type resolveDialer struct { transport adapter.DNSTransportManager router adapter.DNSRouter dialer N.Dialer parallel bool server string initOnce sync.Once initErr error queryOptions adapter.DNSQueryOptions fallbackDelay time.Duration } func NewResolveDialer(ctx context.Context, dialer N.Dialer, parallel bool, server string, queryOptions adapter.DNSQueryOptions, fallbackDelay time.Duration) ResolveDialer { if parallelDialer, isParallel := dialer.(ParallelInterfaceDialer); isParallel { return &resolveParallelNetworkDialer{ resolveDialer{ transport: service.FromContext[adapter.DNSTransportManager](ctx), router: service.FromContext[adapter.DNSRouter](ctx), dialer: dialer, parallel: parallel, server: server, queryOptions: queryOptions, fallbackDelay: fallbackDelay, }, parallelDialer, } } return &resolveDialer{ transport: service.FromContext[adapter.DNSTransportManager](ctx), router: service.FromContext[adapter.DNSRouter](ctx), dialer: dialer, parallel: parallel, server: server, queryOptions: queryOptions, fallbackDelay: fallbackDelay, } } type resolveParallelNetworkDialer struct { resolveDialer dialer ParallelInterfaceDialer } func (d *resolveDialer) initialize() error { d.initOnce.Do(d.initServer) return d.initErr } func (d *resolveDialer) initServer() { if d.server == "" { return } transport, loaded := d.transport.Transport(d.server) if !loaded { d.initErr = E.New("domain resolver not found: " + d.server) return } d.queryOptions.Transport = transport } func (d *resolveDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { err := d.initialize() if err != nil { return nil, err } if !destination.IsDomain() { return d.dialer.DialContext(ctx, network, destination) } ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug) addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions) if err != nil { return nil, err } if d.parallel { return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.queryOptions.Strategy == C.DomainStrategyPreferIPv6, d.fallbackDelay) } else { return N.DialSerial(ctx, d.dialer, network, destination, addresses) } } func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { err := d.initialize() if err != nil { return nil, err } if !destination.IsDomain() { return d.dialer.ListenPacket(ctx, destination) } ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug) addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions) if err != nil { return nil, err } conn, destinationAddress, err := N.ListenSerial(ctx, d.dialer, destination, addresses) if err != nil { return nil, err } return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil } func (d *resolveDialer) QueryOptions() adapter.DNSQueryOptions { return d.queryOptions } func (d *resolveDialer) Upstream() any { return d.dialer } func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) { err := d.initialize() if err != nil { return nil, err } if !destination.IsDomain() { return d.dialer.DialContext(ctx, network, destination) } ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug) addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions) if err != nil { return nil, err } if fallbackDelay == 0 { fallbackDelay = d.fallbackDelay } if d.parallel { return DialParallelNetwork(ctx, d.dialer, network, destination, addresses, d.queryOptions.Strategy == C.DomainStrategyPreferIPv6, strategy, interfaceType, fallbackInterfaceType, fallbackDelay) } else { return DialSerialNetwork(ctx, d.dialer, network, destination, addresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay) } } func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) { err := d.initialize() if err != nil { return nil, err } if !destination.IsDomain() { return d.dialer.ListenPacket(ctx, destination) } ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug) addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions) if err != nil { return nil, err } if fallbackDelay == 0 { fallbackDelay = d.fallbackDelay } conn, destinationAddress, err := ListenSerialNetworkPacket(ctx, d.dialer, destination, addresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay) if err != nil { return nil, err } return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil } func (d *resolveParallelNetworkDialer) QueryOptions() adapter.DNSQueryOptions { return d.queryOptions } func (d *resolveParallelNetworkDialer) Upstream() any { return d.dialer } ================================================ FILE: common/dialer/router.go ================================================ package dialer import ( "context" "net" "github.com/sagernet/sing-box/adapter" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/service" ) type DefaultOutboundDialer struct { outbound adapter.OutboundManager } func NewDefaultOutbound(ctx context.Context) N.Dialer { return &DefaultOutboundDialer{ outbound: service.FromContext[adapter.OutboundManager](ctx), } } func (d *DefaultOutboundDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { return d.outbound.Default().DialContext(ctx, network, destination) } func (d *DefaultOutboundDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { return d.outbound.Default().ListenPacket(ctx, destination) } func (d *DefaultOutboundDialer) Upstream() any { return d.outbound.Default() } ================================================ FILE: common/dialer/tfo.go ================================================ package dialer import ( "context" "io" "net" "os" "sync" "sync/atomic" "time" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/bufio" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/database64128/tfo-go/v2" ) type slowOpenConn struct { dialer *tfo.Dialer ctx context.Context network string destination M.Socksaddr conn atomic.Pointer[net.TCPConn] create chan struct{} done chan struct{} access sync.Mutex closeOnce sync.Once err error } func DialSlowContext(dialer *tfo.Dialer, ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { if dialer.DisableTFO || N.NetworkName(network) != N.NetworkTCP { switch N.NetworkName(network) { case N.NetworkTCP, N.NetworkUDP: return dialer.Dialer.DialContext(ctx, network, destination.String()) default: return dialer.Dialer.DialContext(ctx, network, destination.AddrString()) } } return &slowOpenConn{ dialer: dialer, ctx: ctx, network: network, destination: destination, create: make(chan struct{}), done: make(chan struct{}), }, nil } func (c *slowOpenConn) Read(b []byte) (n int, err error) { conn := c.conn.Load() if conn != nil { return conn.Read(b) } select { case <-c.create: if c.err != nil { return 0, c.err } return c.conn.Load().Read(b) case <-c.done: return 0, os.ErrClosed } } func (c *slowOpenConn) Write(b []byte) (n int, err error) { tcpConn := c.conn.Load() if tcpConn != nil { return tcpConn.Write(b) } c.access.Lock() defer c.access.Unlock() select { case <-c.create: if c.err != nil { return 0, c.err } return c.conn.Load().Write(b) case <-c.done: return 0, os.ErrClosed default: } conn, err := c.dialer.DialContext(c.ctx, c.network, c.destination.String(), b) if err != nil { c.err = err } else { c.conn.Store(conn.(*net.TCPConn)) } n = len(b) close(c.create) return } func (c *slowOpenConn) Close() error { c.closeOnce.Do(func() { close(c.done) conn := c.conn.Load() if conn != nil { conn.Close() } }) return nil } func (c *slowOpenConn) LocalAddr() net.Addr { conn := c.conn.Load() if conn == nil { return M.Socksaddr{} } return conn.LocalAddr() } func (c *slowOpenConn) RemoteAddr() net.Addr { conn := c.conn.Load() if conn == nil { return M.Socksaddr{} } return conn.RemoteAddr() } func (c *slowOpenConn) SetDeadline(t time.Time) error { conn := c.conn.Load() if conn == nil { return os.ErrInvalid } return conn.SetDeadline(t) } func (c *slowOpenConn) SetReadDeadline(t time.Time) error { conn := c.conn.Load() if conn == nil { return os.ErrInvalid } return conn.SetReadDeadline(t) } func (c *slowOpenConn) SetWriteDeadline(t time.Time) error { conn := c.conn.Load() if conn == nil { return os.ErrInvalid } return conn.SetWriteDeadline(t) } func (c *slowOpenConn) Upstream() any { return common.PtrOrNil(c.conn.Load()) } func (c *slowOpenConn) ReaderReplaceable() bool { return c.conn.Load() != nil } func (c *slowOpenConn) WriterReplaceable() bool { return c.conn.Load() != nil } func (c *slowOpenConn) LazyHeadroom() bool { return c.conn.Load() == nil } func (c *slowOpenConn) NeedHandshake() bool { return c.conn.Load() == nil } func (c *slowOpenConn) WriteTo(w io.Writer) (n int64, err error) { conn := c.conn.Load() if conn == nil { select { case <-c.create: if c.err != nil { return 0, c.err } case <-c.done: return 0, c.err } } return bufio.Copy(w, c.conn.Load()) } ================================================ FILE: common/dialer/wireguard.go ================================================ package dialer import ( "github.com/sagernet/sing/common/control" ) type WireGuardListener interface { WireGuardControl() control.Func } ================================================ FILE: common/geoip/reader.go ================================================ package geoip import ( "net/netip" E "github.com/sagernet/sing/common/exceptions" "github.com/oschwald/maxminddb-golang" ) type Reader struct { reader *maxminddb.Reader } func Open(path string) (*Reader, []string, error) { database, err := maxminddb.Open(path) if err != nil { return nil, nil, err } if database.Metadata.DatabaseType != "sing-geoip" { database.Close() return nil, nil, E.New("incorrect database type, expected sing-geoip, got ", database.Metadata.DatabaseType) } return &Reader{database}, database.Metadata.Languages, nil } func (r *Reader) Lookup(addr netip.Addr) string { var code string _ = r.reader.Lookup(addr.AsSlice(), &code) if code != "" { return code } return "unknown" } func (r *Reader) Close() error { return r.reader.Close() } ================================================ FILE: common/geosite/compat_test.go ================================================ package geosite import ( "bufio" "bytes" "encoding/binary" "strings" "testing" "github.com/sagernet/sing/common/varbin" "github.com/stretchr/testify/require" ) // Old implementation using varbin reflection-based serialization func oldWriteString(writer varbin.Writer, value string) error { //nolint:staticcheck return varbin.Write(writer, binary.BigEndian, value) } func oldReadString(reader varbin.Reader) (string, error) { //nolint:staticcheck return varbin.ReadValue[string](reader, binary.BigEndian) } func oldReadItem(reader varbin.Reader) (Item, error) { //nolint:staticcheck return varbin.ReadValue[Item](reader, binary.BigEndian) } func TestStringCompat(t *testing.T) { t.Parallel() cases := []struct { name string input string }{ {"empty", ""}, {"single_char", "a"}, {"ascii", "example.com"}, {"utf8", "测试域名.中国"}, {"special_chars", "\x00\xff\n\t"}, {"127_bytes", strings.Repeat("x", 127)}, {"128_bytes", strings.Repeat("x", 128)}, {"16383_bytes", strings.Repeat("x", 16383)}, {"16384_bytes", strings.Repeat("x", 16384)}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() // Old write var oldBuf bytes.Buffer err := oldWriteString(&oldBuf, tc.input) require.NoError(t, err) // New write var newBuf bytes.Buffer err = writeString(&newBuf, tc.input) require.NoError(t, err) // Bytes must match require.Equal(t, oldBuf.Bytes(), newBuf.Bytes(), "mismatch for %q\nold: %x\nnew: %x", tc.name, oldBuf.Bytes(), newBuf.Bytes()) // New write -> old read readBack, err := oldReadString(bufio.NewReader(bytes.NewReader(newBuf.Bytes()))) require.NoError(t, err) require.Equal(t, tc.input, readBack) // Old write -> new read readBack2, err := readString(bufio.NewReader(bytes.NewReader(oldBuf.Bytes()))) require.NoError(t, err) require.Equal(t, tc.input, readBack2) }) } } func TestItemCompat(t *testing.T) { t.Parallel() // Note: varbin.Write has a bug where struct values (not pointers) don't write their fields // because field.CanSet() returns false for non-addressable values. // The old geosite code passed Item values to varbin.Write, which silently wrote nothing. // The new code correctly writes Type + Value using manual serialization. // This test verifies the new serialization format and round-trip correctness. cases := []struct { name string input Item }{ {"domain_empty", Item{Type: RuleTypeDomain, Value: ""}}, {"domain_normal", Item{Type: RuleTypeDomain, Value: "example.com"}}, {"domain_suffix", Item{Type: RuleTypeDomainSuffix, Value: ".example.com"}}, {"domain_keyword", Item{Type: RuleTypeDomainKeyword, Value: "google"}}, {"domain_regex", Item{Type: RuleTypeDomainRegex, Value: `^.*\.example\.com$`}}, {"utf8_domain", Item{Type: RuleTypeDomain, Value: "测试.com"}}, {"long_domain", Item{Type: RuleTypeDomainSuffix, Value: strings.Repeat("a", 200) + ".com"}}, {"128_bytes_value", Item{Type: RuleTypeDomain, Value: strings.Repeat("x", 128)}}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() // New write var newBuf bytes.Buffer err := newBuf.WriteByte(byte(tc.input.Type)) require.NoError(t, err) err = writeString(&newBuf, tc.input.Value) require.NoError(t, err) // Verify format: Type (1 byte) + Value (uvarint len + bytes) require.True(t, len(newBuf.Bytes()) >= 1, "output too short") require.Equal(t, byte(tc.input.Type), newBuf.Bytes()[0], "type byte mismatch") // New write -> old read (varbin can read correctly when given addressable target) readBack, err := oldReadItem(bufio.NewReader(bytes.NewReader(newBuf.Bytes()))) require.NoError(t, err) require.Equal(t, tc.input, readBack) // New write -> new read reader := bufio.NewReader(bytes.NewReader(newBuf.Bytes())) typeByte, err := reader.ReadByte() require.NoError(t, err) value, err := readString(reader) require.NoError(t, err) require.Equal(t, tc.input, Item{Type: ItemType(typeByte), Value: value}) }) } } func TestGeositeWriteReadCompat(t *testing.T) { t.Parallel() cases := []struct { name string input map[string][]Item }{ { "empty_map", map[string][]Item{}, }, { "single_code_empty_items", map[string][]Item{"test": {}}, }, { "single_code_single_item", map[string][]Item{"test": {{Type: RuleTypeDomain, Value: "a.com"}}}, }, { "single_code_multi_items", map[string][]Item{ "test": { {Type: RuleTypeDomain, Value: "a.com"}, {Type: RuleTypeDomainSuffix, Value: ".b.com"}, {Type: RuleTypeDomainKeyword, Value: "keyword"}, {Type: RuleTypeDomainRegex, Value: `^.*$`}, }, }, }, { "multi_code", map[string][]Item{ "cn": {{Type: RuleTypeDomain, Value: "baidu.com"}, {Type: RuleTypeDomainSuffix, Value: ".cn"}}, "us": {{Type: RuleTypeDomain, Value: "google.com"}}, "jp": {{Type: RuleTypeDomainSuffix, Value: ".jp"}}, }, }, { "utf8_values", map[string][]Item{ "test": { {Type: RuleTypeDomain, Value: "测试.中国"}, {Type: RuleTypeDomainSuffix, Value: ".テスト"}, }, }, }, { "large_items", generateLargeItems(1000), }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() // Write using new implementation var buf bytes.Buffer err := Write(&buf, tc.input) require.NoError(t, err) // Read back and verify reader, codes, err := NewReader(bytes.NewReader(buf.Bytes())) require.NoError(t, err) // Verify all codes exist codeSet := make(map[string]bool) for _, code := range codes { codeSet[code] = true } for code := range tc.input { require.True(t, codeSet[code], "missing code: %s", code) } // Verify items match for code, expectedItems := range tc.input { items, err := reader.Read(code) require.NoError(t, err) require.Equal(t, expectedItems, items, "items mismatch for code: %s", code) } }) } } func generateLargeItems(count int) map[string][]Item { items := make([]Item, count) for i := range count { items[i] = Item{ Type: ItemType(i % 4), Value: strings.Repeat("x", i%200) + ".com", } } return map[string][]Item{"large": items} } ================================================ FILE: common/geosite/geosite_test.go ================================================ package geosite_test import ( "bytes" "testing" "github.com/sagernet/sing-box/common/geosite" "github.com/stretchr/testify/require" ) func TestGeosite(t *testing.T) { t.Parallel() var buffer bytes.Buffer err := geosite.Write(&buffer, map[string][]geosite.Item{ "test": { { Type: geosite.RuleTypeDomain, Value: "example.org", }, }, }) require.NoError(t, err) reader, codes, err := geosite.NewReader(bytes.NewReader(buffer.Bytes())) require.NoError(t, err) require.Equal(t, []string{"test"}, codes) items, err := reader.Read("test") require.NoError(t, err) require.Equal(t, []geosite.Item{{ Type: geosite.RuleTypeDomain, Value: "example.org", }}, items) } ================================================ FILE: common/geosite/reader.go ================================================ package geosite import ( "bufio" "encoding/binary" "io" "os" "sync" "sync/atomic" E "github.com/sagernet/sing/common/exceptions" ) type Reader struct { access sync.Mutex reader io.ReadSeeker bufferedReader *bufio.Reader metadataIndex int64 domainIndex map[string]int domainLength map[string]int } func Open(path string) (*Reader, []string, error) { content, err := os.Open(path) if err != nil { return nil, nil, err } reader, codes, err := NewReader(content) if err != nil { content.Close() return nil, nil, err } return reader, codes, nil } func NewReader(readSeeker io.ReadSeeker) (*Reader, []string, error) { reader := &Reader{ reader: readSeeker, } err := reader.readMetadata() if err != nil { return nil, nil, err } codes := make([]string, 0, len(reader.domainIndex)) for code := range reader.domainIndex { codes = append(codes, code) } return reader, codes, nil } func (r *Reader) readMetadata() error { counter := &readCounter{Reader: r.reader} reader := bufio.NewReader(counter) version, err := reader.ReadByte() if err != nil { return err } if version != 0 { return E.New("unknown version") } entryLength, err := binary.ReadUvarint(reader) if err != nil { return err } keys := make([]string, entryLength) domainIndex := make(map[string]int) domainLength := make(map[string]int) for i := 0; i < int(entryLength); i++ { var ( code string codeIndex uint64 codeLength uint64 ) code, err = readString(reader) if err != nil { return err } keys[i] = code codeIndex, err = binary.ReadUvarint(reader) if err != nil { return err } codeLength, err = binary.ReadUvarint(reader) if err != nil { return err } domainIndex[code] = int(codeIndex) domainLength[code] = int(codeLength) } r.domainIndex = domainIndex r.domainLength = domainLength r.metadataIndex = counter.count - int64(reader.Buffered()) r.bufferedReader = reader return nil } func (r *Reader) Read(code string) ([]Item, error) { r.access.Lock() defer r.access.Unlock() index, exists := r.domainIndex[code] if !exists { return nil, E.New("code ", code, " not exists!") } _, err := r.reader.Seek(r.metadataIndex+int64(index), io.SeekStart) if err != nil { return nil, err } r.bufferedReader.Reset(r.reader) itemList := make([]Item, r.domainLength[code]) for i := range itemList { typeByte, err := r.bufferedReader.ReadByte() if err != nil { return nil, err } itemList[i].Type = ItemType(typeByte) itemList[i].Value, err = readString(r.bufferedReader) if err != nil { return nil, err } } return itemList, nil } func (r *Reader) Upstream() any { return r.reader } type readCounter struct { io.Reader count int64 } func (r *readCounter) Read(p []byte) (n int, err error) { n, err = r.Reader.Read(p) if n > 0 { atomic.AddInt64(&r.count, int64(n)) } return } func readString(reader io.ByteReader) (string, error) { length, err := binary.ReadUvarint(reader) if err != nil { return "", err } bytes := make([]byte, length) for i := range bytes { bytes[i], err = reader.ReadByte() if err != nil { return "", err } } return string(bytes), nil } ================================================ FILE: common/geosite/rule.go ================================================ package geosite import "github.com/sagernet/sing-box/option" type ItemType = uint8 const ( RuleTypeDomain ItemType = iota RuleTypeDomainSuffix RuleTypeDomainKeyword RuleTypeDomainRegex ) type Item struct { Type ItemType Value string } func Compile(code []Item) option.DefaultRule { var domainLength int var domainSuffixLength int var domainKeywordLength int var domainRegexLength int for _, item := range code { switch item.Type { case RuleTypeDomain: domainLength++ case RuleTypeDomainSuffix: domainSuffixLength++ case RuleTypeDomainKeyword: domainKeywordLength++ case RuleTypeDomainRegex: domainRegexLength++ } } var codeRule option.DefaultRule if domainLength > 0 { codeRule.Domain = make([]string, 0, domainLength) } if domainSuffixLength > 0 { codeRule.DomainSuffix = make([]string, 0, domainSuffixLength) } if domainKeywordLength > 0 { codeRule.DomainKeyword = make([]string, 0, domainKeywordLength) } if domainRegexLength > 0 { codeRule.DomainRegex = make([]string, 0, domainRegexLength) } for _, item := range code { switch item.Type { case RuleTypeDomain: codeRule.Domain = append(codeRule.Domain, item.Value) case RuleTypeDomainSuffix: codeRule.DomainSuffix = append(codeRule.DomainSuffix, item.Value) case RuleTypeDomainKeyword: codeRule.DomainKeyword = append(codeRule.DomainKeyword, item.Value) case RuleTypeDomainRegex: codeRule.DomainRegex = append(codeRule.DomainRegex, item.Value) } } return codeRule } func Merge(rules []option.DefaultRule) option.DefaultRule { var domainLength int var domainSuffixLength int var domainKeywordLength int var domainRegexLength int for _, subRule := range rules { domainLength += len(subRule.Domain) domainSuffixLength += len(subRule.DomainSuffix) domainKeywordLength += len(subRule.DomainKeyword) domainRegexLength += len(subRule.DomainRegex) } var rule option.DefaultRule if domainLength > 0 { rule.Domain = make([]string, 0, domainLength) } if domainSuffixLength > 0 { rule.DomainSuffix = make([]string, 0, domainSuffixLength) } if domainKeywordLength > 0 { rule.DomainKeyword = make([]string, 0, domainKeywordLength) } if domainRegexLength > 0 { rule.DomainRegex = make([]string, 0, domainRegexLength) } for _, subRule := range rules { if len(subRule.Domain) > 0 { rule.Domain = append(rule.Domain, subRule.Domain...) } if len(subRule.DomainSuffix) > 0 { rule.DomainSuffix = append(rule.DomainSuffix, subRule.DomainSuffix...) } if len(subRule.DomainKeyword) > 0 { rule.DomainKeyword = append(rule.DomainKeyword, subRule.DomainKeyword...) } if len(subRule.DomainRegex) > 0 { rule.DomainRegex = append(rule.DomainRegex, subRule.DomainRegex...) } } return rule } ================================================ FILE: common/geosite/writer.go ================================================ package geosite import ( "bytes" "sort" "github.com/sagernet/sing/common/varbin" ) func Write(writer varbin.Writer, domains map[string][]Item) error { keys := make([]string, 0, len(domains)) for code := range domains { keys = append(keys, code) } sort.Strings(keys) content := &bytes.Buffer{} index := make(map[string]int) for _, code := range keys { index[code] = content.Len() for _, item := range domains[code] { err := content.WriteByte(byte(item.Type)) if err != nil { return err } err = writeString(content, item.Value) if err != nil { return err } } } err := writer.WriteByte(0) if err != nil { return err } _, err = varbin.WriteUvarint(writer, uint64(len(keys))) if err != nil { return err } for _, code := range keys { err = writeString(writer, code) if err != nil { return err } _, err = varbin.WriteUvarint(writer, uint64(index[code])) if err != nil { return err } _, err = varbin.WriteUvarint(writer, uint64(len(domains[code]))) if err != nil { return err } } _, err = writer.Write(content.Bytes()) if err != nil { return err } return nil } func writeString(writer varbin.Writer, value string) error { _, err := varbin.WriteUvarint(writer, uint64(len(value))) if err != nil { return err } _, err = writer.Write([]byte(value)) return err } ================================================ FILE: common/httpclient/apple_transport_darwin.go ================================================ //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/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" "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 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") } 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 anchorOnly bool userAnchors adapter.AppleAnchors store adapter.CertificateStore 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 } releaseConfig := true defer func() { if releaseConfig { sessionConfig.close() } }() bridge, err := newAppleProxyBridge(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) 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, }, 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.ValidateSystemTLSOptions(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, 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...) } } 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 } 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 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()), 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), 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)), } 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)) } ================================================ FILE: common/httpclient/apple_transport_darwin.h ================================================ #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; void *anchors_cf; 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 ); ================================================ FILE: common/httpclient/apple_transport_darwin.m ================================================ #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 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; 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]; } } 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); } ================================================ FILE: common/httpclient/apple_transport_darwin_test.go ================================================ //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" "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" ) 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 } 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]) 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 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)) } }, }, { 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.userAnchors != nil { t.Fatal("unexpected user anchors") } 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 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) { 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.SplitSeq(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 := range appleHTTPRecoveryLoops { 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) err := innerTransport.Close() if 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 := newAppleHTTPTestContext() 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 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() { 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) } } ================================================ FILE: common/httpclient/apple_transport_stub.go ================================================ //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") } ================================================ FILE: common/httpclient/client.go ================================================ 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, DisableEmptyDirectCheck: options.DisableEmptyDirectCheck, 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.TLSEngineGo: 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 } ================================================ FILE: common/httpclient/context.go ================================================ 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 } ================================================ FILE: common/httpclient/helpers.go ================================================ 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" "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) { 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 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 } tlsConfig := baseTLSConfig.Clone() if tlsConfig.ServerName() == "" && destination.IsValid() { tlsConfig.SetServerName(destination.AddrString()) } tlsConfig.SetNextProtos(nextProtos) return tlsConfig.STDConfig() } ================================================ FILE: common/httpclient/helpers_test.go ================================================ 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) } }) } ================================================ FILE: common/httpclient/http1_transport.go ================================================ 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 } ================================================ FILE: common/httpclient/http2_config.go ================================================ 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 } ================================================ FILE: common/httpclient/http2_fallback_transport.go ================================================ package httpclient import ( "context" stdTLS "crypto/tls" "errors" "net" "net/http" "sync" "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 fallbackAccess sync.RWMutex fallbackAuthority map[string]struct{} } func newHTTP2FallbackTransport(rawDialer N.Dialer, baseTLSConfig tls.Config, options option.HTTP2Options) (*http2FallbackTransport, 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, "http/1.1"}, http2.NextProtoTLS) } return &http2FallbackTransport{ 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) } func (t *http2FallbackTransport) roundTrip(request *http.Request, allowHTTP1Fallback bool) (*http.Response, error) { if request.URL.Scheme != "https" || requestRequiresHTTP1(request) { return t.h1Transport.RoundTrip(request) } authority := requestAuthority(request) if t.isH2Fallback(authority) { 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 } t.markH2Fallback(authority) 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 } ================================================ FILE: common/httpclient/http2_fallback_transport_test.go ================================================ 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") } } ================================================ FILE: common/httpclient/http2_transport.go ================================================ 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 } ================================================ FILE: common/httpclient/http3_transport.go ================================================ //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 http3BrokenEntry struct { until time.Time backoff time.Duration } type http3FallbackTransport struct { h3Transport *http3.Transport h2Fallback innerTransport fallbackDelay time.Duration brokenAccess sync.Mutex broken map[string]http3BrokenEntry } 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, broken: make(map[string]http3BrokenEntry), }, 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) { 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(authority) return response, nil } if !errors.Is(err, http3.ErrNoCachedConn) { t.markH3Broken(authority) return t.h2FallbackRoundTrip(cloneRequestForRetry(request)) } if !requestReplayable(request) { response, err = t.h3Transport.RoundTrip(request) if err == nil { t.clearH3Broken(authority) return response, nil } t.markH3Broken(authority) return nil, err } return t.roundTripHTTP3Race(request, authority) } func (t *http3FallbackTransport) roundTripHTTP3Race(request *http.Request, authority string) (*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(authority) } drainRemaining() return raceResult.response, nil } if raceResult.h3 { t.markH3Broken(authority) 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(authority string) bool { if authority == "" { return false } t.brokenAccess.Lock() defer t.brokenAccess.Unlock() 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(authority string) { if authority == "" { return } t.brokenAccess.Lock() delete(t.broken, authority) t.brokenAccess.Unlock() } func (t *http3FallbackTransport) markH3Broken(authority string) { if authority == "" { return } t.brokenAccess.Lock() defer t.brokenAccess.Unlock() entry := t.broken[authority] if entry.backoff == 0 { entry.backoff = 5 * time.Minute } else { entry.backoff *= 2 if entry.backoff > 48*time.Hour { entry.backoff = 48 * time.Hour } } entry.until = time.Now().Add(entry.backoff) t.broken[authority] = entry } ================================================ FILE: common/httpclient/http3_transport_stub.go ================================================ //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") } ================================================ FILE: common/httpclient/http3_transport_test.go ================================================ //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("") } ================================================ FILE: common/httpclient/managed_transport.go ================================================ 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() } ================================================ FILE: common/httpclient/manager.go ================================================ 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 } 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 { m.logger.Error(E.Cause(err, "create default http client")) return nil } m.managedTransports = append(m.managedTransports, transport) m.defaultTransport = &sharedManagedTransport{ managed: transport, shared: &sharedState{}, } } 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 } ================================================ FILE: common/interrupt/conn.go ================================================ package interrupt import ( "net" "github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/x/list" ) /*type GroupedConn interface { MarkAsInternal() } func MarkAsInternal(conn any) { if groupedConn, isGroupConn := common.Cast[GroupedConn](conn); isGroupConn { groupedConn.MarkAsInternal() } }*/ type Conn struct { net.Conn group *Group element *list.Element[*groupConnItem] } /*func (c *Conn) MarkAsInternal() { c.element.Value.internal = true }*/ func (c *Conn) Close() error { c.group.access.Lock() defer c.group.access.Unlock() c.group.connections.Remove(c.element) return c.Conn.Close() } func (c *Conn) ReaderReplaceable() bool { return true } func (c *Conn) WriterReplaceable() bool { return true } func (c *Conn) Upstream() any { return c.Conn } type PacketConn struct { net.PacketConn group *Group element *list.Element[*groupConnItem] } /*func (c *PacketConn) MarkAsInternal() { c.element.Value.internal = true }*/ func (c *PacketConn) Close() error { c.group.access.Lock() defer c.group.access.Unlock() c.group.connections.Remove(c.element) return c.PacketConn.Close() } func (c *PacketConn) ReaderReplaceable() bool { return true } func (c *PacketConn) WriterReplaceable() bool { return true } func (c *PacketConn) Upstream() any { return bufio.NewPacketConn(c.PacketConn) } ================================================ FILE: common/interrupt/context.go ================================================ package interrupt import "context" type contextKeyIsExternalConnection struct{} func ContextWithIsExternalConnection(ctx context.Context) context.Context { return context.WithValue(ctx, contextKeyIsExternalConnection{}, true) } func IsExternalConnectionFromContext(ctx context.Context) bool { return ctx.Value(contextKeyIsExternalConnection{}) != nil } ================================================ FILE: common/interrupt/group.go ================================================ package interrupt import ( "io" "net" "sync" "github.com/sagernet/sing/common/x/list" ) type Group struct { access sync.Mutex connections list.List[*groupConnItem] } type groupConnItem struct { conn io.Closer isExternal bool } func NewGroup() *Group { return &Group{} } func (g *Group) NewConn(conn net.Conn, isExternal bool) net.Conn { g.access.Lock() defer g.access.Unlock() item := g.connections.PushBack(&groupConnItem{conn, isExternal}) return &Conn{Conn: conn, group: g, element: item} } func (g *Group) NewPacketConn(conn net.PacketConn, isExternal bool) net.PacketConn { g.access.Lock() defer g.access.Unlock() item := g.connections.PushBack(&groupConnItem{conn, isExternal}) return &PacketConn{PacketConn: conn, group: g, element: item} } func (g *Group) Interrupt(interruptExternalConnections bool) { g.access.Lock() defer g.access.Unlock() var toDelete []*list.Element[*groupConnItem] for element := g.connections.Front(); element != nil; element = element.Next() { if !element.Value.isExternal || interruptExternalConnections { element.Value.conn.Close() toDelete = append(toDelete, element) } } for _, element := range toDelete { g.connections.Remove(element) } } ================================================ FILE: common/ja3/LICENSE ================================================ BSD 3-Clause License Copyright (c) 2018, Open Systems AG All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: common/ja3/README.md ================================================ # JA3 mod from: https://github.com/open-ch/ja3 ================================================ FILE: common/ja3/error.go ================================================ // Copyright (c) 2018, Open Systems AG. All rights reserved. // // Use of this source code is governed by a BSD-style license // that can be found in the LICENSE file in the root of the source // tree. package ja3 import "fmt" // Error types const ( LengthErr string = "length check %v failed" ContentTypeErr string = "content type not matching" VersionErr string = "version check %v failed" HandshakeTypeErr string = "handshake type not matching" SNITypeErr string = "SNI type not supported" ) // ParseError can be encountered while parsing a segment type ParseError struct { errType string check int } func (e *ParseError) Error() string { if e.errType == LengthErr || e.errType == VersionErr { return fmt.Sprintf(e.errType, e.check) } return fmt.Sprint(e.errType) } ================================================ FILE: common/ja3/ja3.go ================================================ // Copyright (c) 2018, Open Systems AG. All rights reserved. // // Use of this source code is governed by a BSD-style license // that can be found in the LICENSE file in the root of the source // tree. package ja3 import ( "crypto/md5" "encoding/hex" "golang.org/x/exp/slices" ) type ClientHello struct { Version uint16 CipherSuites []uint16 Extensions []uint16 EllipticCurves []uint16 EllipticCurvePF []uint8 Versions []uint16 SignatureAlgorithms []uint16 ServerName string ja3ByteString []byte ja3Hash string } func (j *ClientHello) Equals(another *ClientHello, ignoreExtensionsSequence bool) bool { if j.Version != another.Version { return false } if !slices.Equal(j.CipherSuites, another.CipherSuites) { return false } if !ignoreExtensionsSequence && !slices.Equal(j.Extensions, another.Extensions) { return false } if ignoreExtensionsSequence && !slices.Equal(j.Extensions, another.sortedExtensions()) { return false } if !slices.Equal(j.EllipticCurves, another.EllipticCurves) { return false } if !slices.Equal(j.EllipticCurvePF, another.EllipticCurvePF) { return false } if !slices.Equal(j.SignatureAlgorithms, another.SignatureAlgorithms) { return false } return true } func (j *ClientHello) sortedExtensions() []uint16 { extensions := make([]uint16, len(j.Extensions)) copy(extensions, j.Extensions) slices.Sort(extensions) return extensions } func Compute(payload []byte) (*ClientHello, error) { ja3 := ClientHello{} err := ja3.parseSegment(payload) return &ja3, err } func (j *ClientHello) String() string { if j.ja3ByteString == nil { j.marshalJA3() } return string(j.ja3ByteString) } func (j *ClientHello) Hash() string { if j.ja3ByteString == nil { j.marshalJA3() } if j.ja3Hash == "" { h := md5.Sum(j.ja3ByteString) j.ja3Hash = hex.EncodeToString(h[:]) } return j.ja3Hash } ================================================ FILE: common/ja3/parser.go ================================================ // Copyright (c) 2018, Open Systems AG. All rights reserved. // // Use of this source code is governed by a BSD-style license // that can be found in the LICENSE file in the root of the source // tree. package ja3 import ( "encoding/binary" "strconv" ) const ( // Constants used for parsing recordLayerHeaderLen int = 5 handshakeHeaderLen int = 6 randomDataLen int = 32 sessionIDHeaderLen int = 1 cipherSuiteHeaderLen int = 2 compressMethodHeaderLen int = 1 extensionsHeaderLen int = 2 extensionHeaderLen int = 4 sniExtensionHeaderLen int = 5 ecExtensionHeaderLen int = 2 ecpfExtensionHeaderLen int = 1 versionExtensionHeaderLen int = 1 signatureAlgorithmsExtensionHeaderLen int = 2 contentType uint8 = 22 handshakeType uint8 = 1 sniExtensionType uint16 = 0 sniNameDNSHostnameType uint8 = 0 ecExtensionType uint16 = 10 ecpfExtensionType uint16 = 11 versionExtensionType uint16 = 43 signatureAlgorithmsExtensionType uint16 = 13 // Versions // The bitmask covers the versions SSL3.0 to TLS1.2 tlsVersionBitmask uint16 = 0xFFFC tls13 uint16 = 0x0304 // GREASE values // The bitmask covers all GREASE values GreaseBitmask uint16 = 0x0F0F // Constants used for marshalling dashByte = byte(45) commaByte = byte(44) ) // parseSegment to populate the corresponding ClientHello object or return an error func (j *ClientHello) parseSegment(segment []byte) error { // Check if we can decode the next fields if len(segment) < recordLayerHeaderLen { return &ParseError{LengthErr, 1} } // Check if we have "Content Type: Handshake (22)" contType := uint8(segment[0]) if contType != contentType { return &ParseError{errType: ContentTypeErr} } // Check if TLS record layer version is supported tlsRecordVersion := uint16(segment[1])<<8 | uint16(segment[2]) if tlsRecordVersion&tlsVersionBitmask != 0x0300 && tlsRecordVersion != tls13 { return &ParseError{VersionErr, 1} } // Check that the Handshake is as long as expected from the length field segmentLen := uint16(segment[3])<<8 | uint16(segment[4]) if len(segment[recordLayerHeaderLen:]) < int(segmentLen) { return &ParseError{LengthErr, 2} } // Keep the Handshake messege, ignore any additional following record types hs := segment[recordLayerHeaderLen : recordLayerHeaderLen+int(segmentLen)] err := j.parseHandshake(hs) return err } // parseHandshake body func (j *ClientHello) parseHandshake(hs []byte) error { // Check if we can decode the next fields if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen { return &ParseError{LengthErr, 3} } // Check if we have "Handshake Type: Client Hello (1)" handshType := uint8(hs[0]) if handshType != handshakeType { return &ParseError{errType: HandshakeTypeErr} } // Check if actual length of handshake matches (this is a great exclusion criterion for false positives, // as these fields have to match the actual length of the rest of the segment) handshakeLen := uint32(hs[1])<<16 | uint32(hs[2])<<8 | uint32(hs[3]) if len(hs[4:]) != int(handshakeLen) { return &ParseError{LengthErr, 4} } // Check if Client Hello version is supported tlsVersion := uint16(hs[4])<<8 | uint16(hs[5]) if tlsVersion&tlsVersionBitmask != 0x0300 && tlsVersion != tls13 { return &ParseError{VersionErr, 2} } j.Version = tlsVersion // Check if we can decode the next fields sessionIDLen := uint8(hs[38]) if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen) { return &ParseError{LengthErr, 5} } // Cipher Suites cs := hs[handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen):] // Check if we can decode the next fields if len(cs) < cipherSuiteHeaderLen { return &ParseError{LengthErr, 6} } csLen := uint16(cs[0])<<8 | uint16(cs[1]) numCiphers := int(csLen / 2) cipherSuites := make([]uint16, 0, numCiphers) // Check if we can decode the next fields if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen { return &ParseError{LengthErr, 7} } for i := range numCiphers { cipherSuite := uint16(cs[2+i<<1])<<8 | uint16(cs[3+i<<1]) cipherSuites = append(cipherSuites, cipherSuite) } j.CipherSuites = cipherSuites // Check if we can decode the next fields compressMethodLen := uint16(cs[cipherSuiteHeaderLen+int(csLen)]) if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen+int(compressMethodLen) { return &ParseError{LengthErr, 8} } // Extensions exs := cs[cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen+int(compressMethodLen):] err := j.parseExtensions(exs) return err } // parseExtensions of the handshake func (j *ClientHello) parseExtensions(exs []byte) error { // Check for no extensions, this fields header is nonexistent if no body is used if len(exs) == 0 { return nil } // Check if we can decode the next fields if len(exs) < extensionsHeaderLen { return &ParseError{LengthErr, 9} } exsLen := uint16(exs[0])<<8 | uint16(exs[1]) exs = exs[extensionsHeaderLen:] // Check if we can decode the next fields if len(exs) < int(exsLen) { return &ParseError{LengthErr, 10} } var sni []byte var extensions, ellipticCurves []uint16 var ellipticCurvePF []uint8 var versions []uint16 var signatureAlgorithms []uint16 for len(exs) > 0 { // Check if we can decode the next fields if len(exs) < extensionHeaderLen { return &ParseError{LengthErr, 11} } exType := uint16(exs[0])<<8 | uint16(exs[1]) exLen := uint16(exs[2])<<8 | uint16(exs[3]) // Ignore any GREASE extensions extensions = append(extensions, exType) // Check if we can decode the next fields if len(exs) < extensionHeaderLen+int(exLen) { return &ParseError{LengthErr, 12} } sex := exs[extensionHeaderLen : extensionHeaderLen+int(exLen)] switch exType { case sniExtensionType: // Extensions: server_name // Check if we can decode the next fields if len(sex) < sniExtensionHeaderLen { return &ParseError{LengthErr, 13} } sniType := uint8(sex[2]) sniLen := uint16(sex[3])<<8 | uint16(sex[4]) sex = sex[sniExtensionHeaderLen:] // Check if we can decode the next fields if len(sex) != int(sniLen) { return &ParseError{LengthErr, 14} } switch sniType { case sniNameDNSHostnameType: sni = sex default: return &ParseError{errType: SNITypeErr} } case ecExtensionType: // Extensions: supported_groups // Check if we can decode the next fields if len(sex) < ecExtensionHeaderLen { return &ParseError{LengthErr, 15} } ecsLen := uint16(sex[0])<<8 | uint16(sex[1]) numCurves := int(ecsLen / 2) ellipticCurves = make([]uint16, 0, numCurves) sex = sex[ecExtensionHeaderLen:] // Check if we can decode the next fields if len(sex) != int(ecsLen) { return &ParseError{LengthErr, 16} } for i := range numCurves { ecType := uint16(sex[i*2])<<8 | uint16(sex[1+i*2]) ellipticCurves = append(ellipticCurves, ecType) } case ecpfExtensionType: // Extensions: ec_point_formats // Check if we can decode the next fields if len(sex) < ecpfExtensionHeaderLen { return &ParseError{LengthErr, 17} } ecpfsLen := uint8(sex[0]) numPF := int(ecpfsLen) ellipticCurvePF = make([]uint8, numPF) sex = sex[ecpfExtensionHeaderLen:] // Check if we can decode the next fields if len(sex) != numPF { return &ParseError{LengthErr, 18} } for i := range numPF { ellipticCurvePF[i] = uint8(sex[i]) } case versionExtensionType: if len(sex) < versionExtensionHeaderLen { return &ParseError{LengthErr, 19} } versionsLen := int(sex[0]) for i := 0; i < versionsLen; i += 2 { versions = append(versions, binary.BigEndian.Uint16(sex[1:][i:])) } case signatureAlgorithmsExtensionType: if len(sex) < signatureAlgorithmsExtensionHeaderLen { return &ParseError{LengthErr, 20} } ssaLen := binary.BigEndian.Uint16(sex) for i := 0; i < int(ssaLen); i += 2 { signatureAlgorithms = append(signatureAlgorithms, binary.BigEndian.Uint16(sex[2:][i:])) } } exs = exs[4+exLen:] } j.ServerName = string(sni) j.Extensions = extensions j.EllipticCurves = ellipticCurves j.EllipticCurvePF = ellipticCurvePF j.Versions = versions j.SignatureAlgorithms = signatureAlgorithms return nil } // marshalJA3 into a byte string func (j *ClientHello) marshalJA3() { // An uint16 can contain numbers with up to 5 digits and an uint8 can contain numbers with up to 3 digits, but we // also need a byte for each separating character, except at the end. byteStringLen := 6*(1+len(j.CipherSuites)+len(j.Extensions)+len(j.EllipticCurves)) + 4*len(j.EllipticCurvePF) - 1 byteString := make([]byte, 0, byteStringLen) // Version byteString = strconv.AppendUint(byteString, uint64(j.Version), 10) byteString = append(byteString, commaByte) // Cipher Suites if len(j.CipherSuites) != 0 { for _, val := range j.CipherSuites { if val&GreaseBitmask != 0x0A0A { continue } byteString = strconv.AppendUint(byteString, uint64(val), 10) byteString = append(byteString, dashByte) } // Replace last dash with a comma byteString[len(byteString)-1] = commaByte } else { byteString = append(byteString, commaByte) } // Extensions if len(j.Extensions) != 0 { for _, val := range j.Extensions { if val&GreaseBitmask != 0x0A0A { continue } byteString = strconv.AppendUint(byteString, uint64(val), 10) byteString = append(byteString, dashByte) } // Replace last dash with a comma byteString[len(byteString)-1] = commaByte } else { byteString = append(byteString, commaByte) } // Elliptic curves if len(j.EllipticCurves) != 0 { for _, val := range j.EllipticCurves { if val&GreaseBitmask != 0x0A0A { continue } byteString = strconv.AppendUint(byteString, uint64(val), 10) byteString = append(byteString, dashByte) } // Replace last dash with a comma byteString[len(byteString)-1] = commaByte } else { byteString = append(byteString, commaByte) } // ECPF if len(j.EllipticCurvePF) != 0 { for _, val := range j.EllipticCurvePF { byteString = strconv.AppendUint(byteString, uint64(val), 10) byteString = append(byteString, dashByte) } // Remove last dash byteString = byteString[:len(byteString)-1] } j.ja3ByteString = byteString } ================================================ FILE: common/ktls/ktls.go ================================================ //go:build linux && go1.25 && badlinkname package ktls import ( "bytes" "context" "crypto/tls" "errors" "io" "net" "os" "syscall" "github.com/sagernet/sing-box/common/badtls" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" N "github.com/sagernet/sing/common/network" aTLS "github.com/sagernet/sing/common/tls" "golang.org/x/sys/unix" ) type Conn struct { aTLS.Conn ctx context.Context logger logger.ContextLogger conn net.Conn rawConn *badtls.RawConn syscallConn syscall.Conn rawSyscallConn syscall.RawConn readWaitOptions N.ReadWaitOptions kernelTx bool kernelRx bool pendingRxSplice bool } func NewConn(ctx context.Context, logger logger.ContextLogger, conn aTLS.Conn, txOffload, rxOffload bool) (aTLS.Conn, error) { err := Load() if err != nil { return nil, err } syscallConn, isSyscallConn := N.CastReader[interface { io.Reader syscall.Conn }](conn.NetConn()) if !isSyscallConn { return nil, os.ErrInvalid } rawSyscallConn, err := syscallConn.SyscallConn() if err != nil { return nil, err } rawConn, err := badtls.NewRawConn(conn) if err != nil { return nil, err } if *rawConn.Vers != tls.VersionTLS13 { return nil, os.ErrInvalid } for rawConn.RawInput.Len() > 0 { err = rawConn.ReadRecord() if err != nil { return nil, err } for rawConn.Hand.Len() > 0 { err = rawConn.HandlePostHandshakeMessage() if err != nil { return nil, E.Cause(err, "handle post-handshake messages") } } } kConn := &Conn{ Conn: conn, ctx: ctx, logger: logger, conn: conn.NetConn(), rawConn: rawConn, syscallConn: syscallConn, rawSyscallConn: rawSyscallConn, } err = kConn.setupKernel(txOffload, rxOffload) if err != nil { return nil, err } return kConn, nil } func (c *Conn) Upstream() any { return c.Conn } func (c *Conn) SyscallConnForRead() syscall.RawConn { if !c.kernelRx { return nil } if !*c.rawConn.IsClient { c.logger.WarnContext(c.ctx, "ktls: RX splice is unavailable on the server size, since it will cause an unknown failure") return nil } c.logger.DebugContext(c.ctx, "ktls: RX splice requested") return c.rawSyscallConn } func (c *Conn) HandleSyscallReadError(inputErr error) ([]byte, error) { if errors.Is(inputErr, unix.EINVAL) { c.pendingRxSplice = true err := c.readRecord() if err != nil { return nil, E.Cause(err, "ktls: handle non-application-data record") } var input bytes.Buffer if c.rawConn.Input.Len() > 0 { _, err = c.rawConn.Input.WriteTo(&input) if err != nil { return nil, err } } return input.Bytes(), nil } else if errors.Is(inputErr, unix.EBADMSG) { return nil, c.rawConn.In.SetErrorLocked(c.sendAlert(alertBadRecordMAC)) } else { return nil, E.Cause(inputErr, "ktls: unexpected errno") } } func (c *Conn) SyscallConnForWrite() syscall.RawConn { if !c.kernelTx { return nil } c.logger.DebugContext(c.ctx, "ktls: TX splice requested") return c.rawSyscallConn } ================================================ FILE: common/ktls/ktls_alert.go ================================================ // Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build linux && go1.25 && badlinkname package ktls import ( "crypto/tls" "net" ) const ( // alert level alertLevelWarning = 1 alertLevelError = 2 ) const ( alertCloseNotify = 0 alertUnexpectedMessage = 10 alertBadRecordMAC = 20 alertDecryptionFailed = 21 alertRecordOverflow = 22 alertDecompressionFailure = 30 alertHandshakeFailure = 40 alertBadCertificate = 42 alertUnsupportedCertificate = 43 alertCertificateRevoked = 44 alertCertificateExpired = 45 alertCertificateUnknown = 46 alertIllegalParameter = 47 alertUnknownCA = 48 alertAccessDenied = 49 alertDecodeError = 50 alertDecryptError = 51 alertExportRestriction = 60 alertProtocolVersion = 70 alertInsufficientSecurity = 71 alertInternalError = 80 alertInappropriateFallback = 86 alertUserCanceled = 90 alertNoRenegotiation = 100 alertMissingExtension = 109 alertUnsupportedExtension = 110 alertCertificateUnobtainable = 111 alertUnrecognizedName = 112 alertBadCertificateStatusResponse = 113 alertBadCertificateHashValue = 114 alertUnknownPSKIdentity = 115 alertCertificateRequired = 116 alertNoApplicationProtocol = 120 alertECHRequired = 121 ) func (c *Conn) sendAlertLocked(err uint8) error { switch err { case alertNoRenegotiation, alertCloseNotify: c.rawConn.Tmp[0] = alertLevelWarning default: c.rawConn.Tmp[0] = alertLevelError } c.rawConn.Tmp[1] = byte(err) _, writeErr := c.writeRecordLocked(recordTypeAlert, c.rawConn.Tmp[0:2]) if err == alertCloseNotify { // closeNotify is a special case in that it isn't an error. return writeErr } return c.rawConn.Out.SetErrorLocked(&net.OpError{Op: "local error", Err: tls.AlertError(err)}) } // sendAlert sends a TLS alert message. func (c *Conn) sendAlert(err uint8) error { c.rawConn.Out.Lock() defer c.rawConn.Out.Unlock() return c.sendAlertLocked(err) } ================================================ FILE: common/ktls/ktls_cipher_suites_linux.go ================================================ // Copyright 2010 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build linux && go1.25 && badlinkname package ktls import ( "crypto/tls" "unsafe" "github.com/sagernet/sing-box/common/badtls" ) type kernelCryptoCipherType uint16 const ( TLS_CIPHER_AES_GCM_128 kernelCryptoCipherType = 51 TLS_CIPHER_AES_GCM_128_IV_SIZE kernelCryptoCipherType = 8 TLS_CIPHER_AES_GCM_128_KEY_SIZE kernelCryptoCipherType = 16 TLS_CIPHER_AES_GCM_128_SALT_SIZE kernelCryptoCipherType = 4 TLS_CIPHER_AES_GCM_128_TAG_SIZE kernelCryptoCipherType = 16 TLS_CIPHER_AES_GCM_128_REC_SEQ_SIZE kernelCryptoCipherType = 8 TLS_CIPHER_AES_GCM_256 kernelCryptoCipherType = 52 TLS_CIPHER_AES_GCM_256_IV_SIZE kernelCryptoCipherType = 8 TLS_CIPHER_AES_GCM_256_KEY_SIZE kernelCryptoCipherType = 32 TLS_CIPHER_AES_GCM_256_SALT_SIZE kernelCryptoCipherType = 4 TLS_CIPHER_AES_GCM_256_TAG_SIZE kernelCryptoCipherType = 16 TLS_CIPHER_AES_GCM_256_REC_SEQ_SIZE kernelCryptoCipherType = 8 TLS_CIPHER_AES_CCM_128 kernelCryptoCipherType = 53 TLS_CIPHER_AES_CCM_128_IV_SIZE kernelCryptoCipherType = 8 TLS_CIPHER_AES_CCM_128_KEY_SIZE kernelCryptoCipherType = 16 TLS_CIPHER_AES_CCM_128_SALT_SIZE kernelCryptoCipherType = 4 TLS_CIPHER_AES_CCM_128_TAG_SIZE kernelCryptoCipherType = 16 TLS_CIPHER_AES_CCM_128_REC_SEQ_SIZE kernelCryptoCipherType = 8 TLS_CIPHER_CHACHA20_POLY1305 kernelCryptoCipherType = 54 TLS_CIPHER_CHACHA20_POLY1305_IV_SIZE kernelCryptoCipherType = 12 TLS_CIPHER_CHACHA20_POLY1305_KEY_SIZE kernelCryptoCipherType = 32 TLS_CIPHER_CHACHA20_POLY1305_SALT_SIZE kernelCryptoCipherType = 0 TLS_CIPHER_CHACHA20_POLY1305_TAG_SIZE kernelCryptoCipherType = 16 TLS_CIPHER_CHACHA20_POLY1305_REC_SEQ_SIZE kernelCryptoCipherType = 8 // TLS_CIPHER_SM4_GCM kernelCryptoCipherType = 55 // TLS_CIPHER_SM4_GCM_IV_SIZE kernelCryptoCipherType = 8 // TLS_CIPHER_SM4_GCM_KEY_SIZE kernelCryptoCipherType = 16 // TLS_CIPHER_SM4_GCM_SALT_SIZE kernelCryptoCipherType = 4 // TLS_CIPHER_SM4_GCM_TAG_SIZE kernelCryptoCipherType = 16 // TLS_CIPHER_SM4_GCM_REC_SEQ_SIZE kernelCryptoCipherType = 8 // TLS_CIPHER_SM4_CCM kernelCryptoCipherType = 56 // TLS_CIPHER_SM4_CCM_IV_SIZE kernelCryptoCipherType = 8 // TLS_CIPHER_SM4_CCM_KEY_SIZE kernelCryptoCipherType = 16 // TLS_CIPHER_SM4_CCM_SALT_SIZE kernelCryptoCipherType = 4 // TLS_CIPHER_SM4_CCM_TAG_SIZE kernelCryptoCipherType = 16 // TLS_CIPHER_SM4_CCM_REC_SEQ_SIZE kernelCryptoCipherType = 8 TLS_CIPHER_ARIA_GCM_128 kernelCryptoCipherType = 57 TLS_CIPHER_ARIA_GCM_128_IV_SIZE kernelCryptoCipherType = 8 TLS_CIPHER_ARIA_GCM_128_KEY_SIZE kernelCryptoCipherType = 16 TLS_CIPHER_ARIA_GCM_128_SALT_SIZE kernelCryptoCipherType = 4 TLS_CIPHER_ARIA_GCM_128_TAG_SIZE kernelCryptoCipherType = 16 TLS_CIPHER_ARIA_GCM_128_REC_SEQ_SIZE kernelCryptoCipherType = 8 TLS_CIPHER_ARIA_GCM_256 kernelCryptoCipherType = 58 TLS_CIPHER_ARIA_GCM_256_IV_SIZE kernelCryptoCipherType = 8 TLS_CIPHER_ARIA_GCM_256_KEY_SIZE kernelCryptoCipherType = 32 TLS_CIPHER_ARIA_GCM_256_SALT_SIZE kernelCryptoCipherType = 4 TLS_CIPHER_ARIA_GCM_256_TAG_SIZE kernelCryptoCipherType = 16 TLS_CIPHER_ARIA_GCM_256_REC_SEQ_SIZE kernelCryptoCipherType = 8 ) type kernelCrypto interface { String() string } type kernelCryptoInfo struct { version uint16 cipher_type kernelCryptoCipherType } var _ kernelCrypto = &kernelCryptoAES128GCM{} type kernelCryptoAES128GCM struct { kernelCryptoInfo iv [TLS_CIPHER_AES_GCM_128_IV_SIZE]byte key [TLS_CIPHER_AES_GCM_128_KEY_SIZE]byte salt [TLS_CIPHER_AES_GCM_128_SALT_SIZE]byte rec_seq [TLS_CIPHER_AES_GCM_128_REC_SEQ_SIZE]byte } func (crypto *kernelCryptoAES128GCM) String() string { crypto.cipher_type = TLS_CIPHER_AES_GCM_128 return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:]) } var _ kernelCrypto = &kernelCryptoAES256GCM{} type kernelCryptoAES256GCM struct { kernelCryptoInfo iv [TLS_CIPHER_AES_GCM_256_IV_SIZE]byte key [TLS_CIPHER_AES_GCM_256_KEY_SIZE]byte salt [TLS_CIPHER_AES_GCM_256_SALT_SIZE]byte rec_seq [TLS_CIPHER_AES_GCM_256_REC_SEQ_SIZE]byte } func (crypto *kernelCryptoAES256GCM) String() string { crypto.cipher_type = TLS_CIPHER_AES_GCM_256 return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:]) } var _ kernelCrypto = &kernelCryptoAES128CCM{} type kernelCryptoAES128CCM struct { kernelCryptoInfo iv [TLS_CIPHER_AES_CCM_128_IV_SIZE]byte key [TLS_CIPHER_AES_CCM_128_KEY_SIZE]byte salt [TLS_CIPHER_AES_CCM_128_SALT_SIZE]byte rec_seq [TLS_CIPHER_AES_CCM_128_REC_SEQ_SIZE]byte } func (crypto *kernelCryptoAES128CCM) String() string { crypto.cipher_type = TLS_CIPHER_AES_CCM_128 return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:]) } var _ kernelCrypto = &kernelCryptoChacha20Poly1035{} type kernelCryptoChacha20Poly1035 struct { kernelCryptoInfo iv [TLS_CIPHER_CHACHA20_POLY1305_IV_SIZE]byte key [TLS_CIPHER_CHACHA20_POLY1305_KEY_SIZE]byte salt [TLS_CIPHER_CHACHA20_POLY1305_SALT_SIZE]byte rec_seq [TLS_CIPHER_CHACHA20_POLY1305_REC_SEQ_SIZE]byte } func (crypto *kernelCryptoChacha20Poly1035) String() string { crypto.cipher_type = TLS_CIPHER_CHACHA20_POLY1305 return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:]) } // var _ kernelCrypto = &kernelCryptoSM4GCM{} // type kernelCryptoSM4GCM struct { // kernelCryptoInfo // iv [TLS_CIPHER_SM4_GCM_IV_SIZE]byte // key [TLS_CIPHER_SM4_GCM_KEY_SIZE]byte // salt [TLS_CIPHER_SM4_GCM_SALT_SIZE]byte // rec_seq [TLS_CIPHER_SM4_GCM_REC_SEQ_SIZE]byte // } // func (crypto *kernelCryptoSM4GCM) String() string { // crypto.cipher_type = TLS_CIPHER_SM4_GCM // return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:]) // } // var _ kernelCrypto = &kernelCryptoSM4CCM{} // type kernelCryptoSM4CCM struct { // kernelCryptoInfo // iv [TLS_CIPHER_SM4_CCM_IV_SIZE]byte // key [TLS_CIPHER_SM4_CCM_KEY_SIZE]byte // salt [TLS_CIPHER_SM4_CCM_SALT_SIZE]byte // rec_seq [TLS_CIPHER_SM4_CCM_REC_SEQ_SIZE]byte // } // func (crypto *kernelCryptoSM4CCM) String() string { // crypto.cipher_type = TLS_CIPHER_SM4_CCM // return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:]) // } var _ kernelCrypto = &kernelCryptoARIA128GCM{} type kernelCryptoARIA128GCM struct { kernelCryptoInfo iv [TLS_CIPHER_ARIA_GCM_128_IV_SIZE]byte key [TLS_CIPHER_ARIA_GCM_128_KEY_SIZE]byte salt [TLS_CIPHER_ARIA_GCM_128_SALT_SIZE]byte rec_seq [TLS_CIPHER_ARIA_GCM_128_REC_SEQ_SIZE]byte } func (crypto *kernelCryptoARIA128GCM) String() string { crypto.cipher_type = TLS_CIPHER_ARIA_GCM_128 return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:]) } var _ kernelCrypto = &kernelCryptoARIA256GCM{} type kernelCryptoARIA256GCM struct { kernelCryptoInfo iv [TLS_CIPHER_ARIA_GCM_256_IV_SIZE]byte key [TLS_CIPHER_ARIA_GCM_256_KEY_SIZE]byte salt [TLS_CIPHER_ARIA_GCM_256_SALT_SIZE]byte rec_seq [TLS_CIPHER_ARIA_GCM_256_REC_SEQ_SIZE]byte } func (crypto *kernelCryptoARIA256GCM) String() string { crypto.cipher_type = TLS_CIPHER_ARIA_GCM_256 return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:]) } func kernelCipher(kernel *Support, hc *badtls.RawHalfConn, cipherSuite uint16, isRX bool) kernelCrypto { if !kernel.TLS { return nil } switch *hc.Version { case tls.VersionTLS12: if isRX && !kernel.TLS_Version13_RX { return nil } case tls.VersionTLS13: if !kernel.TLS_Version13 { return nil } if isRX && !kernel.TLS_Version13_RX { return nil } default: return nil } var key, iv []byte if *hc.Version == tls.VersionTLS13 { key, iv = trafficKey(cipherSuiteTLS13ByID(cipherSuite), *hc.TrafficSecret) /*if isRX { key, iv = trafficKey(cipherSuiteTLS13ByID(cipherSuite), keyLog.RemoteTrafficSecret) } else { key, iv = trafficKey(cipherSuiteTLS13ByID(cipherSuite), keyLog.TrafficSecret) }*/ } else { // csPtr := cipherSuiteByID(cipherSuite) // keysFromMasterSecret(*hc.Version, csPtr, keyLog.Secret, keyLog.Random) return nil } switch cipherSuite { case tls.TLS_AES_128_GCM_SHA256, tls.TLS_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: crypto := new(kernelCryptoAES128GCM) crypto.version = *hc.Version copy(crypto.key[:], key) copy(crypto.iv[:], iv[4:]) copy(crypto.salt[:], iv[:4]) crypto.rec_seq = *hc.Seq return crypto case tls.TLS_AES_256_GCM_SHA384, tls.TLS_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384: if !kernel.TLS_AES_256_GCM { return nil } crypto := new(kernelCryptoAES256GCM) crypto.version = *hc.Version copy(crypto.key[:], key) copy(crypto.iv[:], iv[4:]) copy(crypto.salt[:], iv[:4]) crypto.rec_seq = *hc.Seq return crypto //case tls.TLS_AES_128_CCM_SHA256, tls.TLS_RSA_WITH_AES_128_CCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_CCM_SHA256: // if !kernel.TLS_AES_128_CCM { // return nil // } // // crypto := new(kernelCryptoAES128CCM) // // crypto.version = *hc.Version // copy(crypto.key[:], key) // copy(crypto.iv[:], iv[4:]) // copy(crypto.salt[:], iv[:4]) // crypto.rec_seq = *hc.Seq // // return crypto case tls.TLS_CHACHA20_POLY1305_SHA256, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256: if !kernel.TLS_CHACHA20_POLY1305 { return nil } crypto := new(kernelCryptoChacha20Poly1035) crypto.version = *hc.Version copy(crypto.key[:], key) copy(crypto.iv[:], iv) crypto.rec_seq = *hc.Seq return crypto //case tls.TLS_RSA_WITH_ARIA_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_ARIA_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_ARIA_128_GCM_SHA256: // if !kernel.TLS_ARIA_GCM { // return nil // } // // crypto := new(kernelCryptoARIA128GCM) // // crypto.version = *hc.Version // copy(crypto.key[:], key) // copy(crypto.iv[:], iv[4:]) // copy(crypto.salt[:], iv[:4]) // crypto.rec_seq = *hc.Seq // // return crypto //case tls.TLS_RSA_WITH_ARIA_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_ARIA_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_ARIA_256_GCM_SHA384: // if !kernel.TLS_ARIA_GCM { // return nil // } // // crypto := new(kernelCryptoARIA256GCM) // // crypto.version = *hc.Version // copy(crypto.key[:], key) // copy(crypto.iv[:], iv[4:]) // copy(crypto.salt[:], iv[:4]) // crypto.rec_seq = *hc.Seq // // return crypto default: return nil } } ================================================ FILE: common/ktls/ktls_close.go ================================================ // Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build linux && go1.25 && badlinkname package ktls import ( "fmt" "net" "time" ) func (c *Conn) Close() error { if !c.kernelTx { return c.Conn.Close() } // Interlock with Conn.Write above. var x int32 for { x = c.rawConn.ActiveCall.Load() if x&1 != 0 { return net.ErrClosed } if c.rawConn.ActiveCall.CompareAndSwap(x, x|1) { break } } if x != 0 { // io.Writer and io.Closer should not be used concurrently. // If Close is called while a Write is currently in-flight, // interpret that as a sign that this Close is really just // being used to break the Write and/or clean up resources and // avoid sending the alertCloseNotify, which may block // waiting on handshakeMutex or the c.out mutex. return c.conn.Close() } var alertErr error if c.rawConn.IsHandshakeComplete.Load() { if err := c.closeNotify(); err != nil { alertErr = fmt.Errorf("tls: failed to send closeNotify alert (but connection was closed anyway): %w", err) } } if err := c.conn.Close(); err != nil { return err } return alertErr } func (c *Conn) closeNotify() error { c.rawConn.Out.Lock() defer c.rawConn.Out.Unlock() if !*c.rawConn.CloseNotifySent { // Set a Write Deadline to prevent possibly blocking forever. c.SetWriteDeadline(time.Now().Add(time.Second * 5)) *c.rawConn.CloseNotifyErr = c.sendAlertLocked(alertCloseNotify) *c.rawConn.CloseNotifySent = true // Any subsequent writes will fail. c.SetWriteDeadline(time.Now()) } return *c.rawConn.CloseNotifyErr } ================================================ FILE: common/ktls/ktls_const.go ================================================ // Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build linux && go1.25 && badlinkname package ktls const ( maxPlaintext = 16384 // maximum plaintext payload length maxCiphertext = 16384 + 2048 // maximum ciphertext payload length maxCiphertextTLS13 = 16384 + 256 // maximum ciphertext length in TLS 1.3 recordHeaderLen = 5 // record header length maxHandshake = 65536 // maximum handshake we support (protocol max is 16 MB) maxHandshakeCertificateMsg = 262144 // maximum certificate message size (256 KiB) maxUselessRecords = 16 // maximum number of consecutive non-advancing records ) const ( recordTypeChangeCipherSpec = 20 recordTypeAlert = 21 recordTypeHandshake = 22 recordTypeApplicationData = 23 ) ================================================ FILE: common/ktls/ktls_handshake_messages.go ================================================ // Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build linux && go1.25 && badlinkname package ktls import "golang.org/x/crypto/cryptobyte" // readUint8LengthPrefixed acts like s.ReadUint8LengthPrefixed, but targets a // []byte instead of a cryptobyte.String. func readUint8LengthPrefixed(s *cryptobyte.String, out *[]byte) bool { return s.ReadUint8LengthPrefixed((*cryptobyte.String)(out)) } // readUint16LengthPrefixed acts like s.ReadUint16LengthPrefixed, but targets a // []byte instead of a cryptobyte.String. func readUint16LengthPrefixed(s *cryptobyte.String, out *[]byte) bool { return s.ReadUint16LengthPrefixed((*cryptobyte.String)(out)) } type keyUpdateMsg struct { updateRequested bool } func (m *keyUpdateMsg) marshal() ([]byte, error) { var b cryptobyte.Builder b.AddUint8(typeKeyUpdate) b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) { if m.updateRequested { b.AddUint8(1) } else { b.AddUint8(0) } }) return b.Bytes() } func (m *keyUpdateMsg) unmarshal(data []byte) bool { s := cryptobyte.String(data) var updateRequested uint8 if !s.Skip(4) || // message type and uint24 length field !s.ReadUint8(&updateRequested) || !s.Empty() { return false } switch updateRequested { case 0: m.updateRequested = false case 1: m.updateRequested = true default: return false } return true } // TLS handshake message types. const ( typeHelloRequest uint8 = 0 typeClientHello uint8 = 1 typeServerHello uint8 = 2 typeNewSessionTicket uint8 = 4 typeEndOfEarlyData uint8 = 5 typeEncryptedExtensions uint8 = 8 typeCertificate uint8 = 11 typeServerKeyExchange uint8 = 12 typeCertificateRequest uint8 = 13 typeServerHelloDone uint8 = 14 typeCertificateVerify uint8 = 15 typeClientKeyExchange uint8 = 16 typeFinished uint8 = 20 typeCertificateStatus uint8 = 22 typeKeyUpdate uint8 = 24 typeCompressedCertificate uint8 = 25 typeMessageHash uint8 = 254 // synthetic message ) // TLS extension numbers const ( extensionServerName uint16 = 0 extensionStatusRequest uint16 = 5 extensionSupportedCurves uint16 = 10 // supported_groups in TLS 1.3, see RFC 8446, Section 4.2.7 extensionSupportedPoints uint16 = 11 extensionSignatureAlgorithms uint16 = 13 extensionALPN uint16 = 16 extensionSCT uint16 = 18 extensionPadding uint16 = 21 extensionExtendedMasterSecret uint16 = 23 extensionCompressCertificate uint16 = 27 // compress_certificate in TLS 1.3 extensionSessionTicket uint16 = 35 extensionPreSharedKey uint16 = 41 extensionEarlyData uint16 = 42 extensionSupportedVersions uint16 = 43 extensionCookie uint16 = 44 extensionPSKModes uint16 = 45 extensionCertificateAuthorities uint16 = 47 extensionSignatureAlgorithmsCert uint16 = 50 extensionKeyShare uint16 = 51 extensionQUICTransportParameters uint16 = 57 extensionALPS uint16 = 17513 extensionRenegotiationInfo uint16 = 0xff01 extensionECHOuterExtensions uint16 = 0xfd00 extensionEncryptedClientHello uint16 = 0xfe0d ) type handshakeMessage interface { marshal() ([]byte, error) unmarshal([]byte) bool } type newSessionTicketMsgTLS13 struct { lifetime uint32 ageAdd uint32 nonce []byte label []byte maxEarlyData uint32 } func (m *newSessionTicketMsgTLS13) marshal() ([]byte, error) { var b cryptobyte.Builder b.AddUint8(typeNewSessionTicket) b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) { b.AddUint32(m.lifetime) b.AddUint32(m.ageAdd) b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) { b.AddBytes(m.nonce) }) b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { b.AddBytes(m.label) }) b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { if m.maxEarlyData > 0 { b.AddUint16(extensionEarlyData) b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { b.AddUint32(m.maxEarlyData) }) } }) }) return b.Bytes() } func (m *newSessionTicketMsgTLS13) unmarshal(data []byte) bool { *m = newSessionTicketMsgTLS13{} s := cryptobyte.String(data) var extensions cryptobyte.String if !s.Skip(4) || // message type and uint24 length field !s.ReadUint32(&m.lifetime) || !s.ReadUint32(&m.ageAdd) || !readUint8LengthPrefixed(&s, &m.nonce) || !readUint16LengthPrefixed(&s, &m.label) || !s.ReadUint16LengthPrefixed(&extensions) || !s.Empty() { return false } for !extensions.Empty() { var extension uint16 var extData cryptobyte.String if !extensions.ReadUint16(&extension) || !extensions.ReadUint16LengthPrefixed(&extData) { return false } switch extension { case extensionEarlyData: if !extData.ReadUint32(&m.maxEarlyData) { return false } default: // Ignore unknown extensions. continue } if !extData.Empty() { return false } } return true } ================================================ FILE: common/ktls/ktls_key_update.go ================================================ // Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build linux && go1.25 && badlinkname package ktls import ( "crypto/tls" "errors" "fmt" "io" "os" ) // handlePostHandshakeMessage processes a handshake message arrived after the // handshake is complete. Up to TLS 1.2, it indicates the start of a renegotiation. func (c *Conn) handlePostHandshakeMessage() error { if *c.rawConn.Vers != tls.VersionTLS13 { return errors.New("ktls: kernel does not support TLS 1.2 renegotiation") } msg, err := c.readHandshake(nil) if err != nil { return err } //c.retryCount++ //if c.retryCount > maxUselessRecords { // c.sendAlert(alertUnexpectedMessage) // return c.in.setErrorLocked(errors.New("tls: too many non-advancing records")) //} switch msg := msg.(type) { case *newSessionTicketMsgTLS13: // return errors.New("ktls: received new session ticket") return nil case *keyUpdateMsg: return c.handleKeyUpdate(msg) } // The QUIC layer is supposed to treat an unexpected post-handshake CertificateRequest // as a QUIC-level PROTOCOL_VIOLATION error (RFC 9001, Section 4.4). Returning an // unexpected_message alert here doesn't provide it with enough information to distinguish // this condition from other unexpected messages. This is probably fine. c.sendAlert(alertUnexpectedMessage) return fmt.Errorf("tls: received unexpected handshake message of type %T", msg) } func (c *Conn) handleKeyUpdate(keyUpdate *keyUpdateMsg) error { //if c.quic != nil { // c.sendAlert(alertUnexpectedMessage) // return c.in.setErrorLocked(errors.New("tls: received unexpected key update message")) //} cipherSuite := cipherSuiteTLS13ByID(*c.rawConn.CipherSuite) if cipherSuite == nil { return c.rawConn.In.SetErrorLocked(c.sendAlert(alertInternalError)) } newSecret := nextTrafficSecret(cipherSuite, *c.rawConn.In.TrafficSecret) c.rawConn.In.SetTrafficSecret(cipherSuite, 0 /*tls.QUICEncryptionLevelInitial*/, newSecret) err := c.resetupRX() if err != nil { c.sendAlert(alertInternalError) return c.rawConn.In.SetErrorLocked(fmt.Errorf("ktls: resetupRX failed: %w", err)) } if keyUpdate.updateRequested { c.rawConn.Out.Lock() defer c.rawConn.Out.Unlock() resetup, err := c.resetupTX() if err != nil { c.sendAlertLocked(alertInternalError) return c.rawConn.Out.SetErrorLocked(fmt.Errorf("ktls: resetupTX failed: %w", err)) } msg := &keyUpdateMsg{} msgBytes, err := msg.marshal() if err != nil { return err } _, err = c.writeRecordLocked(recordTypeHandshake, msgBytes) if err != nil { // Surface the error at the next write. c.rawConn.Out.SetErrorLocked(err) return nil } newSecret := nextTrafficSecret(cipherSuite, *c.rawConn.Out.TrafficSecret) c.rawConn.Out.SetTrafficSecret(cipherSuite, 0 /*QUICEncryptionLevelInitial*/, newSecret) err = resetup() if err != nil { return c.rawConn.Out.SetErrorLocked(fmt.Errorf("ktls: resetupTX failed: %w", err)) } } return nil } func (c *Conn) readHandshakeBytes(n int) error { //if c.quic != nil { // return c.quicReadHandshakeBytes(n) //} for c.rawConn.Hand.Len() < n { if err := c.readRecord(); err != nil { return err } } return nil } func (c *Conn) readHandshake(transcript io.Writer) (any, error) { if err := c.readHandshakeBytes(4); err != nil { return nil, err } data := c.rawConn.Hand.Bytes() maxHandshakeSize := maxHandshake // hasVers indicates we're past the first message, forcing someone trying to // make us just allocate a large buffer to at least do the initial part of // the handshake first. //if c.haveVers && data[0] == typeCertificate { // Since certificate messages are likely to be the only messages that // can be larger than maxHandshake, we use a special limit for just // those messages. //maxHandshakeSize = maxHandshakeCertificateMsg //} n := int(data[1])<<16 | int(data[2])<<8 | int(data[3]) if n > maxHandshakeSize { c.sendAlertLocked(alertInternalError) return nil, c.rawConn.In.SetErrorLocked(fmt.Errorf("tls: handshake message of length %d bytes exceeds maximum of %d bytes", n, maxHandshakeSize)) } if err := c.readHandshakeBytes(4 + n); err != nil { return nil, err } data = c.rawConn.Hand.Next(4 + n) return c.unmarshalHandshakeMessage(data, transcript) } func (c *Conn) unmarshalHandshakeMessage(data []byte, transcript io.Writer) (any, error) { var m handshakeMessage switch data[0] { case typeNewSessionTicket: if *c.rawConn.Vers == tls.VersionTLS13 { m = new(newSessionTicketMsgTLS13) } else { return nil, os.ErrInvalid } case typeKeyUpdate: m = new(keyUpdateMsg) default: return nil, c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage)) } // The handshake message unmarshalers // expect to be able to keep references to data, // so pass in a fresh copy that won't be overwritten. data = append([]byte(nil), data...) if !m.unmarshal(data) { return nil, c.rawConn.In.SetErrorLocked(c.sendAlert(alertDecodeError)) } if transcript != nil { transcript.Write(data) } return m, nil } ================================================ FILE: common/ktls/ktls_linux.go ================================================ //go:build linux && go1.25 && badlinkname package ktls import ( "crypto/tls" "errors" "io" "os" "strings" "sync" "syscall" "unsafe" "github.com/sagernet/sing-box/common/badversion" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/shell" "golang.org/x/sys/unix" ) // mod from https://gitlab.com/go-extension/tls const ( TLS_TX = 1 TLS_RX = 2 TLS_TX_ZEROCOPY_RO = 3 // TX zerocopy (only sendfile now) TLS_RX_EXPECT_NO_PAD = 4 // Attempt opportunistic zero-copy, TLS 1.3 only TLS_SET_RECORD_TYPE = 1 TLS_GET_RECORD_TYPE = 2 ) type Support struct { TLS, TLS_RX bool TLS_Version13, TLS_Version13_RX bool TLS_TX_ZEROCOPY bool TLS_RX_NOPADDING bool TLS_AES_256_GCM bool TLS_AES_128_CCM bool TLS_CHACHA20_POLY1305 bool TLS_SM4 bool TLS_ARIA_GCM bool TLS_Version13_KeyUpdate bool } var KernelSupport = sync.OnceValues(func() (*Support, error) { var uname unix.Utsname err := unix.Uname(&uname) if err != nil { return nil, err } kernelVersion := badversion.Parse(strings.Trim(string(uname.Release[:]), "\x00")) if err != nil { return nil, err } var support Support switch { case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 6, Minor: 14}): support.TLS_Version13_KeyUpdate = true fallthrough case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 6, Minor: 1}): support.TLS_ARIA_GCM = true fallthrough case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 6}): support.TLS_Version13_RX = true support.TLS_RX_NOPADDING = true fallthrough case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 5, Minor: 19}): support.TLS_TX_ZEROCOPY = true fallthrough case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 5, Minor: 16}): support.TLS_SM4 = true fallthrough case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 5, Minor: 11}): support.TLS_CHACHA20_POLY1305 = true fallthrough case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 5, Minor: 2}): support.TLS_AES_128_CCM = true fallthrough case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 5, Minor: 1}): support.TLS_AES_256_GCM = true support.TLS_Version13 = true fallthrough case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 4, Minor: 17}): support.TLS_RX = true fallthrough case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 4, Minor: 13}): support.TLS = true } if support.TLS && support.TLS_Version13 { _, err := os.Stat("/sys/module/tls") if err != nil { if os.Getuid() == 0 { output, err := shell.Exec("modprobe", "tls").Read() if err != nil { return nil, E.Extend(E.Cause(err, "modprobe tls"), output) } } else { return nil, E.New("ktls: kernel TLS module not loaded") } } } return &support, nil }) func Load() error { support, err := KernelSupport() if err != nil { return E.Cause(err, "ktls: check availability") } if !support.TLS || !support.TLS_Version13 { return E.New("ktls: kernel does not support TLS 1.3") } return nil } func (c *Conn) setupKernel(txOffload, rxOffload bool) error { if !txOffload && !rxOffload { return os.ErrInvalid } support, err := KernelSupport() if err != nil { return E.Cause(err, "check availability") } if !support.TLS || !support.TLS_Version13 { return E.New("kernel does not support TLS 1.3") } c.rawConn.Out.Lock() defer c.rawConn.Out.Unlock() err = control.Raw(c.rawSyscallConn, func(fd uintptr) error { return syscall.SetsockoptString(int(fd), unix.SOL_TCP, unix.TCP_ULP, "tls") }) if err != nil { return os.NewSyscallError("setsockopt", err) } if txOffload { txCrypto := kernelCipher(support, c.rawConn.Out, *c.rawConn.CipherSuite, false) if txCrypto == nil { return E.New("unsupported cipher suite") } err = control.Raw(c.rawSyscallConn, func(fd uintptr) error { return syscall.SetsockoptString(int(fd), unix.SOL_TLS, TLS_TX, txCrypto.String()) }) if err != nil { return err } if support.TLS_TX_ZEROCOPY { err = control.Raw(c.rawSyscallConn, func(fd uintptr) error { return syscall.SetsockoptInt(int(fd), unix.SOL_TLS, TLS_TX_ZEROCOPY_RO, 1) }) if err != nil { return err } } c.kernelTx = true c.logger.DebugContext(c.ctx, "ktls: kernel TLS TX enabled") } if rxOffload { rxCrypto := kernelCipher(support, c.rawConn.In, *c.rawConn.CipherSuite, true) if rxCrypto == nil { return E.New("unsupported cipher suite") } err = control.Raw(c.rawSyscallConn, func(fd uintptr) error { return syscall.SetsockoptString(int(fd), unix.SOL_TLS, TLS_RX, rxCrypto.String()) }) if err != nil { return err } if *c.rawConn.Vers >= tls.VersionTLS13 && support.TLS_RX_NOPADDING { err = control.Raw(c.rawSyscallConn, func(fd uintptr) error { return syscall.SetsockoptInt(int(fd), unix.SOL_TLS, TLS_RX_EXPECT_NO_PAD, 1) }) if err != nil { return err } } c.kernelRx = true c.logger.DebugContext(c.ctx, "ktls: kernel TLS RX enabled") } return nil } func (c *Conn) resetupTX() (func() error, error) { if !c.kernelTx { return nil, nil } support, err := KernelSupport() if err != nil { return nil, err } if !support.TLS_Version13_KeyUpdate { return nil, errors.New("ktls: kernel does not support rekey") } txCrypto := kernelCipher(support, c.rawConn.Out, *c.rawConn.CipherSuite, false) if txCrypto == nil { return nil, errors.New("ktls: set kernelCipher on unsupported tls session") } return func() error { return control.Raw(c.rawSyscallConn, func(fd uintptr) error { return syscall.SetsockoptString(int(fd), unix.SOL_TLS, TLS_TX, txCrypto.String()) }) }, nil } func (c *Conn) resetupRX() error { if !c.kernelRx { return nil } support, err := KernelSupport() if err != nil { return err } if !support.TLS_Version13_KeyUpdate { return errors.New("ktls: kernel does not support rekey") } rxCrypto := kernelCipher(support, c.rawConn.In, *c.rawConn.CipherSuite, true) if rxCrypto == nil { return errors.New("ktls: set kernelCipher on unsupported tls session") } return control.Raw(c.rawSyscallConn, func(fd uintptr) error { return syscall.SetsockoptString(int(fd), unix.SOL_TLS, TLS_RX, rxCrypto.String()) }) } func (c *Conn) readKernelRecord() (uint8, []byte, error) { if c.rawConn.RawInput.Len() < maxPlaintext { c.rawConn.RawInput.Grow(maxPlaintext - c.rawConn.RawInput.Len()) } data := c.rawConn.RawInput.Bytes()[:maxPlaintext] // cmsg for record type buffer := make([]byte, unix.CmsgSpace(1)) cmsg := (*unix.Cmsghdr)(unsafe.Pointer(&buffer[0])) cmsg.SetLen(unix.CmsgLen(1)) var iov unix.Iovec iov.Base = &data[0] iov.SetLen(len(data)) var msg unix.Msghdr msg.Control = &buffer[0] msg.Controllen = cmsg.Len msg.Iov = &iov msg.Iovlen = 1 var n int var err error er := c.rawSyscallConn.Read(func(fd uintptr) bool { n, err = recvmsg(int(fd), &msg, 0) return err != unix.EAGAIN || c.pendingRxSplice }) if er != nil { return 0, nil, er } switch err { case nil: case syscall.EINVAL, syscall.EAGAIN: return 0, nil, c.rawConn.In.SetErrorLocked(c.sendAlert(alertProtocolVersion)) case syscall.EMSGSIZE: return 0, nil, c.rawConn.In.SetErrorLocked(c.sendAlert(alertRecordOverflow)) case syscall.EBADMSG: return 0, nil, c.rawConn.In.SetErrorLocked(c.sendAlert(alertDecryptError)) default: return 0, nil, err } if n <= 0 { return 0, nil, c.rawConn.In.SetErrorLocked(io.EOF) } if cmsg.Level == unix.SOL_TLS && cmsg.Type == TLS_GET_RECORD_TYPE { typ := buffer[unix.CmsgLen(0)] return typ, data[:n], nil } return recordTypeApplicationData, data[:n], nil } func (c *Conn) writeKernelRecord(typ uint16, data []byte) (int, error) { if typ == recordTypeApplicationData { return c.conn.Write(data) } // cmsg for record type buffer := make([]byte, unix.CmsgSpace(1)) cmsg := (*unix.Cmsghdr)(unsafe.Pointer(&buffer[0])) cmsg.SetLen(unix.CmsgLen(1)) buffer[unix.CmsgLen(0)] = byte(typ) cmsg.Level = unix.SOL_TLS cmsg.Type = TLS_SET_RECORD_TYPE var iov unix.Iovec iov.Base = &data[0] iov.SetLen(len(data)) var msg unix.Msghdr msg.Control = &buffer[0] msg.Controllen = cmsg.Len msg.Iov = &iov msg.Iovlen = 1 var n int var err error ew := c.rawSyscallConn.Write(func(fd uintptr) bool { n, err = sendmsg(int(fd), &msg, 0) return err != unix.EAGAIN }) if ew != nil { return 0, ew } return n, err } //go:linkname recvmsg golang.org/x/sys/unix.recvmsg func recvmsg(fd int, msg *unix.Msghdr, flags int) (n int, err error) //go:linkname sendmsg golang.org/x/sys/unix.sendmsg func sendmsg(fd int, msg *unix.Msghdr, flags int) (n int, err error) ================================================ FILE: common/ktls/ktls_prf.go ================================================ // Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build linux && go1.25 && badlinkname package ktls import "unsafe" //go:linkname cipherSuiteByID github.com/metacubex/utls.cipherSuiteByID func cipherSuiteByID(id uint16) unsafe.Pointer //go:linkname keysFromMasterSecret github.com/metacubex/utls.keysFromMasterSecret func keysFromMasterSecret(version uint16, suite unsafe.Pointer, masterSecret, clientRandom, serverRandom []byte, macLen, keyLen, ivLen int) (clientMAC, serverMAC, clientKey, serverKey, clientIV, serverIV []byte) //go:linkname cipherSuiteTLS13ByID github.com/metacubex/utls.cipherSuiteTLS13ByID func cipherSuiteTLS13ByID(id uint16) unsafe.Pointer //go:linkname nextTrafficSecret github.com/metacubex/utls.(*cipherSuiteTLS13).nextTrafficSecret func nextTrafficSecret(cs unsafe.Pointer, trafficSecret []byte) []byte //go:linkname trafficKey github.com/metacubex/utls.(*cipherSuiteTLS13).trafficKey func trafficKey(cs unsafe.Pointer, trafficSecret []byte) (key, iv []byte) ================================================ FILE: common/ktls/ktls_read.go ================================================ // Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build linux && go1.25 && badlinkname package ktls import ( "bytes" "crypto/tls" "fmt" "io" "net" "unsafe" ) func (c *Conn) Read(b []byte) (int, error) { if !c.kernelRx { return c.Conn.Read(b) } if len(b) == 0 { // Put this after Handshake, in case people were calling // Read(nil) for the side effect of the Handshake. return 0, nil } c.rawConn.In.Lock() defer c.rawConn.In.Unlock() for c.rawConn.Input.Len() == 0 { if err := c.readRecord(); err != nil { return 0, err } for c.rawConn.Hand.Len() > 0 { if err := c.handlePostHandshakeMessage(); err != nil { return 0, err } } } n, _ := c.rawConn.Input.Read(b) // If a close-notify alert is waiting, read it so that we can return (n, // EOF) instead of (n, nil), to signal to the HTTP response reading // goroutine that the connection is now closed. This eliminates a race // where the HTTP response reading goroutine would otherwise not observe // the EOF until its next read, by which time a client goroutine might // have already tried to reuse the HTTP connection for a new request. // See https://golang.org/cl/76400046 and https://golang.org/issue/3514 if n != 0 && c.rawConn.Input.Len() == 0 && c.rawConn.RawInput.Len() > 0 && c.rawConn.RawInput.Bytes()[0] == recordTypeAlert { if err := c.readRecord(); err != nil { return n, err // will be io.EOF on closeNotify } } return n, nil } func (c *Conn) readRecord() error { if *c.rawConn.In.Err != nil { return *c.rawConn.In.Err } typ, data, err := c.readRawRecord() if err != nil { return err } if len(data) > maxPlaintext { return c.rawConn.In.SetErrorLocked(c.sendAlert(alertRecordOverflow)) } // Application Data messages are always protected. if c.rawConn.In.Cipher == nil && typ == recordTypeApplicationData { return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage)) } //if typ != recordTypeAlert && typ != recordTypeChangeCipherSpec && len(data) > 0 { // This is a state-advancing message: reset the retry count. // c.retryCount = 0 //} // Handshake messages MUST NOT be interleaved with other record types in TLS 1.3. if *c.rawConn.Vers == tls.VersionTLS13 && typ != recordTypeHandshake && c.rawConn.Hand.Len() > 0 { return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage)) } switch typ { default: return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage)) case recordTypeAlert: //if c.quic != nil { // return c.rawConn.In.setErrorLocked(c.sendAlert(alertUnexpectedMessage)) //} if len(data) != 2 { return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage)) } if data[1] == alertCloseNotify { return c.rawConn.In.SetErrorLocked(io.EOF) } if *c.rawConn.Vers == tls.VersionTLS13 { // TLS 1.3 removed warning-level alerts except for alertUserCanceled // (RFC 8446, § 6.1). Since at least one major implementation // (https://bugs.openjdk.org/browse/JDK-8323517) misuses this alert, // many TLS stacks now ignore it outright when seen in a TLS 1.3 // handshake (e.g. BoringSSL, NSS, Rustls). if data[1] == alertUserCanceled { // Like TLS 1.2 alertLevelWarning alerts, we drop the record and retry. return c.retryReadRecord( /*expectChangeCipherSpec*/ ) } return c.rawConn.In.SetErrorLocked(&net.OpError{Op: "remote error", Err: tls.AlertError(data[1])}) } switch data[0] { case alertLevelWarning: // Drop the record on the floor and retry. return c.retryReadRecord( /*expectChangeCipherSpec*/ ) case alertLevelError: return c.rawConn.In.SetErrorLocked(&net.OpError{Op: "remote error", Err: tls.AlertError(data[1])}) default: return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage)) } case recordTypeChangeCipherSpec: if len(data) != 1 || data[0] != 1 { return c.rawConn.In.SetErrorLocked(c.sendAlert(alertDecodeError)) } // Handshake messages are not allowed to fragment across the CCS. if c.rawConn.Hand.Len() > 0 { return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage)) } // In TLS 1.3, change_cipher_spec records are ignored until the // Finished. See RFC 8446, Appendix D.4. Note that according to Section // 5, a server can send a ChangeCipherSpec before its ServerHello, when // c.vers is still unset. That's not useful though and suspicious if the // server then selects a lower protocol version, so don't allow that. if *c.rawConn.Vers == tls.VersionTLS13 { return c.retryReadRecord( /*expectChangeCipherSpec*/ ) } // if !expectChangeCipherSpec { return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage)) //} //if err := c.rawConn.In.changeCipherSpec(); err != nil { // return c.rawConn.In.setErrorLocked(c.sendAlert(err.(alert))) //} case recordTypeApplicationData: // Some OpenSSL servers send empty records in order to randomize the // CBC RawIV. Ignore a limited number of empty records. if len(data) == 0 { return c.retryReadRecord( /*expectChangeCipherSpec*/ ) } // Note that data is owned by c.rawInput, following the Next call above, // to avoid copying the plaintext. This is safe because c.rawInput is // not read from or written to until c.input is drained. c.rawConn.Input.Reset(data) case recordTypeHandshake: if len(data) == 0 { return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage)) } c.rawConn.Hand.Write(data) } return nil } //nolint:staticcheck func (c *Conn) readRawRecord() (typ uint8, data []byte, err error) { // Read from kernel. if c.kernelRx { return c.readKernelRecord() } // Read header, payload. if err = c.readFromUntil(c.conn, recordHeaderLen); err != nil { // RFC 8446, Section 6.1 suggests that EOF without an alertCloseNotify // is an error, but popular web sites seem to do this, so we accept it // if and only if at the record boundary. if err == io.ErrUnexpectedEOF && c.rawConn.RawInput.Len() == 0 { err = io.EOF } if e, ok := err.(net.Error); !ok || !e.Temporary() { c.rawConn.In.SetErrorLocked(err) } return } hdr := c.rawConn.RawInput.Bytes()[:recordHeaderLen] typ = hdr[0] vers := uint16(hdr[1])<<8 | uint16(hdr[2]) expectedVers := *c.rawConn.Vers if expectedVers == tls.VersionTLS13 { // All TLS 1.3 records are expected to have 0x0303 (1.2) after // the initial hello (RFC 8446 Section 5.1). expectedVers = tls.VersionTLS12 } n := int(hdr[3])<<8 | int(hdr[4]) if /*c.haveVers && */ vers != expectedVers { c.sendAlert(alertProtocolVersion) msg := fmt.Sprintf("received record with version %x when expecting version %x", vers, expectedVers) err = c.rawConn.In.SetErrorLocked(c.newRecordHeaderError(nil, msg)) return } //if !c.haveVers { // // First message, be extra suspicious: this might not be a TLS // // client. Bail out before reading a full 'body', if possible. // // The current max version is 3.3 so if the version is >= 16.0, // // it's probably not real. // if (typ != recordTypeAlert && typ != recordTypeHandshake) || vers >= 0x1000 { // err = c.rawConn.In.SetErrorLocked(c.newRecordHeaderError(c.conn, "first record does not look like a TLS handshake")) // return // } //} if *c.rawConn.Vers == tls.VersionTLS13 && n > maxCiphertextTLS13 || n > maxCiphertext { c.sendAlert(alertRecordOverflow) msg := fmt.Sprintf("oversized record received with length %d", n) err = c.rawConn.In.SetErrorLocked(c.newRecordHeaderError(nil, msg)) return } if err = c.readFromUntil(c.conn, recordHeaderLen+n); err != nil { if e, ok := err.(net.Error); !ok || !e.Temporary() { c.rawConn.In.SetErrorLocked(err) } return } // Process message. record := c.rawConn.RawInput.Next(recordHeaderLen + n) data, typ, err = c.rawConn.In.Decrypt(record) if err != nil { err = c.rawConn.In.SetErrorLocked(c.sendAlert(*(*uint8)((*[2]unsafe.Pointer)(unsafe.Pointer(&err))[1]))) return } return } // retryReadRecord recurs into readRecordOrCCS to drop a non-advancing record, like // a warning alert, empty application_data, or a change_cipher_spec in TLS 1.3. func (c *Conn) retryReadRecord( /*expectChangeCipherSpec bool*/ ) error { //c.retryCount++ //if c.retryCount > maxUselessRecords { // c.sendAlert(alertUnexpectedMessage) // return c.in.setErrorLocked(errors.New("tls: too many ignored records")) //} return c.readRecord( /*expectChangeCipherSpec*/ ) } // atLeastReader reads from R, stopping with EOF once at least N bytes have been // read. It is different from an io.LimitedReader in that it doesn't cut short // the last Read call, and in that it considers an early EOF an error. type atLeastReader struct { R io.Reader N int64 } func (r *atLeastReader) Read(p []byte) (int, error) { if r.N <= 0 { return 0, io.EOF } n, err := r.R.Read(p) r.N -= int64(n) // won't underflow unless len(p) >= n > 9223372036854775809 if r.N > 0 && err == io.EOF { return n, io.ErrUnexpectedEOF } if r.N <= 0 && err == nil { return n, io.EOF } return n, err } // readFromUntil reads from r into c.rawConn.RawInput until c.rawConn.RawInput contains // at least n bytes or else returns an error. func (c *Conn) readFromUntil(r io.Reader, n int) error { if c.rawConn.RawInput.Len() >= n { return nil } needs := n - c.rawConn.RawInput.Len() // There might be extra input waiting on the wire. Make a best effort // attempt to fetch it so that it can be used in (*Conn).Read to // "predict" closeNotify alerts. c.rawConn.RawInput.Grow(needs + bytes.MinRead) _, err := c.rawConn.RawInput.ReadFrom(&atLeastReader{r, int64(needs)}) return err } func (c *Conn) newRecordHeaderError(conn net.Conn, msg string) (err tls.RecordHeaderError) { err.Msg = msg err.Conn = conn copy(err.RecordHeader[:], c.rawConn.RawInput.Bytes()) return err } ================================================ FILE: common/ktls/ktls_read_wait.go ================================================ // Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build linux && go1.25 && badlinkname package ktls import ( "github.com/sagernet/sing/common/buf" N "github.com/sagernet/sing/common/network" ) func (c *Conn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { c.readWaitOptions = options return false } func (c *Conn) WaitReadBuffer() (buffer *buf.Buffer, err error) { c.rawConn.In.Lock() defer c.rawConn.In.Unlock() for c.rawConn.Input.Len() == 0 { err = c.readRecord() if err != nil { return } } buffer = c.readWaitOptions.NewBuffer() n, err := c.rawConn.Input.Read(buffer.FreeBytes()) if err != nil { buffer.Release() return } buffer.Truncate(n) if n != 0 && c.rawConn.Input.Len() == 0 && c.rawConn.Input.Len() > 0 && c.rawConn.RawInput.Bytes()[0] == recordTypeAlert { _ = c.rawConn.ReadRecord() } c.readWaitOptions.PostReturn(buffer) return } ================================================ FILE: common/ktls/ktls_stub_nolinkname.go ================================================ //go:build linux && go1.25 && !badlinkname package ktls import ( "context" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" aTLS "github.com/sagernet/sing/common/tls" ) func NewConn(ctx context.Context, logger logger.ContextLogger, conn aTLS.Conn, txOffload, rxOffload bool) (aTLS.Conn, error) { return nil, E.New("kTLS requires build flags `badlinkname` and `-ldflags=-checklinkname=0`, please recompile your binary") } ================================================ FILE: common/ktls/ktls_stub_nonlinux.go ================================================ //go:build !linux package ktls import ( "context" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" aTLS "github.com/sagernet/sing/common/tls" ) func NewConn(ctx context.Context, logger logger.ContextLogger, conn aTLS.Conn, txOffload, rxOffload bool) (aTLS.Conn, error) { return nil, E.New("kTLS is only supported on Linux") } ================================================ FILE: common/ktls/ktls_stub_oldgo.go ================================================ //go:build linux && !go1.25 package ktls import ( "context" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" aTLS "github.com/sagernet/sing/common/tls" ) func NewConn(ctx context.Context, logger logger.ContextLogger, conn aTLS.Conn, txOffload, rxOffload bool) (aTLS.Conn, error) { return nil, E.New("kTLS requires Go 1.25 or later, please recompile your binary") } ================================================ FILE: common/ktls/ktls_write.go ================================================ // Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build linux && go1.25 && badlinkname package ktls import ( "crypto/cipher" "crypto/tls" "errors" "net" ) func (c *Conn) Write(b []byte) (int, error) { if !c.kernelTx { return c.Conn.Write(b) } // interlock with Close below for { x := c.rawConn.ActiveCall.Load() if x&1 != 0 { return 0, net.ErrClosed } if c.rawConn.ActiveCall.CompareAndSwap(x, x+2) { break } } defer c.rawConn.ActiveCall.Add(-2) //if err := c.Conn.HandshakeContext(context.Background()); err != nil { // return 0, err //} c.rawConn.Out.Lock() defer c.rawConn.Out.Unlock() if err := *c.rawConn.Out.Err; err != nil { return 0, err } if !c.rawConn.IsHandshakeComplete.Load() { return 0, tls.AlertError(alertInternalError) } if *c.rawConn.CloseNotifySent { // return 0, errShutdown return 0, errors.New("tls: protocol is shutdown") } // TLS 1.0 is susceptible to a chosen-plaintext // attack when using block mode ciphers due to predictable IVs. // This can be prevented by splitting each Application Data // record into two records, effectively randomizing the RawIV. // // https://www.openssl.org/~bodo/tls-cbc.txt // https://bugzilla.mozilla.org/show_bug.cgi?id=665814 // https://www.imperialviolet.org/2012/01/15/beastfollowup.html var m int if len(b) > 1 && *c.rawConn.Vers == tls.VersionTLS10 { if _, ok := (*c.rawConn.Out.Cipher).(cipher.BlockMode); ok { n, err := c.writeRecordLocked(recordTypeApplicationData, b[:1]) if err != nil { return n, c.rawConn.Out.SetErrorLocked(err) } m, b = 1, b[1:] } } n, err := c.writeRecordLocked(recordTypeApplicationData, b) return n + m, c.rawConn.Out.SetErrorLocked(err) } func (c *Conn) writeRecordLocked(typ uint16, data []byte) (n int, err error) { if !c.kernelTx { return c.rawConn.WriteRecordLocked(typ, data) } return c.writeKernelRecord(typ, data) } ================================================ FILE: common/listener/listener.go ================================================ package listener import ( "context" "net" "net/netip" "runtime" "strings" "sync/atomic" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/settings" "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/vishvananda/netns" ) type Listener struct { ctx context.Context logger logger.ContextLogger network []string listenOptions option.ListenOptions connHandler adapter.ConnectionHandler packetHandler adapter.PacketHandler oobPacketHandler adapter.OOBPacketHandler threadUnsafePacketWriter bool disablePacketOutput bool setSystemProxy bool systemProxySOCKS bool tproxy bool tcpListener net.Listener systemProxy settings.SystemProxy udpConn *net.UDPConn udpAddr M.Socksaddr packetOutbound chan *N.PacketBuffer packetOutboundClosed chan struct{} shutdown atomic.Bool } type Options struct { Context context.Context Logger logger.ContextLogger Network []string Listen option.ListenOptions ConnectionHandler adapter.ConnectionHandler PacketHandler adapter.PacketHandler OOBPacketHandler adapter.OOBPacketHandler ThreadUnsafePacketWriter bool DisablePacketOutput bool SetSystemProxy bool SystemProxySOCKS bool TProxy bool } func New( options Options, ) *Listener { return &Listener{ ctx: options.Context, logger: options.Logger, network: options.Network, listenOptions: options.Listen, connHandler: options.ConnectionHandler, packetHandler: options.PacketHandler, oobPacketHandler: options.OOBPacketHandler, threadUnsafePacketWriter: options.ThreadUnsafePacketWriter, disablePacketOutput: options.DisablePacketOutput, setSystemProxy: options.SetSystemProxy, systemProxySOCKS: options.SystemProxySOCKS, tproxy: options.TProxy, } } func (l *Listener) Start() error { if common.Contains(l.network, N.NetworkTCP) { _, err := l.ListenTCP() if err != nil { return err } go l.loopTCPIn() } if common.Contains(l.network, N.NetworkUDP) { _, err := l.ListenUDP() if err != nil { return err } l.packetOutboundClosed = make(chan struct{}) l.packetOutbound = make(chan *N.PacketBuffer, 64) go l.loopUDPIn() if !l.disablePacketOutput { go l.loopUDPOut() } } if l.setSystemProxy { listenPort := M.SocksaddrFromNet(l.tcpListener.Addr()).Port var listenAddrString string listenAddr := l.listenOptions.Listen.Build(netip.IPv4Unspecified()) if listenAddr.IsUnspecified() { listenAddrString = "127.0.0.1" } else { listenAddrString = listenAddr.String() } systemProxy, err := settings.NewSystemProxy(l.ctx, M.ParseSocksaddrHostPort(listenAddrString, listenPort), l.systemProxySOCKS) if err != nil { return E.Cause(err, "initialize system proxy") } err = systemProxy.Enable() if err != nil { return E.Cause(err, "set system proxy") } l.systemProxy = systemProxy } return nil } func (l *Listener) Close() error { l.shutdown.Store(true) var err error if l.systemProxy != nil && l.systemProxy.IsEnabled() { err = l.systemProxy.Disable() } return E.Errors(err, common.Close( l.tcpListener, common.PtrOrNil(l.udpConn), )) } func (l *Listener) TCPListener() net.Listener { return l.tcpListener } func (l *Listener) UDPConn() *net.UDPConn { return l.udpConn } func (l *Listener) ListenOptions() option.ListenOptions { return l.listenOptions } func ListenNetworkNamespace[T any](nameOrPath string, block func() (T, error)) (T, error) { if nameOrPath != "" { runtime.LockOSThread() defer runtime.UnlockOSThread() currentNs, err := netns.Get() if err != nil { return common.DefaultValue[T](), E.Cause(err, "get current netns") } defer currentNs.Close() defer netns.Set(currentNs) var targetNs netns.NsHandle if strings.HasPrefix(nameOrPath, "/") { targetNs, err = netns.GetFromPath(nameOrPath) } else { targetNs, err = netns.GetFromName(nameOrPath) } if err != nil { return common.DefaultValue[T](), E.Cause(err, "get netns ", nameOrPath) } defer targetNs.Close() err = netns.Set(targetNs) if err != nil { return common.DefaultValue[T](), E.Cause(err, "set netns to ", nameOrPath) } } return block() } ================================================ FILE: common/listener/listener_tcp.go ================================================ package listener import ( "net" "net/netip" "strings" "syscall" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/redir" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing/common/control" 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/database64128/tfo-go/v2" ) func (l *Listener) ListenTCP() (net.Listener, error) { //nolint:staticcheck if l.listenOptions.ProxyProtocol || l.listenOptions.ProxyProtocolAcceptNoHeader { return nil, E.New("Proxy Protocol is deprecated and removed in sing-box 1.6.0") } var err error bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1})), l.listenOptions.ListenPort) var listenConfig net.ListenConfig if l.listenOptions.BindInterface != "" { listenConfig.Control = control.Append(listenConfig.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1)) } if l.listenOptions.RoutingMark != 0 { listenConfig.Control = control.Append(listenConfig.Control, control.RoutingMark(uint32(l.listenOptions.RoutingMark))) } if l.listenOptions.ReuseAddr { listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr()) } if l.listenOptions.DisableTCPKeepAlive { listenConfig.KeepAlive = -1 listenConfig.KeepAliveConfig.Enable = false } else { keepIdle := time.Duration(l.listenOptions.TCPKeepAlive) if keepIdle == 0 { keepIdle = C.TCPKeepAliveInitial } keepInterval := time.Duration(l.listenOptions.TCPKeepAliveInterval) if keepInterval == 0 { keepInterval = C.TCPKeepAliveInterval } listenConfig.KeepAliveConfig = net.KeepAliveConfig{ Enable: true, Idle: keepIdle, Interval: keepInterval, } } if l.listenOptions.TCPMultiPath { listenConfig.SetMultipathTCP(true) } if l.tproxy { listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error { return control.Raw(conn, func(fd uintptr) error { return redir.TProxy(fd, !strings.HasSuffix(network, "4"), false) }) }) } tcpListener, err := ListenNetworkNamespace[net.Listener](l.listenOptions.NetNs, func() (net.Listener, error) { if l.listenOptions.TCPFastOpen { var tfoConfig tfo.ListenConfig tfoConfig.ListenConfig = listenConfig return tfoConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String()) } else { return listenConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String()) } }) if err != nil { return nil, err } l.logger.Info("tcp server started at ", tcpListener.Addr()) l.tcpListener = tcpListener return tcpListener, err } func (l *Listener) loopTCPIn() { tcpListener := l.tcpListener var metadata adapter.InboundContext for { conn, err := tcpListener.Accept() if err != nil { //nolint:staticcheck if netError, isNetError := err.(net.Error); isNetError && netError.Temporary() { l.logger.Error(err) continue } if l.shutdown.Load() && E.IsClosed(err) { return } l.tcpListener.Close() l.logger.Error("tcp listener closed: ", err) continue } //nolint:staticcheck metadata.InboundDetour = l.listenOptions.Detour metadata.Source = M.SocksaddrFromNet(conn.RemoteAddr()).Unwrap() metadata.OriginDestination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap() ctx := log.ContextWithNewID(l.ctx) l.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) go l.connHandler.NewConnection(ctx, conn, metadata, nil) } } ================================================ FILE: common/listener/listener_udp.go ================================================ package listener import ( "context" "net" "net/netip" "os" "strings" "syscall" "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" N "github.com/sagernet/sing/common/network" "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 if l.listenOptions.BindInterface != "" { listenConfig.Control = control.Append(listenConfig.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1)) } if l.listenOptions.RoutingMark != 0 { listenConfig.Control = control.Append(listenConfig.Control, control.RoutingMark(uint32(l.listenOptions.RoutingMark))) } if l.listenOptions.ReuseAddr { listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr()) } var udpFragment bool if l.listenOptions.UDPFragment != nil { udpFragment = *l.listenOptions.UDPFragment } else { udpFragment = l.listenOptions.UDPFragmentDefault } if !udpFragment { listenConfig.Control = control.Append(listenConfig.Control, control.DisableUDPFragment()) } if l.tproxy { listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error { return control.Raw(conn, func(fd uintptr) error { return redir.TProxy(fd, !strings.HasSuffix(network, "4"), true) }) }) } udpConn, err := ListenNetworkNamespace[net.PacketConn](l.listenOptions.NetNs, func() (net.PacketConn, error) { return listenConfig.ListenPacket(l.ctx, M.NetworkFromNetAddr(N.NetworkUDP, bindAddr.Addr), bindAddr.String()) }) if err != nil { return nil, err } l.udpConn = udpConn.(*net.UDPConn) l.udpAddr = bindAddr l.logger.Info("udp server started at ", udpConn.LocalAddr()) return udpConn, err } func (l *Listener) DialContext(dialer net.Dialer, ctx context.Context, network string, address string) (net.Conn, error) { return ListenNetworkNamespace[net.Conn](l.listenOptions.NetNs, func() (net.Conn, error) { if l.listenOptions.BindInterface != "" { dialer.Control = control.Append(dialer.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1)) } if l.listenOptions.RoutingMark != 0 { dialer.Control = control.Append(dialer.Control, control.RoutingMark(uint32(l.listenOptions.RoutingMark))) } if l.listenOptions.ReuseAddr { dialer.Control = control.Append(dialer.Control, control.ReuseAddr()) } return dialer.DialContext(ctx, network, address) }) } func (l *Listener) ListenPacket(listenConfig net.ListenConfig, ctx context.Context, network string, address string) (net.PacketConn, error) { return ListenNetworkNamespace[net.PacketConn](l.listenOptions.NetNs, func() (net.PacketConn, error) { if l.listenOptions.BindInterface != "" { listenConfig.Control = control.Append(listenConfig.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1)) } if l.listenOptions.RoutingMark != 0 { listenConfig.Control = control.Append(listenConfig.Control, control.RoutingMark(uint32(l.listenOptions.RoutingMark))) } if l.listenOptions.ReuseAddr { listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr()) } return listenConfig.ListenPacket(ctx, network, address) }) } func (l *Listener) UDPAddr() M.Socksaddr { return l.udpAddr } func (l *Listener) PacketWriter() N.PacketWriter { return (*packetWriter)(l) } 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() defer buffer.Release() buffer.IncRef() defer buffer.DecRef() } if l.oobPacketHandler != nil { oob := make([]byte, 1024) for { if l.threadUnsafePacketWriter { buffer = buf.NewPacket() } else { buffer.Reset() } n, oobN, _, addr, err := l.udpConn.ReadMsgUDPAddrPort(buffer.FreeBytes(), oob) if err != nil { if l.threadUnsafePacketWriter { buffer.Release() } if l.shutdown.Load() && E.IsClosed(err) { return } l.udpConn.Close() l.logger.Error("udp listener closed: ", err) return } buffer.Truncate(n) l.oobPacketHandler.NewPacket(buffer, oob[:oobN], M.SocksaddrFromNetIP(addr).Unwrap()) } } else { for { if l.threadUnsafePacketWriter { buffer = buf.NewPacket() } else { buffer.Reset() } n, addr, err := l.udpConn.ReadFromUDPAddrPort(buffer.FreeBytes()) if err != nil { if l.threadUnsafePacketWriter { buffer.Release() } if l.shutdown.Load() && E.IsClosed(err) { return } l.udpConn.Close() l.logger.Error("udp listener closed: ", err) return } buffer.Truncate(n) 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: packets = append(packets, packet) case <-l.packetOutboundClosed: l.releasePacketOutbound() return } drain: for len(packets) < udpOutputBatchSize { select { case packet := <-l.packetOutbound: 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 } } } type packetWriter Listener func (w *packetWriter) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { packet := N.NewPacketBuffer() packet.Buffer = buffer packet.Destination = destination select { case w.packetOutbound <- packet: return nil default: buffer.Release() N.PutPacketBuffer(packet) if w.shutdown.Load() { return os.ErrClosed } w.logger.Trace("dropped packet to ", destination) return nil } } 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() { } ================================================ FILE: common/mux/client.go ================================================ package mux import ( "context" "net" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-mux" 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" ) type Client = mux.Client func NewClientWithOptions(dialer N.Dialer, logger logger.Logger, options option.OutboundMultiplexOptions) (*Client, error) { if !options.Enabled { return nil, nil } var brutalOptions mux.BrutalOptions if options.Brutal != nil && options.Brutal.Enabled { brutalOptions = mux.BrutalOptions{ Enabled: true, SendBPS: uint64(options.Brutal.UpMbps * C.MbpsToBps), ReceiveBPS: uint64(options.Brutal.DownMbps * C.MbpsToBps), } if brutalOptions.SendBPS < mux.BrutalMinSpeedBPS { return nil, E.New("brutal: invalid upload speed") } if brutalOptions.ReceiveBPS < mux.BrutalMinSpeedBPS { return nil, E.New("brutal: invalid download speed") } } return mux.NewClient(mux.Options{ Dialer: &clientDialer{dialer}, Logger: logger, Protocol: options.Protocol, MaxConnections: options.MaxConnections, MinStreams: options.MinStreams, MaxStreams: options.MaxStreams, Padding: options.Padding, Brutal: brutalOptions, }) } type clientDialer struct { N.Dialer } func (d *clientDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { return d.Dialer.DialContext(adapter.OverrideContext(ctx), network, destination) } func (d *clientDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { return d.Dialer.ListenPacket(adapter.OverrideContext(ctx), destination) } ================================================ FILE: common/mux/router.go ================================================ package mux import ( "context" "net" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-mux" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" N "github.com/sagernet/sing/common/network" ) type Router struct { router adapter.ConnectionRouterEx service *mux.Service } func NewRouterWithOptions(router adapter.ConnectionRouterEx, logger logger.ContextLogger, options option.InboundMultiplexOptions) (adapter.ConnectionRouterEx, error) { if !options.Enabled { return router, nil } var brutalOptions mux.BrutalOptions if options.Brutal != nil && options.Brutal.Enabled { brutalOptions = mux.BrutalOptions{ Enabled: true, SendBPS: uint64(options.Brutal.UpMbps * C.MbpsToBps), ReceiveBPS: uint64(options.Brutal.DownMbps * C.MbpsToBps), } if brutalOptions.SendBPS < mux.BrutalMinSpeedBPS { return nil, E.New("brutal: invalid upload speed") } if brutalOptions.ReceiveBPS < mux.BrutalMinSpeedBPS { return nil, E.New("brutal: invalid download speed") } } service, err := mux.NewService(mux.ServiceOptions{ NewStreamContext: func(ctx context.Context, conn net.Conn) context.Context { return log.ContextWithNewID(ctx) }, Logger: logger, HandlerEx: adapter.NewRouteContextHandler(router), Padding: options.Padding, Brutal: brutalOptions, }) if err != nil { return nil, err } return &Router{router, service}, nil } // Deprecated: Use RouteConnectionEx instead. func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { if metadata.Destination == mux.Destination { // TODO: check if WithContext is necessary return r.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, adapter.UpstreamMetadata(metadata)) } else { return r.router.RouteConnection(ctx, conn, metadata) } } // Deprecated: Use RoutePacketConnectionEx instead. func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { return r.router.RoutePacketConnection(ctx, conn, metadata) } func (r *Router) RouteConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { if metadata.Destination == mux.Destination { r.service.NewConnectionEx(adapter.WithContext(ctx, &metadata), conn, metadata.Source, metadata.Destination, onClose) return } r.router.RouteConnectionEx(ctx, conn, metadata, onClose) } func (r *Router) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { r.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } ================================================ FILE: common/networkquality/http.go ================================================ 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) } ================================================ FILE: common/networkquality/http3.go ================================================ //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 } wrappedConn := 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 } ================================================ FILE: common/networkquality/http3_stub.go ================================================ //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 } ================================================ FILE: common/networkquality/networkquality.go ================================================ package networkquality import ( "context" "crypto/tls" "encoding/json" "io" "math" "math/rand" "net/http" "net/http/httptrace" "net/url" "slices" "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 closeMeasurementClient(c.client) 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 } 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() 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 := max(currentInterval-settings.movingAvgDistance+1, 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 := max(currentInterval-settings.movingAvgDistance+1, 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) closeMeasurementClient(client) if err != nil { return 0, 0, err } latencies = append(latencies, measurement.total.Milliseconds()) if measurement.bytes > maxProbeBytes { maxProbeBytes = measurement.bytes } } slices.Sort(latencies) 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 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) } 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 } ================================================ FILE: common/pipelistener/listener.go ================================================ package pipelistener import ( "io" "net" ) var _ net.Listener = (*Listener)(nil) type Listener struct { pipe chan net.Conn done chan struct{} } func New(channelSize int) *Listener { return &Listener{ pipe: make(chan net.Conn, channelSize), done: make(chan struct{}), } } func (l *Listener) Serve(conn net.Conn) { l.pipe <- conn } func (l *Listener) Accept() (net.Conn, error) { select { case conn := <-l.pipe: return conn, nil case <-l.done: return nil, io.ErrClosedPipe } } func (l *Listener) Close() error { select { case <-l.done: return io.ErrClosedPipe default: } close(l.done) return nil } func (l *Listener) Addr() net.Addr { return addr{} } type addr struct{} func (a addr) Network() string { return "pipe" } func (a addr) String() string { return "pipe" } ================================================ FILE: common/process/searcher.go ================================================ package process import ( "context" "net/netip" "os/user" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-tun" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" ) type Searcher interface { FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) Close() error } var ErrNotFound = E.New("process not found") type Config struct { Logger log.ContextLogger PackageManager tun.PackageManager } func FindProcessInfo(searcher Searcher, ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { info, err := searcher.FindProcessInfo(ctx, network, source, destination) if err != nil { return nil, err } if info.UserId != -1 && info.UserName == "" { osUser, _ := user.LookupId(F.ToString(info.UserId)) if osUser != nil { info.UserName = osUser.Username } } return info, nil } ================================================ FILE: common/process/searcher_android.go ================================================ package process import ( "context" "net/netip" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" ) var _ Searcher = (*androidSearcher)(nil) type androidSearcher struct { packageManager tun.PackageManager } func NewSearcher(config Config) (Searcher, error) { return &androidSearcher{config.PackageManager}, nil } func (s *androidSearcher) Close() error { return nil } func (s *androidSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { family, protocol, err := socketDiagSettings(network, source) if err != nil { return nil, err } _, uid, err := querySocketDiagOnce(family, protocol, source) if err != nil { return nil, err } appID := uid % 100000 var packageNames []string if sharedPackage, loaded := s.packageManager.SharedPackageByID(appID); loaded { packageNames = append(packageNames, sharedPackage) } if packages, loaded := s.packageManager.PackagesByID(appID); loaded { packageNames = append(packageNames, packages...) } packageNames = common.Uniq(packageNames) return &adapter.ConnectionOwner{ UserId: int32(uid), AndroidPackageNames: packageNames, }, nil } ================================================ FILE: common/process/searcher_darwin.go ================================================ //go:build darwin package process import ( "context" "net/netip" "strconv" "strings" "syscall" "github.com/sagernet/sing-box/adapter" ) var _ Searcher = (*darwinSearcher)(nil) type darwinSearcher struct{} func NewSearcher(_ Config) (Searcher, error) { return &darwinSearcher{}, nil } func (d *darwinSearcher) Close() error { return nil } func (d *darwinSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { return FindDarwinConnectionOwner(network, source, destination) } var structSize = func() int { value, _ := syscall.Sysctl("kern.osrelease") major, _, _ := strings.Cut(value, ".") n, _ := strconv.ParseInt(major, 10, 64) switch true { case n >= 22: return 408 default: // from darwin-xnu/bsd/netinet/in_pcblist.c:get_pcblist_n // size/offset are round up (aligned) to 8 bytes in darwin // rup8(sizeof(xinpcb_n)) + rup8(sizeof(xsocket_n)) + // 2 * rup8(sizeof(xsockbuf_n)) + rup8(sizeof(xsockstat_n)) return 384 } }() ================================================ FILE: common/process/searcher_darwin_shared.go ================================================ //go:build darwin package process import ( "encoding/binary" "net/netip" "os" "sync" "syscall" "time" "unsafe" "github.com/sagernet/sing-box/adapter" N "github.com/sagernet/sing/common/network" "golang.org/x/sys/unix" ) const ( darwinSnapshotTTL = 200 * time.Millisecond darwinXinpgenSize = 24 darwinXsocketOffset = 104 darwinXinpcbForeignPort = 16 darwinXinpcbLocalPort = 18 darwinXinpcbVFlag = 44 darwinXinpcbForeignAddr = 48 darwinXinpcbLocalAddr = 64 darwinXinpcbIPv4Addr = 12 darwinXsocketUID = 64 darwinXsocketLastPID = 68 darwinTCPExtraStructSize = 208 ) type darwinConnectionEntry struct { localAddr netip.Addr remoteAddr netip.Addr localPort uint16 remotePort uint16 pid uint32 uid int32 } type darwinConnectionMatchKind uint8 const ( darwinConnectionMatchExact darwinConnectionMatchKind = iota darwinConnectionMatchLocalFallback darwinConnectionMatchWildcardFallback ) type darwinSnapshot struct { createdAt time.Time entries []darwinConnectionEntry } type darwinConnectionFinder struct { access sync.Mutex ttl time.Duration snapshots map[string]darwinSnapshot builder func(string) (darwinSnapshot, error) } var sharedDarwinConnectionFinder = newDarwinConnectionFinder(darwinSnapshotTTL) func newDarwinConnectionFinder(ttl time.Duration) *darwinConnectionFinder { return &darwinConnectionFinder{ ttl: ttl, snapshots: make(map[string]darwinSnapshot), builder: buildDarwinSnapshot, } } func FindDarwinConnectionOwner(network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { return sharedDarwinConnectionFinder.find(network, source, destination) } func (f *darwinConnectionFinder) find(network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { networkName := N.NetworkName(network) source = normalizeDarwinAddrPort(source) destination = normalizeDarwinAddrPort(destination) var lastOwner *adapter.ConnectionOwner for attempt := range 2 { snapshot, fromCache, err := f.loadSnapshot(networkName, attempt > 0) if err != nil { return nil, err } entry, matchKind, err := matchDarwinConnectionEntry(snapshot.entries, networkName, source, destination) if err != nil { if err == ErrNotFound && fromCache { continue } return nil, err } if fromCache && matchKind != darwinConnectionMatchExact { continue } owner := &adapter.ConnectionOwner{ UserId: entry.uid, } lastOwner = owner if entry.pid == 0 { return owner, nil } processPath, err := getExecPathFromPID(entry.pid) if err == nil { owner.ProcessPath = processPath return owner, nil } if fromCache { continue } return owner, nil } if lastOwner != nil { return lastOwner, nil } return nil, ErrNotFound } func (f *darwinConnectionFinder) loadSnapshot(network string, forceRefresh bool) (darwinSnapshot, bool, error) { f.access.Lock() defer f.access.Unlock() if !forceRefresh { if snapshot, loaded := f.snapshots[network]; loaded && time.Since(snapshot.createdAt) < f.ttl { return snapshot, true, nil } } snapshot, err := f.builder(network) if err != nil { return darwinSnapshot{}, false, err } f.snapshots[network] = snapshot return snapshot, false, nil } func buildDarwinSnapshot(network string) (darwinSnapshot, error) { spath, itemSize, err := darwinSnapshotSettings(network) if err != nil { return darwinSnapshot{}, err } value, err := unix.SysctlRaw(spath) if err != nil { return darwinSnapshot{}, err } return darwinSnapshot{ createdAt: time.Now(), entries: parseDarwinSnapshot(value, itemSize), }, nil } func darwinSnapshotSettings(network string) (string, int, error) { itemSize := structSize switch network { case N.NetworkTCP: return "net.inet.tcp.pcblist_n", itemSize + darwinTCPExtraStructSize, nil case N.NetworkUDP: return "net.inet.udp.pcblist_n", itemSize, nil default: return "", 0, os.ErrInvalid } } func parseDarwinSnapshot(buf []byte, itemSize int) []darwinConnectionEntry { entries := make([]darwinConnectionEntry, 0, (len(buf)-darwinXinpgenSize)/itemSize) for i := darwinXinpgenSize; i+itemSize <= len(buf); i += itemSize { inp := i so := i + darwinXsocketOffset entry, ok := parseDarwinConnectionEntry(buf[inp:so], buf[so:so+structSize-darwinXsocketOffset]) if ok { entries = append(entries, entry) } } return entries } func parseDarwinConnectionEntry(inp []byte, so []byte) (darwinConnectionEntry, bool) { if len(inp) < darwinXsocketOffset || len(so) < structSize-darwinXsocketOffset { return darwinConnectionEntry{}, false } entry := darwinConnectionEntry{ remotePort: binary.BigEndian.Uint16(inp[darwinXinpcbForeignPort : darwinXinpcbForeignPort+2]), localPort: binary.BigEndian.Uint16(inp[darwinXinpcbLocalPort : darwinXinpcbLocalPort+2]), pid: binary.NativeEndian.Uint32(so[darwinXsocketLastPID : darwinXsocketLastPID+4]), uid: int32(binary.NativeEndian.Uint32(so[darwinXsocketUID : darwinXsocketUID+4])), } flag := inp[darwinXinpcbVFlag] switch { case flag&0x1 != 0: entry.remoteAddr = netip.AddrFrom4([4]byte(inp[darwinXinpcbForeignAddr+darwinXinpcbIPv4Addr : darwinXinpcbForeignAddr+darwinXinpcbIPv4Addr+4])) entry.localAddr = netip.AddrFrom4([4]byte(inp[darwinXinpcbLocalAddr+darwinXinpcbIPv4Addr : darwinXinpcbLocalAddr+darwinXinpcbIPv4Addr+4])) return entry, true case flag&0x2 != 0: entry.remoteAddr = netip.AddrFrom16([16]byte(inp[darwinXinpcbForeignAddr : darwinXinpcbForeignAddr+16])) entry.localAddr = netip.AddrFrom16([16]byte(inp[darwinXinpcbLocalAddr : darwinXinpcbLocalAddr+16])) return entry, true default: return darwinConnectionEntry{}, false } } func matchDarwinConnectionEntry(entries []darwinConnectionEntry, network string, source netip.AddrPort, destination netip.AddrPort) (darwinConnectionEntry, darwinConnectionMatchKind, error) { sourceAddr := source.Addr() if !sourceAddr.IsValid() { return darwinConnectionEntry{}, darwinConnectionMatchExact, os.ErrInvalid } var localFallback darwinConnectionEntry var hasLocalFallback bool var wildcardFallback darwinConnectionEntry var hasWildcardFallback bool for _, entry := range entries { if entry.localPort != source.Port() || sourceAddr.BitLen() != entry.localAddr.BitLen() { continue } if entry.localAddr == sourceAddr && destination.IsValid() && entry.remotePort == destination.Port() && entry.remoteAddr == destination.Addr() { return entry, darwinConnectionMatchExact, nil } if !destination.IsValid() && entry.localAddr == sourceAddr { return entry, darwinConnectionMatchExact, nil } if network != N.NetworkUDP { continue } if !hasLocalFallback && entry.localAddr == sourceAddr { hasLocalFallback = true localFallback = entry } if !hasWildcardFallback && entry.localAddr.IsUnspecified() { hasWildcardFallback = true wildcardFallback = entry } } if hasLocalFallback { return localFallback, darwinConnectionMatchLocalFallback, nil } if hasWildcardFallback { return wildcardFallback, darwinConnectionMatchWildcardFallback, nil } return darwinConnectionEntry{}, darwinConnectionMatchExact, ErrNotFound } func normalizeDarwinAddrPort(addrPort netip.AddrPort) netip.AddrPort { if !addrPort.IsValid() { return addrPort } return netip.AddrPortFrom(addrPort.Addr().Unmap(), addrPort.Port()) } func getExecPathFromPID(pid uint32) (string, error) { const ( procpidpathinfo = 0xb procpidpathinfosize = 1024 proccallnumpidinfo = 0x2 ) buf := make([]byte, procpidpathinfosize) _, _, errno := syscall.Syscall6( syscall.SYS_PROC_INFO, proccallnumpidinfo, uintptr(pid), procpidpathinfo, 0, uintptr(unsafe.Pointer(&buf[0])), procpidpathinfosize) if errno != 0 { return "", errno } return unix.ByteSliceToString(buf), nil } ================================================ FILE: common/process/searcher_linux.go ================================================ //go:build linux && !android package process import ( "context" "errors" "net/netip" "syscall" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/log" E "github.com/sagernet/sing/common/exceptions" ) var _ Searcher = (*linuxSearcher)(nil) type linuxSearcher struct { logger log.ContextLogger diagConns [4]*socketDiagConn processPathCache *uidProcessPathCache } func NewSearcher(config Config) (Searcher, error) { searcher := &linuxSearcher{ logger: config.Logger, processPathCache: newUIDProcessPathCache(time.Second), } for _, family := range []uint8{syscall.AF_INET, syscall.AF_INET6} { for _, protocol := range []uint8{syscall.IPPROTO_TCP, syscall.IPPROTO_UDP} { searcher.diagConns[socketDiagConnIndex(family, protocol)] = newSocketDiagConn(family, protocol) } } return searcher, nil } func (s *linuxSearcher) Close() error { var errs []error for _, conn := range s.diagConns { if conn == nil { continue } errs = append(errs, conn.Close()) } return E.Errors(errs...) } func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { inode, uid, err := s.resolveSocketByNetlink(network, source, destination) if err != nil { return nil, err } processInfo := &adapter.ConnectionOwner{ UserId: int32(uid), } processPath, err := s.processPathCache.findProcessPath(inode, uid) if err != nil { s.logger.DebugContext(ctx, "find process path: ", err) } else { processInfo.ProcessPath = processPath } return processInfo, nil } func (s *linuxSearcher) resolveSocketByNetlink(network string, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) { family, protocol, err := socketDiagSettings(network, source) if err != nil { return 0, 0, err } conn := s.diagConns[socketDiagConnIndex(family, protocol)] if conn == nil { return 0, 0, E.New("missing socket diag connection for family=", family, " protocol=", protocol) } if destination.IsValid() && source.Addr().BitLen() == destination.Addr().BitLen() { inode, uid, err = conn.query(source, destination) if err == nil { return inode, uid, nil } if !errors.Is(err, ErrNotFound) { return 0, 0, err } } return querySocketDiagOnce(family, protocol, source) } ================================================ FILE: common/process/searcher_linux_shared.go ================================================ //go:build linux //nolint:unused package process import ( "encoding/binary" "errors" "net/netip" "os" "path/filepath" "strings" "sync" "syscall" "time" "unicode" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/contrab/freelru" "github.com/sagernet/sing/contrab/maphash" ) const ( sizeOfSocketDiagRequestData = 56 sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + sizeOfSocketDiagRequestData socketDiagResponseMinSize = 72 socketDiagByFamily = 20 pathProc = "/proc" ) type socketDiagConn struct { access sync.Mutex family uint8 protocol uint8 fd int } type uidProcessPathCache struct { cache freelru.Cache[uint32, *uidProcessPaths] } type uidProcessPaths struct { entries map[uint32]string } func newSocketDiagConn(family, protocol uint8) *socketDiagConn { return &socketDiagConn{ family: family, protocol: protocol, fd: -1, } } func socketDiagConnIndex(family, protocol uint8) int { index := 0 if protocol == syscall.IPPROTO_UDP { index += 2 } if family == syscall.AF_INET6 { index++ } return index } func socketDiagSettings(network string, source netip.AddrPort) (family, protocol uint8, err error) { switch network { case N.NetworkTCP: protocol = syscall.IPPROTO_TCP case N.NetworkUDP: protocol = syscall.IPPROTO_UDP default: return 0, 0, os.ErrInvalid } switch { case source.Addr().Is4(): family = syscall.AF_INET case source.Addr().Is6(): family = syscall.AF_INET6 default: return 0, 0, os.ErrInvalid } return family, protocol, nil } func newUIDProcessPathCache(ttl time.Duration) *uidProcessPathCache { cache := common.Must1(freelru.NewSharded[uint32, *uidProcessPaths](64, maphash.NewHasher[uint32]().Hash32)) cache.SetLifetime(ttl) return &uidProcessPathCache{cache: cache} } func (c *uidProcessPathCache) findProcessPath(targetInode, uid uint32) (string, error) { if cached, ok := c.cache.Get(uid); ok { if processPath, found := cached.entries[targetInode]; found { return processPath, nil } } processPaths, err := buildProcessPathByUIDCache(uid) if err != nil { return "", err } c.cache.Add(uid, &uidProcessPaths{entries: processPaths}) processPath, found := processPaths[targetInode] if !found { return "", E.New("process of uid(", uid, "), inode(", targetInode, ") not found") } return processPath, nil } func (c *socketDiagConn) Close() error { c.access.Lock() defer c.access.Unlock() return c.closeLocked() } func (c *socketDiagConn) query(source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) { c.access.Lock() defer c.access.Unlock() request := packSocketDiagRequest(c.family, c.protocol, source, destination, false) for range 2 { err = c.ensureOpenLocked() if err != nil { return 0, 0, E.Cause(err, "dial netlink") } inode, uid, err = querySocketDiag(c.fd, request) if err == nil || errors.Is(err, ErrNotFound) { return inode, uid, err } if !shouldRetrySocketDiag(err) { return 0, 0, err } _ = c.closeLocked() } return 0, 0, err } func querySocketDiagOnce(family, protocol uint8, source netip.AddrPort) (inode, uid uint32, err error) { fd, err := openSocketDiag() if err != nil { return 0, 0, E.Cause(err, "dial netlink") } defer syscall.Close(fd) return querySocketDiag(fd, packSocketDiagRequest(family, protocol, source, netip.AddrPort{}, true)) } func (c *socketDiagConn) ensureOpenLocked() error { if c.fd != -1 { return nil } fd, err := openSocketDiag() if err != nil { return err } c.fd = fd return nil } func openSocketDiag() (int, error) { fd, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_DGRAM|syscall.SOCK_CLOEXEC, syscall.NETLINK_INET_DIAG) if err != nil { return -1, err } timeout := &syscall.Timeval{Usec: 100} if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, timeout); err != nil { syscall.Close(fd) return -1, err } if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, timeout); err != nil { syscall.Close(fd) return -1, err } if err = syscall.Connect(fd, &syscall.SockaddrNetlink{ Family: syscall.AF_NETLINK, Pid: 0, Groups: 0, }); err != nil { syscall.Close(fd) return -1, err } return fd, nil } func (c *socketDiagConn) closeLocked() error { if c.fd == -1 { return nil } err := syscall.Close(c.fd) c.fd = -1 return err } func packSocketDiagRequest(family, protocol byte, source netip.AddrPort, destination netip.AddrPort, dump bool) []byte { request := make([]byte, sizeOfSocketDiagRequest) binary.NativeEndian.PutUint32(request[0:4], sizeOfSocketDiagRequest) binary.NativeEndian.PutUint16(request[4:6], socketDiagByFamily) flags := uint16(syscall.NLM_F_REQUEST) if dump { flags |= syscall.NLM_F_DUMP } binary.NativeEndian.PutUint16(request[6:8], flags) binary.NativeEndian.PutUint32(request[8:12], 0) binary.NativeEndian.PutUint32(request[12:16], 0) request[16] = family request[17] = protocol request[18] = 0 request[19] = 0 if dump { binary.NativeEndian.PutUint32(request[20:24], 0xFFFFFFFF) } requestSource := source requestDestination := destination if protocol == syscall.IPPROTO_UDP && !dump && destination.IsValid() { // udp_dump_one expects the exact-match endpoints reversed for historical reasons. requestSource, requestDestination = destination, source } binary.BigEndian.PutUint16(request[24:26], requestSource.Port()) binary.BigEndian.PutUint16(request[26:28], requestDestination.Port()) if family == syscall.AF_INET6 { copy(request[28:44], requestSource.Addr().AsSlice()) if requestDestination.IsValid() { copy(request[44:60], requestDestination.Addr().AsSlice()) } } else { copy(request[28:32], requestSource.Addr().AsSlice()) if requestDestination.IsValid() { copy(request[44:48], requestDestination.Addr().AsSlice()) } } binary.NativeEndian.PutUint32(request[60:64], 0) binary.NativeEndian.PutUint64(request[64:72], 0xFFFFFFFFFFFFFFFF) return request } func querySocketDiag(fd int, request []byte) (inode, uid uint32, err error) { _, err = syscall.Write(fd, request) if err != nil { return 0, 0, E.Cause(err, "write netlink request") } buffer := make([]byte, 64<<10) n, err := syscall.Read(fd, buffer) if err != nil { return 0, 0, E.Cause(err, "read netlink response") } messages, err := syscall.ParseNetlinkMessage(buffer[:n]) if err != nil { return 0, 0, E.Cause(err, "parse netlink message") } return unpackSocketDiagMessages(messages) } func unpackSocketDiagMessages(messages []syscall.NetlinkMessage) (inode, uid uint32, err error) { for _, message := range messages { switch message.Header.Type { case syscall.NLMSG_DONE: continue case syscall.NLMSG_ERROR: err = unpackSocketDiagError(&message) if err != nil { return 0, 0, err } case socketDiagByFamily: inode, uid = unpackSocketDiagResponse(&message) if inode != 0 || uid != 0 { return inode, uid, nil } } } return 0, 0, ErrNotFound } func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid uint32) { if len(msg.Data) < socketDiagResponseMinSize { return 0, 0 } uid = binary.NativeEndian.Uint32(msg.Data[64:68]) inode = binary.NativeEndian.Uint32(msg.Data[68:72]) return inode, uid } func unpackSocketDiagError(msg *syscall.NetlinkMessage) error { if len(msg.Data) < 4 { return E.New("netlink message: NLMSG_ERROR") } errno := int32(binary.NativeEndian.Uint32(msg.Data[:4])) if errno == 0 { return nil } if errno < 0 { errno = -errno } sysErr := syscall.Errno(errno) switch sysErr { case syscall.ENOENT, syscall.ESRCH: return ErrNotFound default: return E.New("netlink message: ", sysErr) } } func shouldRetrySocketDiag(err error) bool { return err != nil && !errors.Is(err, ErrNotFound) } func buildProcessPathByUIDCache(uid uint32) (map[uint32]string, error) { files, err := os.ReadDir(pathProc) if err != nil { return nil, err } buffer := make([]byte, syscall.PathMax) processPaths := make(map[uint32]string) for _, file := range files { if !file.IsDir() || !isPid(file.Name()) { continue } info, err := file.Info() if err != nil { if isIgnorableProcError(err) { continue } return nil, err } if info.Sys().(*syscall.Stat_t).Uid != uid { continue } processPath := filepath.Join(pathProc, file.Name()) fdPath := filepath.Join(processPath, "fd") exePath, err := os.Readlink(filepath.Join(processPath, "exe")) if err != nil { if isIgnorableProcError(err) { continue } return nil, err } fds, err := os.ReadDir(fdPath) if err != nil { continue } for _, fd := range fds { n, err := syscall.Readlink(filepath.Join(fdPath, fd.Name()), buffer) if err != nil { continue } inode, ok := parseSocketInode(buffer[:n]) if !ok { continue } if _, loaded := processPaths[inode]; !loaded { processPaths[inode] = exePath } } } return processPaths, nil } func isIgnorableProcError(err error) bool { return os.IsNotExist(err) || os.IsPermission(err) } func parseSocketInode(link []byte) (uint32, bool) { const socketPrefix = "socket:[" if len(link) <= len(socketPrefix) || string(link[:len(socketPrefix)]) != socketPrefix || link[len(link)-1] != ']' { return 0, false } var inode uint64 for _, char := range link[len(socketPrefix) : len(link)-1] { if char < '0' || char > '9' { return 0, false } inode = inode*10 + uint64(char-'0') if inode > uint64(^uint32(0)) { return 0, false } } return uint32(inode), true } func isPid(s string) bool { return strings.IndexFunc(s, func(r rune) bool { return !unicode.IsDigit(r) }) == -1 } ================================================ FILE: common/process/searcher_linux_shared_test.go ================================================ //go:build linux package process import ( "net" "net/netip" "os" "syscall" "testing" "time" "github.com/stretchr/testify/require" ) func TestQuerySocketDiagUDPExact(t *testing.T) { t.Parallel() server, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0}) require.NoError(t, err) defer server.Close() client, err := net.DialUDP("udp4", nil, server.LocalAddr().(*net.UDPAddr)) require.NoError(t, err) defer client.Close() err = client.SetDeadline(time.Now().Add(time.Second)) require.NoError(t, err) _, err = client.Write([]byte{0}) require.NoError(t, err) err = server.SetReadDeadline(time.Now().Add(time.Second)) require.NoError(t, err) buffer := make([]byte, 1) _, _, err = server.ReadFromUDP(buffer) require.NoError(t, err) source := addrPortFromUDPAddr(t, client.LocalAddr()) destination := addrPortFromUDPAddr(t, client.RemoteAddr()) fd, err := openSocketDiag() require.NoError(t, err) defer syscall.Close(fd) inode, uid, err := querySocketDiag(fd, packSocketDiagRequest(syscall.AF_INET, syscall.IPPROTO_UDP, source, destination, false)) require.NoError(t, err) require.NotZero(t, inode) require.EqualValues(t, os.Getuid(), uid) } func addrPortFromUDPAddr(t *testing.T, addr net.Addr) netip.AddrPort { t.Helper() udpAddr, ok := addr.(*net.UDPAddr) require.True(t, ok) ip, ok := netip.AddrFromSlice(udpAddr.IP) require.True(t, ok) return netip.AddrPortFrom(ip.Unmap(), uint16(udpAddr.Port)) } ================================================ FILE: common/process/searcher_stub.go ================================================ //go:build !linux && !windows && !darwin package process import ( "os" ) func NewSearcher(_ Config) (Searcher, error) { return nil, os.ErrInvalid } ================================================ FILE: common/process/searcher_windows.go ================================================ package process import ( "context" "net/netip" "syscall" "github.com/sagernet/sing-box/adapter" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/winiphlpapi" "golang.org/x/sys/windows" ) var _ Searcher = (*windowsSearcher)(nil) type windowsSearcher struct{} func NewSearcher(_ Config) (Searcher, error) { err := initWin32API() if err != nil { return nil, E.Cause(err, "init win32 api") } return &windowsSearcher{}, nil } func initWin32API() error { return winiphlpapi.LoadExtendedTable() } func (s *windowsSearcher) Close() error { return nil } func (s *windowsSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { pid, err := winiphlpapi.FindPid(network, source) if err != nil { return nil, err } path, err := getProcessPath(pid) if err != nil { return &adapter.ConnectionOwner{ProcessID: pid, UserId: -1}, err } return &adapter.ConnectionOwner{ProcessID: pid, ProcessPath: path, UserId: -1}, nil } func getProcessPath(pid uint32) (string, error) { switch pid { case 0: return ":System Idle Process", nil case 4: return ":System", nil } handle, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid) if err != nil { return "", err } defer windows.CloseHandle(handle) size := uint32(syscall.MAX_LONG_PATH) buf := make([]uint16, syscall.MAX_LONG_PATH) err = windows.QueryFullProcessImageName(handle, 0, &buf[0], &size) if err != nil { return "", err } return windows.UTF16ToString(buf[:size]), nil } ================================================ FILE: common/proxybridge/bridge.go ================================================ 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) } ================================================ FILE: common/redir/redir_darwin.go ================================================ package redir import ( "net" "net/netip" "syscall" "unsafe" M "github.com/sagernet/sing/common/metadata" ) const ( PF_OUT = 0x2 DIOCNATLOOK = 0xc0544417 ) func GetOriginalDestination(conn net.Conn) (destination netip.AddrPort, err error) { fd, err := syscall.Open("/dev/pf", 0, syscall.O_RDONLY) if err != nil { return netip.AddrPort{}, err } defer syscall.Close(fd) nl := struct { saddr, daddr, rsaddr, rdaddr [16]byte sxport, dxport, rsxport, rdxport [4]byte af, proto, protoVariant, direction uint8 }{ af: syscall.AF_INET, proto: syscall.IPPROTO_TCP, direction: PF_OUT, } la := conn.LocalAddr().(*net.TCPAddr) ra := conn.RemoteAddr().(*net.TCPAddr) raIP, laIP := ra.IP, la.IP raPort, laPort := ra.Port, la.Port switch { case raIP.To4() != nil: copy(nl.saddr[:net.IPv4len], raIP.To4()) copy(nl.daddr[:net.IPv4len], laIP.To4()) nl.af = syscall.AF_INET default: copy(nl.saddr[:], raIP.To16()) copy(nl.daddr[:], laIP.To16()) nl.af = syscall.AF_INET6 } nl.sxport[0], nl.sxport[1] = byte(raPort>>8), byte(raPort) nl.dxport[0], nl.dxport[1] = byte(laPort>>8), byte(laPort) if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), DIOCNATLOOK, uintptr(unsafe.Pointer(&nl))); errno != 0 { return netip.AddrPort{}, errno } var ip net.IP switch nl.af { case syscall.AF_INET: ip = make(net.IP, net.IPv4len) copy(ip, nl.rdaddr[:net.IPv4len]) case syscall.AF_INET6: ip = make(net.IP, net.IPv6len) copy(ip, nl.rdaddr[:]) } port := uint16(nl.rdxport[0])<<8 | uint16(nl.rdxport[1]) destination = netip.AddrPortFrom(M.AddrFromIP(ip), port) return } ================================================ FILE: common/redir/redir_linux.go ================================================ package redir import ( "encoding/binary" "net" "net/netip" "os" "syscall" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/control" M "github.com/sagernet/sing/common/metadata" ) func GetOriginalDestination(conn net.Conn) (destination netip.AddrPort, err error) { syscallConn, ok := common.Cast[syscall.Conn](conn) if !ok { return netip.AddrPort{}, os.ErrInvalid } err = control.Conn(syscallConn, func(fd uintptr) error { const SO_ORIGINAL_DST = 80 if conn.RemoteAddr().(*net.TCPAddr).IP.To4() != nil { raw, err := syscall.GetsockoptIPv6Mreq(int(fd), syscall.IPPROTO_IP, SO_ORIGINAL_DST) if err != nil { return err } destination = netip.AddrPortFrom(M.AddrFromIP(raw.Multiaddr[4:8]), uint16(raw.Multiaddr[2])<<8+uint16(raw.Multiaddr[3])) } else { raw, err := syscall.GetsockoptIPv6MTUInfo(int(fd), syscall.IPPROTO_IPV6, SO_ORIGINAL_DST) if err != nil { return err } var port [2]byte binary.BigEndian.PutUint16(port[:], raw.Addr.Port) destination = netip.AddrPortFrom(M.AddrFromIP(raw.Addr.Addr[:]), binary.LittleEndian.Uint16(port[:])) } return nil }) return } ================================================ FILE: common/redir/redir_other.go ================================================ //go:build !linux && !darwin package redir import ( "net" "net/netip" "os" ) func GetOriginalDestination(conn net.Conn) (destination netip.AddrPort, err error) { return netip.AddrPort{}, os.ErrInvalid } ================================================ FILE: common/redir/tproxy_linux.go ================================================ package redir import ( "encoding/binary" "net/netip" "syscall" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" "golang.org/x/sys/unix" ) func TProxy(fd uintptr, isIPv6 bool, isUDP bool) error { err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) if err == nil { err = syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_TRANSPARENT, 1) } if err == nil && isIPv6 { err = syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, unix.IPV6_TRANSPARENT, 1) } if isUDP { if err == nil { err = syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_RECVORIGDSTADDR, 1) } if err == nil && isIPv6 { err = syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, unix.IPV6_RECVORIGDSTADDR, 1) } } return err } func TProxyWriteBack() control.Func { return func(network, address string, conn syscall.RawConn) error { return control.Raw(conn, func(fd uintptr) error { if M.ParseSocksaddr(address).Addr.Is6() { return syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, unix.IPV6_TRANSPARENT, 1) } else { return syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_TRANSPARENT, 1) } }) } } func GetOriginalDestinationFromOOB(oob []byte) (netip.AddrPort, error) { controlMessages, err := unix.ParseSocketControlMessage(oob) if err != nil { return netip.AddrPort{}, err } for _, message := range controlMessages { if message.Header.Level == unix.SOL_IP && message.Header.Type == unix.IP_RECVORIGDSTADDR { return netip.AddrPortFrom(M.AddrFromIP(message.Data[4:8]), binary.BigEndian.Uint16(message.Data[2:4])), nil } else if message.Header.Level == unix.SOL_IPV6 && message.Header.Type == unix.IPV6_RECVORIGDSTADDR { return netip.AddrPortFrom(M.AddrFromIP(message.Data[8:24]), binary.BigEndian.Uint16(message.Data[2:4])), nil } } return netip.AddrPort{}, E.New("not found") } ================================================ FILE: common/redir/tproxy_other.go ================================================ //go:build !linux package redir import ( "net/netip" "os" "github.com/sagernet/sing/common/control" ) func TProxy(fd uintptr, isIPv6 bool, isUDP bool) error { return os.ErrInvalid } func TProxyWriteBack() control.Func { return nil } func GetOriginalDestinationFromOOB(oob []byte) (netip.AddrPort, error) { return netip.AddrPort{}, os.ErrInvalid } ================================================ FILE: common/schannel/doc.go ================================================ // 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 ================================================ FILE: common/schannel/schannel_windows.go ================================================ 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() } ================================================ FILE: common/schannel/schannel_windows_test.go ================================================ //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])), } } ================================================ FILE: common/schannel/syscall_windows.go ================================================ 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 } ================================================ FILE: common/schannel/types_windows.go ================================================ 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 { _ uint32 // cAlpnIds _ uintptr // rgstrAlpnIds grbitDisabledProtocols uint32 _ uint32 // cDisabledCrypto _ uintptr // pDisabledCrypto _ uint32 // dwFlags } 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 } ================================================ FILE: common/schannel/zsyscall_windows.go ================================================ // 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 } ================================================ FILE: common/settings/proxy_android.go ================================================ package settings import ( "context" "os" "strings" C "github.com/sagernet/sing-box/constant" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/shell" ) type AndroidSystemProxy struct { useRish bool rishPath string serverAddr M.Socksaddr supportSOCKS bool isEnabled bool } func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*AndroidSystemProxy, error) { userId := os.Getuid() var ( useRish bool rishPath string ) if userId == 0 || userId == 1000 || userId == 2000 { useRish = false } else { rishPath, useRish = C.FindPath("rish") if !useRish { return nil, E.Cause(os.ErrPermission, "root or system (adb) permission is required for set system proxy") } } return &AndroidSystemProxy{ useRish: useRish, rishPath: rishPath, serverAddr: serverAddr, supportSOCKS: supportSOCKS, }, nil } func (p *AndroidSystemProxy) IsEnabled() bool { return p.isEnabled } func (p *AndroidSystemProxy) Enable() error { err := p.runAndroidShell("settings", "put", "global", "http_proxy", p.serverAddr.String()) if err != nil { return err } p.isEnabled = true return nil } func (p *AndroidSystemProxy) Disable() error { err := p.runAndroidShell("settings", "put", "global", "http_proxy", ":0") if err != nil { return err } p.isEnabled = false return nil } func (p *AndroidSystemProxy) runAndroidShell(name string, args ...string) error { if !p.useRish { return shell.Exec(name, args...).Attach().Run() } else { return shell.Exec("sh", p.rishPath, "-c", F.ToString(name, " ", strings.Join(args, " "))).Attach().Run() } } ================================================ FILE: common/settings/proxy_darwin.go ================================================ package settings import ( "context" "strconv" "strings" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/shell" "github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/service" ) type DarwinSystemProxy struct { monitor tun.DefaultInterfaceMonitor interfaceName string element *list.Element[tun.DefaultInterfaceUpdateCallback] serverAddr M.Socksaddr supportSOCKS bool isEnabled bool } func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*DarwinSystemProxy, error) { interfaceMonitor := service.FromContext[adapter.NetworkManager](ctx).InterfaceMonitor() if interfaceMonitor == nil { return nil, E.New("missing interface monitor") } proxy := &DarwinSystemProxy{ monitor: interfaceMonitor, serverAddr: serverAddr, supportSOCKS: supportSOCKS, } proxy.element = interfaceMonitor.RegisterCallback(proxy.routeUpdate) return proxy, nil } func (p *DarwinSystemProxy) IsEnabled() bool { return p.isEnabled } func (p *DarwinSystemProxy) Enable() error { return p.update0() } func (p *DarwinSystemProxy) Disable() error { interfaceDisplayName, err := getInterfaceDisplayName(p.interfaceName) if err != nil { return err } if p.supportSOCKS { err = shell.Exec("networksetup", "-setsocksfirewallproxystate", interfaceDisplayName, "off").Attach().Run() } if err == nil { err = shell.Exec("networksetup", "-setwebproxystate", interfaceDisplayName, "off").Attach().Run() } if err == nil { err = shell.Exec("networksetup", "-setsecurewebproxystate", interfaceDisplayName, "off").Attach().Run() } if err == nil { p.isEnabled = false } return err } func (p *DarwinSystemProxy) routeUpdate(defaultInterface *control.Interface, flags int) { if !p.isEnabled || defaultInterface == nil { return } _ = p.update0() } func (p *DarwinSystemProxy) update0() error { newInterface := p.monitor.DefaultInterface() if p.interfaceName == newInterface.Name { return nil } if p.interfaceName != "" { _ = p.Disable() } p.interfaceName = newInterface.Name interfaceDisplayName, err := getInterfaceDisplayName(p.interfaceName) if err != nil { return err } if p.supportSOCKS { err = shell.Exec("networksetup", "-setsocksfirewallproxy", interfaceDisplayName, p.serverAddr.AddrString(), strconv.Itoa(int(p.serverAddr.Port))).Attach().Run() } if err != nil { return err } err = shell.Exec("networksetup", "-setwebproxy", interfaceDisplayName, p.serverAddr.AddrString(), strconv.Itoa(int(p.serverAddr.Port))).Attach().Run() if err != nil { return err } err = shell.Exec("networksetup", "-setsecurewebproxy", interfaceDisplayName, p.serverAddr.AddrString(), strconv.Itoa(int(p.serverAddr.Port))).Attach().Run() if err != nil { return err } p.isEnabled = true return nil } func getInterfaceDisplayName(name string) (string, error) { content, err := shell.Exec("networksetup", "-listallhardwareports").ReadOutput() if err != nil { return "", err } for deviceSpan := range strings.SplitSeq(string(content), "Ethernet Address") { if strings.Contains(deviceSpan, "Device: "+name) { substr := "Hardware Port: " deviceSpan = deviceSpan[strings.Index(deviceSpan, substr)+len(substr):] deviceSpan = deviceSpan[:strings.Index(deviceSpan, "\n")] return deviceSpan, nil } } return "", E.New(name, " not found in networksetup -listallhardwareports") } ================================================ FILE: common/settings/proxy_linux.go ================================================ //go:build linux && !android package settings import ( "context" "os" "os/exec" "strings" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/shell" ) type LinuxSystemProxy struct { hasGSettings bool kWriteConfigCmd string sudoUser string serverAddr M.Socksaddr supportSOCKS bool isEnabled bool } func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*LinuxSystemProxy, error) { hasGSettings := common.Error(exec.LookPath("gsettings")) == nil kWriteConfigCmds := []string{ "kwriteconfig5", "kwriteconfig6", } var kWriteConfigCmd string for _, cmd := range kWriteConfigCmds { if common.Error(exec.LookPath(cmd)) == nil { kWriteConfigCmd = cmd break } } var sudoUser string if os.Getuid() == 0 { sudoUser = os.Getenv("SUDO_USER") } if !hasGSettings && kWriteConfigCmd == "" { return nil, E.New("unsupported desktop environment") } return &LinuxSystemProxy{ hasGSettings: hasGSettings, kWriteConfigCmd: kWriteConfigCmd, sudoUser: sudoUser, serverAddr: serverAddr, supportSOCKS: supportSOCKS, }, nil } func (p *LinuxSystemProxy) IsEnabled() bool { return p.isEnabled } func (p *LinuxSystemProxy) Enable() error { if p.hasGSettings { err := p.runAsUser("gsettings", "set", "org.gnome.system.proxy.http", "enabled", "true") if err != nil { return err } if p.supportSOCKS { err = p.setGnomeProxy("ftp", "http", "https", "socks") } else { err = p.setGnomeProxy("http", "https") } if err != nil { return err } err = p.runAsUser("gsettings", "set", "org.gnome.system.proxy", "use-same-proxy", F.ToString(p.supportSOCKS)) if err != nil { return err } err = p.runAsUser("gsettings", "set", "org.gnome.system.proxy", "mode", "manual") if err != nil { return err } } if p.kWriteConfigCmd != "" { err := p.runAsUser(p.kWriteConfigCmd, "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "ProxyType", "1") if err != nil { return err } if p.supportSOCKS { err = p.setKDEProxy("ftp", "http", "https", "socks") } else { err = p.setKDEProxy("http", "https") } if err != nil { return err } err = p.runAsUser(p.kWriteConfigCmd, "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "Authmode", "0") if err != nil { return err } err = p.runAsUser("dbus-send", "--type=signal", "/KIO/Scheduler", "org.kde.KIO.Scheduler.reparseSlaveConfiguration", "string:''") if err != nil { return err } } p.isEnabled = true return nil } func (p *LinuxSystemProxy) Disable() error { if p.hasGSettings { err := p.runAsUser("gsettings", "set", "org.gnome.system.proxy", "mode", "none") if err != nil { return err } } if p.kWriteConfigCmd != "" { err := p.runAsUser(p.kWriteConfigCmd, "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "ProxyType", "0") if err != nil { return err } err = p.runAsUser("dbus-send", "--type=signal", "/KIO/Scheduler", "org.kde.KIO.Scheduler.reparseSlaveConfiguration", "string:''") if err != nil { return err } } p.isEnabled = false return nil } func (p *LinuxSystemProxy) runAsUser(name string, args ...string) error { if os.Getuid() != 0 { return shell.Exec(name, args...).Attach().Run() } else if p.sudoUser != "" { return shell.Exec("su", "-", p.sudoUser, "-c", F.ToString(name, " ", strings.Join(args, " "))).Attach().Run() } else { return E.New("set system proxy: unable to set as root") } } func (p *LinuxSystemProxy) setGnomeProxy(proxyTypes ...string) error { for _, proxyType := range proxyTypes { err := p.runAsUser("gsettings", "set", "org.gnome.system.proxy."+proxyType, "host", p.serverAddr.AddrString()) if err != nil { return err } err = p.runAsUser("gsettings", "set", "org.gnome.system.proxy."+proxyType, "port", F.ToString(p.serverAddr.Port)) if err != nil { return err } } return nil } func (p *LinuxSystemProxy) setKDEProxy(proxyTypes ...string) error { for _, proxyType := range proxyTypes { var proxyUrl string if proxyType == "socks" { proxyUrl = "socks://" + p.serverAddr.String() } else { proxyUrl = "http://" + p.serverAddr.String() } err := p.runAsUser( p.kWriteConfigCmd, "--file", "kioslaverc", "--group", "Proxy Settings", "--key", proxyType+"Proxy", proxyUrl, ) if err != nil { return err } } return nil } ================================================ FILE: common/settings/proxy_stub.go ================================================ //go:build !(windows || linux || darwin) package settings import ( "context" "os" M "github.com/sagernet/sing/common/metadata" ) func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (SystemProxy, error) { return nil, os.ErrInvalid } ================================================ FILE: common/settings/proxy_windows.go ================================================ package settings import ( "context" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/wininet" ) type WindowsSystemProxy struct { serverAddr M.Socksaddr supportSOCKS bool isEnabled bool } func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*WindowsSystemProxy, error) { return &WindowsSystemProxy{ serverAddr: serverAddr, supportSOCKS: supportSOCKS, }, nil } func (p *WindowsSystemProxy) IsEnabled() bool { return p.isEnabled } func (p *WindowsSystemProxy) Enable() error { err := wininet.SetSystemProxy("http://"+p.serverAddr.String(), "") if err != nil { return err } p.isEnabled = true return nil } func (p *WindowsSystemProxy) Disable() error { err := wininet.ClearSystemProxy() if err != nil { return err } p.isEnabled = false return nil } ================================================ FILE: common/settings/system_proxy.go ================================================ package settings type SystemProxy interface { IsEnabled() bool Enable() error Disable() error } ================================================ FILE: common/settings/wifi.go ================================================ package settings import "github.com/sagernet/sing-box/adapter" type WIFIMonitor interface { ReadWIFIState() adapter.WIFIState Start() error Close() error } ================================================ FILE: common/settings/wifi_linux.go ================================================ package settings import ( "github.com/sagernet/sing-box/adapter" E "github.com/sagernet/sing/common/exceptions" ) type LinuxWIFIMonitor struct { monitor WIFIMonitor } func NewWIFIMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) { monitors := []func(func(adapter.WIFIState)) (WIFIMonitor, error){ newNetworkManagerMonitor, newIWDMonitor, newWpaSupplicantMonitor, newConnManMonitor, } var errors []error for _, factory := range monitors { monitor, err := factory(callback) if err == nil { return &LinuxWIFIMonitor{monitor: monitor}, nil } errors = append(errors, err) } return nil, E.Cause(E.Errors(errors...), "no supported WIFI manager found") } func (m *LinuxWIFIMonitor) ReadWIFIState() adapter.WIFIState { return m.monitor.ReadWIFIState() } func (m *LinuxWIFIMonitor) Start() error { if m.monitor != nil { return m.monitor.Start() } return nil } func (m *LinuxWIFIMonitor) Close() error { if m.monitor != nil { return m.monitor.Close() } return nil } ================================================ FILE: common/settings/wifi_linux_connman.go ================================================ //go:build linux package settings import ( "context" "strings" "time" "github.com/sagernet/sing-box/adapter" "github.com/godbus/dbus/v5" ) type connmanMonitor struct { conn *dbus.Conn callback func(adapter.WIFIState) cancel context.CancelFunc signalChan chan *dbus.Signal } func newConnManMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) { conn, err := dbus.ConnectSystemBus() if err != nil { return nil, err } cmObj := conn.Object("net.connman", "/") ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() call := cmObj.CallWithContext(ctx, "net.connman.Manager.GetServices", 0) if call.Err != nil { conn.Close() return nil, call.Err } return &connmanMonitor{conn: conn, callback: callback}, nil } func (m *connmanMonitor) ReadWIFIState() adapter.WIFIState { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() cmObj := m.conn.Object("net.connman", "/") var services []any err := cmObj.CallWithContext(ctx, "net.connman.Manager.GetServices", 0).Store(&services) if err != nil { return adapter.WIFIState{} } for _, service := range services { servicePair, ok := service.([]any) if !ok || len(servicePair) != 2 { continue } serviceProps, ok := servicePair[1].(map[string]dbus.Variant) if !ok { continue } typeVariant, hasType := serviceProps["Type"] if !hasType { continue } serviceType, ok := typeVariant.Value().(string) if !ok || serviceType != "wifi" { continue } stateVariant, hasState := serviceProps["State"] if !hasState { continue } state, ok := stateVariant.Value().(string) if !ok || (state != "online" && state != "ready") { continue } nameVariant, hasName := serviceProps["Name"] if !hasName { continue } ssid, ok := nameVariant.Value().(string) if !ok || ssid == "" { continue } bssidVariant, hasBSSID := serviceProps["BSSID"] if !hasBSSID { return adapter.WIFIState{SSID: ssid} } bssid, ok := bssidVariant.Value().(string) if !ok { return adapter.WIFIState{SSID: ssid} } return adapter.WIFIState{ SSID: ssid, BSSID: strings.ToUpper(strings.ReplaceAll(bssid, ":", "")), } } return adapter.WIFIState{} } func (m *connmanMonitor) Start() error { if m.callback == nil { return nil } ctx, cancel := context.WithCancel(context.Background()) m.cancel = cancel m.signalChan = make(chan *dbus.Signal, 10) m.conn.Signal(m.signalChan) err := m.conn.AddMatchSignal( dbus.WithMatchInterface("net.connman.Service"), dbus.WithMatchSender("net.connman"), ) if err != nil { return err } state := m.ReadWIFIState() go m.monitorSignals(ctx, m.signalChan, state) m.callback(state) return nil } func (m *connmanMonitor) monitorSignals(ctx context.Context, signalChan chan *dbus.Signal, lastState adapter.WIFIState) { for { select { case <-ctx.Done(): return case signal, ok := <-signalChan: if !ok { return } // godbus Signal.Name uses "interface.member" format (e.g. "net.connman.Service.PropertyChanged"), // not just the member name. This differs from the D-Bus signal member in the match rule. if signal.Name == "net.connman.Service.PropertyChanged" { state := m.ReadWIFIState() if state != lastState { lastState = state m.callback(state) } } } } } func (m *connmanMonitor) Close() error { if m.cancel != nil { m.cancel() } if m.signalChan != nil { m.conn.RemoveSignal(m.signalChan) close(m.signalChan) } if m.conn != nil { m.conn.RemoveMatchSignal( dbus.WithMatchInterface("net.connman.Service"), dbus.WithMatchSender("net.connman"), ) return m.conn.Close() } return nil } ================================================ FILE: common/settings/wifi_linux_iwd.go ================================================ //go:build linux package settings import ( "context" "strings" "time" "github.com/sagernet/sing-box/adapter" "github.com/godbus/dbus/v5" ) type iwdMonitor struct { conn *dbus.Conn callback func(adapter.WIFIState) cancel context.CancelFunc signalChan chan *dbus.Signal } func newIWDMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) { conn, err := dbus.ConnectSystemBus() if err != nil { return nil, err } iwdObj := conn.Object("net.connman.iwd", "/") ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() call := iwdObj.CallWithContext(ctx, "org.freedesktop.DBus.ObjectManager.GetManagedObjects", 0) if call.Err != nil { conn.Close() return nil, call.Err } return &iwdMonitor{conn: conn, callback: callback}, nil } func (m *iwdMonitor) ReadWIFIState() adapter.WIFIState { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() iwdObj := m.conn.Object("net.connman.iwd", "/") var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant err := iwdObj.CallWithContext(ctx, "org.freedesktop.DBus.ObjectManager.GetManagedObjects", 0).Store(&objects) if err != nil { return adapter.WIFIState{} } for _, interfaces := range objects { stationProps, hasStation := interfaces["net.connman.iwd.Station"] if !hasStation { continue } stateVariant, hasState := stationProps["State"] if !hasState { continue } state, ok := stateVariant.Value().(string) if !ok || state != "connected" { continue } connectedNetworkVariant, hasNetwork := stationProps["ConnectedNetwork"] if !hasNetwork { continue } networkPath, ok := connectedNetworkVariant.Value().(dbus.ObjectPath) if !ok || networkPath == "/" { continue } networkInterfaces, hasNetworkPath := objects[networkPath] if !hasNetworkPath { continue } networkProps, hasNetworkInterface := networkInterfaces["net.connman.iwd.Network"] if !hasNetworkInterface { continue } nameVariant, hasName := networkProps["Name"] if !hasName { continue } ssid, ok := nameVariant.Value().(string) if !ok { continue } connectedBSSVariant, hasBSS := stationProps["ConnectedAccessPoint"] if !hasBSS { return adapter.WIFIState{SSID: ssid} } bssPath, ok := connectedBSSVariant.Value().(dbus.ObjectPath) if !ok || bssPath == "/" { return adapter.WIFIState{SSID: ssid} } bssInterfaces, hasBSSPath := objects[bssPath] if !hasBSSPath { return adapter.WIFIState{SSID: ssid} } bssProps, hasBSSInterface := bssInterfaces["net.connman.iwd.BasicServiceSet"] if !hasBSSInterface { return adapter.WIFIState{SSID: ssid} } addressVariant, hasAddress := bssProps["Address"] if !hasAddress { return adapter.WIFIState{SSID: ssid} } bssid, ok := addressVariant.Value().(string) if !ok { return adapter.WIFIState{SSID: ssid} } return adapter.WIFIState{ SSID: ssid, BSSID: strings.ToUpper(strings.ReplaceAll(bssid, ":", "")), } } return adapter.WIFIState{} } func (m *iwdMonitor) Start() error { if m.callback == nil { return nil } ctx, cancel := context.WithCancel(context.Background()) m.cancel = cancel m.signalChan = make(chan *dbus.Signal, 10) m.conn.Signal(m.signalChan) err := m.conn.AddMatchSignal( dbus.WithMatchInterface("org.freedesktop.DBus.Properties"), dbus.WithMatchSender("net.connman.iwd"), ) if err != nil { return err } state := m.ReadWIFIState() go m.monitorSignals(ctx, m.signalChan, state) m.callback(state) return nil } func (m *iwdMonitor) monitorSignals(ctx context.Context, signalChan chan *dbus.Signal, lastState adapter.WIFIState) { for { select { case <-ctx.Done(): return case signal, ok := <-signalChan: if !ok { return } if signal.Name == "org.freedesktop.DBus.Properties.PropertiesChanged" { state := m.ReadWIFIState() if state != lastState { lastState = state m.callback(state) } } } } } func (m *iwdMonitor) Close() error { if m.cancel != nil { m.cancel() } if m.signalChan != nil { m.conn.RemoveSignal(m.signalChan) close(m.signalChan) } if m.conn != nil { m.conn.RemoveMatchSignal( dbus.WithMatchInterface("org.freedesktop.DBus.Properties"), dbus.WithMatchSender("net.connman.iwd"), ) return m.conn.Close() } return nil } ================================================ FILE: common/settings/wifi_linux_nm.go ================================================ //go:build linux package settings import ( "context" "strings" "time" "github.com/sagernet/sing-box/adapter" "github.com/godbus/dbus/v5" ) type networkManagerMonitor struct { conn *dbus.Conn callback func(adapter.WIFIState) cancel context.CancelFunc signalChan chan *dbus.Signal } func newNetworkManagerMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) { conn, err := dbus.ConnectSystemBus() if err != nil { return nil, err } nmObj := conn.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager") ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() var state uint32 err = nmObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager", "State").Store(&state) if err != nil { conn.Close() return nil, err } return &networkManagerMonitor{conn: conn, callback: callback}, nil } func (m *networkManagerMonitor) ReadWIFIState() adapter.WIFIState { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() nmObj := m.conn.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager") var activeConnectionPaths []dbus.ObjectPath err := nmObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager", "ActiveConnections").Store(&activeConnectionPaths) if err != nil || len(activeConnectionPaths) == 0 { return adapter.WIFIState{} } for _, connectionPath := range activeConnectionPaths { connObj := m.conn.Object("org.freedesktop.NetworkManager", connectionPath) var devicePaths []dbus.ObjectPath err = connObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.Connection.Active", "Devices").Store(&devicePaths) if err != nil || len(devicePaths) == 0 { continue } for _, devicePath := range devicePaths { deviceObj := m.conn.Object("org.freedesktop.NetworkManager", devicePath) var deviceType uint32 err = deviceObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.Device", "DeviceType").Store(&deviceType) if err != nil || deviceType != 2 { continue } var accessPointPath dbus.ObjectPath err = deviceObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.Device.Wireless", "ActiveAccessPoint").Store(&accessPointPath) if err != nil || accessPointPath == "/" { continue } apObj := m.conn.Object("org.freedesktop.NetworkManager", accessPointPath) var ssidBytes []byte err = apObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.AccessPoint", "Ssid").Store(&ssidBytes) if err != nil { continue } var hwAddress string err = apObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.AccessPoint", "HwAddress").Store(&hwAddress) if err != nil { continue } ssid := strings.TrimSpace(string(ssidBytes)) if ssid == "" { continue } return adapter.WIFIState{ SSID: ssid, BSSID: strings.ToUpper(strings.ReplaceAll(hwAddress, ":", "")), } } } return adapter.WIFIState{} } func (m *networkManagerMonitor) Start() error { if m.callback == nil { return nil } ctx, cancel := context.WithCancel(context.Background()) m.cancel = cancel m.signalChan = make(chan *dbus.Signal, 10) m.conn.Signal(m.signalChan) err := m.conn.AddMatchSignal( dbus.WithMatchSender("org.freedesktop.NetworkManager"), dbus.WithMatchInterface("org.freedesktop.DBus.Properties"), ) if err != nil { return err } state := m.ReadWIFIState() go m.monitorSignals(ctx, m.signalChan, state) m.callback(state) return nil } func (m *networkManagerMonitor) monitorSignals(ctx context.Context, signalChan chan *dbus.Signal, lastState adapter.WIFIState) { for { select { case <-ctx.Done(): return case signal, ok := <-signalChan: if !ok { return } if signal.Name == "org.freedesktop.DBus.Properties.PropertiesChanged" { state := m.ReadWIFIState() if state != lastState { lastState = state m.callback(state) } } } } } func (m *networkManagerMonitor) Close() error { if m.cancel != nil { m.cancel() } if m.signalChan != nil { m.conn.RemoveSignal(m.signalChan) close(m.signalChan) } if m.conn != nil { m.conn.RemoveMatchSignal( dbus.WithMatchSender("org.freedesktop.NetworkManager"), dbus.WithMatchInterface("org.freedesktop.DBus.Properties"), ) return m.conn.Close() } return nil } ================================================ FILE: common/settings/wifi_linux_wpa.go ================================================ //nolint:unused package settings import ( "bufio" "context" "fmt" "net" "os" "path/filepath" "strings" "sync" "sync/atomic" "time" "github.com/sagernet/sing-box/adapter" ) var wpaSocketCounter atomic.Uint64 type wpaSupplicantMonitor struct { socketPath string callback func(adapter.WIFIState) cancel context.CancelFunc monitorConn *net.UnixConn connMutex sync.Mutex } func newWpaSupplicantMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) { socketDirs := []string{"/var/run/wpa_supplicant", "/run/wpa_supplicant"} for _, socketDir := range socketDirs { entries, err := os.ReadDir(socketDir) if err != nil { continue } for _, entry := range entries { if entry.IsDir() || entry.Name() == "." || entry.Name() == ".." { continue } socketPath := filepath.Join(socketDir, entry.Name()) id := wpaSocketCounter.Add(1) localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-%d-%d", os.Getpid(), id), Net: "unixgram"} remoteAddr := &net.UnixAddr{Name: socketPath, Net: "unixgram"} conn, err := net.DialUnix("unixgram", localAddr, remoteAddr) if err != nil { continue } conn.Close() return &wpaSupplicantMonitor{socketPath: socketPath, callback: callback}, nil } } return nil, os.ErrNotExist } func (m *wpaSupplicantMonitor) ReadWIFIState() adapter.WIFIState { id := wpaSocketCounter.Add(1) localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-%d-%d", os.Getpid(), id), Net: "unixgram"} remoteAddr := &net.UnixAddr{Name: m.socketPath, Net: "unixgram"} conn, err := net.DialUnix("unixgram", localAddr, remoteAddr) if err != nil { return adapter.WIFIState{} } defer conn.Close() conn.SetDeadline(time.Now().Add(3 * time.Second)) status, err := m.sendCommand(conn, "STATUS") if err != nil { return adapter.WIFIState{} } var ssid, bssid string var connected bool scanner := bufio.NewScanner(strings.NewReader(status)) for scanner.Scan() { line := scanner.Text() if after, ok := strings.CutPrefix(line, "wpa_state="); ok { state := after connected = state == "COMPLETED" } else if after, ok := strings.CutPrefix(line, "ssid="); ok { ssid = after } else if after, ok := strings.CutPrefix(line, "bssid="); ok { bssid = after } } if !connected || ssid == "" { return adapter.WIFIState{} } return adapter.WIFIState{ SSID: ssid, BSSID: strings.ToUpper(strings.ReplaceAll(bssid, ":", "")), } } // sendCommand sends a command to wpa_supplicant and returns the response. // Commands are sent without trailing newlines per the wpa_supplicant control // interface protocol - the official wpa_ctrl.c sends raw command strings. func (m *wpaSupplicantMonitor) sendCommand(conn *net.UnixConn, command string) (string, error) { _, err := conn.Write([]byte(command)) if err != nil { return "", err } buf := make([]byte, 4096) n, err := conn.Read(buf) if err != nil { return "", err } response := string(buf[:n]) if strings.HasPrefix(response, "FAIL") { return "", os.ErrInvalid } return strings.TrimSpace(response), nil } func (m *wpaSupplicantMonitor) Start() error { if m.callback == nil { return nil } ctx, cancel := context.WithCancel(context.Background()) m.cancel = cancel state := m.ReadWIFIState() go m.monitorEvents(ctx, state) m.callback(state) return nil } func (m *wpaSupplicantMonitor) monitorEvents(ctx context.Context, lastState adapter.WIFIState) { var consecutiveErrors int var debounceTimer *time.Timer var debounceMutex sync.Mutex localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-mon-%d", os.Getpid()), Net: "unixgram"} remoteAddr := &net.UnixAddr{Name: m.socketPath, Net: "unixgram"} conn, err := net.DialUnix("unixgram", localAddr, remoteAddr) if err != nil { return } defer conn.Close() m.connMutex.Lock() m.monitorConn = conn m.connMutex.Unlock() // ATTACH/DETACH commands use os_strcmp() for exact matching in wpa_supplicant, // so they must be sent without trailing newlines. // See: https://w1.fi/cgit/hostap/tree/wpa_supplicant/ctrl_iface_unix.c _, err = conn.Write([]byte("ATTACH")) if err != nil { return } buf := make([]byte, 4096) n, err := conn.Read(buf) if err != nil || !strings.HasPrefix(string(buf[:n]), "OK") { return } for { select { case <-ctx.Done(): debounceMutex.Lock() if debounceTimer != nil { debounceTimer.Stop() } debounceMutex.Unlock() conn.Write([]byte("DETACH")) return default: } conn.SetReadDeadline(time.Now().Add(30 * time.Second)) n, err := conn.Read(buf) if err != nil { if netErr, ok := err.(net.Error); ok && netErr.Timeout() { continue } select { case <-ctx.Done(): return default: } consecutiveErrors++ if consecutiveErrors > 10 { return } time.Sleep(time.Second) continue } consecutiveErrors = 0 msg := string(buf[:n]) if strings.Contains(msg, "CTRL-EVENT-CONNECTED") || strings.Contains(msg, "CTRL-EVENT-DISCONNECTED") { debounceMutex.Lock() if debounceTimer != nil { debounceTimer.Stop() } debounceTimer = time.AfterFunc(500*time.Millisecond, func() { state := m.ReadWIFIState() if state != lastState { lastState = state m.callback(state) } }) debounceMutex.Unlock() } } } func (m *wpaSupplicantMonitor) Close() error { if m.cancel != nil { m.cancel() } m.connMutex.Lock() if m.monitorConn != nil { m.monitorConn.Close() } m.connMutex.Unlock() return nil } ================================================ FILE: common/settings/wifi_stub.go ================================================ //go:build !linux && !windows //nolint:unused package settings import ( "os" "github.com/sagernet/sing-box/adapter" ) type stubWIFIMonitor struct{} func NewWIFIMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) { return nil, os.ErrInvalid } func (m *stubWIFIMonitor) ReadWIFIState() adapter.WIFIState { return adapter.WIFIState{} } func (m *stubWIFIMonitor) Start() error { return nil } func (m *stubWIFIMonitor) Close() error { return nil } ================================================ FILE: common/settings/wifi_windows.go ================================================ //go:build windows package settings import ( "context" "fmt" "strings" "sync" "syscall" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing/common/winwlanapi" "golang.org/x/sys/windows" ) type windowsWIFIMonitor struct { handle windows.Handle callback func(adapter.WIFIState) cancel context.CancelFunc lastState adapter.WIFIState mutex sync.Mutex } func NewWIFIMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) { handle, err := winwlanapi.OpenHandle() if err != nil { return nil, err } interfaces, err := winwlanapi.EnumInterfaces(handle) if err != nil { winwlanapi.CloseHandle(handle) return nil, err } if len(interfaces) == 0 { winwlanapi.CloseHandle(handle) return nil, fmt.Errorf("no wireless interfaces found") } return &windowsWIFIMonitor{ handle: handle, callback: callback, }, nil } func (m *windowsWIFIMonitor) ReadWIFIState() adapter.WIFIState { interfaces, err := winwlanapi.EnumInterfaces(m.handle) if err != nil || len(interfaces) == 0 { return adapter.WIFIState{} } for _, iface := range interfaces { if iface.InterfaceState != winwlanapi.InterfaceStateConnected { continue } guid := iface.InterfaceGUID attrs, err := winwlanapi.QueryCurrentConnection(m.handle, &guid) if err != nil { continue } ssidLength := attrs.AssociationAttributes.SSID.Length if ssidLength == 0 || ssidLength > winwlanapi.Dot11SSIDMaxLength { continue } ssid := string(attrs.AssociationAttributes.SSID.SSID[:ssidLength]) bssid := formatBSSID(attrs.AssociationAttributes.BSSID) return adapter.WIFIState{ SSID: strings.TrimSpace(ssid), BSSID: bssid, } } return adapter.WIFIState{} } func formatBSSID(mac winwlanapi.Dot11MacAddress) string { return fmt.Sprintf("%02X%02X%02X%02X%02X%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]) } func (m *windowsWIFIMonitor) Start() error { if m.callback == nil { return nil } ctx, cancel := context.WithCancel(context.Background()) m.cancel = cancel m.lastState = m.ReadWIFIState() callbackFunc := func(data *winwlanapi.NotificationData, callbackContext uintptr) uintptr { if data.NotificationSource != winwlanapi.NotificationSourceACM { return 0 } switch data.NotificationCode { case winwlanapi.NotificationACMConnectionComplete, winwlanapi.NotificationACMDisconnected: m.checkAndNotify() } return 0 } callbackPointer := syscall.NewCallback(callbackFunc) err := winwlanapi.RegisterNotification(m.handle, winwlanapi.NotificationSourceACM, callbackPointer, 0) if err != nil { cancel() return err } go func() { <-ctx.Done() }() m.callback(m.lastState) return nil } func (m *windowsWIFIMonitor) checkAndNotify() { m.mutex.Lock() defer m.mutex.Unlock() state := m.ReadWIFIState() if state != m.lastState { m.lastState = state if m.callback != nil { m.callback(state) } } } func (m *windowsWIFIMonitor) Close() error { if m.cancel != nil { m.cancel() } winwlanapi.UnregisterNotification(m.handle) return winwlanapi.CloseHandle(m.handle) } ================================================ FILE: common/sniff/bittorrent.go ================================================ package sniff import ( "bytes" "context" "encoding/binary" "io" "os" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" E "github.com/sagernet/sing/common/exceptions" ) const ( trackerConnectFlag = 0 trackerProtocolID = 0x41727101980 trackerConnectMinSize = 16 ) // BitTorrent detects if the stream is a BitTorrent connection. // For the BitTorrent protocol specification, see https://www.bittorrent.org/beps/bep_0003.html func BitTorrent(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error { var first byte err := binary.Read(reader, binary.BigEndian, &first) if err != nil { return E.Cause1(ErrNeedMoreData, err) } if first != 19 { return os.ErrInvalid } const header = "BitTorrent protocol" var protocol [19]byte var n int n, err = reader.Read(protocol[:]) if string(protocol[:n]) != header[:n] { return os.ErrInvalid } if err != nil { return E.Cause1(ErrNeedMoreData, err) } if n < 19 { return ErrNeedMoreData } metadata.Protocol = C.ProtocolBitTorrent return nil } // UTP detects if the packet is a uTP connection packet. // For the uTP protocol specification, see // 1. https://www.bittorrent.org/beps/bep_0029.html // 2. https://github.com/bittorrent/libutp/blob/2b364cbb0650bdab64a5de2abb4518f9f228ec44/utp_internal.cpp#L112 func UTP(_ context.Context, metadata *adapter.InboundContext, packet []byte) error { // A valid uTP packet must be at least 20 bytes long. if len(packet) < 20 { return os.ErrInvalid } version := packet[0] & 0x0F ty := packet[0] >> 4 if version != 1 || ty > 4 { return os.ErrInvalid } // Validate the extensions extension := packet[1] reader := bytes.NewReader(packet[20:]) for extension != 0 { err := binary.Read(reader, binary.BigEndian, &extension) if err != nil { return err } if extension > 0x04 { return os.ErrInvalid } var length byte err = binary.Read(reader, binary.BigEndian, &length) if err != nil { return err } _, err = reader.Seek(int64(length), io.SeekCurrent) if err != nil { return err } } metadata.Protocol = C.ProtocolBitTorrent return nil } // UDPTracker detects if the packet is a UDP Tracker Protocol packet. // For the UDP Tracker Protocol specification, see https://www.bittorrent.org/beps/bep_0015.html func UDPTracker(_ context.Context, metadata *adapter.InboundContext, packet []byte) error { if len(packet) < trackerConnectMinSize { return os.ErrInvalid } if binary.BigEndian.Uint64(packet[:8]) != trackerProtocolID { return os.ErrInvalid } if binary.BigEndian.Uint32(packet[8:12]) != trackerConnectFlag { return os.ErrInvalid } metadata.Protocol = C.ProtocolBitTorrent return nil } ================================================ FILE: common/sniff/bittorrent_test.go ================================================ package sniff_test import ( "bytes" "context" "encoding/hex" "testing" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/sniff" C "github.com/sagernet/sing-box/constant" "github.com/stretchr/testify/require" ) func TestSniffBittorrent(t *testing.T) { t.Parallel() packets := []string{ "13426974546f7272656e742070726f746f636f6c0000000000100000e21ea9569b69bab33c97851d0298bdfa89bc90922d5554313631302dea812fcd6a3563e3be40c1d1", "13426974546f7272656e742070726f746f636f6c00000000001000052aa4f5a7e209e54b32803d43670971c4c8caaa052d5452333030302d653369733079647675763638", "13426974546f7272656e742070726f746f636f6c00000000001000052aa4f5a7e209e54b32803d43670971c4c8caaa052d5452343035302d6f7a316c6e79377931716130", } for _, pkt := range packets { pkt, err := hex.DecodeString(pkt) require.NoError(t, err) var metadata adapter.InboundContext err = sniff.BitTorrent(context.TODO(), &metadata, bytes.NewReader(pkt)) require.NoError(t, err) require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol) } } func TestSniffIncompleteBittorrent(t *testing.T) { t.Parallel() pkt, err := hex.DecodeString("13426974546f7272656e74") require.NoError(t, err) var metadata adapter.InboundContext err = sniff.BitTorrent(context.TODO(), &metadata, bytes.NewReader(pkt)) require.ErrorIs(t, err, sniff.ErrNeedMoreData) } func TestSniffNotBittorrent(t *testing.T) { t.Parallel() pkt, err := hex.DecodeString("13426974546f7272656e75") require.NoError(t, err) var metadata adapter.InboundContext err = sniff.BitTorrent(context.TODO(), &metadata, bytes.NewReader(pkt)) require.NotEmpty(t, err) require.NotErrorIs(t, err, sniff.ErrNeedMoreData) } func TestSniffUTP(t *testing.T) { t.Parallel() packets := []string{ "010041a282d7ee7b583afb160004000006d8318da776968f92d666f7963f32dae23ba0d2c810d8b8209cc4939f54fde9eeaa521c2c20c9ba7f43f4fb0375f28de06643b5e3ca4685ab7ac76adca99783be72ef05ed59ef4234f5712b75b4c7c0d7bee8fe2ca20ad626ba5bb0ffcc16bf06790896f888048cf72716419a07db1a3dca4550fbcea75b53e97235168a221cf3e553dfbb723961bd719fab038d86e0ecb74747f5a2cd669de1c4b9ad375f3a492d09d98cdfad745435625401315bbba98d35d32086299801377b93495a63a9efddb8d05f5b37a5c5b1c0a25e917f12007bb5e05013ada8aff544fab8cadf61d80ddb0b60f12741e44515a109d144fd53ef845acb4b5ccf0d6fc302d7003d76df3fc3423bb0237301c9e88f900c2d392a8e0fdb36d143cf7527a93fd0a2638b746e72f6699fffcd4fd15348fce780d4caa04382fd9faf1ca0ae377ca805da7536662b84f5ee18dd3ae38fcb095a7543e55f9069ae92c8cf54ae44e97b558d35e2545c66601ed2149cbc32bd6df199a2be7cf0da8b2ff137e0d23e776bc87248425013876d3a3cc31a83b424b752bd0346437f24b532978005d8f5b1b0be1a37a2489c32a18a9ad3118e3f9d30eb299bffae18e1f0677c2a5c185e62519093fe6bc2b7339299ea50a587989f726ca6443a75dd5bb936f6367c6355d80fae53ff529d740b2e5576e3eefdf1fdbfc69c3c8d8ac750512635de63e054bee1d3b689bc1b2bc3d2601e42a00b5c89066d173d4ae7ffedfd2274e5cf6d868fbe640aedb69b8246142f00b32d459974287537ddd5373460dcbc92f5cfdd7a3ed6020822ae922d947893752ca1983d0d32977374c384ac8f5ab566859019b7351526b9f13e932037a55bb052d9deb3b3c23317e0784fdc51a64f2159bfea3b069cf5caf02ee2c3c1a6b6b427bb16165713e8802d95b5c8ed77953690e994bd38c9ae113fedaf6ee7fc2b96c032ceafc2a530ad0422e84546b9c6ad8ef6ea02fa508abddd1805c38a7b42e9b7c971b1b636865ebec06ed754bb404cd6b4e6cc8cb77bd4a0c43410d5cd5ef8fe853a66d49b3b9e06cb141236cdbfdd5761601dc54d1250b86c660e0f898fe62526fdd9acf0eab60a3bbbb2151970461f28f10b31689594bea646c4b03ee197d63bdef4e5a7c22716b3bb9494a83b78ecd81b338b80ac6c09c43485b1b09ba41c74343832c78f0520c1d659ac9eb1502094141e82fb9e5e620970ebc0655514c43c294a7714cbf9a499d277daf089f556398a01589a77494bec8bfb60a108f3813b55368672b88c1af40f6b3c8b513f7c70c3e0efce85228b8b9ec67ba0393f9f7305024d8e2da6a26cf85613d14f249170ce1000089df4c9c260df7f8292aa2ecb5d5bac97656d59aa248caedea2d198e51ce87baece338716d114b458de02d65c9ff808ca5b5b73723b4d1e962d9ac2d98176544dc9984cf8554d07820ef3dd0861cfe57b478328046380de589adad94ee44743ffac73bb7361feca5d56f07cf8ce75080e261282ae30350d7882679b15cab9e7e53ddf93310b33f7390ae5d318bb53f387e6af5d0ef4f947fc9cb8e7e38b52c7f8d772ece6156b38d88796ea19df02c53723b44df7c76315a0de9462f27287e682d2b4cda1a68fe00d7e48c51ee981be44e1ca940fb5190c12655edb4a83c3a4f33e48a015692df4f0b3d61656e362aca657b5ae8c12db5a0db3db1e45135ee918b66918f40e53c4f83e9da0cddfe63f736ae751ab3837a30ae3220d8e8e311487093a7b90c7e7e40dd54ca750e19452f9193aa892aa6a6229ab493dadae988b1724f7898ee69c36d3eb7364c4adbeca811cfe2065873e78c2b6dfdf1595f7a7831c07e03cda82e4f86f76438dfb2b07c13638ce7b509cfa71b88b5102b39a203b423202088e1c2103319cb32c13c1e546ff8612fa194c95a7808ab767c265a1bd5fa0efed5c8ec1701876a00ec8", "01001ecb68176f215d04326300100000dbcf30292d14b54e9ee2d115ee5b8ebc7fad3e882d4fcdd0c14c6b917c11cb4c6a9f410b52a33ae97c2ac77c7a2b122b8955e09af3c5c595f1b2e79ca57cfe44c44e069610773b9bc9ba223d7f6b383e3adddd03fb88a8476028e30979c2ef321ffc97c5c132bcf9ac5b410bbb5ec6cefca3c7209202a14c5ae922b6b157b0a80249d13ffe5b996af0bc8e54ba576d148372494303e7ead0602b05b9c8fc97d48508a028a04d63a1fd28b0edfcd5c51715f63188b53eefede98a76912dca98518551a8856567307a56a702cbfcc115ea0c755b418bc2c7b57721239b82f09fb24328a4b0ce0f109bcb2a64e04b8aadb1f8487585425acdf8fc4ec8ea93cfcec5ac098bb29d42ddef6e46b03f34a5de28316726699b7cb5195c33e5c48abe87d591d63f9991c84c30819d186d6e0e95fd83c8dff07aa669c4430989bcaccfeacb9bcadbdb4d8f1964dbeb9687745656edd30b21c66cc0a1d742a78717d134a19a7f02d285a4973b1a198c00cfdff4676608dc4f3e817e3463c3b4e2c80d3e8d4fbac541a58a2fb7ad6939f607f8144eff6c8b0adc28ee5609ea158987519892fb", "21001ecb6817f2805d044fd700100000dbd03029", "410277ef0b1fb1f60000000000040000c233000000080000000000000000", } for _, pkt := range packets { pkt, err := hex.DecodeString(pkt) require.NoError(t, err) var metadata adapter.InboundContext err = sniff.UTP(context.TODO(), &metadata, pkt) require.NoError(t, err) require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol) } } func TestSniffUDPTracker(t *testing.T) { t.Parallel() connectPackets := []string{ "00000417271019800000000078e90560", "00000417271019800000000022c5d64d", "000004172710198000000000b3863541", } for _, pkt := range connectPackets { pkt, err := hex.DecodeString(pkt) require.NoError(t, err) var metadata adapter.InboundContext err = sniff.UDPTracker(context.TODO(), &metadata, pkt) require.NoError(t, err) require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol) } } func TestSniffNotUTP(t *testing.T) { t.Parallel() packets := []string{ "0102736470696e674958d580121500000000000079aaed6717a39c27b07c0c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", } for _, pkt := range packets { pkt, err := hex.DecodeString(pkt) require.NoError(t, err) var metadata adapter.InboundContext err = sniff.UTP(context.TODO(), &metadata, pkt) require.Error(t, err) } } ================================================ FILE: common/sniff/dns.go ================================================ package sniff import ( "context" "encoding/binary" "io" "os" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" mDNS "github.com/miekg/dns" ) func StreamDomainNameQuery(readCtx context.Context, metadata *adapter.InboundContext, reader io.Reader) error { var length uint16 err := binary.Read(reader, binary.BigEndian, &length) if err != nil { return E.Cause1(ErrNeedMoreData, err) } if length < 12 { return os.ErrInvalid } buffer := buf.NewSize(int(length)) defer buffer.Release() var n int n, err = buffer.ReadFullFrom(reader, buffer.FreeLen()) packet := buffer.Bytes() if n > 2 && packet[2]&0x80 != 0 { // QR return os.ErrInvalid } if n > 5 && packet[4] == 0 && packet[5] == 0 { // QDCOUNT return os.ErrInvalid } for i := 6; i < 10; i++ { // ANCOUNT, NSCOUNT if n > i && packet[i] != 0 { return os.ErrInvalid } } if err != nil { return E.Cause1(ErrNeedMoreData, err) } return DomainNameQuery(readCtx, metadata, packet) } func DomainNameQuery(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error { var msg mDNS.Msg err := msg.Unpack(packet) if err != nil || msg.Response || len(msg.Question) == 0 || len(msg.Answer) > 0 || len(msg.Ns) > 0 { return err } metadata.Protocol = C.ProtocolDNS return nil } ================================================ FILE: common/sniff/dns_test.go ================================================ package sniff_test import ( "bytes" "context" "encoding/hex" "testing" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/sniff" C "github.com/sagernet/sing-box/constant" "github.com/stretchr/testify/require" ) func TestSniffDNS(t *testing.T) { t.Parallel() query, err := hex.DecodeString("740701000001000000000000012a06676f6f676c6503636f6d0000010001") require.NoError(t, err) var metadata adapter.InboundContext err = sniff.DomainNameQuery(context.TODO(), &metadata, query) require.NoError(t, err) require.Equal(t, C.ProtocolDNS, metadata.Protocol) } func TestSniffStreamDNS(t *testing.T) { t.Parallel() query, err := hex.DecodeString("001e740701000001000000000000012a06676f6f676c6503636f6d0000010001") require.NoError(t, err) var metadata adapter.InboundContext err = sniff.StreamDomainNameQuery(context.TODO(), &metadata, bytes.NewReader(query)) require.NoError(t, err) require.Equal(t, C.ProtocolDNS, metadata.Protocol) } func TestSniffIncompleteStreamDNS(t *testing.T) { t.Parallel() query, err := hex.DecodeString("001e740701000001000000000000") require.NoError(t, err) var metadata adapter.InboundContext err = sniff.StreamDomainNameQuery(context.TODO(), &metadata, bytes.NewReader(query)) require.ErrorIs(t, err, sniff.ErrNeedMoreData) } func TestSniffNotStreamDNS(t *testing.T) { t.Parallel() query, err := hex.DecodeString("001e740701000000000000000000") require.NoError(t, err) var metadata adapter.InboundContext err = sniff.StreamDomainNameQuery(context.TODO(), &metadata, bytes.NewReader(query)) require.NotEmpty(t, err) require.NotErrorIs(t, err, sniff.ErrNeedMoreData) } ================================================ FILE: common/sniff/dtls.go ================================================ package sniff import ( "context" "os" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" ) func DTLSRecord(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error { const fixedHeaderSize = 13 if len(packet) < fixedHeaderSize { return os.ErrInvalid } contentType := packet[0] switch contentType { case 20, 21, 22, 23, 25: default: return os.ErrInvalid } versionMajor := packet[1] if versionMajor != 0xfe { return os.ErrInvalid } versionMinor := packet[2] if versionMinor != 0xff && versionMinor != 0xfd { return os.ErrInvalid } metadata.Protocol = C.ProtocolDTLS return nil } ================================================ FILE: common/sniff/dtls_test.go ================================================ package sniff_test import ( "context" "encoding/hex" "testing" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/sniff" C "github.com/sagernet/sing-box/constant" "github.com/stretchr/testify/require" ) func TestSniffDTLSClientHello(t *testing.T) { t.Parallel() packet, err := hex.DecodeString("16fefd0000000000000000007e010000720000000000000072fefd668a43523798e064bd806d0c87660de9c611a59bbdfc3892c4e072d94f2cafc40000000cc02bc02fc00ac014c02cc0300100003c000d0010000e0403050306030401050106010807ff01000100000a00080006001d00170018000b00020100000e000900060008000700010000170000") require.NoError(t, err) var metadata adapter.InboundContext err = sniff.DTLSRecord(context.Background(), &metadata, packet) require.NoError(t, err) require.Equal(t, metadata.Protocol, C.ProtocolDTLS) } func TestSniffDTLSClientApplicationData(t *testing.T) { t.Parallel() packet, err := hex.DecodeString("17fefd000100000000000100440001000000000001a4f682b77ecadd10f3f3a2f78d90566212366ff8209fd77314f5a49352f9bb9bd12f4daba0b4736ae29e46b9714d3b424b3e6d0234736619b5aa0d3f") require.NoError(t, err) var metadata adapter.InboundContext err = sniff.DTLSRecord(context.Background(), &metadata, packet) require.NoError(t, err) require.Equal(t, metadata.Protocol, C.ProtocolDTLS) } ================================================ FILE: common/sniff/http.go ================================================ package sniff import ( std_bufio "bufio" "context" "errors" "io" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/protocol/http" ) func HTTPHost(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error { request, err := http.ReadRequest(std_bufio.NewReader(reader)) if err != nil { if errors.Is(err, io.ErrUnexpectedEOF) { return E.Cause1(ErrNeedMoreData, err) } else { return err } } metadata.Protocol = C.ProtocolHTTP metadata.Domain = M.ParseSocksaddr(request.Host).AddrString() return nil } ================================================ FILE: common/sniff/http_test.go ================================================ package sniff_test import ( "context" "strings" "testing" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/sniff" "github.com/stretchr/testify/require" ) func TestSniffHTTP1(t *testing.T) { t.Parallel() pkt := "GET / HTTP/1.1\r\nHost: www.google.com\r\nAccept: */*\r\n\r\n" var metadata adapter.InboundContext err := sniff.HTTPHost(context.Background(), &metadata, strings.NewReader(pkt)) require.NoError(t, err) require.Equal(t, metadata.Domain, "www.google.com") } func TestSniffHTTP1WithPort(t *testing.T) { t.Parallel() pkt := "GET / HTTP/1.1\r\nHost: www.gov.cn:8080\r\nAccept: */*\r\n\r\n" var metadata adapter.InboundContext err := sniff.HTTPHost(context.Background(), &metadata, strings.NewReader(pkt)) require.NoError(t, err) require.Equal(t, metadata.Domain, "www.gov.cn") } ================================================ FILE: common/sniff/internal/qtls/qtls.go ================================================ package qtls import ( "crypto" "crypto/aes" "crypto/cipher" "encoding/binary" "io" "golang.org/x/crypto/hkdf" ) const ( VersionDraft29 = 0xff00001d Version1 = 0x1 Version2 = 0x6b3343cf ) var ( SaltOld = []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99} SaltV1 = []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a} SaltV2 = []byte{0x0d, 0xed, 0xe3, 0xde, 0xf7, 0x00, 0xa6, 0xdb, 0x81, 0x93, 0x81, 0xbe, 0x6e, 0x26, 0x9d, 0xcb, 0xf9, 0xbd, 0x2e, 0xd9} ) const ( HKDFLabelKeyV1 = "quic key" HKDFLabelKeyV2 = "quicv2 key" HKDFLabelIVV1 = "quic iv" HKDFLabelIVV2 = "quicv2 iv" HKDFLabelHeaderProtectionV1 = "quic hp" HKDFLabelHeaderProtectionV2 = "quicv2 hp" ) func AEADAESGCMTLS13(key, nonceMask []byte) cipher.AEAD { if len(nonceMask) != 12 { panic("tls: internal error: wrong nonce length") } aes, err := aes.NewCipher(key) if err != nil { panic(err) } aead, err := cipher.NewGCM(aes) if err != nil { panic(err) } ret := &xorNonceAEAD{aead: aead} copy(ret.nonceMask[:], nonceMask) return ret } type xorNonceAEAD struct { nonceMask [12]byte aead cipher.AEAD } func (f *xorNonceAEAD) NonceSize() int { return 8 } // 64-bit sequence number func (f *xorNonceAEAD) Overhead() int { return f.aead.Overhead() } func (f *xorNonceAEAD) Seal(out, nonce, plaintext, additionalData []byte) []byte { for i, b := range nonce { f.nonceMask[4+i] ^= b } result := f.aead.Seal(out, f.nonceMask[:], plaintext, additionalData) for i, b := range nonce { f.nonceMask[4+i] ^= b } return result } func (f *xorNonceAEAD) Open(out, nonce, ciphertext, additionalData []byte) ([]byte, error) { for i, b := range nonce { f.nonceMask[4+i] ^= b } result, err := f.aead.Open(out, f.nonceMask[:], ciphertext, additionalData) for i, b := range nonce { f.nonceMask[4+i] ^= b } return result, err } func HKDFExpandLabel(hash crypto.Hash, secret, context []byte, label string, length int) []byte { b := make([]byte, 3, 3+6+len(label)+1+len(context)) binary.BigEndian.PutUint16(b, uint16(length)) b[2] = uint8(6 + len(label)) b = append(b, []byte("tls13 ")...) b = append(b, []byte(label)...) b = b[:3+6+len(label)+1] b[3+6+len(label)] = uint8(len(context)) b = append(b, context...) out := make([]byte, length) n, err := hkdf.Expand(hash.New, secret, b).Read(out) if err != nil || n != length { panic("quic: HKDF-Expand-Label invocation failed unexpectedly") } return out } func ReadUvarint(r io.ByteReader) (uint64, error) { firstByte, err := r.ReadByte() if err != nil { return 0, err } // the first two bits of the first byte encode the length len := 1 << ((firstByte & 0xc0) >> 6) b1 := firstByte & (0xff - 0xc0) if len == 1 { return uint64(b1), nil } b2, err := r.ReadByte() if err != nil { return 0, err } if len == 2 { return uint64(b2) + uint64(b1)<<8, nil } b3, err := r.ReadByte() if err != nil { return 0, err } b4, err := r.ReadByte() if err != nil { return 0, err } if len == 4 { return uint64(b4) + uint64(b3)<<8 + uint64(b2)<<16 + uint64(b1)<<24, nil } b5, err := r.ReadByte() if err != nil { return 0, err } b6, err := r.ReadByte() if err != nil { return 0, err } b7, err := r.ReadByte() if err != nil { return 0, err } b8, err := r.ReadByte() if err != nil { return 0, err } return uint64(b8) + uint64(b7)<<8 + uint64(b6)<<16 + uint64(b5)<<24 + uint64(b4)<<32 + uint64(b3)<<40 + uint64(b2)<<48 + uint64(b1)<<56, nil } ================================================ FILE: common/sniff/ntp.go ================================================ package sniff import ( "context" "encoding/binary" "os" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" ) func NTP(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error { // NTP packets must be at least 48 bytes long (standard NTP header size). pLen := len(packet) if pLen < 48 { return os.ErrInvalid } // Check the LI (Leap Indicator) and Version Number (VN) in the first byte. // We'll primarily focus on ensuring the version is valid for NTP. // Many NTP versions are used, but let's check for generally accepted ones (3 & 4 for IPv4, plus potential extensions/customizations) firstByte := packet[0] li := (firstByte >> 6) & 0x03 // Extract LI vn := (firstByte >> 3) & 0x07 // Extract VN mode := firstByte & 0x07 // Extract Mode // Leap Indicator should be a valid value (0-3). if li > 3 { return os.ErrInvalid } // Version Check (common NTP versions are 3 and 4) if vn != 3 && vn != 4 { return os.ErrInvalid } // Check the Mode field for a client request (Mode 3). This validates it *is* a request. if mode != 3 { return os.ErrInvalid } // Check Root Delay and Root Dispersion. While not strictly *required* for a request, // we can check if they appear to be reasonable values (not excessively large). rootDelay := binary.BigEndian.Uint32(packet[4:8]) rootDispersion := binary.BigEndian.Uint32(packet[8:12]) // Check for unreasonably large root delay and dispersion. NTP RFC specifies max values of approximately 16 seconds. // Convert to milliseconds for easy comparison. Each unit is 1/2^16 seconds. if float64(rootDelay)/65536.0 > 16.0 { return os.ErrInvalid } if float64(rootDispersion)/65536.0 > 16.0 { return os.ErrInvalid } metadata.Protocol = C.ProtocolNTP return nil } ================================================ FILE: common/sniff/ntp_test.go ================================================ package sniff_test import ( "context" "encoding/hex" "os" "testing" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/sniff" C "github.com/sagernet/sing-box/constant" "github.com/stretchr/testify/require" ) func TestSniffNTP(t *testing.T) { t.Parallel() packet, err := hex.DecodeString("1b0006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") require.NoError(t, err) var metadata adapter.InboundContext err = sniff.NTP(context.Background(), &metadata, packet) require.NoError(t, err) require.Equal(t, metadata.Protocol, C.ProtocolNTP) } func TestSniffNTPFailed(t *testing.T) { t.Parallel() packet, err := hex.DecodeString("400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") require.NoError(t, err) var metadata adapter.InboundContext err = sniff.NTP(context.Background(), &metadata, packet) require.ErrorIs(t, err, os.ErrInvalid) } ================================================ FILE: common/sniff/quic.go ================================================ package sniff import ( "bytes" "context" "crypto" "crypto/aes" "crypto/tls" "encoding/binary" "io" "os" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/ja3" "github.com/sagernet/sing-box/common/sniff/internal/qtls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" "golang.org/x/crypto/hkdf" ) func QUICClientHello(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error { reader := bytes.NewReader(packet) typeByte, err := reader.ReadByte() if err != nil { return err } if typeByte&0x40 == 0 { return E.New("bad type byte") } var versionNumber uint32 err = binary.Read(reader, binary.BigEndian, &versionNumber) if err != nil { return err } if versionNumber != qtls.VersionDraft29 && versionNumber != qtls.Version1 && versionNumber != qtls.Version2 { return E.New("bad version") } packetType := (typeByte & 0x30) >> 4 if packetType == 0 && versionNumber == qtls.Version2 || packetType == 2 && versionNumber != qtls.Version2 || packetType > 2 { return E.New("bad packet type") } destConnIDLen, err := reader.ReadByte() if err != nil { return err } if destConnIDLen == 0 || destConnIDLen > 20 { return E.New("bad destination connection id length") } destConnID := make([]byte, destConnIDLen) _, err = io.ReadFull(reader, destConnID) if err != nil { return err } srcConnIDLen, err := reader.ReadByte() if err != nil { return err } _, err = io.CopyN(io.Discard, reader, int64(srcConnIDLen)) if err != nil { return err } tokenLen, err := qtls.ReadUvarint(reader) if err != nil { return err } _, err = io.CopyN(io.Discard, reader, int64(tokenLen)) if err != nil { return err } packetLen, err := qtls.ReadUvarint(reader) if err != nil { return err } hdrLen := int(reader.Size()) - reader.Len() if hdrLen+int(packetLen) > len(packet) { return os.ErrInvalid } _, err = io.CopyN(io.Discard, reader, 4) if err != nil { return err } pnBytes := make([]byte, aes.BlockSize) _, err = io.ReadFull(reader, pnBytes) if err != nil { return err } var salt []byte switch versionNumber { case qtls.Version1: salt = qtls.SaltV1 case qtls.Version2: salt = qtls.SaltV2 default: salt = qtls.SaltOld } var hkdfHeaderProtectionLabel string switch versionNumber { case qtls.Version2: hkdfHeaderProtectionLabel = qtls.HKDFLabelHeaderProtectionV2 default: hkdfHeaderProtectionLabel = qtls.HKDFLabelHeaderProtectionV1 } initialSecret := hkdf.Extract(crypto.SHA256.New, destConnID, salt) secret := qtls.HKDFExpandLabel(crypto.SHA256, initialSecret, []byte{}, "client in", crypto.SHA256.Size()) hpKey := qtls.HKDFExpandLabel(crypto.SHA256, secret, []byte{}, hkdfHeaderProtectionLabel, 16) block, err := aes.NewCipher(hpKey) if err != nil { return err } mask := make([]byte, aes.BlockSize) block.Encrypt(mask, pnBytes) newPacket := make([]byte, len(packet)) copy(newPacket, packet) newPacket[0] ^= mask[0] & 0xf for i := range newPacket[hdrLen : hdrLen+4] { newPacket[hdrLen+i] ^= mask[i+1] } packetNumberLength := newPacket[0]&0x3 + 1 if hdrLen+int(packetNumberLength) > int(packetLen)+hdrLen { return os.ErrInvalid } var packetNumber uint32 switch packetNumberLength { case 1: packetNumber = uint32(newPacket[hdrLen]) case 2: packetNumber = uint32(binary.BigEndian.Uint16(newPacket[hdrLen:])) case 3: packetNumber = uint32(newPacket[hdrLen+2]) | uint32(newPacket[hdrLen+1])<<8 | uint32(newPacket[hdrLen])<<16 case 4: packetNumber = binary.BigEndian.Uint32(newPacket[hdrLen:]) default: return E.New("bad packet number length") } extHdrLen := hdrLen + int(packetNumberLength) copy(newPacket[extHdrLen:hdrLen+4], packet[extHdrLen:]) data := newPacket[extHdrLen : int(packetLen)+hdrLen] var keyLabel string var ivLabel string switch versionNumber { case qtls.Version2: keyLabel = qtls.HKDFLabelKeyV2 ivLabel = qtls.HKDFLabelIVV2 default: keyLabel = qtls.HKDFLabelKeyV1 ivLabel = qtls.HKDFLabelIVV1 } key := qtls.HKDFExpandLabel(crypto.SHA256, secret, []byte{}, keyLabel, 16) iv := qtls.HKDFExpandLabel(crypto.SHA256, secret, []byte{}, ivLabel, 12) cipher := qtls.AEADAESGCMTLS13(key, iv) nonce := make([]byte, int32(cipher.NonceSize())) binary.BigEndian.PutUint64(nonce[len(nonce)-8:], uint64(packetNumber)) decrypted, err := cipher.Open(newPacket[extHdrLen:extHdrLen], nonce, data, newPacket[:extHdrLen]) if err != nil { return err } var frameType byte var fragments []qCryptoFragment decryptedReader := bytes.NewReader(decrypted) const ( frameTypePadding = 0x00 frameTypePing = 0x01 frameTypeAck = 0x02 frameTypeAck2 = 0x03 frameTypeCrypto = 0x06 frameTypeConnectionClose = 0x1c ) var frameTypeList []uint8 for { frameType, err = decryptedReader.ReadByte() if err == io.EOF { break } frameTypeList = append(frameTypeList, frameType) switch frameType { case frameTypePadding: continue case frameTypePing: continue case frameTypeAck, frameTypeAck2: _, err = qtls.ReadUvarint(decryptedReader) // Largest Acknowledged if err != nil { return err } _, err = qtls.ReadUvarint(decryptedReader) // ACK Delay if err != nil { return err } ackRangeCount, err := qtls.ReadUvarint(decryptedReader) // ACK Range Count if err != nil { return err } _, err = qtls.ReadUvarint(decryptedReader) // First ACK Range if err != nil { return err } for i := 0; i < int(ackRangeCount); i++ { _, err = qtls.ReadUvarint(decryptedReader) // Gap if err != nil { return err } _, err = qtls.ReadUvarint(decryptedReader) // ACK Range Length if err != nil { return err } } if frameType == 0x03 { _, err = qtls.ReadUvarint(decryptedReader) // ECT0 Count if err != nil { return err } _, err = qtls.ReadUvarint(decryptedReader) // ECT1 Count if err != nil { return err } _, err = qtls.ReadUvarint(decryptedReader) // ECN-CE Count if err != nil { return err } } case frameTypeCrypto: var offset uint64 offset, err = qtls.ReadUvarint(decryptedReader) if err != nil { return err } var length uint64 length, err = qtls.ReadUvarint(decryptedReader) if err != nil { return err } index := len(decrypted) - decryptedReader.Len() fragments = append(fragments, qCryptoFragment{offset, length, decrypted[index : index+int(length)]}) _, err = decryptedReader.Seek(int64(length), io.SeekCurrent) if err != nil { return err } case frameTypeConnectionClose: _, err = qtls.ReadUvarint(decryptedReader) // Error Code if err != nil { return err } _, err = qtls.ReadUvarint(decryptedReader) // Frame Type if err != nil { return err } var length uint64 length, err = qtls.ReadUvarint(decryptedReader) // Reason Phrase Length if err != nil { return err } _, err = decryptedReader.Seek(int64(length), io.SeekCurrent) // Reason Phrase if err != nil { return err } default: return os.ErrInvalid } } if metadata.SniffContext != nil { fragments = append(fragments, metadata.SniffContext.([]qCryptoFragment)...) metadata.SniffContext = nil } var frameLen uint64 for _, fragment := range fragments { frameLen += fragment.length } buffer := buf.NewSize(5 + int(frameLen)) defer buffer.Release() buffer.WriteByte(0x16) binary.Write(buffer, binary.BigEndian, uint16(0x0303)) binary.Write(buffer, binary.BigEndian, uint16(frameLen)) var index uint64 var length int find: for { for _, fragment := range fragments { if fragment.offset == index { buffer.Write(fragment.payload) index = fragment.offset + fragment.length length++ continue find } } break } metadata.Protocol = C.ProtocolQUIC fingerprint, err := ja3.Compute(buffer.Bytes()) if err != nil { metadata.SniffContext = fragments return E.Cause1(ErrNeedMoreData, err) } metadata.Domain = fingerprint.ServerName for metadata.Client == "" { if len(frameTypeList) == 1 { metadata.Client = C.ClientFirefox break } if frameTypeList[0] == frameTypeCrypto && isZero(frameTypeList[1:]) { if len(fingerprint.Versions) == 2 && fingerprint.Versions[0]&ja3.GreaseBitmask == 0x0A0A && len(fingerprint.EllipticCurves) == 5 && fingerprint.EllipticCurves[0]&ja3.GreaseBitmask == 0x0A0A { metadata.Client = C.ClientSafari break } if len(fingerprint.CipherSuites) == 1 && fingerprint.CipherSuites[0] == tls.TLS_AES_256_GCM_SHA384 && len(fingerprint.EllipticCurves) == 1 && fingerprint.EllipticCurves[0] == uint16(tls.X25519) && len(fingerprint.SignatureAlgorithms) == 1 && fingerprint.SignatureAlgorithms[0] == uint16(tls.ECDSAWithP256AndSHA256) { metadata.Client = C.ClientSafari break } } if frameTypeList[len(frameTypeList)-1] == frameTypeCrypto && isZero(frameTypeList[:len(frameTypeList)-1]) { metadata.Client = C.ClientQUICGo break } if count(frameTypeList, frameTypeCrypto) > 1 || count(frameTypeList, frameTypePing) > 0 { if isQUICGo(fingerprint) { metadata.Client = C.ClientQUICGo } else { metadata.Client = C.ClientChromium } break } metadata.Client = C.ClientUnknown //nolint:staticcheck break } return nil } func isZero(slices []uint8) bool { for _, slice := range slices { if slice != 0 { return false } } return true } func count(slices []uint8, value uint8) int { var times int for _, slice := range slices { if slice == value { times++ } } return times } type qCryptoFragment struct { offset uint64 length uint64 payload []byte } ================================================ FILE: common/sniff/quic_blacklist.go ================================================ package sniff import ( "slices" "github.com/sagernet/sing-box/common/ja3" ) const ( // X25519Kyber768Draft00 - post-quantum curve used by Go crypto/tls x25519Kyber768Draft00 uint16 = 0x11EC // 4588 // renegotiation_info extension used by Go crypto/tls extensionRenegotiationInfo uint16 = 0xFF01 // 65281 ) // isQUICGo detects native quic-go by checking for Go crypto/tls specific features. // Note: uQUIC with Chromium mimicry cannot be reliably distinguished from real Chromium // since it uses the same TLS fingerprint, so it will be identified as Chromium. func isQUICGo(fingerprint *ja3.ClientHello) bool { if slices.Contains(fingerprint.EllipticCurves, x25519Kyber768Draft00) { return true } return slices.Contains(fingerprint.Extensions, extensionRenegotiationInfo) } ================================================ FILE: common/sniff/quic_capture_test.go ================================================ package sniff_test import ( "context" "crypto/tls" "encoding/hex" "errors" "net" "testing" "time" "github.com/sagernet/quic-go" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/sniff" "github.com/stretchr/testify/require" ) func TestSniffQUICQuicGoFingerprint(t *testing.T) { t.Parallel() const testSNI = "test.example.com" udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0}) require.NoError(t, err) defer udpConn.Close() serverAddr := udpConn.LocalAddr().(*net.UDPAddr) packetsChan := make(chan [][]byte, 1) go func() { var packets [][]byte udpConn.SetReadDeadline(time.Now().Add(3 * time.Second)) for range 10 { buf := make([]byte, 2048) n, _, err := udpConn.ReadFromUDP(buf) if err != nil { break } packets = append(packets, buf[:n]) } packetsChan <- packets }() clientConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0}) require.NoError(t, err) defer clientConn.Close() tlsConfig := &tls.Config{ ServerName: testSNI, InsecureSkipVerify: true, NextProtos: []string{"h3"}, } ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() _, _ = quic.Dial(ctx, clientConn, serverAddr, tlsConfig, &quic.Config{}) select { case packets := <-packetsChan: t.Logf("Captured %d packets", len(packets)) var metadata adapter.InboundContext for i, pkt := range packets { err := sniff.QUICClientHello(context.Background(), &metadata, pkt) t.Logf("Packet %d: err=%v, domain=%s, client=%s", i, err, metadata.Domain, metadata.Client) if metadata.Domain != "" { break } } t.Logf("\n=== quic-go TLS Fingerprint Analysis ===") t.Logf("Domain: %s", metadata.Domain) t.Logf("Client: %s", metadata.Client) t.Logf("Protocol: %s", metadata.Protocol) // The client should be identified as quic-go, not chromium // Current issue: it's being identified as chromium if metadata.Client == "chromium" { t.Log("WARNING: quic-go is being misidentified as chromium!") } case <-time.After(5 * time.Second): t.Fatal("Timeout") } } func TestSniffQUICInitialFromQuicGo(t *testing.T) { t.Parallel() const testSNI = "test.example.com" // Create UDP listener to capture ALL initial packets udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0}) require.NoError(t, err) defer udpConn.Close() serverAddr := udpConn.LocalAddr().(*net.UDPAddr) // Channel to receive captured packets packetsChan := make(chan [][]byte, 1) // Start goroutine to capture packets go func() { var packets [][]byte udpConn.SetReadDeadline(time.Now().Add(3 * time.Second)) for range 5 { // Capture up to 5 packets buf := make([]byte, 2048) n, _, err := udpConn.ReadFromUDP(buf) if err != nil { break } packets = append(packets, buf[:n]) } packetsChan <- packets }() // Create QUIC client connection (will fail but we capture the initial packet) clientConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0}) require.NoError(t, err) defer clientConn.Close() tlsConfig := &tls.Config{ ServerName: testSNI, InsecureSkipVerify: true, NextProtos: []string{"h3"}, } ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() // This will fail (no server) but sends initial packet _, _ = quic.Dial(ctx, clientConn, serverAddr, tlsConfig, &quic.Config{}) // Wait for captured packets select { case packets := <-packetsChan: t.Logf("Captured %d QUIC packets", len(packets)) for i, packet := range packets { t.Logf("Packet %d: length=%d, first 30 bytes: %x", i, len(packet), packet[:min(30, len(packet))]) } // Test sniffer with first packet if len(packets) > 0 { var metadata adapter.InboundContext err := sniff.QUICClientHello(context.Background(), &metadata, packets[0]) t.Logf("First packet sniff error: %v", err) t.Logf("Protocol: %s", metadata.Protocol) t.Logf("Domain: %s", metadata.Domain) t.Logf("Client: %s", metadata.Client) // If first packet needs more data, try with subsequent packets // IMPORTANT: reuse metadata to accumulate CRYPTO fragments via SniffContext if errors.Is(err, sniff.ErrNeedMoreData) && len(packets) > 1 { t.Log("First packet needs more data, trying subsequent packets with shared context...") for i := 1; i < len(packets); i++ { // Reuse same metadata to accumulate fragments err = sniff.QUICClientHello(context.Background(), &metadata, packets[i]) t.Logf("Packet %d sniff result: err=%v, domain=%s, sniffCtx=%v", i, err, metadata.Domain, metadata.SniffContext != nil) if metadata.Domain != "" || (err != nil && !errors.Is(err, sniff.ErrNeedMoreData)) { break } } } // Print hex dump for debugging t.Logf("First packet hex:\n%s", hex.Dump(packets[0][:min(256, len(packets[0]))])) // Log final results t.Logf("Final: Protocol=%s, Domain=%s, Client=%s", metadata.Protocol, metadata.Domain, metadata.Client) // Verify SNI extraction if metadata.Domain == "" { t.Errorf("Failed to extract SNI, expected: %s", testSNI) } else { require.Equal(t, testSNI, metadata.Domain, "SNI should match") } // Check client identification - quic-go should be identified as quic-go, not chromium t.Logf("Client identified as: %s (expected: quic-go)", metadata.Client) } case <-time.After(5 * time.Second): t.Fatal("Timeout waiting for QUIC packets") } } ================================================ FILE: common/sniff/quic_test.go ================================================ package sniff_test import ( "context" "encoding/hex" "testing" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/sniff" C "github.com/sagernet/sing-box/constant" "github.com/stretchr/testify/require" ) func TestSniffQUICChromeNew(t *testing.T) { t.Parallel() pkt, err := hex.DecodeString("ca0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea4489ad89c322f75f9a383c90d126a0b21104cb519c2bb32e6a134e86896452e942b26c519b8c7ac9e4c99fae5e1f65cf08fb98443b30e4567932e8fb0789820d8f33037b59ac8113530258c9467dfb52489396dae01f099d28b234efa107fa411f2a1ffa2abe74988e03d662d4296024e95ce0fe1671724937157f77b84990478a2d4060676cf0827b4e8c600654111750414dafa0cccb332f3020c2922a015f445df5edc9c7d2d1ceea9fddcc9ff821c9183aa39a70da20fcc057579e1051c1c899148d6cf9d08b4919822082d040d1ce03ca4f216be6cb7ef03db6df0993ef1ccce5c8c648980554f41704526e1809d2545739f5872e75ec797db1c99f5682e2eda9363cb32aa367b7b363c782ddbacf874183cc15c8a2db068dd4093eebdd096ad33832a7939deb0a872279744f5a56dc001ba62fac973bf680f3b362bdd336add4dd102f462b773bf70bfce1921070a802a92025273a177186d1a643081b42175eb789ccddadb71033ef4feacbf6fd282ab622cf61669d73cda559e411c6ccdd8f003443b6933b7729b7a357aa4aa2fba0f365f829a4d497afb5dc2648a53bc9f3e786d955069d0a4781088a5463747dfe9958ea19ea444eae947ec6a67640955f710f93640084f3fbb8ad259b68dbc0ee0b7fab2d81bffd83ed8a6d33522dbfef43bec0a0fb4bdf1cb712dc4ced0680c0687fa240fd157baa232b1c84e14adce6421cf9270f9b3972f98fc67b344b8a4f1fb551e26f7f76d484ed9f8197f231dc5d9a44cc0ddce73d7f810a620851f4e97eb5037ab5135d7c3be5b80cc32d19910b8387aca64c93c02dc3e35238b78e6aff470722078982e58802844932b6041446bfdcc97ba640cbb86721bcd0f40f27b77aa6287ce5674ec1720134b9302875482c3269787e004b9edb483d44f326eef38c0e83cb46af96488c2e696bc2524567fb29c1e8edcd5a73615496d172d46a9d29e0505c0018b7bbb00165eca0389e09c4b1d73b6cc4a2f735a720650134a2e98e8105e20695cf231b92586237dfe0f99c897414e51c21627496276535f07abb53fb2b554376fe520fa45a3e944fd91dfe7a72aead08842b6b63d8edf861fb911954c83bd9a896eb9da4af5eff646455069d747facd4e77c254096843bff7c3e9031dbdf8dc37ea45f1122922fcbc322ec1378f3c7c1af0da62e1052e6210f1b23073f93a82d90e14cb20bc4501d487a1c848674d57a7c269b13590b3a99d8b8b4f6d0dfbd1d2cbbe7a32c0d5c84ae7ec438b0b19f3862d8fabaa828d06c7e3c6967405cd56a1ae90f38633e2ee0e3ecfca3df399fe12f029e0860a1a30da010300d0c94f0bf56091d00011488c1429928b21c739ebf50ba8be91116315d3173f6d2c56735722478c4d74392ba84d1727036b3d64e8c2263b0f33cb8086be587ca6b3940259c06afa2683868856529303ae12e91d7ca874568be7f2bfaa0656dfab0ed31ed90eaea10fb7f3433ec59a334abe6211d547fa0c825ac45d3691e749d15432008de83e9f6d98f368359137ae803d9189b3386f800c7c0cf4b615d1983cf82d9981a8105b60a80fe66c9b0d439b5ba153dd19e9e7483a01cf3b02b4597540b38e658d4eb8455e030b2bf2690bdd78c23f16fe5") require.NoError(t, err) var metadata adapter.InboundContext err = sniff.QUICClientHello(context.Background(), &metadata, pkt) require.Equal(t, metadata.Protocol, C.ProtocolQUIC) require.Empty(t, metadata.Client) require.ErrorIs(t, err, sniff.ErrNeedMoreData) pkt, err = hex.DecodeString("cc0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea44894b626c685cd5d5c965f7e97b3a1bdc520b75813e747f37a3ae83ad38b9ca2acb0de4fc9424839a50c8fb815a62b498609fbbc59145698860e0509cc08a04d1b119daef844ba2f09c16e2665e5cc0b47624b71f7b950c54fd56b4a1fbb826cba44eeeee3949ced8f5de60d4c81b19ee59f75aa1abb33f22c6b13c27095eb1e99cff01fdc93e6e88da2622ee18c08a79f508befd7e33e99bca60e64bef9a47b764384bd93823daeeb6fcb4d7cfbc4ab53eff59b3636f6dcaaf229b5a94941b5712807166b9bd5e82cb4a9708a71451c4cd6f6e33fb2fe40c8c70dd51a30b37ff9c5e35783debde0093fde19ce074b4887b3c90980b107b9c0f32cf61a66f37c251b789abc4d27fc421207966846c8cc7faa42d9af6ad355a6bc94cb78223b612be8b3e2a4df61fee83a674a0ceb8b7c3a29b97102cda22fecdf6a4628e5b612bc17eab64d6f75feedd0b106c0419e484e66725759964cb5935ac5125e5ae920cd280bd40df57c1d7ae1845700bd4eb7b7ab12bc0850950bfe6e69edd6ac1daa5db2c2b07484327196e561c513462d72872dc6771c39f6b60d46a1f2c92343b7338450a0ef8e39f97fa70652b3a12cd04043698951627aaaa82cc95e76df92021d30e8014c984f12eea0143de8b17e5e4a36ec07bf4814251b391f168a59ef75afcd2319249aaba930f06bb7a11b9491e6f71b3d5774a6503a965e94edd0a67737282fc9cb0271779ff14151b7aa9267bb8f7d643185512515aeea513c0c98bfae782381a3317064195d8825cf8b25c17cdab5fced02612a3f2870e40df57e6ca3f08228a2b04e8de1425eb4b970118f9bbdc212223ff86a5d6b648cdf2366722f21de4b14a1014879eadb69215cdb1aa2a9f4f310ecfe3116214fe3ab0a23f4775a0a54b48d7dfd8f7283ed687b3ac7e1a7e42a0bdc3478aba8651c03e1e9cc9df17d106b8130afe854269b0103b7a696f452721887b19d8181830073c9f10684c65f96d3a6c6efbae044eec03d6399e001fa44d54635dc72f9b8ea6b87d0f452cad1e1e32273e2b47c40f2730235adcae8523b8282f86b8cf1ab63ae54aaa06130df3bbf6ecac7d7d1d43d2a87aea837267ff8ccfaa4b7e47b7ded909e6603d0b928a304f8915c839153598adc4178eb48bc0e98ad7793d7980275e1e491ba4847a4a04ae30fe7f5cc7d4b6f4f63a525e9964d72245860ca76a668a4654adb6619f16e9db79131e5675b93cafb96c92f1da8464d4fef2a22e7f9db695965fe2cc27ea30974629c8fe17cfa2f860179e1eb9faaa88a91ec9ce6da28c1a2894c3b932b5e1c807146718cc77ca13c61eaae00c7c99e019f599772064b198c5c2c5e863336367673630b417ac845ddb7c93b0856317e5d64bab208c5730abc2c63536784fbeaaec139dffc917e775715f1e42164ddef5138d4d163609ab3fbdcab968f8738385c0e7e34ff3cf7771a1dc5ba25a8850fdf96dabafa21f9065f307457ce9af4b7a73450c9d20a3b46fa8d3a1163d22bd01a7d17f0ec274181bf9640fa941427694bfeb1346089f7a851efe0fbb7a2041fa6bb6541ccbad77dd3e1a97999fc05f1fef070e7b5c4b385b8b2a8cc32483fdeba6a373970de2fa4139ba18e5916f949aab0aab2894") require.NoError(t, err) err = sniff.QUICClientHello(context.Background(), &metadata, pkt) require.ErrorIs(t, err, sniff.ErrNeedMoreData) pkt, err = hex.DecodeString("c20000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea4489e2ff30c43a5f63beb2e4501ce7754085bcbe838003a0b4bccb53863c0766df7eac073c2bdc170772b157997945acdc2ab2e84750cc9aa0ffa0fdc023da7fc565a14f87f7c563dbc9183dd226aab79957d263f66e64b85a1b15a24516bd2c7c04eea4fa0a34ef9849c21585db2e4adb7c05e265c4f38d8ffe4cbed0f3b0e68f3693bf1f726c3fb135b8e32a5d22931d7c55fc2ff4b9a354933ab14544df3cdaf3e3217dfb8d7feb3465dc34df6320ea486f12e5b2d609aaa5f4515c20c86fc440f8087be0ee3d339835746ae2573c2afdee6bb6ef7e9eb541feae9209391b2902cfb0bdaccd9da8d290714638b7da588d4a656ca6eabba78b7363922d6037cf060b161a42019d4feb4156459103cffdeefd0e63114af2b0e0c39e70ebc7fecb8dd1ebb8d60b2137f509bb7dcef5f1d3e06ab1d391466652d57440a410fb4f58a6ce1fb62feb453241f64e110709f59a3d9ebdac94f811337d0e4a80fd6b56b2a70cd6eebbf98e1661291da6bf5beb8b8afc376dfd20eb76afe709e8e8f28e0ef82105954e346546ad25973df43f4acddbec0ffd9b215f62abebebf71305b5ea993560316f69430bf5afe50420340622f802b5830f3bcebffff04980c75a59d28902879e5d51a4fb21062a4ae13c42297075b21d54ee04303879c1157e7470c1451673c98a2f3921f2f3e8f6acfe85b01caaca66b59e5ebffbfe68e5e9ab17e9a1b857eb409df91cb76767fc1814fd3c522a9b117edd0b02526e469cb4afb291a4dcc74c79b47ec6e7ce558c597129366f83ec306b11d2598c705fd4ee9ee99df6b7039bef13b08fc6f26853ad213829d24f895747d45a47414f931c583fb6c3e4f6c27d0c2b81a5f3cee390ec6314e1fec637e8d28b675e97caafdfbf8c25d34a635083a7553d219dd80dbb39087d74c6ad6192ca6f48a3ff8d47db41b2a492c63fcd780012780931dae0a325f9dcbd772d09a700f132c4bc1d9809b25b9751b694eb72a8ba4db7208d2b1bab63e1845208e4f841ea30218a559db98751589716b6d059ca673378f5fe7c7d8a1c82e14a561c47313bbcc278412ba86ffb2b87ec308eab9df696f5b4b54f8e361731bf232820a02a35fda7e5d4bf01b8f005ad299a055116e7b23c181f15a66442cf6032ca477bccc55b79d424eb4f245847bd81a581dc369dd20b1a4892733bde3c38e492c0039f69f2b947a4dc251a49ee7ccc0f36b3b75a555fa1d126db75f94dab60f52f6b15a877a0c380b59f82d35c570bc5f8051e9ef87db51f52383d47b50829b7f9e947ccc67aa280566aa48b4a85c1c7eca6f542789d8abcc050f1aa3cc221b6859656a21454aa21c7bfb9d12115f61c3ed46263ade68a8d3679fa62a659a5da7817406bd16618fccf33ed208ada1b03584e8b485d3cb6ed80a0774e60b6cd55aff64169ea998cf8235997049515abac58e0169ca07fb1c8c4c8b2803ba9d27b44c045d0a1cac86e5e188195c68001f53eb44851b6d821fc01ccbb41e27f38e6ddd66540c2d62ed6e0d551e22c0f26b60078c74a6302a1ed3d9e8fc0861257a63f6ac4e759fd54bff088becd28e30944a6c15db4fc8ae6244346869add946d9d92c430d737e042fa18b28a8ed64d1e8987ad9061cdc1335f") require.NoError(t, err) err = sniff.QUICClientHello(context.Background(), &metadata, pkt) require.NoError(t, err) require.Equal(t, "www.google.com", metadata.Domain) } func TestSniffQUICChromium(t *testing.T) { t.Parallel() pkt, err := hex.DecodeString("c30000000108f40d654cc09b27f5000044d08a94548e57e43cc5483f129986187c432d58d46674830442988f869566a6e31e2ae37c9f7acbf61cc81621594fab0b3dfdc1635460b32389563dc8e74006315661cd22694114612973c1c45910621713a48b375854f095e8a77ccf3afa64e972f0f7f7002f50e0b014b1b146ea47c07fb20b73ad5587872b51a0b3fafdf1c4cf4fe6f8b112142392efa25d993abe2f42582be145148bdfe12edcd96c3655b65a4781b093e5594ba8e3ae5320f12e8314fc3ca374128cc43381046c322b964681ed4395c813b28534505118201459665a44b8f0abead877de322e9040631d20b05f15b81fa7ff785d4041aecc37c7e2ccdc5d1532787ce566517e8985fd5c200dbfd1e67bc255efaba94cfc07bb52fea4a90887413b134f2715b5643542aa897c6116486f428d82da64d2a2c1e1bdd40bd592558901a554b003d6966ac5a7b8b9413eddbf6ef21f28386c74981e3ce1d724c341e95494907626659692720c81114ca4acea35a14c402cfa3dc2228446e78dc1b81fa4325cf7e314a9cad6a6bdff33b3351dcba74eb15fae67f1227283aa4cdd64bcadf8f19358333f8549b596f4350297b5c65274565869d497398339947b9d3d064e5b06d39d34b436d8a41c1a3880de10bd26c3b1c5b4e2a49b0d4d07b8d90cd9e92bc611564d19ea8ec33099e92033caf21f5307dbeaa4708b99eb313bff99e2081ac25fd12d6a72e8335e0724f6718fe023cd0ad0d6e6a6309f09c9c391eec2bc08e9c3210a043c08e1759f354c121f6517fff4d6e20711a871e41285d48d930352fddffb92c96ba57df045ce99f8bfdfa8edc0969ce68a51e9fbb4f54b956d9df74a9e4af27ed2b27839bce1cffeca8333c0aaee81a570217442f9029ba8fedb84a2cf4be4d910982d891ea00e816c7fb98e8020e896a9c6fdd9106611da0a99dde18df1b7a8f6327acb1eed9ad93314451e48cb0dfb9571728521ca3db2ac0968159d5622556a55d51a422d11995b650949aaefc5d24c16080446dfc4fbc10353f9f93ce161ab513367bb89ab83988e0630b689e174e27bcfcc31996ee7b0bca909e251b82d69a28fee5a5d662e127508cd19dbbe5097b7d5b62a49203d66764197a527e472e2627e44a93d44177dace9d60e7d0e03305ddf4cfe47cdf2362e14de79ef46a6763ce696cd7854a48d9419a0817507a4713ffd4977b906d4f2b5fb6dbe1bd15bc505d5fea582190bf531a45d5ee026da8918547fd5105f15e5d061c7b0cf80a34990366ed8e91e13c2f0d85e5dad537298808d193cf54b7eaac33f10051f74cb6b75e52f81618c36f03d86aef613ba237a1a793ba1539938a38f62ccaf7bd5f6c5e0ce53cde4012fcf2b758214a0422d2faaa798e86e19d7481b42df2b36a73d287ff28c20cce01ce598771fec16a8f1f00305c06010126013a6c1de9f589b4e79d693717cd88ad1c42a2d99fa96617ba0bc6365b68e21a70ebc447904aa27979e1514433cfd83bfec09f137c747d47582cb63eb28f873fb94cf7a59ff764ddfbb687d79a58bb10f85949269f7f72c611a5e0fbb52adfa298ff060ec2eb7216fd7302ea8fb07798cbb3be25cb53ac8161aac2b5bbcfbcfb01c113d28bd1cb0333fb89ac82a95930f7abded0a2f5a623cc6a1f62bf3f38ef1b81c1e50a634f657dbb6770e4af45879e2fb1e00c742e7b52205c8015b5c0f5b1e40186ff9aa7288ab3e01a51fb87761f9bc6837082af109b39cc9f620") require.NoError(t, err) var metadata adapter.InboundContext err = sniff.QUICClientHello(context.Background(), &metadata, pkt) require.Equal(t, metadata.Protocol, C.ProtocolQUIC) require.Empty(t, metadata.Client) require.ErrorIs(t, err, sniff.ErrNeedMoreData) pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28") require.NoError(t, err) err = sniff.QUICClientHello(context.Background(), &metadata, pkt) require.NoError(t, err) require.Equal(t, metadata.Domain, "google.com") } func TestSniffUQUICChrome115(t *testing.T) { t.Parallel() pkt, err := hex.DecodeString("cb0000000108181e17c387120abc000044d0705b6a3ef9ee37a8d3949a7d393ed078243c2ee2c3627fad1c3f107c117f4f071131ad61848068fcbbe5c65803c147f7f8ec5e2cd77b77beea23ba779d936dccac540f8396400e3190ea35cc2942af4171a04cb14272491920f90124959f44e80143678c0b52f5d31af319aaa589db2f940f004562724d0af40f737e1bb0002a071e6a1dbc9f52c64f070806a5010abed0298053634d9c9126bd7949ae5087998ade762c0ad06691d99c0875a38c601fc1ee77bfc3b8c11381829f2c9bdd022f4499c43ff1d6aee1a0d296861461dda217d22c568b276016ef3929e59d2f7d7ddf7809920fb7dc805641608949f3f8466ab3d37149aac501f0b107d808f3add4acfc657e4a82e2b88e97a6c74a00c419548760ab3414ba13915c78a1ca79dceee8d59fbe299f20b671ac44823218368b2a026baa55170cf549519ac21dbb6d31d248bd339438a4e663bcdca1fe3ae3f045a5dc19b122e9db9d7af9757076666dda4e9ace1c67def77fa14786f0cab3ebf7a270ea6e2b37838318c95779f80c3b8471948d0046c3614b3a13477c939a39a7855d85d13522a45ae0765739cd5eedef87237e824a929983ace27640c6495dbf5a72fa0b96893dc5d28f3988249a57bdb458d460b4a57043de3da750a76b6e5d2259247ca27cd864ea18f0d09aa62ab6eb7c014fb43179b2a1963d170b756cce83eeaebff78a828d025c811848e16ff862a8080d093478cd2208c8ab0803178325bc0d9d6bb25e62fa50c4ad15cf80916da6578796932036c72e43eb480d1e423ed812ac75a97722f8416529b82ba8ee2219c535012282bb17066bd53e78b87a71abdb7ebdb2a7c2766ff8397962e87d0f85485b64b4ee81cc84f99c47f33f2b0872716441992773f59186e38d32dbf5609a6fda94cb928cd25f5a7a3ab736b5a4236b6d5409ab18892c6a4d3480fc2350abfdf0bab1cedb55bdf0760fdb703e6688f4de596254eed4ed3e67eb03d0717b8e15b31e735214e588c87ae36bc6c310e1894b4c15143e4ccf287b2dbc707a946bf9671ae3c574f9486b2c82eec784bba4cbc76113cbe0f97ac8c13cfa38f2925ab9d06887a612ce48280a91d7e074e6caf898d88e2bbf71360899abf48a03f9a70cf2891199f2d63b116f4871af0ebb4f4906792f66cc21d1609f189138532875c129a68c73e7bcd3b5d8100beac1d8ac4b20d94a59ac8df5a5af58a9acb20413eadf97189f5f19ff889155f0c4d37514ec184eb6903967ff38a41fc087abb0f2cad3761d6e3f95f92a09a72f5c065b16e188088b87460241f27ecdb1bc6ece92c8d36b2d68b58d0fb4d4b3c928c579ade8ae5a995833aadd297c30a37f7bc35440fc97070e1b198e0fac00157452177d16d2803b4239997452b4ad3a951173bdec47a033fd7f8a7942accaa9aaa905b3c5a2175e7c3e07c48bf25331727fd69cd1e64d74d8c9d4a6f8f4491adb7bc911505cb19877083d8f21a12475e313fccf57877ff3556318e81ed9145dd9427f2b65275440893035f417481f721c69215af8ae103530cd0a1d35bf2cb5a27628f8d44d7c6f5ec12ce79d0a8333e0eb48771115d0a191304e46b8db19bbe5c40f1c346dde98e76ff5e21ff38d2c34e60cb07766ed529dd6d2cbacd7fbf1ed8a0e6e40decad0ca5021e91552be87c156d3ae2fffef41c65b14ba6d488f2c3227a1ab11ffce0e2dc47723a69da27a67a7f26e1cb13a7103af9b87a8db8e18ea") require.NoError(t, err) var metadata adapter.InboundContext err = sniff.QUICClientHello(context.Background(), &metadata, pkt) require.NoError(t, err) require.Equal(t, metadata.Protocol, C.ProtocolQUIC) require.Equal(t, metadata.Client, C.ClientChromium) require.Equal(t, metadata.Domain, "www.google.com") } func TestSniffQUICFirefox(t *testing.T) { t.Parallel() pkt, err := hex.DecodeString("c8000000010867f174d7ebfe1b0803cd9c20004286de068f7963cf1736349ee6ebe0ddcd3e4cd0041a51ced3f7ce9eea1fb595458e74bdb4b792b16449bd8cae71419862c4fcbe766eaec7d1af65cd298e1dd46f8bd94a77ab4ca28c54b8e9773de3f02d7cb2463c9f7dcacfb311f024b0266ec6ab7bfb615b4148333fb4d4ece7c4cd90029ca30c2cbae2216b428499ec873fa125797e71c5a5da85087760ad37ca610020f71b76e82651c47576e20bf33cf676cb2d400b8c09d3c8cb4e21c47d2b21f6b68732bef30c8cefd5c723fc23eb29e6f7f65a5e52aad9055c1fb3d8b1811f0380b38d7e2eee8eb37dd5bd5d4ca4b66540175d916289d88a9df7c161964d713999c5057d27edb298ef5164352568b0d4bac3c15d90456e8fd460e41b81d0ec1b1e94b87d3333cc6908b018e0914ae1f214d73e75398da3d55a0106161d3a75897b4eb66e98c59010fae75f0d367d38be48c3a5c58bc8a30773c3fff50690ac9d487822f85d4f5713d626baa92d36e858dd21259cf814bce0b90d18da88a1ade40113e5a088cdb304a2558879152a8cf15c1839e056378aa41acba6fcb9974dee54bd50b5d4eb2c475654e06c0ec06b7f18f4462c808684843a1071041b9bfb2688324e0120144944416e30e83eedbbbcbc275b1f53762d3db18f0998ce54f0e1c512946b4098f07781d49264fa148f4c8220a3b02e73d7f15554aa370aafeff73cb75c52c494edf90f0261abfdd32a4d670f729de50266162687aa8efe14b8506f313b058b02aaaab5825428f5f4510b8e49451fdcb7b5a4af4b59c831afcb89fb4f64dba78e3b38387e87e9e8cdaa1f3b700a87c7d442388863b8950296e5773b38f308d62f52548c0bbf308e40540747cca5bf99b1345bc0d70b8f0e69a83b85a8d69f795b87f93e2bfccf52b529afea4ff6fd456957000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") require.NoError(t, err) var metadata adapter.InboundContext err = sniff.QUICClientHello(context.Background(), &metadata, pkt) require.NoError(t, err) require.Equal(t, metadata.Protocol, C.ProtocolQUIC) require.Equal(t, metadata.Client, C.ClientFirefox) require.Equal(t, metadata.Domain, "www.google.com") } func TestSniffQUICSafari(t *testing.T) { t.Parallel() pkt, err := hex.DecodeString("c70000000108e4e75af2e223198a0000449ef2d83cb4473a62765eba67424cd4a5817315cbf55a9e8daaca360904b0bae60b1629cfeba11e2dfbbf5ea4c588cb134e31af36fd7a409fb0fcc0187e9b56037ac37964ed20a8c1ca19fd6cfd53398324b3d0c71537294f769db208fa998b6811234a4a7eb3b5eceb457ae92e3a2d98f7c110702db8064b5c29fa3298eb1d0529fd445a84a5fd6ff8709be90f8af4f94998d8a8f2953bb05ad08c80668eca784c6aec959114e68e5b827e7c41c79f2277c716a967e7fcc8d1b77442e6cb18329dbedb34b473516b468cba5fc20659e655fbe37f36408289b9a475fcee091bd82828d3be00367e9e5cec9423bb97854abdada1d7562a3777756eb3bddef826ddc1ef46137cb01bb504a54d410d9bcb74cd5f959050c84edf343fa6a49708c228a758ee7adbbadf260b2f1984911489712e2cb364a3d6520badba4b7e539b9c163eeddfd96c0abb0de151e47496bb9750be76ee17ccdb61d35d2c6795174037d6f9d282c3f36c4d9a90b64f3b6ddd0cf4d9ed8e6f7805e25928fa04b087e63ae02761df30720cc01dfc32b64c575c8a66ef82e9a17400ff80cd8609b93ba16d668f4aa734e71c4a5d145f14ee1151bec970214e0ff83fc3e1e85d8694f2975f9155c57c18b7b69bb6a36832a9435f1f4b346a7be188f3a75f9ad2cc6ad0a3d26d6fa7d4c1179bd49bd5989d15ba43ff602890107db96484695086627356750d7b2b3b714ba65d564654e8f60ac10f5b6d3bfb507e8eaa31bab1da2d676195046d165c7f8b32829c9f9b68d97b2af7ac04a1369357e4b65de2b2f24eaf27cc8d95e05db001adebe726f927a94e43e62ce671e6e306e16f05aafcbe6c49080e80286d7939f375023d110a5ad9069364ae928ca480454a9dcddd61bc48b7efeb716a5bd6c7cd39c486ceb20c738af6abf22ba1ddd8b4a3b781fc2f251173409e1aadccbd7514e97106d0ebfc3af6e59445f74cd733a1ba99b10fce3fb4e9f7c88f5e25b567f5ba2b8dabacd375e7faf7634bfa178cbe51aee63032c5126b196ea47b02385fc3062a000fb7e4b4d0d12e74579f8830ede20d10829496032b2cc56743287f9a9b4d5091877a82fea44deb2cffac8a379f78a151d99e28cbc74d732c083bf06d50584e3f18f254e71a48d6ababaf6fff6f425e9be001510dfbe6a32a27792c00ada036b62ddb90c706d7b882c76a7072f5dd11c69a1f49d4ba183cb0b57545419fa27b9b9706098848935ae9c9e8fbe9fac165d1339128b991a73d20e7795e8d6a8c6adfbf20bf13ada43f2aef3ba78c14697910507132623f721387dce60c4707225b84d9782d469a5d9eaa099f35d6a590ef142ddef766495cf3337815ceef5ff2b3ed352637e72b5c23a2a8ff7d7440236a19b981d47f8e519a0431ebfbc0b78d8a36798b4c060c0c6793499f1e2e818862560a5b501c8d02ba1517be1941da2af5b174e0189c62978d878eb0f9c9db3a9221c28fb94645cf6e85ff2eea8c65ba3083a7382b131b83102dd67aa5453ad7375a4eb8c69fc479fbd29dab8924f801d253f2c997120b705c6e5217fb74702e2f1038917dd5fb0eeb7ae1bf7a668fc7d50c034b4cd5a057a8482e6bc9c921297f44e76967265623a167cd9883eb6e64bc77856dc333bd605d7df3bed0e5cecb5a99fe8b62873d58530f") require.NoError(t, err) var metadata adapter.InboundContext err = sniff.QUICClientHello(context.Background(), &metadata, pkt) require.NoError(t, err) require.Equal(t, metadata.Protocol, C.ProtocolQUIC) require.Equal(t, metadata.Client, C.ClientSafari) require.Equal(t, metadata.Domain, "www.google.com") } func FuzzSniffQUIC(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte) { var metadata adapter.InboundContext err := sniff.QUICClientHello(context.Background(), &metadata, data) require.Error(t, err) }) } ================================================ FILE: common/sniff/rdp.go ================================================ package sniff import ( "context" "encoding/binary" "io" "os" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/rw" ) func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error { var tpktVersion uint8 err := binary.Read(reader, binary.BigEndian, &tpktVersion) if err != nil { return E.Cause1(ErrNeedMoreData, err) } if tpktVersion != 0x03 { return os.ErrInvalid } var tpktReserved uint8 err = binary.Read(reader, binary.BigEndian, &tpktReserved) if err != nil { return E.Cause1(ErrNeedMoreData, err) } if tpktReserved != 0x00 { return os.ErrInvalid } var tpktLength uint16 err = binary.Read(reader, binary.BigEndian, &tpktLength) if err != nil { return E.Cause1(ErrNeedMoreData, err) } if tpktLength != 19 { return os.ErrInvalid } var cotpLength uint8 err = binary.Read(reader, binary.BigEndian, &cotpLength) if err != nil { return E.Cause1(ErrNeedMoreData, err) } if cotpLength != 14 { return os.ErrInvalid } var cotpTpduType uint8 err = binary.Read(reader, binary.BigEndian, &cotpTpduType) if err != nil { return E.Cause1(ErrNeedMoreData, err) } if cotpTpduType != 0xE0 { return os.ErrInvalid } err = rw.SkipN(reader, 5) if err != nil { return E.Cause1(ErrNeedMoreData, err) } var rdpType uint8 err = binary.Read(reader, binary.BigEndian, &rdpType) if err != nil { return E.Cause1(ErrNeedMoreData, err) } if rdpType != 0x01 { return os.ErrInvalid } var rdpFlags uint8 err = binary.Read(reader, binary.BigEndian, &rdpFlags) if err != nil { return E.Cause1(ErrNeedMoreData, err) } var rdpLength uint8 err = binary.Read(reader, binary.BigEndian, &rdpLength) if err != nil { return E.Cause1(ErrNeedMoreData, err) } if rdpLength != 8 { return os.ErrInvalid } metadata.Protocol = C.ProtocolRDP return nil } ================================================ FILE: common/sniff/rdp_test.go ================================================ package sniff_test import ( "bytes" "context" "encoding/hex" "testing" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/sniff" C "github.com/sagernet/sing-box/constant" "github.com/stretchr/testify/require" ) func TestSniffRDP(t *testing.T) { t.Parallel() pkt, err := hex.DecodeString("030000130ee00000000000010008000b000000010008000b000000") require.NoError(t, err) var metadata adapter.InboundContext err = sniff.RDP(context.TODO(), &metadata, bytes.NewReader(pkt)) require.NoError(t, err) require.Equal(t, C.ProtocolRDP, metadata.Protocol) } ================================================ FILE: common/sniff/sniff.go ================================================ package sniff import ( "bytes" "context" "errors" "io" "net" "time" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" ) type ( StreamSniffer = func(ctx context.Context, metadata *adapter.InboundContext, reader io.Reader) error PacketSniffer = func(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error ) var ErrNeedMoreData = E.New("need more data") func Skip(metadata *adapter.InboundContext) bool { // skip server first protocols switch metadata.Destination.Port { case 25, 465, 587: // SMTP return true case 143, 993: // IMAP return true case 110, 995: // POP3 return true } return false } func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.Conn, buffers []*buf.Buffer, buffer *buf.Buffer, timeout time.Duration, sniffers ...StreamSniffer) error { if timeout == 0 { timeout = C.ReadPayloadTimeout } deadline := time.Now().Add(timeout) var sniffError error for i := 0; ; i++ { err := conn.SetReadDeadline(deadline) if err != nil { return E.Cause(err, "set read deadline") } _, err = buffer.ReadOnceFrom(conn) _ = conn.SetReadDeadline(time.Time{}) if err != nil { if i > 0 { break } return E.Cause(err, "read payload") } sniffError = nil for _, sniffer := range sniffers { reader := io.MultiReader(common.Map(append(buffers, buffer), func(it *buf.Buffer) io.Reader { return bytes.NewReader(it.Bytes()) })...) err = sniffer(ctx, metadata, reader) if err == nil { return nil } sniffError = E.Errors(sniffError, err) } if !errors.Is(sniffError, ErrNeedMoreData) { break } } return sniffError } func PeekPacket(ctx context.Context, metadata *adapter.InboundContext, packet []byte, sniffers ...PacketSniffer) error { var sniffError []error for _, sniffer := range sniffers { err := sniffer(ctx, metadata, packet) if err == nil { return nil } sniffError = append(sniffError, err) } return E.Errors(sniffError...) } ================================================ FILE: common/sniff/ssh.go ================================================ package sniff import ( "bufio" "context" "io" "os" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" E "github.com/sagernet/sing/common/exceptions" ) func SSH(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error { const sshPrefix = "SSH-2.0-" bReader := bufio.NewReader(reader) prefix, err := bReader.Peek(len(sshPrefix)) if string(prefix[:]) != sshPrefix[:len(prefix)] { return os.ErrInvalid } if err != nil { return E.Cause1(ErrNeedMoreData, err) } fistLine, _, err := bReader.ReadLine() if err != nil { return err } metadata.Protocol = C.ProtocolSSH metadata.Client = string(fistLine)[8:] return nil } ================================================ FILE: common/sniff/ssh_test.go ================================================ package sniff_test import ( "bytes" "context" "encoding/hex" "testing" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/sniff" C "github.com/sagernet/sing-box/constant" "github.com/stretchr/testify/require" ) func TestSniffSSH(t *testing.T) { t.Parallel() pkt, err := hex.DecodeString("5353482d322e302d64726f70626561720d0a000001a40a1492892570d1223aef61b0d647972c8bd30000009f637572766532353531392d7368613235362c637572766532353531392d736861323536406c69627373682e6f72672c6469666669652d68656c6c6d616e2d67726f757031342d7368613235362c6469666669652d68656c6c6d616e2d67726f757031342d736861312c6b6578677565737332406d6174742e7563632e61736e2e61752c6b65782d7374726963742d732d763030406f70656e7373682e636f6d000000207373682d656432353531392c7273612d736861322d3235362c7373682d7273610000003363686163686132302d706f6c7931333035406f70656e7373682e636f6d2c6165733132382d6374722c6165733235362d6374720000003363686163686132302d706f6c7931333035406f70656e7373682e636f6d2c6165733132382d6374722c6165733235362d63747200000017686d61632d736861312c686d61632d736861322d32353600000017686d61632d736861312c686d61632d736861322d323536000000046e6f6e65000000046e6f6e65000000000000000000000000002aa6ed090585b7d635b6") require.NoError(t, err) var metadata adapter.InboundContext err = sniff.SSH(context.TODO(), &metadata, bytes.NewReader(pkt)) require.NoError(t, err) require.Equal(t, C.ProtocolSSH, metadata.Protocol) require.Equal(t, "dropbear", metadata.Client) } func TestSniffIncompleteSSH(t *testing.T) { t.Parallel() pkt, err := hex.DecodeString("5353482d322e30") require.NoError(t, err) var metadata adapter.InboundContext err = sniff.SSH(context.TODO(), &metadata, bytes.NewReader(pkt)) require.ErrorIs(t, err, sniff.ErrNeedMoreData) } func TestSniffNotSSH(t *testing.T) { t.Parallel() pkt, err := hex.DecodeString("5353482d322e31") require.NoError(t, err) var metadata adapter.InboundContext err = sniff.SSH(context.TODO(), &metadata, bytes.NewReader(pkt)) require.NotEmpty(t, err) require.NotErrorIs(t, err, sniff.ErrNeedMoreData) } ================================================ FILE: common/sniff/stun.go ================================================ package sniff import ( "context" "encoding/binary" "os" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" ) func STUNMessage(_ context.Context, metadata *adapter.InboundContext, packet []byte) error { pLen := len(packet) if pLen < 20 { return os.ErrInvalid } if binary.BigEndian.Uint32(packet[4:8]) != 0x2112A442 { return os.ErrInvalid } if len(packet) < 20+int(binary.BigEndian.Uint16(packet[2:4])) { return os.ErrInvalid } metadata.Protocol = C.ProtocolSTUN return nil } ================================================ FILE: common/sniff/stun_test.go ================================================ package sniff_test import ( "context" "encoding/hex" "testing" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/sniff" C "github.com/sagernet/sing-box/constant" "github.com/stretchr/testify/require" ) func TestSniffSTUN(t *testing.T) { t.Parallel() packet, err := hex.DecodeString("000100002112a44224b1a025d0c180c484341306") require.NoError(t, err) var metadata adapter.InboundContext err = sniff.STUNMessage(context.Background(), &metadata, packet) require.NoError(t, err) require.Equal(t, metadata.Protocol, C.ProtocolSTUN) } func FuzzSniffSTUN(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte) { var metadata adapter.InboundContext if err := sniff.STUNMessage(context.Background(), &metadata, data); err == nil { t.Fail() } }) } ================================================ FILE: common/sniff/tls.go ================================================ package sniff import ( "context" "crypto/tls" "errors" "io" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" ) func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reader io.Reader) error { var clientHello *tls.ClientHelloInfo err := tls.Server(bufio.NewReadOnlyConn(reader), &tls.Config{ GetConfigForClient: func(argHello *tls.ClientHelloInfo) (*tls.Config, error) { clientHello = argHello return nil, nil }, }).HandshakeContext(ctx) if clientHello != nil { metadata.Protocol = C.ProtocolTLS metadata.Domain = clientHello.ServerName return nil } if errors.Is(err, io.ErrUnexpectedEOF) { return E.Cause1(ErrNeedMoreData, err) } else { return err } } ================================================ FILE: common/srs/binary.go ================================================ package srs import ( "bufio" "compress/zlib" "encoding/binary" "io" "net/netip" "unsafe" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/domain" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json/badjson" "github.com/sagernet/sing/common/json/badoption" "github.com/sagernet/sing/common/varbin" "go4.org/netipx" ) var MagicBytes = [3]byte{0x53, 0x52, 0x53} // SRS const ( ruleItemQueryType uint8 = iota ruleItemNetwork ruleItemDomain ruleItemDomainKeyword ruleItemDomainRegex ruleItemSourceIPCIDR ruleItemIPCIDR ruleItemSourcePort ruleItemSourcePortRange ruleItemPort ruleItemPortRange ruleItemProcessName ruleItemProcessPath ruleItemPackageName ruleItemWIFISSID ruleItemWIFIBSSID ruleItemAdGuardDomain ruleItemProcessPathRegex ruleItemNetworkType ruleItemNetworkIsExpensive ruleItemNetworkIsConstrained ruleItemNetworkInterfaceAddress ruleItemDefaultInterfaceAddress ruleItemPackageNameRegex ruleItemFinal uint8 = 0xFF ) func Read(reader io.Reader, recover bool) (ruleSetCompat option.PlainRuleSetCompat, err error) { var magicBytes [3]byte _, err = io.ReadFull(reader, magicBytes[:]) if err != nil { return } if magicBytes != MagicBytes { err = E.New("invalid sing-box rule-set file") return } var version uint8 err = binary.Read(reader, binary.BigEndian, &version) if err != nil { return ruleSetCompat, err } if version > C.RuleSetVersionCurrent { return ruleSetCompat, E.New("unsupported version: ", version) } compressReader, err := zlib.NewReader(reader) if err != nil { return } bReader := bufio.NewReader(compressReader) length, err := binary.ReadUvarint(bReader) if err != nil { return } ruleSetCompat.Version = version ruleSetCompat.Options.Rules = make([]option.HeadlessRule, length) for i := range length { ruleSetCompat.Options.Rules[i], err = readRule(bReader, recover) if err != nil { err = E.Cause(err, "read rule[", i, "]") return } } return } func Write(writer io.Writer, ruleSet option.PlainRuleSet, generateVersion uint8) error { _, err := writer.Write(MagicBytes[:]) if err != nil { return err } err = binary.Write(writer, binary.BigEndian, generateVersion) if err != nil { return err } compressWriter, err := zlib.NewWriterLevel(writer, zlib.BestCompression) if err != nil { return err } bWriter := bufio.NewWriter(compressWriter) _, err = varbin.WriteUvarint(bWriter, uint64(len(ruleSet.Rules))) if err != nil { return err } for _, rule := range ruleSet.Rules { err = writeRule(bWriter, rule, generateVersion) if err != nil { return err } } err = bWriter.Flush() if err != nil { return err } return compressWriter.Close() } func readRule(reader varbin.Reader, recover bool) (rule option.HeadlessRule, err error) { var ruleType uint8 err = binary.Read(reader, binary.BigEndian, &ruleType) if err != nil { return } switch ruleType { case 0: rule.Type = C.RuleTypeDefault rule.DefaultOptions, err = readDefaultRule(reader, recover) case 1: rule.Type = C.RuleTypeLogical rule.LogicalOptions, err = readLogicalRule(reader, recover) default: err = E.New("unknown rule type: ", ruleType) } return } func writeRule(writer varbin.Writer, rule option.HeadlessRule, generateVersion uint8) error { switch rule.Type { case C.RuleTypeDefault: return writeDefaultRule(writer, rule.DefaultOptions, generateVersion) case C.RuleTypeLogical: return writeLogicalRule(writer, rule.LogicalOptions, generateVersion) default: panic("unknown rule type: " + rule.Type) } } func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHeadlessRule, err error) { var lastItemType uint8 for { var itemType uint8 err = binary.Read(reader, binary.BigEndian, &itemType) if err != nil { return } switch itemType { case ruleItemQueryType: var rawQueryType []uint16 rawQueryType, err = readRuleItemUint16(reader) if err != nil { return } rule.QueryType = common.Map(rawQueryType, func(it uint16) option.DNSQueryType { return option.DNSQueryType(it) }) case ruleItemNetwork: rule.Network, err = readRuleItemString(reader) case ruleItemDomain: var matcher *domain.Matcher matcher, err = domain.ReadMatcher(reader) if err != nil { return } rule.DomainMatcher = matcher if recover { rule.Domain, rule.DomainSuffix = matcher.Dump() } case ruleItemDomainKeyword: rule.DomainKeyword, err = readRuleItemString(reader) case ruleItemDomainRegex: rule.DomainRegex, err = readRuleItemString(reader) case ruleItemSourceIPCIDR: rule.SourceIPSet, err = readIPSet(reader) if err != nil { return } if recover { rule.SourceIPCIDR = common.Map(rule.SourceIPSet.Prefixes(), netip.Prefix.String) } case ruleItemIPCIDR: rule.IPSet, err = readIPSet(reader) if err != nil { return } if recover { rule.IPCIDR = common.Map(rule.IPSet.Prefixes(), netip.Prefix.String) } case ruleItemSourcePort: rule.SourcePort, err = readRuleItemUint16(reader) case ruleItemSourcePortRange: rule.SourcePortRange, err = readRuleItemString(reader) case ruleItemPort: rule.Port, err = readRuleItemUint16(reader) case ruleItemPortRange: rule.PortRange, err = readRuleItemString(reader) case ruleItemProcessName: rule.ProcessName, err = readRuleItemString(reader) case ruleItemProcessPath: rule.ProcessPath, err = readRuleItemString(reader) case ruleItemProcessPathRegex: 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: rule.WIFIBSSID, err = readRuleItemString(reader) case ruleItemAdGuardDomain: var matcher *domain.AdGuardMatcher matcher, err = domain.ReadAdGuardMatcher(reader) if err != nil { return } rule.AdGuardDomainMatcher = matcher if recover { rule.AdGuardDomain = matcher.Dump() } case ruleItemNetworkType: rule.NetworkType, err = readRuleItemUint8[option.InterfaceType](reader) case ruleItemNetworkIsExpensive: rule.NetworkIsExpensive = true case ruleItemNetworkIsConstrained: rule.NetworkIsConstrained = true case ruleItemNetworkInterfaceAddress: rule.NetworkInterfaceAddress = new(badjson.TypedMap[option.InterfaceType, badoption.Listable[*badoption.Prefixable]]) var size uint64 size, err = binary.ReadUvarint(reader) if err != nil { return } for i := uint64(0); i < size; i++ { var key uint8 err = binary.Read(reader, binary.BigEndian, &key) if err != nil { return } var value []*badoption.Prefixable var prefixCount uint64 prefixCount, err = binary.ReadUvarint(reader) if err != nil { return } for j := uint64(0); j < prefixCount; j++ { var prefix netip.Prefix prefix, err = readPrefix(reader) if err != nil { return } value = append(value, common.Ptr(badoption.Prefixable(prefix))) } rule.NetworkInterfaceAddress.Put(option.InterfaceType(key), value) } case ruleItemDefaultInterfaceAddress: var value []*badoption.Prefixable var prefixCount uint64 prefixCount, err = binary.ReadUvarint(reader) if err != nil { return } for j := uint64(0); j < prefixCount; j++ { var prefix netip.Prefix prefix, err = readPrefix(reader) if err != nil { return } value = append(value, common.Ptr(badoption.Prefixable(prefix))) } rule.DefaultInterfaceAddress = value case ruleItemFinal: err = binary.Read(reader, binary.BigEndian, &rule.Invert) return default: err = E.New("unknown rule item type: ", itemType, ", last type: ", lastItemType) } if err != nil { return } lastItemType = itemType } } func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, generateVersion uint8) error { err := binary.Write(writer, binary.BigEndian, uint8(0)) if err != nil { return err } if len(rule.QueryType) > 0 { err = writeRuleItemUint16(writer, ruleItemQueryType, common.Map(rule.QueryType, func(it option.DNSQueryType) uint16 { return uint16(it) })) if err != nil { return err } } if len(rule.Network) > 0 { err = writeRuleItemString(writer, ruleItemNetwork, rule.Network) if err != nil { return err } } if len(rule.Domain) > 0 || len(rule.DomainSuffix) > 0 { err = binary.Write(writer, binary.BigEndian, ruleItemDomain) if err != nil { return err } err = domain.NewMatcher(rule.Domain, rule.DomainSuffix, generateVersion == C.RuleSetVersion1).Write(writer) if err != nil { return err } } if len(rule.DomainKeyword) > 0 { err = writeRuleItemString(writer, ruleItemDomainKeyword, rule.DomainKeyword) if err != nil { return err } } if len(rule.DomainRegex) > 0 { err = writeRuleItemString(writer, ruleItemDomainRegex, rule.DomainRegex) if err != nil { return err } } if len(rule.SourceIPCIDR) > 0 { err = writeRuleItemCIDR(writer, ruleItemSourceIPCIDR, rule.SourceIPCIDR) if err != nil { return E.Cause(err, "source_ip_cidr") } } if len(rule.IPCIDR) > 0 { err = writeRuleItemCIDR(writer, ruleItemIPCIDR, rule.IPCIDR) if err != nil { return E.Cause(err, "ipcidr") } } if len(rule.SourcePort) > 0 { err = writeRuleItemUint16(writer, ruleItemSourcePort, rule.SourcePort) if err != nil { return err } } if len(rule.SourcePortRange) > 0 { err = writeRuleItemString(writer, ruleItemSourcePortRange, rule.SourcePortRange) if err != nil { return err } } if len(rule.Port) > 0 { err = writeRuleItemUint16(writer, ruleItemPort, rule.Port) if err != nil { return err } } if len(rule.PortRange) > 0 { err = writeRuleItemString(writer, ruleItemPortRange, rule.PortRange) if err != nil { return err } } if len(rule.ProcessName) > 0 { err = writeRuleItemString(writer, ruleItemProcessName, rule.ProcessName) if err != nil { return err } } if len(rule.ProcessPath) > 0 { err = writeRuleItemString(writer, ruleItemProcessPath, rule.ProcessPath) if err != nil { return err } } if len(rule.ProcessPathRegex) > 0 { err = writeRuleItemString(writer, ruleItemProcessPathRegex, rule.ProcessPathRegex) if err != nil { return err } } if len(rule.PackageName) > 0 { err = writeRuleItemString(writer, ruleItemPackageName, rule.PackageName) if err != nil { 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") } err = writeRuleItemUint8(writer, ruleItemNetworkType, rule.NetworkType) if err != nil { return err } } if rule.NetworkIsExpensive { if generateVersion < C.RuleSetVersion3 { return E.New("`network_is_expensive` rule item is only supported in version 3 or later") } err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsExpensive) if err != nil { return err } } if rule.NetworkIsConstrained { if generateVersion < C.RuleSetVersion3 { return E.New("`network_is_constrained` rule item is only supported in version 3 or later") } err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsConstrained) if err != nil { return err } } if rule.NetworkInterfaceAddress != nil && rule.NetworkInterfaceAddress.Size() > 0 { if generateVersion < C.RuleSetVersion4 { return E.New("`network_interface_address` rule item is only supported in version 4 or later") } err = writer.WriteByte(ruleItemNetworkInterfaceAddress) if err != nil { return err } _, err = varbin.WriteUvarint(writer, uint64(rule.NetworkInterfaceAddress.Size())) if err != nil { return err } for _, entry := range rule.NetworkInterfaceAddress.Entries() { err = binary.Write(writer, binary.BigEndian, uint8(entry.Key.Build())) if err != nil { return err } _, err = varbin.WriteUvarint(writer, uint64(len(entry.Value))) if err != nil { return err } for _, rawPrefix := range entry.Value { err = writePrefix(writer, rawPrefix.Build(netip.Prefix{})) if err != nil { return err } } } } if len(rule.DefaultInterfaceAddress) > 0 { if generateVersion < C.RuleSetVersion4 { return E.New("`default_interface_address` rule item is only supported in version 4 or later") } err = writer.WriteByte(ruleItemDefaultInterfaceAddress) if err != nil { return err } _, err = varbin.WriteUvarint(writer, uint64(len(rule.DefaultInterfaceAddress))) if err != nil { return err } for _, rawPrefix := range rule.DefaultInterfaceAddress { err = writePrefix(writer, rawPrefix.Build(netip.Prefix{})) if err != nil { return err } } } if len(rule.WIFISSID) > 0 { err = writeRuleItemString(writer, ruleItemWIFISSID, rule.WIFISSID) if err != nil { return err } } if len(rule.WIFIBSSID) > 0 { err = writeRuleItemString(writer, ruleItemWIFIBSSID, rule.WIFIBSSID) if err != nil { return err } } if len(rule.AdGuardDomain) > 0 { if generateVersion < C.RuleSetVersion2 { return E.New("AdGuard rule items is only supported in version 2 or later") } err = binary.Write(writer, binary.BigEndian, ruleItemAdGuardDomain) if err != nil { return err } err = domain.NewAdGuardMatcher(rule.AdGuardDomain).Write(writer) if err != nil { return err } } err = binary.Write(writer, binary.BigEndian, ruleItemFinal) if err != nil { return err } err = binary.Write(writer, binary.BigEndian, rule.Invert) if err != nil { return err } return nil } func readRuleItemString(reader varbin.Reader) ([]string, error) { length, err := binary.ReadUvarint(reader) if err != nil { return nil, err } result := make([]string, length) for i := range result { strLen, err := binary.ReadUvarint(reader) if err != nil { return nil, err } buf := make([]byte, strLen) _, err = io.ReadFull(reader, buf) if err != nil { return nil, err } result[i] = string(buf) } return result, nil } func writeRuleItemString(writer varbin.Writer, itemType uint8, value []string) error { err := writer.WriteByte(itemType) if err != nil { return err } _, err = varbin.WriteUvarint(writer, uint64(len(value))) if err != nil { return err } for _, s := range value { _, err = varbin.WriteUvarint(writer, uint64(len(s))) if err != nil { return err } _, err = writer.Write([]byte(s)) if err != nil { return err } } return nil } func readRuleItemUint8[E ~uint8](reader varbin.Reader) ([]E, error) { length, err := binary.ReadUvarint(reader) if err != nil { return nil, err } result := make([]E, length) _, err = io.ReadFull(reader, *(*[]byte)(unsafe.Pointer(&result))) if err != nil { return nil, err } return result, nil } func writeRuleItemUint8[E ~uint8](writer varbin.Writer, itemType uint8, value []E) error { err := writer.WriteByte(itemType) if err != nil { return err } _, err = varbin.WriteUvarint(writer, uint64(len(value))) if err != nil { return err } _, err = writer.Write(*(*[]byte)(unsafe.Pointer(&value))) return err } func readRuleItemUint16(reader varbin.Reader) ([]uint16, error) { length, err := binary.ReadUvarint(reader) if err != nil { return nil, err } result := make([]uint16, length) err = binary.Read(reader, binary.BigEndian, result) if err != nil { return nil, err } return result, nil } func writeRuleItemUint16(writer varbin.Writer, itemType uint8, value []uint16) error { err := writer.WriteByte(itemType) if err != nil { return err } _, err = varbin.WriteUvarint(writer, uint64(len(value))) if err != nil { return err } return binary.Write(writer, binary.BigEndian, value) } func writeRuleItemCIDR(writer varbin.Writer, itemType uint8, value []string) error { var builder netipx.IPSetBuilder for i, prefixString := range value { prefix, err := netip.ParsePrefix(prefixString) if err == nil { builder.AddPrefix(prefix) continue } addr, addrErr := netip.ParseAddr(prefixString) if addrErr == nil { builder.Add(addr) continue } return E.Cause(err, "parse [", i, "]") } ipSet, err := builder.IPSet() if err != nil { return err } err = binary.Write(writer, binary.BigEndian, itemType) if err != nil { return err } return writeIPSet(writer, ipSet) } func readLogicalRule(reader varbin.Reader, recovery bool) (logicalRule option.LogicalHeadlessRule, err error) { mode, err := reader.ReadByte() if err != nil { return } switch mode { case 0: logicalRule.Mode = C.LogicalTypeAnd case 1: logicalRule.Mode = C.LogicalTypeOr default: err = E.New("unknown logical mode: ", mode) return } length, err := binary.ReadUvarint(reader) if err != nil { return } logicalRule.Rules = make([]option.HeadlessRule, length) for i := range length { logicalRule.Rules[i], err = readRule(reader, recovery) if err != nil { err = E.Cause(err, "read logical rule [", i, "]") return } } err = binary.Read(reader, binary.BigEndian, &logicalRule.Invert) if err != nil { return } return } func writeLogicalRule(writer varbin.Writer, logicalRule option.LogicalHeadlessRule, generateVersion uint8) error { err := binary.Write(writer, binary.BigEndian, uint8(1)) if err != nil { return err } switch logicalRule.Mode { case C.LogicalTypeAnd: err = binary.Write(writer, binary.BigEndian, uint8(0)) case C.LogicalTypeOr: err = binary.Write(writer, binary.BigEndian, uint8(1)) default: panic("unknown logical mode: " + logicalRule.Mode) } if err != nil { return err } _, err = varbin.WriteUvarint(writer, uint64(len(logicalRule.Rules))) if err != nil { return err } for _, rule := range logicalRule.Rules { err = writeRule(writer, rule, generateVersion) if err != nil { return err } } err = binary.Write(writer, binary.BigEndian, logicalRule.Invert) if err != nil { return err } return nil } ================================================ FILE: common/srs/compat_test.go ================================================ package srs import ( "bufio" "bytes" "encoding/binary" "net/netip" "strings" "testing" "unsafe" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/varbin" "github.com/stretchr/testify/require" "go4.org/netipx" ) // Old implementations using varbin reflection-based serialization func oldWriteStringSlice(writer varbin.Writer, value []string) error { //nolint:staticcheck return varbin.Write(writer, binary.BigEndian, value) } func oldReadStringSlice(reader varbin.Reader) ([]string, error) { //nolint:staticcheck return varbin.ReadValue[[]string](reader, binary.BigEndian) } func oldWriteUint8Slice[E ~uint8](writer varbin.Writer, value []E) error { //nolint:staticcheck return varbin.Write(writer, binary.BigEndian, value) } func oldReadUint8Slice[E ~uint8](reader varbin.Reader) ([]E, error) { //nolint:staticcheck return varbin.ReadValue[[]E](reader, binary.BigEndian) } func oldWriteUint16Slice(writer varbin.Writer, value []uint16) error { //nolint:staticcheck return varbin.Write(writer, binary.BigEndian, value) } func oldReadUint16Slice(reader varbin.Reader) ([]uint16, error) { //nolint:staticcheck return varbin.ReadValue[[]uint16](reader, binary.BigEndian) } func oldWritePrefix(writer varbin.Writer, prefix netip.Prefix) error { //nolint:staticcheck err := varbin.Write(writer, binary.BigEndian, prefix.Addr().AsSlice()) if err != nil { return err } return binary.Write(writer, binary.BigEndian, uint8(prefix.Bits())) } type oldIPRangeData struct { From []byte To []byte } // Note: The old writeIPSet had a bug where varbin.Write(writer, binary.BigEndian, data) // with a struct VALUE (not pointer) silently wrote nothing because field.CanSet() returned false. // This caused IP range data to be missing from the output. // The new implementation correctly writes all range data. // // The old readIPSet used varbin.Read with a pre-allocated slice, which worked because // slice elements are addressable and CanSet() returns true for them. // // For compatibility testing, we verify: // 1. New write produces correct output with range data // 2. New read can parse the new format correctly // 3. Round-trip works correctly func oldReadIPSet(reader varbin.Reader) (*netipx.IPSet, error) { version, err := reader.ReadByte() if err != nil { return nil, err } if version != 1 { return nil, err } var length uint64 err = binary.Read(reader, binary.BigEndian, &length) if err != nil { return nil, err } ranges := make([]oldIPRangeData, length) //nolint:staticcheck err = varbin.Read(reader, binary.BigEndian, &ranges) if err != nil { return nil, err } mySet := &myIPSet{ rr: make([]myIPRange, len(ranges)), } for i, rangeData := range ranges { mySet.rr[i].from = M.AddrFromIP(rangeData.From) mySet.rr[i].to = M.AddrFromIP(rangeData.To) } return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil } // New write functions (without itemType prefix for testing) func newWriteStringSlice(writer varbin.Writer, value []string) error { _, err := varbin.WriteUvarint(writer, uint64(len(value))) if err != nil { return err } for _, s := range value { _, err = varbin.WriteUvarint(writer, uint64(len(s))) if err != nil { return err } _, err = writer.Write([]byte(s)) if err != nil { return err } } return nil } func newWriteUint8Slice[E ~uint8](writer varbin.Writer, value []E) error { _, err := varbin.WriteUvarint(writer, uint64(len(value))) if err != nil { return err } _, err = writer.Write(*(*[]byte)(unsafe.Pointer(&value))) return err } func newWriteUint16Slice(writer varbin.Writer, value []uint16) error { _, err := varbin.WriteUvarint(writer, uint64(len(value))) if err != nil { return err } return binary.Write(writer, binary.BigEndian, value) } func newWritePrefix(writer varbin.Writer, prefix netip.Prefix) error { addrSlice := prefix.Addr().AsSlice() _, err := varbin.WriteUvarint(writer, uint64(len(addrSlice))) if err != nil { return err } _, err = writer.Write(addrSlice) if err != nil { return err } return writer.WriteByte(uint8(prefix.Bits())) } // Tests func TestStringSliceCompat(t *testing.T) { t.Parallel() cases := []struct { name string input []string }{ {"nil", nil}, {"empty", []string{}}, {"single_empty", []string{""}}, {"single", []string{"test"}}, {"multi", []string{"a", "b", "c"}}, {"with_empty", []string{"a", "", "c"}}, {"utf8", []string{"测试", "テスト", "тест"}}, {"long_string", []string{strings.Repeat("x", 128)}}, {"many_elements", generateStrings(128)}, {"many_elements_256", generateStrings(256)}, {"127_byte_string", []string{strings.Repeat("x", 127)}}, {"128_byte_string", []string{strings.Repeat("x", 128)}}, {"mixed_lengths", []string{"a", strings.Repeat("b", 100), "", strings.Repeat("c", 200)}}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() // Old write var oldBuf bytes.Buffer err := oldWriteStringSlice(&oldBuf, tc.input) require.NoError(t, err) // New write var newBuf bytes.Buffer err = newWriteStringSlice(&newBuf, tc.input) require.NoError(t, err) // Bytes must match require.Equal(t, oldBuf.Bytes(), newBuf.Bytes(), "mismatch for %q\nold: %x\nnew: %x", tc.name, oldBuf.Bytes(), newBuf.Bytes()) // New write -> old read readBack, err := oldReadStringSlice(bufio.NewReader(bytes.NewReader(newBuf.Bytes()))) require.NoError(t, err) requireStringSliceEqual(t, tc.input, readBack) // Old write -> new read readBack2, err := readRuleItemString(bufio.NewReader(bytes.NewReader(oldBuf.Bytes()))) require.NoError(t, err) requireStringSliceEqual(t, tc.input, readBack2) }) } } func TestUint8SliceCompat(t *testing.T) { t.Parallel() cases := []struct { name string input []uint8 }{ {"nil", nil}, {"empty", []uint8{}}, {"single_zero", []uint8{0}}, {"single_max", []uint8{255}}, {"multi", []uint8{0, 1, 127, 128, 255}}, {"boundary", []uint8{0x00, 0x7f, 0x80, 0xff}}, {"sequential", generateUint8Slice(256)}, {"127_elements", generateUint8Slice(127)}, {"128_elements", generateUint8Slice(128)}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() // Old write var oldBuf bytes.Buffer err := oldWriteUint8Slice(&oldBuf, tc.input) require.NoError(t, err) // New write var newBuf bytes.Buffer err = newWriteUint8Slice(&newBuf, tc.input) require.NoError(t, err) // Bytes must match require.Equal(t, oldBuf.Bytes(), newBuf.Bytes(), "mismatch for %q\nold: %x\nnew: %x", tc.name, oldBuf.Bytes(), newBuf.Bytes()) // New write -> old read readBack, err := oldReadUint8Slice[uint8](bufio.NewReader(bytes.NewReader(newBuf.Bytes()))) require.NoError(t, err) requireUint8SliceEqual(t, tc.input, readBack) // Old write -> new read readBack2, err := readRuleItemUint8[uint8](bufio.NewReader(bytes.NewReader(oldBuf.Bytes()))) require.NoError(t, err) requireUint8SliceEqual(t, tc.input, readBack2) }) } } func TestUint16SliceCompat(t *testing.T) { t.Parallel() cases := []struct { name string input []uint16 }{ {"nil", nil}, {"empty", []uint16{}}, {"single_zero", []uint16{0}}, {"single_max", []uint16{65535}}, {"multi", []uint16{0, 255, 256, 32767, 32768, 65535}}, {"ports", []uint16{80, 443, 8080, 8443}}, {"127_elements", generateUint16Slice(127)}, {"128_elements", generateUint16Slice(128)}, {"256_elements", generateUint16Slice(256)}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() // Old write var oldBuf bytes.Buffer err := oldWriteUint16Slice(&oldBuf, tc.input) require.NoError(t, err) // New write var newBuf bytes.Buffer err = newWriteUint16Slice(&newBuf, tc.input) require.NoError(t, err) // Bytes must match require.Equal(t, oldBuf.Bytes(), newBuf.Bytes(), "mismatch for %q\nold: %x\nnew: %x", tc.name, oldBuf.Bytes(), newBuf.Bytes()) // New write -> old read readBack, err := oldReadUint16Slice(bufio.NewReader(bytes.NewReader(newBuf.Bytes()))) require.NoError(t, err) requireUint16SliceEqual(t, tc.input, readBack) // Old write -> new read readBack2, err := readRuleItemUint16(bufio.NewReader(bytes.NewReader(oldBuf.Bytes()))) require.NoError(t, err) requireUint16SliceEqual(t, tc.input, readBack2) }) } } func TestPrefixCompat(t *testing.T) { t.Parallel() cases := []struct { name string input netip.Prefix }{ {"ipv4_0", netip.MustParsePrefix("0.0.0.0/0")}, {"ipv4_8", netip.MustParsePrefix("10.0.0.0/8")}, {"ipv4_16", netip.MustParsePrefix("192.168.0.0/16")}, {"ipv4_24", netip.MustParsePrefix("192.168.1.0/24")}, {"ipv4_32", netip.MustParsePrefix("1.2.3.4/32")}, {"ipv6_0", netip.MustParsePrefix("::/0")}, {"ipv6_64", netip.MustParsePrefix("2001:db8::/64")}, {"ipv6_128", netip.MustParsePrefix("::1/128")}, {"ipv6_full", netip.MustParsePrefix("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128")}, {"ipv4_private", netip.MustParsePrefix("172.16.0.0/12")}, {"ipv6_link_local", netip.MustParsePrefix("fe80::/10")}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() // Old write var oldBuf bytes.Buffer err := oldWritePrefix(&oldBuf, tc.input) require.NoError(t, err) // New write var newBuf bytes.Buffer err = newWritePrefix(&newBuf, tc.input) require.NoError(t, err) // Bytes must match require.Equal(t, oldBuf.Bytes(), newBuf.Bytes(), "mismatch for %q\nold: %x\nnew: %x", tc.name, oldBuf.Bytes(), newBuf.Bytes()) // New write -> new read (no old read for prefix) readBack, err := readPrefix(bufio.NewReader(bytes.NewReader(newBuf.Bytes()))) require.NoError(t, err) require.Equal(t, tc.input, readBack) // Old write -> new read readBack2, err := readPrefix(bufio.NewReader(bytes.NewReader(oldBuf.Bytes()))) require.NoError(t, err) require.Equal(t, tc.input, readBack2) }) } } func TestIPSetCompat(t *testing.T) { t.Parallel() // Note: The old writeIPSet was buggy (varbin.Write with struct values wrote nothing). // This test verifies the new implementation writes correct data and round-trips correctly. cases := []struct { name string input *netipx.IPSet }{ {"single_ipv4", buildIPSet("1.2.3.4")}, {"ipv4_range", buildIPSet("192.168.0.0/16")}, {"multi_ipv4", buildIPSet("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16")}, {"single_ipv6", buildIPSet("::1")}, {"ipv6_range", buildIPSet("2001:db8::/32")}, {"mixed", buildIPSet("10.0.0.0/8", "::1", "2001:db8::/32")}, {"large", buildLargeIPSet(100)}, {"adjacent_ranges", buildIPSet("192.168.0.0/24", "192.168.1.0/24", "192.168.2.0/24")}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() // New write var newBuf bytes.Buffer err := writeIPSet(&newBuf, tc.input) require.NoError(t, err) // Verify format starts with version byte (1) + uint64 count require.True(t, len(newBuf.Bytes()) >= 9, "output too short") require.Equal(t, byte(1), newBuf.Bytes()[0], "version byte mismatch") // New write -> old read (varbin.Read with pre-allocated slice works correctly) readBack, err := oldReadIPSet(bufio.NewReader(bytes.NewReader(newBuf.Bytes()))) require.NoError(t, err) requireIPSetEqual(t, tc.input, readBack) // New write -> new read readBack2, err := readIPSet(bufio.NewReader(bytes.NewReader(newBuf.Bytes()))) require.NoError(t, err) requireIPSetEqual(t, tc.input, readBack2) }) } } // Helper functions func generateStrings(count int) []string { result := make([]string, count) for i := range result { result[i] = strings.Repeat("x", i%50) } return result } func generateUint8Slice(count int) []uint8 { result := make([]uint8, count) for i := range result { result[i] = uint8(i % 256) } return result } func generateUint16Slice(count int) []uint16 { result := make([]uint16, count) for i := range result { result[i] = uint16(i * 257) } return result } func buildIPSet(cidrs ...string) *netipx.IPSet { var builder netipx.IPSetBuilder for _, cidr := range cidrs { prefix, err := netip.ParsePrefix(cidr) if err != nil { addr, err := netip.ParseAddr(cidr) if err != nil { panic(err) } builder.Add(addr) } else { builder.AddPrefix(prefix) } } set, _ := builder.IPSet() return set } func buildLargeIPSet(count int) *netipx.IPSet { var builder netipx.IPSetBuilder for i := range count { prefix := netip.PrefixFrom(netip.AddrFrom4([4]byte{10, byte(i / 256), byte(i % 256), 0}), 24) builder.AddPrefix(prefix) } set, _ := builder.IPSet() return set } func requireStringSliceEqual(t *testing.T, expected, actual []string) { t.Helper() if len(expected) == 0 && len(actual) == 0 { return } require.Equal(t, expected, actual) } func requireUint8SliceEqual(t *testing.T, expected, actual []uint8) { t.Helper() if len(expected) == 0 && len(actual) == 0 { return } require.Equal(t, expected, actual) } func requireUint16SliceEqual(t *testing.T, expected, actual []uint16) { t.Helper() if len(expected) == 0 && len(actual) == 0 { return } require.Equal(t, expected, actual) } func requireIPSetEqual(t *testing.T, expected, actual *netipx.IPSet) { t.Helper() expectedRanges := expected.Ranges() actualRanges := actual.Ranges() require.Equal(t, len(expectedRanges), len(actualRanges), "range count mismatch") for i := range expectedRanges { require.Equal(t, expectedRanges[i].From(), actualRanges[i].From(), "range[%d].from mismatch", i) require.Equal(t, expectedRanges[i].To(), actualRanges[i].To(), "range[%d].to mismatch", i) } } ================================================ FILE: common/srs/ip_cidr.go ================================================ package srs import ( "encoding/binary" "io" "net/netip" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/varbin" ) func readPrefix(reader varbin.Reader) (netip.Prefix, error) { addrLen, err := binary.ReadUvarint(reader) if err != nil { return netip.Prefix{}, err } addrSlice := make([]byte, addrLen) _, err = io.ReadFull(reader, addrSlice) if err != nil { return netip.Prefix{}, err } prefixBits, err := reader.ReadByte() if err != nil { return netip.Prefix{}, err } return netip.PrefixFrom(M.AddrFromIP(addrSlice), int(prefixBits)), nil } func writePrefix(writer varbin.Writer, prefix netip.Prefix) error { addrSlice := prefix.Addr().AsSlice() _, err := varbin.WriteUvarint(writer, uint64(len(addrSlice))) if err != nil { return err } _, err = writer.Write(addrSlice) if err != nil { return err } err = writer.WriteByte(uint8(prefix.Bits())) if err != nil { return err } return nil } ================================================ FILE: common/srs/ip_set.go ================================================ package srs import ( "encoding/binary" "io" "net/netip" "os" "unsafe" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/varbin" "go4.org/netipx" ) type myIPSet struct { rr []myIPRange } type myIPRange struct { from netip.Addr to netip.Addr } func readIPSet(reader varbin.Reader) (*netipx.IPSet, error) { version, err := reader.ReadByte() if err != nil { return nil, err } if version != 1 { return nil, os.ErrInvalid } // WTF why using uint64 here var length uint64 err = binary.Read(reader, binary.BigEndian, &length) if err != nil { return nil, err } mySet := &myIPSet{ rr: make([]myIPRange, length), } for i := range mySet.rr { fromLen, err := binary.ReadUvarint(reader) if err != nil { return nil, err } fromBytes := make([]byte, fromLen) _, err = io.ReadFull(reader, fromBytes) if err != nil { return nil, err } toLen, err := binary.ReadUvarint(reader) if err != nil { return nil, err } toBytes := make([]byte, toLen) _, err = io.ReadFull(reader, toBytes) if err != nil { return nil, err } mySet.rr[i].from = M.AddrFromIP(fromBytes) mySet.rr[i].to = M.AddrFromIP(toBytes) } return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil } func writeIPSet(writer varbin.Writer, set *netipx.IPSet) error { err := writer.WriteByte(1) if err != nil { return err } mySet := (*myIPSet)(unsafe.Pointer(set)) err = binary.Write(writer, binary.BigEndian, uint64(len(mySet.rr))) if err != nil { return err } for _, rr := range mySet.rr { fromBytes := rr.from.AsSlice() _, err = varbin.WriteUvarint(writer, uint64(len(fromBytes))) if err != nil { return err } _, err = writer.Write(fromBytes) if err != nil { return err } toBytes := rr.to.AsSlice() _, err = varbin.WriteUvarint(writer, uint64(len(toBytes))) if err != nil { return err } _, err = writer.Write(toBytes) if err != nil { return err } } return nil } ================================================ FILE: common/stun/stun.go ================================================ package stun import ( "context" "crypto/rand" "encoding/binary" "fmt" "net" "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" ) 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() }() if deadline.NeedAdditionalReadDeadline(packetConn) { packetConn = deadline.NewPacketConn(bufio.NewPacketConn(packetConn)) } 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 } ================================================ FILE: common/taskmonitor/monitor.go ================================================ package taskmonitor import ( "time" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/logger" ) type Monitor struct { logger logger.Logger timeout time.Duration timer *time.Timer } func New(logger logger.Logger, timeout time.Duration) *Monitor { return &Monitor{ logger: logger, timeout: timeout, } } func (m *Monitor) Start(taskName ...any) { m.timer = time.AfterFunc(m.timeout, func() { m.logger.Warn(F.ToString(taskName...), " take too much time to finish!") }) } func (m *Monitor) Finish() { m.timer.Stop() } ================================================ FILE: common/tls/acme.go ================================================ //go:build with_acme package tls import ( "context" "crypto/tls" "slices" "strings" "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/logger" "github.com/caddyserver/certmagic" "github.com/libdns/acmedns" "github.com/libdns/alidns" "github.com/libdns/cloudflare" "github.com/mholt/acmez/v3/acme" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) type acmeWrapper struct { ctx context.Context cfg *certmagic.Config cache *certmagic.Cache domain []string } func (w *acmeWrapper) Start() error { return w.cfg.ManageSync(w.ctx, w.domain) } func (w *acmeWrapper) Close() error { w.cache.Stop() return nil } func startACME(ctx context.Context, logger logger.Logger, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) { 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, nil, E.New("unsupported acme provider: " + options.Provider) } acmeServer = options.Provider } 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(ACMEEncoderConfig()), &ACMELogWriter{Logger: logger}, zap.DebugLevel, )) config := &certmagic.Config{ DefaultServerName: options.DefaultServerName, Storage: storage, Logger: zapLogger, } profile := options.Profile if profile == "" && acmeServer == certmagic.LetsEncryptProductionCA && slices.ContainsFunc(options.Domain, certmagic.SubjectIsIP) { profile = "shortlived" } acmeConfig := certmagic.ACMEIssuer{ CA: acmeServer, Email: options.Email, Agreed: true, Profile: profile, DisableHTTPChallenge: options.DisableHTTPChallenge, DisableTLSALPNChallenge: options.DisableTLSALPNChallenge, AltHTTPPort: int(options.AlternativeHTTPPort), AltTLSALPNPort: int(options.AlternativeTLSPort), Logger: zapLogger, } if dnsOptions := options.DNS01Challenge; dnsOptions != nil && dnsOptions.Provider != "" { var solver certmagic.DNS01Solver 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, } case C.DNSProviderACMEDNS: solver.DNSProvider = &acmedns.Provider{ Username: dnsOptions.ACMEDNSOptions.Username, Password: dnsOptions.ACMEDNSOptions.Password, Subdomain: dnsOptions.ACMEDNSOptions.Subdomain, ServerURL: dnsOptions.ACMEDNSOptions.ServerURL, } default: return nil, nil, E.New("unsupported ACME DNS01 provider type: " + dnsOptions.Provider) } acmeConfig.DNS01Solver = &solver } if options.ExternalAccount != nil && options.ExternalAccount.KeyID != "" { acmeConfig.ExternalAccount = (*acme.EAB)(options.ExternalAccount) } config.Issuers = []certmagic.Issuer{certmagic.NewACMEIssuer(config, acmeConfig)} cache := certmagic.NewCache(certmagic.CacheOptions{ GetConfigForCert: func(certificate certmagic.Certificate) (*certmagic.Config, error) { return config, nil }, Logger: zapLogger, }) config = certmagic.New(cache, *config) var tlsConfig *tls.Config if acmeConfig.DisableTLSALPNChallenge || acmeConfig.DNS01Solver != nil { tlsConfig = &tls.Config{ GetCertificate: config.GetCertificate, } } else { tlsConfig = &tls.Config{ GetCertificate: config.GetCertificate, NextProtos: []string{C.ACMETLS1Protocol}, } } return tlsConfig, &acmeWrapper{ctx: ctx, cfg: config, cache: cache, domain: options.Domain}, nil } ================================================ FILE: common/tls/acme_logger.go ================================================ 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 } ================================================ FILE: common/tls/acme_stub.go ================================================ //go:build !with_acme package tls import ( "context" "crypto/tls" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" ) func startACME(ctx context.Context, logger logger.Logger, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) { return nil, nil, E.New(`ACME is not included in this build, rebuild with -tags with_acme`) } ================================================ FILE: common/tls/apple_client.go ================================================ //go:build darwin && cgo package tls import ( "context" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/certificate" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common/logger" ) const appleTLSEngineName = "Apple TLS engine" type appleClientConfig struct { systemTLSConfig userPEM []byte } func (c *appleClientConfig) Clone() Config { return &appleClientConfig{ systemTLSConfig: c.systemTLSConfig.clone(), userPEM: append([]byte(nil), c.userPEM...), } } func (c *appleClientConfig) resolveAnchors() (adapter.AppleAnchors, error) { if len(c.userPEM) > 0 { return certificate.NewAppleAnchors(c.userPEM) } return certificate.AcquireAnchors(nil, c.store), nil } 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 nil, err } return &appleClientConfig{ systemTLSConfig: base, userPEM: append([]byte(nil), validated.UserPEM...), }, nil } ================================================ FILE: common/tls/apple_client_platform.go ================================================ //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" "runtime/cgo" "strings" "sync" "time" "unsafe" "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) { tcpConn, ok := N.UnwrapReader(conn).(*net.TCPConn) if !ok { return nil, E.New("apple TLS: requires fd-backed TCP connection") } syscallConn, err := tcpConn.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) anchors, err := c.resolveAnchors() if err != nil { return nil, err } var anchorsRef unsafe.Pointer if anchors != nil { anchorsRef = anchors.Ref() } 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), 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)) 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 appleTLSWriteChunkSize = 32 * 1024 ) func waitAppleTLSClientReady(ctx context.Context, client *C.box_apple_tls_client_t) error { for { err := ctx.Err() if err != nil { C.box_apple_tls_client_cancel(client) return err } waitTimeout := appleTLSHandshakePollInterval deadline, loaded := ctx.Deadline() if loaded { remaining := time.Until(deadline) if remaining <= 0 { C.box_apple_tls_client_cancel(client) err = ctx.Err() if 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 } var ( _ N.ExtendedConn = (*appleTLSConn)(nil) _ N.ReadWaitCreator = (*appleTLSConn)(nil) ) 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 } 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 } 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 } client, err := c.acquireClient() if err != nil { return 0, err } defer c.releaseClient() deadline, err := c.prepareWriteDeadline() if err != nil { return 0, err } 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 written, c.errorFromPointer(errorPtr) } 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 { 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) prepareWriteDeadline() (time.Time, error) { c.deadlineAccess.Lock() defer c.deadlineAccess.Unlock() if c.writeTimedOut { return time.Time{}, os.ErrDeadlineExceeded } _, expired := deadlineTimeoutMs(c.writeDeadline) if expired { c.writeTimedOut = true return time.Time{}, os.ErrDeadlineExceeded } return c.writeDeadline, 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) 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 } 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)) } } ================================================ FILE: common/tls/apple_client_platform_benchmark_test.go ================================================ //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 } ================================================ FILE: common/tls/apple_client_platform_darwin.h ================================================ #include #include #include #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; 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, void *anchors_cf, 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_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); ================================================ FILE: common/tls/apple_client_platform_darwin.m ================================================ #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; void *anchors; 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; 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; } 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 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; } 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->anchors != NULL) { CFRelease((CFTypeRef)client->anchors); } 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 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; 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 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; } // 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); 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) { memcpy(state->peer_cert_chain, chain_data.bytes, chain_data.length); state->peer_cert_chain_len = chain_data.length; } } } 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, void *anchors_cf, 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); if (anchors_cf != NULL) { client->anchors = (void *)CFRetain(anchors_cf); } NSArray *alpnList = box_split_lines(alpn, alpn_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); 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, 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); } 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)) { 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)); } 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) { @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); } }); 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; } return (ssize_t)content_len; } } 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) { @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); } }); 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_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; } } 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; } if (eof_out != NULL) { *eof_out = result->eof; } dispatch_data_t content = box_apple_tls_read_result_content(result); if (content == nil) { return 0; } 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); } free(result); } 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); } ================================================ FILE: common/tls/apple_client_platform_dispatch_test.go ================================================ //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) } } ================================================ FILE: common/tls/apple_client_platform_dispatch_testhelper_darwin.go ================================================ //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 } ================================================ FILE: common/tls/apple_client_platform_test.go ================================================ //go:build darwin && cgo 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 const ( appleTLSSuccessHandshakeLoops = 20 appleTLSFailureRecoveryLoops = 10 ) type appleTLSServerResult struct { state stdtls.ConnectionState err error } var ( _ N.ExtendedConn = (*appleTLSConn)(nil) _ N.ReadWaitCreator = (*appleTLSConn)(nil) ) func TestAppleClientHandshakeAppliesALPNAndVersion(t *testing.T) { serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost") for index := range appleTLSSuccessHandshakeLoops { 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 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{ 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 := range appleTLSFailureRecoveryLoops { 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 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 := range appleTLSFailureRecoveryLoops { 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{ 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 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)) 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.TB, serverAddress string, options option.OutboundTLSOptions) (Conn, error) { t.Helper() clientConfig, err := NewClientWithOptions(ClientOptions{ Context: context.Background(), Logger: logger.NOP(), ServerAddress: "", Options: options, }) 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 { return nil, err } tlsConn, err := ClientHandshake(ctx, conn, clientConfig) if err != nil { conn.Close() return nil, err } return tlsConn, nil } ================================================ FILE: common/tls/apple_client_stub.go ================================================ //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") } ================================================ FILE: common/tls/client.go ================================================ package tls import ( "context" "crypto/tls" "errors" "net" "os" "strings" "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" "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 parseTLSSpoofOptions(serverName string, options option.OutboundTLSOptions) (string, tlsspoof.Method, error) { spoof, method, err := tlsspoof.ParseOptions(options.Spoof, options.SpoofMethod) if err != nil { return "", 0, err } 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(spoof, serverName) { return "", 0, E.New("`spoof` must differ from `server_name`") } return spoof, method, nil } func applyTLSSpoof(conn net.Conn, spoof string, method tlsspoof.Method) (net.Conn, error) { if spoof == "" { return conn, 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) { if !options.Enabled { return dialer, nil } config, err := NewClientWithOptions(ClientOptions{ Context: ctx, Logger: logger, ServerAddress: serverAddress, Options: options, }) if err != nil { return nil, err } return NewDialer(dialer, config), nil } func NewClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) { return NewClientWithOptions(ClientOptions{ Context: ctx, Logger: logger, ServerAddress: serverAddress, Options: options, }) } type ClientOptions struct { Context context.Context Logger logger.ContextLogger ServerAddress string Options option.OutboundTLSOptions AllowEmptyServerName bool KTLSCompatible bool } func NewClientWithOptions(options ClientOptions) (Config, error) { if !options.Options.Enabled { return nil, nil } if !options.KTLSCompatible { if options.Options.KernelTx { options.Logger.Warn("enabling kTLS TX in current scenarios will definitely reduce performance, please checkout https://sing-box.sagernet.org/configuration/shared/tls/#kernel_tx") } } 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.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) } if options.Options.Reality != nil && options.Options.Reality.Enabled { 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, options.AllowEmptyServerName) } return newSTDClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName) } func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) { tlsConn, err := aTLS.ClientHandshake(ctx, conn, config) if err != nil { return nil, err } readWaitConn, err := badtls.NewReadWaitConn(tlsConn) if err == nil { return readWaitConn, nil } else if err != os.ErrInvalid { return nil, err } return tlsConn, nil } type Dialer interface { N.Dialer DialTLSContext(ctx context.Context, destination M.Socksaddr) (Conn, error) } type defaultDialer struct { dialer N.Dialer config Config } func NewDialer(dialer N.Dialer, config Config) Dialer { return &defaultDialer{dialer, config} } func (d *defaultDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { if N.NetworkName(network) != N.NetworkTCP { return nil, os.ErrInvalid } return d.DialTLSContext(ctx, destination) } func (d *defaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { return nil, os.ErrInvalid } func (d *defaultDialer) DialTLSContext(ctx context.Context, destination M.Socksaddr) (Conn, error) { return d.dialContext(ctx, destination, true) } func (d *defaultDialer) dialContext(ctx context.Context, destination M.Socksaddr, echRetry bool) (Conn, error) { conn, err := d.dialer.DialContext(ctx, N.NetworkTCP, destination) if err != nil { return nil, err } tlsConn, err := aTLS.ClientHandshake(ctx, conn, d.config) if err != nil { conn.Close() var echErr *tls.ECHRejectionError if echRetry && errors.As(err, &echErr) && len(echErr.RetryConfigList) > 0 { if echConfig, isECH := d.config.(ECHCapableConfig); isECH { echConfig.SetECHConfigList(echErr.RetryConfigList) return d.dialContext(ctx, destination, false) } } return nil, err } return tlsConn, nil } func (d *defaultDialer) Upstream() any { return d.dialer } ================================================ FILE: common/tls/common.go ================================================ package tls const ( VersionTLS10 = 0x0301 VersionTLS11 = 0x0302 VersionTLS12 = 0x0303 VersionTLS13 = 0x0304 // Deprecated: SSLv3 is cryptographically broken, and is no longer // supported by this package. See golang.org/issue/32716. VersionSSL30 = 0x0300 ) ================================================ FILE: common/tls/config.go ================================================ package tls import ( "crypto/tls" E "github.com/sagernet/sing/common/exceptions" aTLS "github.com/sagernet/sing/common/tls" ) type ( Config = aTLS.Config ConfigCompat = aTLS.ConfigCompat ServerConfig = aTLS.ServerConfig ServerConfigCompat = aTLS.ServerConfigCompat WithSessionIDGenerator = aTLS.WithSessionIDGenerator Conn = aTLS.Conn STDConfig = tls.Config STDConn = tls.Conn ConnectionState = tls.ConnectionState CurveID = tls.CurveID ) func ParseTLSVersion(version string) (uint16, error) { switch version { case "1.0": return tls.VersionTLS10, nil case "1.1": return tls.VersionTLS11, nil case "1.2": return tls.VersionTLS12, nil case "1.3": return tls.VersionTLS13, nil default: return 0, E.New("unknown tls version:", version) } } ================================================ FILE: common/tls/ech.go ================================================ //go:build go1.24 package tls import ( "context" "crypto/tls" "encoding/base64" "encoding/pem" "net" "os" "strings" "sync" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" aTLS "github.com/sagernet/sing/common/tls" "github.com/sagernet/sing/service" mDNS "github.com/miekg/dns" "golang.org/x/crypto/cryptobyte" ) func parseECHClientConfig(ctx context.Context, clientConfig ECHCapableConfig, options option.OutboundTLSOptions) (Config, error) { var echConfig []byte if len(options.ECH.Config) > 0 { echConfig = []byte(strings.Join(options.ECH.Config, "\n")) } else if options.ECH.ConfigPath != "" { content, err := os.ReadFile(options.ECH.ConfigPath) if err != nil { return nil, E.Cause(err, "read ECH config") } echConfig = content } //nolint:staticcheck if options.ECH.PQSignatureSchemesEnabled || options.ECH.DynamicRecordSizingDisabled { return nil, E.New("legacy ECH options are deprecated in sing-box 1.12.0 and removed in sing-box 1.13.0") } if len(echConfig) > 0 { block, rest := pem.Decode(echConfig) if block == nil || block.Type != "ECH CONFIGS" || len(rest) > 0 { return nil, E.New("invalid ECH configs pem") } clientConfig.SetECHConfigList(block.Bytes) return clientConfig, nil } else { return &ECHClientConfig{ ECHCapableConfig: clientConfig, dnsRouter: service.FromContext[adapter.DNSRouter](ctx), queryServerName: options.ECH.QueryServerName, }, nil } } func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions, tlsConfig *tls.Config, echKeyPath *string) error { var echKey []byte if len(options.ECH.Key) > 0 { echKey = []byte(strings.Join(options.ECH.Key, "\n")) } else if options.ECH.KeyPath != "" { content, err := os.ReadFile(options.ECH.KeyPath) if err != nil { return E.Cause(err, "read ECH keys") } echKey = content *echKeyPath = options.ECH.KeyPath } else { return E.New("missing ECH keys") } echKeys, err := parseECHKeys(echKey) if err != nil { return E.Cause(err, "parse ECH keys") } tlsConfig.EncryptedClientHelloKeys = echKeys //nolint:staticcheck if options.ECH.PQSignatureSchemesEnabled || options.ECH.DynamicRecordSizingDisabled { return E.New("legacy ECH options are deprecated in sing-box 1.12.0 and removed in sing-box 1.13.0") } return nil } func (c *STDServerConfig) setECHServerConfig(echKey []byte) error { echKeys, err := parseECHKeys(echKey) if err != nil { return err } c.access.Lock() config := c.config.Clone() config.EncryptedClientHelloKeys = echKeys c.config = config c.access.Unlock() return nil } func parseECHKeys(echKey []byte) ([]tls.EncryptedClientHelloKey, error) { block, _ := pem.Decode(echKey) if block == nil || block.Type != "ECH KEYS" { return nil, E.New("invalid ECH keys pem") } echKeys, err := UnmarshalECHKeys(block.Bytes) if err != nil { return nil, E.Cause(err, "parse ECH keys") } return echKeys, nil } type ECHClientConfig struct { ECHCapableConfig access sync.Mutex dnsRouter adapter.DNSRouter queryServerName string lastTTL time.Duration lastUpdate time.Time } func (s *ECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) { tlsConn, err := s.fetchAndHandshake(ctx, conn) if err != nil { return nil, err } err = tlsConn.HandshakeContext(ctx) if err != nil { return nil, err } return tlsConn, nil } func (s *ECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) { s.access.Lock() defer s.access.Unlock() if len(s.ECHConfigList()) == 0 || s.lastTTL == 0 || time.Since(s.lastUpdate) > s.lastTTL { queryServerName := s.queryServerName if queryServerName == "" { queryServerName = s.ServerName() } message := &mDNS.Msg{ MsgHdr: mDNS.MsgHdr{ RecursionDesired: true, }, Question: []mDNS.Question{ { Name: mDNS.Fqdn(queryServerName), Qtype: mDNS.TypeHTTPS, Qclass: mDNS.ClassINET, }, }, } response, err := s.dnsRouter.Exchange(ctx, message, adapter.DNSQueryOptions{}) if err != nil { return nil, E.Cause(err, "fetch ECH config list") } if response.Rcode != mDNS.RcodeSuccess { return nil, E.Cause(dns.RcodeError(response.Rcode), "fetch ECH config list") } match: for _, rr := range response.Answer { switch resource := rr.(type) { case *mDNS.HTTPS: for _, value := range resource.Value { if value.Key().String() == "ech" { echConfigList, err := base64.StdEncoding.DecodeString(value.String()) if err != nil { return nil, E.Cause(err, "decode ECH config") } s.lastTTL = time.Duration(rr.Header().Ttl) * time.Second s.lastUpdate = time.Now() s.SetECHConfigList(echConfigList) break match } } } } if len(s.ECHConfigList()) == 0 { return nil, E.New("no ECH config found in DNS records") } } return s.Client(conn) } func (s *ECHClientConfig) Clone() Config { return &ECHClientConfig{ ECHCapableConfig: s.ECHCapableConfig.Clone().(ECHCapableConfig), dnsRouter: s.dnsRouter, queryServerName: s.queryServerName, lastUpdate: s.lastUpdate, } } func UnmarshalECHKeys(raw []byte) ([]tls.EncryptedClientHelloKey, error) { var keys []tls.EncryptedClientHelloKey rawString := cryptobyte.String(raw) for !rawString.Empty() { var key tls.EncryptedClientHelloKey if !rawString.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.PrivateKey)) { return nil, E.New("error parsing private key") } if !rawString.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.Config)) { return nil, E.New("error parsing config") } keys = append(keys, key) } if len(keys) == 0 { return nil, E.New("empty ECH keys") } return keys, nil } ================================================ FILE: common/tls/ech_shared.go ================================================ package tls import ( "crypto/ecdh" "crypto/rand" "encoding/pem" "golang.org/x/crypto/cryptobyte" ) type ECHCapableConfig interface { Config ECHConfigList() []byte SetECHConfigList([]byte) } func ECHKeygenDefault(publicName string) (configPem string, keyPem string, err error) { echKey, err := ecdh.X25519().GenerateKey(rand.Reader) if err != nil { return } echConfig, err := marshalECHConfig(0, echKey.PublicKey().Bytes(), publicName, 0) if err != nil { return } configBuilder := cryptobyte.NewBuilder(nil) configBuilder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { builder.AddBytes(echConfig) }) configBytes, err := configBuilder.Bytes() if err != nil { return } keyBuilder := cryptobyte.NewBuilder(nil) keyBuilder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { builder.AddBytes(echKey.Bytes()) }) keyBuilder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { builder.AddBytes(echConfig) }) keyBytes, err := keyBuilder.Bytes() if err != nil { return } configPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH CONFIGS", Bytes: configBytes})) keyPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: keyBytes})) return } func marshalECHConfig(id uint8, pubKey []byte, publicName string, maxNameLen uint8) ([]byte, error) { const extensionEncryptedClientHello = 0xfe0d const DHKEM_X25519_HKDF_SHA256 = 0x0020 const KDF_HKDF_SHA256 = 0x0001 builder := cryptobyte.NewBuilder(nil) builder.AddUint16(extensionEncryptedClientHello) builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { builder.AddUint8(id) builder.AddUint16(DHKEM_X25519_HKDF_SHA256) // The only DHKEM we support builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { builder.AddBytes(pubKey) }) builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { const ( AEAD_AES_128_GCM = 0x0001 AEAD_AES_256_GCM = 0x0002 AEAD_ChaCha20Poly1305 = 0x0003 ) for _, aeadID := range []uint16{AEAD_AES_128_GCM, AEAD_AES_256_GCM, AEAD_ChaCha20Poly1305} { builder.AddUint16(KDF_HKDF_SHA256) // The only KDF we support builder.AddUint16(aeadID) } }) builder.AddUint8(maxNameLen) builder.AddUint8LengthPrefixed(func(builder *cryptobyte.Builder) { builder.AddBytes([]byte(publicName)) }) builder.AddUint16(0) // extensions }) return builder.Bytes() } ================================================ FILE: common/tls/ech_tag_stub.go ================================================ //go:build with_ech package tls var _ int = "Due to the migration to stdlib, the separate `with_ech` build tag has been deprecated and is no longer needed, please update your build configuration." ================================================ FILE: common/tls/ktls.go ================================================ package tls import ( "context" "net" "github.com/sagernet/sing-box/common/ktls" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" aTLS "github.com/sagernet/sing/common/tls" ) type KTLSClientConfig struct { Config logger logger.ContextLogger kernelTx, kernelRx bool } func (w *KTLSClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) { tlsConn, err := aTLS.ClientHandshake(ctx, conn, w.Config) if err != nil { return nil, err } kConn, err := ktls.NewConn(ctx, w.logger, tlsConn, w.kernelTx, w.kernelRx) if err != nil { tlsConn.Close() return nil, E.Cause(err, "initialize kernel TLS") } return kConn, nil } func (w *KTLSClientConfig) Clone() Config { return &KTLSClientConfig{ w.Config.Clone(), w.logger, w.kernelTx, w.kernelRx, } } type KTlSServerConfig struct { ServerConfig logger logger.ContextLogger kernelTx, kernelRx bool } func (w *KTlSServerConfig) ServerHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) { tlsConn, err := aTLS.ServerHandshake(ctx, conn, w.ServerConfig) if err != nil { return nil, err } kConn, err := ktls.NewConn(ctx, w.logger, tlsConn, w.kernelTx, w.kernelRx) if err != nil { tlsConn.Close() return nil, E.Cause(err, "initialize kernel TLS") } return kConn, nil } func (w *KTlSServerConfig) Clone() Config { return &KTlSServerConfig{ w.ServerConfig.Clone().(ServerConfig), w.logger, w.kernelTx, w.kernelRx, } } ================================================ FILE: common/tls/mkcert.go ================================================ package tls import ( "crypto/rand" "crypto/rsa" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "math/big" "time" ) func GenerateKeyPair(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string) (*tls.Certificate, error) { if timeFunc == nil { timeFunc = time.Now } privateKeyPem, publicKeyPem, err := GenerateCertificate(parent, parentKey, timeFunc, serverName, timeFunc().Add(time.Hour)) if err != nil { return nil, err } certificate, err := tls.X509KeyPair(publicKeyPem, privateKeyPem) if err != nil { return nil, err } return &certificate, err } func GenerateCertificate(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string, expire time.Time) (privateKeyPem []byte, publicKeyPem []byte, err error) { key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return } serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) if err != nil { return } template := &x509.Certificate{ SerialNumber: serialNumber, NotBefore: timeFunc().Add(time.Hour * -1), NotAfter: expire, KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, Subject: pkix.Name{ CommonName: serverName, }, DNSNames: []string{serverName}, } if parent == nil { parent = template parentKey = key } publicDer, err := x509.CreateCertificate(rand.Reader, template, parent, key.Public(), parentKey) if err != nil { return } privateDer, err := x509.MarshalPKCS8PrivateKey(key) if err != nil { return } publicKeyPem = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: publicDer}) privateKeyPem = pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateDer}) return } ================================================ FILE: common/tls/reality_client.go ================================================ //go:build with_utls package tls import ( "bytes" "context" "crypto/aes" "crypto/cipher" "crypto/ecdh" "crypto/ed25519" "crypto/hmac" "crypto/sha256" "crypto/sha512" "crypto/tls" "crypto/x509" "encoding/base64" "encoding/binary" "encoding/hex" "fmt" "io" mRand "math/rand" "net" "net/http" "reflect" "strings" "time" "unsafe" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/debug" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/ntp" aTLS "github.com/sagernet/sing/common/tls" utls "github.com/metacubex/utls" "golang.org/x/crypto/hkdf" "golang.org/x/net/http2" ) var _ ConfigCompat = (*RealityClientConfig)(nil) type RealityClientConfig struct { ctx context.Context uClient *UTLSClientConfig publicKey []byte shortID [8]byte } 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") } 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 { return nil, err } publicKey, err := base64.RawURLEncoding.DecodeString(options.Reality.PublicKey) if err != nil { return nil, E.Cause(err, "decode public_key") } if len(publicKey) != 32 { return nil, E.New("invalid public_key") } var shortID [8]byte decodedLen, err := hex.Decode(shortID[:], []byte(options.Reality.ShortID)) if err != nil { return nil, E.Cause(err, "decode short_id") } if decodedLen > 8 { return nil, E.New("invalid short_id") } var config Config = &RealityClientConfig{ctx, uClient.(*UTLSClientConfig), publicKey, shortID} if options.KernelRx || options.KernelTx { if !C.IsLinux { return nil, E.New("kTLS is only supported on Linux") } config = &KTLSClientConfig{ Config: config, logger: logger, kernelTx: options.KernelTx, kernelRx: options.KernelRx, } } return config, nil } func (e *RealityClientConfig) ServerName() string { return e.uClient.ServerName() } func (e *RealityClientConfig) SetServerName(serverName string) { e.uClient.SetServerName(serverName) } func (e *RealityClientConfig) NextProtos() []string { return e.uClient.NextProtos() } 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") } func (e *RealityClientConfig) Client(conn net.Conn) (Conn, error) { return ClientHandshake(context.Background(), conn, e) } func (e *RealityClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) { verifier := &realityVerifier{ serverName: e.uClient.ServerName(), } uConfig := e.uClient.config.Clone() uConfig.InsecureSkipVerify = true uConfig.SessionTicketsDisabled = true uConfig.VerifyPeerCertificate = verifier.VerifyPeerCertificate uConn := utls.UClient(conn, uConfig, e.uClient.id) verifier.UConn = uConn err := uConn.BuildHandshakeState() if err != nil { return nil, err } for _, extension := range uConn.Extensions { if ce, ok := extension.(*utls.SupportedCurvesExtension); ok { ce.Curves = common.Filter(ce.Curves, func(curveID utls.CurveID) bool { return curveID != utls.X25519MLKEM768 }) } if ks, ok := extension.(*utls.KeyShareExtension); ok { ks.KeyShares = common.Filter(ks.KeyShares, func(share utls.KeyShare) bool { return share.Group != utls.X25519MLKEM768 }) } } err = uConn.BuildHandshakeState() if err != nil { return nil, err } if len(uConfig.NextProtos) > 0 { for _, extension := range uConn.Extensions { if alpnExtension, isALPN := extension.(*utls.ALPNExtension); isALPN { alpnExtension.AlpnProtocols = uConfig.NextProtos break } } } hello := uConn.HandshakeState.Hello hello.SessionId = make([]byte, 32) copy(hello.Raw[39:], hello.SessionId) var nowTime time.Time if uConfig.Time != nil { nowTime = uConfig.Time() } else { nowTime = time.Now() } binary.BigEndian.PutUint64(hello.SessionId, uint64(nowTime.Unix())) hello.SessionId[0] = 1 hello.SessionId[1] = 8 hello.SessionId[2] = 1 binary.BigEndian.PutUint32(hello.SessionId[4:], uint32(time.Now().Unix())) copy(hello.SessionId[8:], e.shortID[:]) if debug.Enabled { fmt.Printf("REALITY hello.sessionId[:16]: %v\n", hello.SessionId[:16]) } publicKey, err := ecdh.X25519().NewPublicKey(e.publicKey) if err != nil { return nil, err } keyShareKeys := uConn.HandshakeState.State13.KeyShareKeys if keyShareKeys == nil { return nil, E.New("nil KeyShareKeys") } ecdheKey := keyShareKeys.Ecdhe if ecdheKey == nil { return nil, E.New("nil ecdheKey") } authKey, err := ecdheKey.ECDH(publicKey) if err != nil { return nil, err } if authKey == nil { return nil, E.New("nil auth_key") } verifier.authKey = authKey _, err = hkdf.New(sha256.New, authKey, hello.Random[:20], []byte("REALITY")).Read(authKey) if err != nil { return nil, err } aesBlock, _ := aes.NewCipher(authKey) aesGcmCipher, _ := cipher.NewGCM(aesBlock) aesGcmCipher.Seal(hello.SessionId[:0], hello.Random[20:], hello.SessionId[:16], hello.Raw) copy(hello.Raw[39:], hello.SessionId) if debug.Enabled { fmt.Printf("REALITY hello.sessionId: %v\n", hello.SessionId) fmt.Printf("REALITY uConn.AuthKey: %v\n", authKey) } err = uConn.HandshakeContext(ctx) if err != nil { return nil, err } if debug.Enabled { fmt.Printf("REALITY Conn.Verified: %v\n", verifier.verified) } if !verifier.verified { go realityClientFallback(e.ctx, uConn, e.uClient.ServerName(), e.uClient.id) return nil, E.New("reality verification failed") } return &realityClientConnWrapper{uConn}, nil } func realityClientFallback(ctx context.Context, uConn net.Conn, serverName string, fingerprint utls.ClientHelloID) { defer uConn.Close() client := &http.Client{ Transport: &http2.Transport{ DialTLSContext: func(ctx context.Context, network, addr string, config *tls.Config) (net.Conn, error) { return uConn, nil }, TLSClientConfig: &tls.Config{ Time: ntp.TimeFuncFromContext(ctx), RootCAs: adapter.RootPoolFromContext(ctx), }, }, } request, _ := http.NewRequest("GET", "https://"+serverName, nil) request.Header.Set("User-Agent", fingerprint.Client) request.AddCookie(&http.Cookie{Name: "padding", Value: strings.Repeat("0", mRand.Intn(32)+30)}) response, err := client.Do(request) if err != nil { return } _, _ = io.Copy(io.Discard, response.Body) response.Body.Close() } func (e *RealityClientConfig) Clone() Config { return &RealityClientConfig{ e.ctx, e.uClient.Clone().(*UTLSClientConfig), e.publicKey, e.shortID, } } type realityVerifier struct { *utls.UConn serverName string authKey []byte verified bool } func (c *realityVerifier) VerifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { p, _ := reflect.TypeFor[utls.Conn]().FieldByName("peerCertificates") certs := *(*([]*x509.Certificate))(unsafe.Add(unsafe.Pointer(c.Conn), p.Offset)) if pub, ok := certs[0].PublicKey.(ed25519.PublicKey); ok { h := hmac.New(sha512.New, c.authKey) h.Write(pub) if bytes.Equal(h.Sum(nil), certs[0].Signature) { c.verified = true return nil } } opts := x509.VerifyOptions{ DNSName: c.serverName, Intermediates: x509.NewCertPool(), } for _, cert := range certs[1:] { opts.Intermediates.AddCert(cert) } if _, err := certs[0].Verify(opts); err != nil { return err } return nil } type realityClientConnWrapper struct { *utls.UConn } func (c *realityClientConnWrapper) ConnectionState() tls.ConnectionState { state := c.Conn.ConnectionState() //nolint:staticcheck return tls.ConnectionState{ Version: state.Version, HandshakeComplete: state.HandshakeComplete, DidResume: state.DidResume, CipherSuite: state.CipherSuite, NegotiatedProtocol: state.NegotiatedProtocol, NegotiatedProtocolIsMutual: state.NegotiatedProtocolIsMutual, ServerName: state.ServerName, PeerCertificates: state.PeerCertificates, VerifiedChains: state.VerifiedChains, SignedCertificateTimestamps: state.SignedCertificateTimestamps, OCSPResponse: state.OCSPResponse, TLSUnique: state.TLSUnique, } } func (c *realityClientConnWrapper) Upstream() any { return c.UConn } // Due to low implementation quality, the reality server intercepted half close and caused memory leaks. // We fixed it by calling Close() directly. func (c *realityClientConnWrapper) CloseWrite() error { return c.Close() } func (c *realityClientConnWrapper) ReaderReplaceable() bool { return true } func (c *realityClientConnWrapper) WriterReplaceable() bool { return true } ================================================ FILE: common/tls/reality_server.go ================================================ //go:build with_utls package tls import ( "context" "crypto/tls" "encoding/base64" "encoding/hex" "fmt" "net" "time" "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/common/ntp" utls "github.com/metacubex/utls" ) var _ ServerConfigCompat = (*RealityServerConfig)(nil) type RealityServerConfig struct { config *utls.RealityConfig handshakeTimeout time.Duration } 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") } tlsConfig.Time = ntp.TimeFuncFromContext(ctx) if options.ServerName != "" { tlsConfig.ServerName = options.ServerName } if len(options.ALPN) > 0 { tlsConfig.NextProtos = append(tlsConfig.NextProtos, options.ALPN...) } if options.MinVersion != "" { minVersion, err := ParseTLSVersion(options.MinVersion) if err != nil { return nil, E.Cause(err, "parse min_version") } tlsConfig.MinVersion = minVersion } if options.MaxVersion != "" { maxVersion, err := ParseTLSVersion(options.MaxVersion) if err != nil { return nil, E.Cause(err, "parse max_version") } tlsConfig.MaxVersion = maxVersion } if options.CipherSuites != nil { find: for _, cipherSuite := range options.CipherSuites { for _, tlsCipherSuite := range tls.CipherSuites() { if cipherSuite == tlsCipherSuite.Name { tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, tlsCipherSuite.ID) continue find } } return nil, E.New("unknown cipher_suite: ", cipherSuite) } } if len(options.CurvePreferences) > 0 { return nil, E.New("curve preferences is unavailable in reality") } if len(options.Certificate) > 0 || options.CertificatePath != "" || len(options.ClientCertificatePublicKeySHA256) > 0 { return nil, E.New("certificate is unavailable in reality") } if len(options.Key) > 0 || options.KeyPath != "" { return nil, E.New("key is unavailable in reality") } tlsConfig.SessionTicketsDisabled = true tlsConfig.Log = func(format string, v ...any) { if logger != nil { logger.Trace(fmt.Sprintf(format, v...)) } } tlsConfig.Type = N.NetworkTCP tlsConfig.Dest = options.Reality.Handshake.ServerOptions.Build().String() tlsConfig.ServerNames = map[string]bool{options.ServerName: true} privateKey, err := base64.RawURLEncoding.DecodeString(options.Reality.PrivateKey) if err != nil { return nil, E.Cause(err, "decode private key") } if len(privateKey) != 32 { return nil, E.New("invalid private key") } tlsConfig.PrivateKey = privateKey tlsConfig.MaxTimeDiff = time.Duration(options.Reality.MaxTimeDifference) tlsConfig.ShortIds = make(map[[8]byte]bool) if len(options.Reality.ShortID) == 0 { tlsConfig.ShortIds[[8]byte{0}] = true } else { for i, shortIDString := range options.Reality.ShortID { var shortID [8]byte decodedLen, err := hex.Decode(shortID[:], []byte(shortIDString)) if err != nil { return nil, E.Cause(err, "decode short_id[", i, "]: ", shortIDString) } if decodedLen > 8 { return nil, E.New("invalid short_id[", i, "]: ", shortIDString) } tlsConfig.ShortIds[shortID] = true } } handshakeDialer, err := dialer.New(ctx, options.Reality.Handshake.DialerOptions, options.Reality.Handshake.ServerIsDomain()) if err != nil { return nil, err } tlsConfig.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { return handshakeDialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) } if options.ECH != nil && options.ECH.Enabled { return nil, E.New("Reality is conflict with ECH") } 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") } config = &KTlSServerConfig{ ServerConfig: config, logger: logger, kernelTx: options.KernelTx, kernelRx: options.KernelRx, } } return config, nil } func (c *RealityServerConfig) ServerName() string { return c.config.ServerName } func (c *RealityServerConfig) SetServerName(serverName string) { c.config.ServerName = serverName } func (c *RealityServerConfig) NextProtos() []string { return c.config.NextProtos } 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") } func (c *RealityServerConfig) Client(conn net.Conn) (Conn, error) { return ClientHandshake(context.Background(), conn, c) } func (c *RealityServerConfig) Start() error { return nil } func (c *RealityServerConfig) Close() error { return nil } func (c *RealityServerConfig) Server(conn net.Conn) (Conn, error) { return ServerHandshake(context.Background(), conn, c) } func (c *RealityServerConfig) ServerHandshake(ctx context.Context, conn net.Conn) (Conn, error) { tlsConn, err := utls.RealityServer(ctx, conn, c.config) if err != nil { return nil, err } return &realityConnWrapper{Conn: tlsConn}, nil } func (c *RealityServerConfig) Clone() Config { return &RealityServerConfig{ config: c.config.Clone(), handshakeTimeout: c.handshakeTimeout, } } var _ Conn = (*realityConnWrapper)(nil) type realityConnWrapper struct { *utls.Conn } func (c *realityConnWrapper) ConnectionState() ConnectionState { state := c.Conn.ConnectionState() //nolint:staticcheck return tls.ConnectionState{ Version: state.Version, HandshakeComplete: state.HandshakeComplete, DidResume: state.DidResume, CipherSuite: state.CipherSuite, NegotiatedProtocol: state.NegotiatedProtocol, NegotiatedProtocolIsMutual: state.NegotiatedProtocolIsMutual, ServerName: state.ServerName, PeerCertificates: state.PeerCertificates, VerifiedChains: state.VerifiedChains, SignedCertificateTimestamps: state.SignedCertificateTimestamps, OCSPResponse: state.OCSPResponse, TLSUnique: state.TLSUnique, } } func (c *realityConnWrapper) Upstream() any { return c.Conn } // Due to low implementation quality, the reality server intercepted half close and caused memory leaks. // We fixed it by calling Close() directly. func (c *realityConnWrapper) CloseWrite() error { return c.Close() } func (c *realityConnWrapper) ReaderReplaceable() bool { return true } func (c *realityConnWrapper) WriterReplaceable() bool { return true } ================================================ FILE: common/tls/reality_stub.go ================================================ //go:build with_reality_server package tls var _ int = "The separate `with_reality_server` build tag has been merged into `with_utls` and is no longer needed, please update your build configuration." ================================================ FILE: common/tls/server.go ================================================ package tls import ( "context" "net" "os" "github.com/sagernet/sing-box/common/badtls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" aTLS "github.com/sagernet/sing/common/tls" ) type ServerOptions struct { Context context.Context Logger log.ContextLogger Options option.InboundTLSOptions KTLSCompatible bool } func NewServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) { return NewServerWithOptions(ServerOptions{ Context: ctx, Logger: logger, Options: options, }) } func NewServerWithOptions(options ServerOptions) (ServerConfig, error) { if !options.Options.Enabled { return nil, nil } if !options.KTLSCompatible { if options.Options.KernelTx { options.Logger.Warn("enabling kTLS TX in current scenarios will definitely reduce performance, please checkout https://sing-box.sagernet.org/configuration/shared/tls/#kernel_tx") } } 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") } if options.Options.Reality != nil && options.Options.Reality.Enabled { return NewRealityServer(options.Context, options.Logger, options.Options) } return NewSTDServer(options.Context, options.Logger, options.Options) } func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) { 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 } readWaitConn, err := badtls.NewReadWaitConn(tlsConn) if err == nil { return readWaitConn, nil } else if err != os.ErrInvalid { return nil, err } return tlsConn, nil } ================================================ FILE: common/tls/std_client.go ================================================ package tls import ( "bytes" "context" "crypto/sha256" "crypto/tls" "crypto/x509" "encoding/base64" "net" "os" "strings" "time" "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" "github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/ntp" ) 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 spoof string spoofMethod tlsspoof.Method } func (c *STDClientConfig) ServerName() string { 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 } func (c *STDClientConfig) NextProtos() []string { return c.config.NextProtos } 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 } func (c *STDClientConfig) Client(conn net.Conn) (Conn, error) { 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) if err != nil { return nil, err } return tls.Client(conn, c.config), nil } func (c *STDClientConfig) Clone() Config { 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, spoof: c.spoof, spoofMethod: c.spoofMethod, } cloned.SetServerName(cloned.serverName) return cloned } func (c *STDClientConfig) ECHConfigList() []byte { return c.config.EncryptedClientHelloConfigList } func (c *STDClientConfig) SetECHConfigList(EncryptedClientHelloConfigList []byte) { c.config.EncryptedClientHelloConfigList = EncryptedClientHelloConfigList } 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 && !allowEmptyServerName { return nil, errMissingServerName } var tlsConfig tls.Config tlsConfig.Time = ntp.TimeFuncFromContext(ctx) tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx) if options.Insecure { tlsConfig.InsecureSkipVerify = options.Insecure } else if options.DisableSNI { tlsConfig.InsecureSkipVerify = true } if len(options.CertificatePublicKeySHA256) > 0 { if len(options.Certificate) > 0 || options.CertificatePath != "" { return nil, E.New("certificate_public_key_sha256 is conflict with certificate or certificate_path") } tlsConfig.InsecureSkipVerify = true tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { return VerifyPublicKeySHA256(options.CertificatePublicKeySHA256, rawCerts) } } if len(options.ALPN) > 0 { tlsConfig.NextProtos = options.ALPN } if options.MinVersion != "" { minVersion, err := ParseTLSVersion(options.MinVersion) if err != nil { return nil, E.Cause(err, "parse min_version") } tlsConfig.MinVersion = minVersion } if options.MaxVersion != "" { maxVersion, err := ParseTLSVersion(options.MaxVersion) if err != nil { return nil, E.Cause(err, "parse max_version") } tlsConfig.MaxVersion = maxVersion } if options.CipherSuites != nil { find: for _, cipherSuite := range options.CipherSuites { for _, tlsCipherSuite := range tls.CipherSuites() { if cipherSuite == tlsCipherSuite.Name { tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, tlsCipherSuite.ID) continue find } } return nil, E.New("unknown cipher_suite: ", cipherSuite) } } for _, curve := range options.CurvePreferences { tlsConfig.CurvePreferences = append(tlsConfig.CurvePreferences, tls.CurveID(curve)) } var certificate []byte if len(options.Certificate) > 0 { certificate = []byte(strings.Join(options.Certificate, "\n")) } else if options.CertificatePath != "" { content, err := os.ReadFile(options.CertificatePath) if err != nil { return nil, E.Cause(err, "read certificate") } certificate = content } if len(certificate) > 0 { certPool := x509.NewCertPool() if !certPool.AppendCertsFromPEM(certificate) { return nil, E.New("failed to parse certificate:\n\n", certificate) } tlsConfig.RootCAs = certPool } var clientCertificate []byte if len(options.ClientCertificate) > 0 { clientCertificate = []byte(strings.Join(options.ClientCertificate, "\n")) } else if options.ClientCertificatePath != "" { content, err := os.ReadFile(options.ClientCertificatePath) if err != nil { return nil, E.Cause(err, "read client certificate") } clientCertificate = content } var clientKey []byte if len(options.ClientKey) > 0 { clientKey = []byte(strings.Join(options.ClientKey, "\n")) } else if options.ClientKeyPath != "" { content, err := os.ReadFile(options.ClientKeyPath) if err != nil { return nil, E.Cause(err, "read client key") } clientKey = content } if len(clientCertificate) > 0 && len(clientKey) > 0 { keyPair, err := tls.X509KeyPair(clientCertificate, clientKey) if err != nil { return nil, E.Cause(err, "parse client x509 key pair") } tlsConfig.Certificates = []tls.Certificate{keyPair} } 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 } spoof, spoofMethod, err := parseTLSSpoofOptions(serverName, options) if err != nil { return nil, err } 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, spoof: spoof, spoofMethod: spoofMethod, } config.SetServerName(serverName) if options.ECH != nil && options.ECH.Enabled { var err error config, err = parseECHClientConfig(ctx, config.(ECHCapableConfig), options) if err != nil { return nil, err } } if options.KernelRx || options.KernelTx { if !C.IsLinux { return nil, E.New("kTLS is only supported on Linux") } config = &KTLSClientConfig{ Config: config, logger: logger, kernelTx: options.KernelTx, kernelRx: options.KernelRx, } } return config, nil } 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 } return verifySystemTLSPeer(rootCAs, serverName, timeFunc, state.PeerCertificates) } } 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") } pubKeyBytes, err := x509.MarshalPKIXPublicKey(leafCertificate.PublicKey) if err != nil { return E.Cause(err, "failed to marshal public key") } hashValue := sha256.Sum256(pubKeyBytes) for _, value := range knownHashValues { if bytes.Equal(value, hashValue[:]) { return nil } } return E.New("unrecognized remote public key: ", base64.StdEncoding.EncodeToString(hashValue[:])) } ================================================ FILE: common/tls/std_server.go ================================================ package tls import ( "context" "crypto/tls" "crypto/x509" "net" "os" "strings" "sync" "time" "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 handshakeTimeout time.Duration logger log.Logger certificateProvider managedCertificateProvider acmeService adapter.SimpleLifecycle certificate []byte key []byte certificatePath string keyPath string clientCertificatePath []string echKeyPath string watcher *fswatch.Watcher } func (c *STDServerConfig) ServerName() string { c.access.RLock() defer c.access.RUnlock() return c.config.ServerName } func (c *STDServerConfig) SetServerName(serverName string) { c.access.Lock() defer c.access.Unlock() config := c.config.Clone() config.ServerName = serverName c.config = config } func (c *STDServerConfig) NextProtos() []string { c.access.RLock() defer c.access.RUnlock() if c.hasACMEALPN() && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == C.ACMETLS1Protocol { return c.config.NextProtos[1:] } return c.config.NextProtos } func (c *STDServerConfig) SetNextProtos(nextProto []string) { c.access.Lock() defer c.access.Unlock() config := c.config.Clone() 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 } 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 } 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 } func (c *STDServerConfig) Client(conn net.Conn) (Conn, error) { return tls.Client(conn, c.config), nil } func (c *STDServerConfig) Server(conn net.Conn) (Conn, error) { return tls.Server(conn, c.config), nil } func (c *STDServerConfig) Clone() Config { return &STDServerConfig{ config: c.config.Clone(), handshakeTimeout: c.handshakeTimeout, } } 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 { err := c.acmeService.Start() if err != nil { return err } } err := c.startWatcher() if err != nil { c.logger.Warn("create fsnotify watcher: ", err) } return nil } func (c *STDServerConfig) startWatcher() error { var watchPath []string if c.certificatePath != "" { watchPath = append(watchPath, c.certificatePath) } if c.keyPath != "" { watchPath = append(watchPath, c.keyPath) } if c.echKeyPath != "" { watchPath = append(watchPath, c.echKeyPath) } if len(c.clientCertificatePath) > 0 { watchPath = append(watchPath, c.clientCertificatePath...) } if len(watchPath) == 0 { return nil } watcher, err := fswatch.NewWatcher(fswatch.Options{ Path: watchPath, Callback: func(path string) { err := c.certificateUpdated(path) if err != nil { c.logger.Error(E.Cause(err, "reload certificate")) } }, }) if err != nil { return err } err = watcher.Start() if err != nil { return err } c.watcher = watcher return nil } func (c *STDServerConfig) certificateUpdated(path string) error { if path == c.certificatePath || path == c.keyPath { switch path { case c.certificatePath: certificate, err := os.ReadFile(c.certificatePath) if err != nil { return E.Cause(err, "reload certificate from ", c.certificatePath) } c.certificate = certificate case c.keyPath: key, err := os.ReadFile(c.keyPath) if err != nil { return E.Cause(err, "reload key from ", c.keyPath) } c.key = key } keyPair, err := tls.X509KeyPair(c.certificate, c.key) if err != nil { return E.Cause(err, "reload key pair") } c.access.Lock() config := c.config.Clone() config.Certificates = []tls.Certificate{keyPair} c.config = config c.access.Unlock() c.logger.Info("reloaded TLS certificate") } else if common.Contains(c.clientCertificatePath, path) { clientCertificateCA := x509.NewCertPool() var reloaded bool for _, certPath := range c.clientCertificatePath { content, err := os.ReadFile(certPath) if err != nil { c.logger.Error(E.Cause(err, "reload certificate from ", c.clientCertificatePath)) continue } if !clientCertificateCA.AppendCertsFromPEM(content) { c.logger.Error(E.New("invalid client certificate file: ", certPath)) continue } reloaded = true } if !reloaded { return E.New("client certificates is empty") } c.access.Lock() config := c.config.Clone() config.ClientCAs = clientCertificateCA c.config = config c.access.Unlock() c.logger.Info("reloaded client certificates") } else if path == c.echKeyPath { echKey, err := os.ReadFile(c.echKeyPath) if err != nil { return E.Cause(err, "reload ECH keys from ", c.echKeyPath) } err = c.setECHServerConfig(echKey) if err != nil { return err } c.logger.Info("reloaded ECH keys") } return nil } func (c *STDServerConfig) Close() error { return common.Close(c.certificateProvider, c.acmeService, common.PtrOrNil(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.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 { return nil, err } if options.Insecure { return nil, errInsecureUnused } } else { tlsConfig = &tls.Config{} } tlsConfig.Time = ntp.TimeFuncFromContext(ctx) if options.ServerName != "" { tlsConfig.ServerName = options.ServerName } if len(options.ALPN) > 0 { tlsConfig.NextProtos = append(options.ALPN, tlsConfig.NextProtos...) } if options.MinVersion != "" { minVersion, err := ParseTLSVersion(options.MinVersion) if err != nil { return nil, E.Cause(err, "parse min_version") } tlsConfig.MinVersion = minVersion } if options.MaxVersion != "" { maxVersion, err := ParseTLSVersion(options.MaxVersion) if err != nil { return nil, E.Cause(err, "parse max_version") } tlsConfig.MaxVersion = maxVersion } if options.CipherSuites != nil { find: for _, cipherSuite := range options.CipherSuites { for _, tlsCipherSuite := range tls.CipherSuites() { if cipherSuite == tlsCipherSuite.Name { tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, tlsCipherSuite.ID) continue find } } return nil, E.New("unknown cipher_suite: ", cipherSuite) } } for _, curveID := range options.CurvePreferences { tlsConfig.CurvePreferences = append(tlsConfig.CurvePreferences, tls.CurveID(curveID)) } tlsConfig.ClientAuth = tls.ClientAuthType(options.ClientAuthentication) var ( certificate []byte key []byte ) if certificateProvider == nil && acmeService == nil { if len(options.Certificate) > 0 { certificate = []byte(strings.Join(options.Certificate, "\n")) } else if options.CertificatePath != "" { content, err := os.ReadFile(options.CertificatePath) if err != nil { return nil, E.Cause(err, "read certificate") } certificate = content } if len(options.Key) > 0 { key = []byte(strings.Join(options.Key, "\n")) } else if options.KeyPath != "" { content, err := os.ReadFile(options.KeyPath) if err != nil { return nil, E.Cause(err, "read key") } key = content } if certificate == nil && key == nil && options.Insecure { timeFunc := ntp.TimeFuncFromContext(ctx) if timeFunc == nil { timeFunc = time.Now } tlsConfig.GetCertificate = func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { return GenerateKeyPair(nil, nil, timeFunc, info.ServerName) } } else { if certificate == nil { return nil, E.New("missing certificate") } else if key == nil { return nil, E.New("missing key") } keyPair, err := tls.X509KeyPair(certificate, key) if err != nil { return nil, E.Cause(err, "parse x509 key pair") } tlsConfig.Certificates = []tls.Certificate{keyPair} } } if len(options.ClientCertificate) > 0 || len(options.ClientCertificatePath) > 0 { if tlsConfig.ClientAuth == tls.NoClientCert { tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert } } if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert { if len(options.ClientCertificate) > 0 { clientCertificateCA := x509.NewCertPool() if !clientCertificateCA.AppendCertsFromPEM([]byte(strings.Join(options.ClientCertificate, "\n"))) { return nil, E.New("invalid client certificate strings") } tlsConfig.ClientCAs = clientCertificateCA } else if len(options.ClientCertificatePath) > 0 { clientCertificateCA := x509.NewCertPool() for _, path := range options.ClientCertificatePath { content, err := os.ReadFile(path) if err != nil { return nil, E.Cause(err, "read client certificate from ", path) } if !clientCertificateCA.AppendCertsFromPEM(content) { return nil, E.New("invalid client certificate file: ", path) } } tlsConfig.ClientCAs = clientCertificateCA } else if len(options.ClientCertificatePublicKeySHA256) > 0 { switch tlsConfig.ClientAuth { case tls.RequireAndVerifyClientCert: tlsConfig.ClientAuth = tls.RequireAnyClientCert case tls.VerifyClientCertIfGiven: tlsConfig.ClientAuth = tls.RequestClientCert } tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { 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") } } var echKeyPath string if options.ECH != nil && options.ECH.Enabled { err = parseECHServerConfig(ctx, options, tlsConfig, &echKeyPath) if err != nil { 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, certificate: certificate, key: key, certificatePath: options.CertificatePath, clientCertificatePath: options.ClientCertificatePath, keyPath: options.KeyPath, echKeyPath: echKeyPath, } serverConfig.config.GetConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) { serverConfig.access.RLock() defer serverConfig.access.RUnlock() return serverConfig.config, nil } var config ServerConfig = serverConfig if options.KernelTx || options.KernelRx { if !C.IsLinux { return nil, E.New("kTLS is only supported on Linux") } config = &KTlSServerConfig{ ServerConfig: config, logger: logger, kernelTx: options.KernelTx, kernelRx: options.KernelRx, } } 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 } ================================================ FILE: common/tls/system_client.go ================================================ package tls import ( "context" "crypto/x509" "os" "strings" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/service" ) 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 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 } ================================================ FILE: common/tls/system_client_engine.go ================================================ //go:build (darwin && cgo) || windows package tls import ( "context" "net" "os" "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" ) 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, } } 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 } ================================================ FILE: common/tls/time_wrapper.go ================================================ package tls import ( "time" "github.com/sagernet/sing/common/ntp" ) type TimeServiceWrapper struct { ntp.TimeService } func (w *TimeServiceWrapper) TimeFunc() func() time.Time { return func() time.Time { if w.TimeService != nil { return w.TimeService.TimeFunc()() } else { return time.Now() } } } func (w *TimeServiceWrapper) Upstream() any { return w.TimeService } ================================================ FILE: common/tls/utls_client.go ================================================ //go:build with_utls package tls import ( "context" "crypto/tls" "crypto/x509" "math/rand" "net" "os" "strings" "time" "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" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/ntp" utls "github.com/metacubex/utls" "golang.org/x/net/http2" ) 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 recordFragment bool spoof string spoofMethod tlsspoof.Method } func (c *UTLSClientConfig) ServerName() string { 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 } func (c *UTLSClientConfig) NextProtos() []string { return c.config.NextProtos } func (c *UTLSClientConfig) SetNextProtos(nextProto []string) { if len(nextProto) == 1 && nextProto[0] == http2.NextProtoTLS { nextProto = append(nextProto, "http/1.1") } 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") } func (c *UTLSClientConfig) Client(conn net.Conn) (Conn, error) { 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) if err != nil { return nil, err } return &utlsALPNWrapper{utlsConnWrapper{utls.UClient(conn, c.config.Clone(), c.id)}, c.config.NextProtos}, nil } func (c *UTLSClientConfig) SetSessionIDGenerator(generator func(clientHello []byte, sessionID []byte) error) { c.config.SessionIDGenerator = generator } func (c *UTLSClientConfig) Clone() Config { 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, spoof: c.spoof, spoofMethod: c.spoofMethod, } cloned.SetServerName(cloned.serverName) return cloned } func (c *UTLSClientConfig) ECHConfigList() []byte { return c.config.EncryptedClientHelloConfigList } func (c *UTLSClientConfig) SetECHConfigList(EncryptedClientHelloConfigList []byte) { c.config.EncryptedClientHelloConfigList = EncryptedClientHelloConfigList } type utlsConnWrapper struct { *utls.UConn } func (c *utlsConnWrapper) ConnectionState() tls.ConnectionState { state := c.Conn.ConnectionState() //nolint:staticcheck return tls.ConnectionState{ Version: state.Version, HandshakeComplete: state.HandshakeComplete, DidResume: state.DidResume, CipherSuite: state.CipherSuite, NegotiatedProtocol: state.NegotiatedProtocol, NegotiatedProtocolIsMutual: state.NegotiatedProtocolIsMutual, ServerName: state.ServerName, PeerCertificates: state.PeerCertificates, VerifiedChains: state.VerifiedChains, SignedCertificateTimestamps: state.SignedCertificateTimestamps, OCSPResponse: state.OCSPResponse, TLSUnique: state.TLSUnique, } } func (c *utlsConnWrapper) Upstream() any { return c.UConn } func (c *utlsConnWrapper) ReaderReplaceable() bool { return true } func (c *utlsConnWrapper) WriterReplaceable() bool { return true } type utlsALPNWrapper struct { utlsConnWrapper nextProtocols []string } func (c *utlsALPNWrapper) HandshakeContext(ctx context.Context) error { if len(c.nextProtocols) > 0 { err := c.BuildHandshakeState() if err != nil { return err } for _, extension := range c.Extensions { if alpnExtension, isALPN := extension.(*utls.ALPNExtension); isALPN { alpnExtension.AlpnProtocols = c.nextProtocols err = c.BuildHandshakeState() if err != nil { return err } break } } } return c.UConn.HandshakeContext(ctx) } 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 && !allowEmptyServerName { return nil, errMissingServerName } var tlsConfig utls.Config tlsConfig.Time = ntp.TimeFuncFromContext(ctx) tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx) 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") } } if len(options.CertificatePublicKeySHA256) > 0 { if len(options.Certificate) > 0 || options.CertificatePath != "" { return nil, E.New("certificate_public_key_sha256 is conflict with certificate or certificate_path") } tlsConfig.InsecureSkipVerify = true tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { return VerifyPublicKeySHA256(options.CertificatePublicKeySHA256, rawCerts) } } if len(options.ALPN) > 0 { tlsConfig.NextProtos = options.ALPN } if options.MinVersion != "" { minVersion, err := ParseTLSVersion(options.MinVersion) if err != nil { return nil, E.Cause(err, "parse min_version") } tlsConfig.MinVersion = minVersion } if options.MaxVersion != "" { maxVersion, err := ParseTLSVersion(options.MaxVersion) if err != nil { return nil, E.Cause(err, "parse max_version") } tlsConfig.MaxVersion = maxVersion } if options.CipherSuites != nil { find: for _, cipherSuite := range options.CipherSuites { for _, tlsCipherSuite := range tls.CipherSuites() { if cipherSuite == tlsCipherSuite.Name { tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, tlsCipherSuite.ID) continue find } } return nil, E.New("unknown cipher_suite: ", cipherSuite) } } var certificate []byte if len(options.Certificate) > 0 { certificate = []byte(strings.Join(options.Certificate, "\n")) } else if options.CertificatePath != "" { content, err := os.ReadFile(options.CertificatePath) if err != nil { return nil, E.Cause(err, "read certificate") } certificate = content } if len(certificate) > 0 { certPool := x509.NewCertPool() if !certPool.AppendCertsFromPEM(certificate) { return nil, E.New("failed to parse certificate:\n\n", certificate) } tlsConfig.RootCAs = certPool } var clientCertificate []byte if len(options.ClientCertificate) > 0 { clientCertificate = []byte(strings.Join(options.ClientCertificate, "\n")) } else if options.ClientCertificatePath != "" { content, err := os.ReadFile(options.ClientCertificatePath) if err != nil { return nil, E.Cause(err, "read client certificate") } clientCertificate = content } var clientKey []byte if len(options.ClientKey) > 0 { clientKey = []byte(strings.Join(options.ClientKey, "\n")) } else if options.ClientKeyPath != "" { content, err := os.ReadFile(options.ClientKeyPath) if err != nil { return nil, E.Cause(err, "read client key") } clientKey = content } if len(clientCertificate) > 0 && len(clientKey) > 0 { keyPair, err := utls.X509KeyPair(clientCertificate, clientKey) if err != nil { return nil, E.Cause(err, "parse client x509 key pair") } tlsConfig.Certificates = []utls.Certificate{keyPair} } 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 } spoof, spoofMethod, err := parseTLSSpoofOptions(serverName, options) if err != nil { return nil, err } id, err := uTLSClientHelloID(options.UTLS.Fingerprint) if err != nil { return nil, err } 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, spoof: spoof, spoofMethod: spoofMethod, } 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") } config, err = parseECHClientConfig(ctx, config.(ECHCapableConfig), options) if err != nil { return nil, err } } if (options.KernelRx || options.KernelTx) && !common.PtrValueOrDefault(options.Reality).Enabled { if !C.IsLinux { return nil, E.New("kTLS is only supported on Linux") } config = &KTLSClientConfig{ Config: config, logger: logger, kernelTx: options.KernelTx, kernelRx: options.KernelRx, } } return config, nil } var ( randomFingerprint utls.ClientHelloID randomizedFingerprint utls.ClientHelloID ) func init() { modernFingerprints := []utls.ClientHelloID{ utls.HelloChrome_Auto, utls.HelloFirefox_Auto, utls.HelloEdge_Auto, utls.HelloSafari_Auto, utls.HelloIOS_Auto, } randomFingerprint = modernFingerprints[rand.Intn(len(modernFingerprints))] weights := utls.DefaultWeights weights.TLSVersMax_Set_VersionTLS13 = 1 weights.FirstKeyShare_Set_CurveP256 = 0 randomizedFingerprint = utls.HelloRandomized randomizedFingerprint.Seed, _ = utls.NewPRNGSeed() randomizedFingerprint.Weights = &weights } func uTLSClientHelloID(name string) (utls.ClientHelloID, error) { switch name { case "chrome_psk", "chrome_psk_shuffle", "chrome_padding_psk_shuffle", "chrome_pq", "chrome_pq_psk": fallthrough case "chrome", "": return utls.HelloChrome_Auto, nil case "firefox": return utls.HelloFirefox_Auto, nil case "edge": return utls.HelloEdge_Auto, nil case "safari": return utls.HelloSafari_Auto, nil case "360": return utls.Hello360_Auto, nil case "qq": return utls.HelloQQ_Auto, nil case "ios": return utls.HelloIOS_Auto, nil case "android": return utls.HelloAndroid_11_OkHttp, nil case "random": return randomFingerprint, nil case "randomized": return randomizedFingerprint, nil default: return utls.ClientHelloID{}, E.New("unknown uTLS fingerprint: ", name) } } ================================================ FILE: common/tls/utls_client_test.go ================================================ //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") } ================================================ FILE: common/tls/utls_stub.go ================================================ //go:build !with_utls package tls import ( "context" "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" ) 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`) } func NewRealityServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) { return nil, E.New(`uTLS, which is required by reality is not included in this build, rebuild with -tags with_utls`) } ================================================ FILE: common/tls/windows_client.go ================================================ //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 } ================================================ FILE: common/tls/windows_client_stub.go ================================================ //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") } ================================================ FILE: common/tls/windows_client_test.go ================================================ //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 := range 100 { 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 range messageCount { 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 := range messageCount { 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 } ================================================ FILE: common/tlsfragment/conn.go ================================================ package tf import ( "bytes" "context" "encoding/binary" "math/rand" "net" "strings" "time" C "github.com/sagernet/sing-box/constant" N "github.com/sagernet/sing/common/network" "golang.org/x/net/publicsuffix" ) type Conn struct { net.Conn tcpConn *net.TCPConn ctx context.Context firstPacketWritten bool splitPacket bool splitRecord bool fallbackDelay time.Duration } func NewConn(conn net.Conn, ctx context.Context, splitPacket bool, splitRecord bool, fallbackDelay time.Duration) *Conn { if fallbackDelay == 0 { fallbackDelay = C.TLSFragmentFallbackDelay } tcpConn, _ := N.UnwrapReader(conn).(*net.TCPConn) return &Conn{ Conn: conn, tcpConn: tcpConn, ctx: ctx, splitPacket: splitPacket, splitRecord: splitRecord, fallbackDelay: fallbackDelay, } } func (c *Conn) Write(b []byte) (n int, err error) { if !c.firstPacketWritten { defer func() { c.firstPacketWritten = true }() serverName := IndexTLSServerName(b) if serverName != nil { if c.splitPacket { if c.tcpConn != nil { err = c.tcpConn.SetNoDelay(true) if err != nil { return } } } splits := strings.Split(serverName.ServerName, ".") currentIndex := serverName.Index if publicSuffix := publicsuffix.List.PublicSuffix(serverName.ServerName); publicSuffix != "" { splits = splits[:len(splits)-strings.Count(serverName.ServerName, ".")] } if len(splits) > 1 && splits[0] == "..." { currentIndex += len(splits[0]) + 1 splits = splits[1:] } var splitIndexes []int for i, split := range splits { splitAt := rand.Intn(len(split)) splitIndexes = append(splitIndexes, currentIndex+splitAt) currentIndex += len(split) if i != len(splits)-1 { currentIndex++ } } var buffer bytes.Buffer for i := 0; i <= len(splitIndexes); i++ { var payload []byte if i == 0 { payload = b[:splitIndexes[i]] if c.splitRecord { payload = payload[recordLayerHeaderLen:] } } else if i == len(splitIndexes) { payload = b[splitIndexes[i-1]:] } else { payload = b[splitIndexes[i-1]:splitIndexes[i]] } if c.splitRecord { if c.splitPacket { buffer.Reset() } payloadLen := uint16(len(payload)) buffer.Write(b[:3]) binary.Write(&buffer, binary.BigEndian, payloadLen) buffer.Write(payload) if c.splitPacket { payload = buffer.Bytes() } } if c.splitPacket { if c.tcpConn != nil && i != len(splitIndexes) { err = writeAndWaitAck(c.ctx, c.tcpConn, payload, c.fallbackDelay) if err != nil { return } } else { _, err = c.Conn.Write(payload) if err != nil { return } if i != len(splitIndexes) { time.Sleep(c.fallbackDelay) } } } } if c.splitRecord && !c.splitPacket { _, err = c.Conn.Write(buffer.Bytes()) if err != nil { return } } if c.tcpConn != nil { err = c.tcpConn.SetNoDelay(false) if err != nil { return } } return len(b), nil } } return c.Conn.Write(b) } func (c *Conn) ReaderReplaceable() bool { return true } func (c *Conn) WriterReplaceable() bool { return c.firstPacketWritten } func (c *Conn) Upstream() any { return c.Conn } ================================================ FILE: common/tlsfragment/conn_test.go ================================================ package tf_test import ( "context" "crypto/tls" "net" "testing" tf "github.com/sagernet/sing-box/common/tlsfragment" "github.com/stretchr/testify/require" ) func TestTLSFragment(t *testing.T) { t.Parallel() tcpConn, err := net.Dial("tcp", "1.1.1.1:443") require.NoError(t, err) tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), true, false, 0), &tls.Config{ ServerName: "www.cloudflare.com", }) require.NoError(t, tlsConn.Handshake()) } func TestTLSRecordFragment(t *testing.T) { t.Parallel() tcpConn, err := net.Dial("tcp", "1.1.1.1:443") require.NoError(t, err) tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), false, true, 0), &tls.Config{ ServerName: "www.cloudflare.com", }) require.NoError(t, tlsConn.Handshake()) } func TestTLS2Fragment(t *testing.T) { t.Parallel() tcpConn, err := net.Dial("tcp", "1.1.1.1:443") require.NoError(t, err) tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), true, true, 0), &tls.Config{ ServerName: "www.cloudflare.com", }) require.NoError(t, tlsConn.Handshake()) } ================================================ FILE: common/tlsfragment/index.go ================================================ package tf import ( "encoding/binary" ) const ( recordLayerHeaderLen int = 5 handshakeHeaderLen int = 6 randomDataLen int = 32 sessionIDHeaderLen int = 1 cipherSuiteHeaderLen int = 2 compressMethodHeaderLen int = 1 extensionsHeaderLen int = 2 extensionHeaderLen int = 4 sniExtensionHeaderLen int = 5 contentType uint8 = 22 handshakeType uint8 = 1 sniExtensionType uint16 = 0 sniNameDNSHostnameType uint8 = 0 tlsVersionBitmask uint16 = 0xFFFC tls13 uint16 = 0x0304 ) type MyServerName struct { Index int Length int ServerName string ExtensionsListLengthIndex int } func IndexTLSServerName(payload []byte) *MyServerName { if len(payload) < recordLayerHeaderLen || payload[0] != contentType { return nil } segmentLen := binary.BigEndian.Uint16(payload[3:5]) if len(payload) < recordLayerHeaderLen+int(segmentLen) { return nil } serverName := indexTLSServerNameFromHandshake(payload[recordLayerHeaderLen:]) if serverName == nil { return nil } serverName.Index += recordLayerHeaderLen serverName.ExtensionsListLengthIndex += recordLayerHeaderLen return serverName } func indexTLSServerNameFromHandshake(handshake []byte) *MyServerName { if len(handshake) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen { return nil } if handshake[0] != handshakeType { return nil } handshakeLen := uint32(handshake[1])<<16 | uint32(handshake[2])<<8 | uint32(handshake[3]) if len(handshake[4:]) != int(handshakeLen) { return nil } tlsVersion := uint16(handshake[4])<<8 | uint16(handshake[5]) if tlsVersion&tlsVersionBitmask != 0x0300 && tlsVersion != tls13 { return nil } sessionIDLen := handshake[38] currentIndex := handshakeHeaderLen + randomDataLen + sessionIDHeaderLen + int(sessionIDLen) if len(handshake) < currentIndex { return nil } cipherSuites := handshake[currentIndex:] if len(cipherSuites) < cipherSuiteHeaderLen { return nil } csLen := uint16(cipherSuites[0])<<8 | uint16(cipherSuites[1]) if len(cipherSuites) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen { return nil } compressMethodLen := uint16(cipherSuites[cipherSuiteHeaderLen+int(csLen)]) currentIndex += cipherSuiteHeaderLen + int(csLen) + compressMethodHeaderLen + int(compressMethodLen) if len(handshake) < currentIndex { return nil } serverName := indexTLSServerNameFromExtensions(handshake[currentIndex:]) if serverName == nil { return nil } serverName.Index += currentIndex serverName.ExtensionsListLengthIndex = currentIndex return serverName } func indexTLSServerNameFromExtensions(exs []byte) *MyServerName { if len(exs) == 0 { return nil } if len(exs) < extensionsHeaderLen { return nil } exsLen := uint16(exs[0])<<8 | uint16(exs[1]) exs = exs[extensionsHeaderLen:] if len(exs) < int(exsLen) { return nil } for currentIndex := extensionsHeaderLen; len(exs) > 0; { if len(exs) < extensionHeaderLen { return nil } exType := uint16(exs[0])<<8 | uint16(exs[1]) exLen := uint16(exs[2])<<8 | uint16(exs[3]) if len(exs) < extensionHeaderLen+int(exLen) { return nil } sex := exs[extensionHeaderLen : extensionHeaderLen+int(exLen)] switch exType { case sniExtensionType: if len(sex) < sniExtensionHeaderLen { return nil } sniType := sex[2] if sniType != sniNameDNSHostnameType { return nil } sniLen := uint16(sex[3])<<8 | uint16(sex[4]) sex = sex[sniExtensionHeaderLen:] return &MyServerName{ Index: currentIndex + extensionHeaderLen + sniExtensionHeaderLen, Length: int(sniLen), ServerName: string(sex), } } exs = exs[4+exLen:] currentIndex += 4 + int(exLen) } return nil } ================================================ FILE: common/tlsfragment/index_test.go ================================================ package tf_test import ( "encoding/hex" "testing" "github.com/sagernet/sing-box/common/tlsfragment" "github.com/stretchr/testify/require" ) func TestIndexTLSServerName(t *testing.T) { t.Parallel() payload, err := hex.DecodeString("16030105f8010005f403036e35de7389a679c54029cf452611f2211c70d9ac3897271de589ab6155f8e4ab20637d225f1ef969ad87ed78bfb9d171300bcb1703b6f314ccefb964f79b7d0961002a0a0a130213031301c02cc02bcca9c030c02fcca8c00ac009c014c013009d009c0035002fc008c012000a01000581baba00000000000f000d00000a6769746875622e636f6d00170000ff01000100000a000e000c3a3a11ec001d001700180019000b000201000010000e000c02683208687474702f312e31000500050100000000000d00160014040308040401050308050805050108060601020100120000003304ef04ed3a3a00010011ec04c0aeb2250c092a3463161cccb29d9183331a424964248579507ed23a180b0ceab2a5f5d9ce41547e497a89055471ea572867ba3a1fc3c9e45025274a20f60c6b60e62476b6afed0403af59ab83660ef4112ae20386a602010d0a5d454c0ed34c84ed4423e750213e6a2baab1bf9c4367a6007ab40a33d95220c2dcaa44f257024a5626b545db0510f4311b1a60714154909c6a61fdfca011fb2626d657aeb6070bf078508babe3b584555013e34acc56198ed4663742b3155a664a9901794c4586820a7dc162c01827291f3792e1237f801a8d1ef096013c181c4a58d2f6859ba75022d18cc4418bd4f351d5c18f83a58857d05af860c4b9ac018a5b63f17184e591532c6bc2cf2215d4a282c8a8a4f6f7aee110422c8bc9ebd3b1d609c568523aaae555db320e6c269473d87af38c256cbb9febc20aea6380c32a8916f7a373c8b1e37554e3260bf6621f6b804ee80b3c516b1d01985bf4c603b6daa9a5991de6a7a29f3a7122b8afb843a7660110fce62b43c615f5bcc2db688ba012649c0952b0a2c031e732d2b454c6b2968683cb8d244be2c9a7fa163222979eaf92722b92b862d81a3d94450c2b60c318421ebb4307c42d1f0473592a5c30e42039cc68cda9721e61aa63f49def17c15221680ed444896340133bbee67556f56b9f9d78a4df715f926a12add0cc9c862e46ea8b7316ae468282c18601b2771c9c9322f982228cf93effaacd3f80cbd12bce5fc36f56e2a3caf91e578a5fae00c9b23a8ed1a66764f4433c3628a70b8f0a6196adc60a4cb4226f07ba4c6b363fe9065563bfc1347452946386bab488686e837ab979c64f9047417fca635fe1bb4f074f256cc8af837c7b455e280426547755af90a61640169ef180aea3a77e662bb6dac1b6c3696027129b1a5edf495314e9c7f4b6110e16378ec893fa24642330a40aba1a85326101acb97c620fd8d71389e69eaed7bdb01bbe1fd428d66191150c7b2cd1ad4257391676a82ba8ce07fb2667c3b289f159003a7c7bc31d361b7b7f49a802961739d950dfcc0fa1c7abce5abdd2245101da391151490862028110465950b9e9c03d08a90998ab83267838d2e74a0593bc81f74cdf734519a05b351c0e5488c68dd810e6e9142ccc1e2f4a7f464297eb340e27acc6b9d64e12e38cce8492b3d939140b5a9e149a75597f10a23874c84323a07cdd657274378f887c85c4259b9c04cd33ba58ed630ef2a744f8e19dd34843dff331d2a6be7e2332c599289cd248a611c73d7481cd4a9bd43449a3836f14b2af18a1739e17999e4c67e85cc5bcecabb14185e5bcaff3c96098f03dc5aba819f29587758f49f940585354a2a780830528d68ccd166920dadcaa25cab5fc1907272a826aba3f08bc6b88757776812ecb6c7cec69a223ec0a13a7b62a2349a0f63ed7a27a3b15ba21d71fe6864ec6e089ae17cadd433fa3138f7ee24353c11365818f8fc34f43a05542d18efaac24bfccc1f748a0cc1a67ad379468b76fd34973dba785f5c91d618333cd810fe0700d1bbc8422029782628070a624c52c5309a4a64d625b11f8033ab28df34a1add297517fcc06b92b6817b3c5144438cf260867c57bde68c8c4b82e6a135ef676a52fbae5708002a404e6189a60e2836de565ad1b29e3819e5ed49f6810bcb28e1bd6de57306f94b79d9dae1cc4624d2a068499beef81cd5fe4b76dcbfff2a2008001d002001976128c6d5a934533f28b9914d2480aab2a8c1ab03d212529ce8b27640a716002d00020101002b000706caca03040303001b00030200015a5a000100") require.NoError(t, err) serverName := tf.IndexTLSServerName(payload) require.NotNil(t, serverName) require.Equal(t, serverName.ServerName, string(payload[serverName.Index:serverName.Index+serverName.Length])) require.Equal(t, "github.com", serverName.ServerName) } ================================================ FILE: common/tlsfragment/wait_darwin.go ================================================ package tf import ( "context" "net" "time" "github.com/sagernet/sing/common/control" "golang.org/x/sys/unix" ) /* const tcpMaxNotifyAck = 10 type tcpNotifyAckID uint32 type tcpNotifyAckComplete struct { NotifyPending uint32 NotifyCompleteCount uint32 NotifyCompleteID [tcpMaxNotifyAck]tcpNotifyAckID } var sizeOfTCPNotifyAckComplete = int(unsafe.Sizeof(tcpNotifyAckComplete{})) func getsockoptTCPNotifyAckComplete(fd, level, opt int) (*tcpNotifyAckComplete, error) { var value tcpNotifyAckComplete vallen := uint32(sizeOfTCPNotifyAckComplete) err := getsockopt(fd, level, opt, unsafe.Pointer(&value), &vallen) return &value, err } //go:linkname getsockopt golang.org/x/sys/unix.getsockopt func getsockopt(s int, level int, name int, val unsafe.Pointer, vallen *uint32) error func waitAck(ctx context.Context, conn *net.TCPConn, _ time.Duration) error { const TCP_NOTIFY_ACKNOWLEDGEMENT = 0x212 return control.Conn(conn, func(fd uintptr) error { err := unix.SetsockoptInt(int(fd), unix.IPPROTO_TCP, TCP_NOTIFY_ACKNOWLEDGEMENT, 1) if err != nil { if errors.Is(err, unix.EINVAL) { return waitAckFallback(ctx, conn, 0) } return err } for { select { case <-ctx.Done(): return ctx.Err() default: } var ackComplete *tcpNotifyAckComplete ackComplete, err = getsockoptTCPNotifyAckComplete(int(fd), unix.IPPROTO_TCP, TCP_NOTIFY_ACKNOWLEDGEMENT) if err != nil { return err } if ackComplete.NotifyPending == 0 { return nil } time.Sleep(10 * time.Millisecond) } }) } */ func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fallbackDelay time.Duration) error { _, err := conn.Write(payload) if err != nil { return err } return control.Conn(conn, func(fd uintptr) error { start := time.Now() for { select { case <-ctx.Done(): return ctx.Err() default: } unacked, err := unix.GetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_NWRITE) if err != nil { return err } if unacked == 0 { if time.Since(start) <= 20*time.Millisecond { // under transparent proxy time.Sleep(fallbackDelay) } return nil } time.Sleep(10 * time.Millisecond) } }) } ================================================ FILE: common/tlsfragment/wait_linux.go ================================================ package tf import ( "context" "net" "time" "github.com/sagernet/sing/common/control" "golang.org/x/sys/unix" ) func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fallbackDelay time.Duration) error { _, err := conn.Write(payload) if err != nil { return err } return control.Conn(conn, func(fd uintptr) error { start := time.Now() for { select { case <-ctx.Done(): return ctx.Err() default: } tcpInfo, err := unix.GetsockoptTCPInfo(int(fd), unix.IPPROTO_TCP, unix.TCP_INFO) if err != nil { return err } if tcpInfo.Unacked == 0 { if time.Since(start) <= 20*time.Millisecond { // under transparent proxy time.Sleep(fallbackDelay) } return nil } time.Sleep(10 * time.Millisecond) } }) } ================================================ FILE: common/tlsfragment/wait_stub.go ================================================ //go:build !(linux || darwin || windows) package tf import ( "context" "net" "time" ) func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fallbackDelay time.Duration) error { _, err := conn.Write(payload) if err != nil { return err } time.Sleep(fallbackDelay) return nil } ================================================ FILE: common/tlsfragment/wait_windows.go ================================================ package tf import ( "context" "errors" "net" "time" "github.com/sagernet/sing/common/winiphlpapi" "golang.org/x/sys/windows" ) func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fallbackDelay time.Duration) error { start := time.Now() err := winiphlpapi.WriteAndWaitAck(ctx, conn, payload) if err != nil { if errors.Is(err, windows.ERROR_ACCESS_DENIED) { if _, err := conn.Write(payload); err != nil { return err } time.Sleep(fallbackDelay) return nil } return err } if time.Since(start) <= 20*time.Millisecond { time.Sleep(fallbackDelay) } return nil } ================================================ FILE: common/tlsspoof/README.md ================================================ # tls spoof idea from https://github.com/therealaleph/sni-spoofing-rust ================================================ FILE: common/tlsspoof/client_hello.go ================================================ package tlsspoof import ( "bytes" "context" "crypto/tls" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" ) // 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") } 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") } return buf.Bytes(), nil } ================================================ FILE: common/tlsspoof/endpoints.go ================================================ //go:build linux || darwin || (windows && (amd64 || 386)) 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 } ================================================ FILE: common/tlsspoof/integration_darwin_test.go ================================================ //go:build darwin package tlsspoof const loopbackInterface = "lo0" ================================================ FILE: common/tlsspoof/integration_linux_test.go ================================================ //go:build linux package tlsspoof const loopbackInterface = "lo" ================================================ FILE: common/tlsspoof/integration_test.go ================================================ //go:build linux || darwin package tlsspoof import ( "bufio" "context" "fmt" "io" "net" "os" "os/exec" "strings" "sync" "testing" "time" "github.com/stretchr/testify/require" ) func requireRoot(t *testing.T) { t.Helper() if os.Geteuid() != 0 { 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() 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 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() { line := scanner.Text() access.Lock() for _, needle := range needles { if !found[needle] && strings.Contains(line, needle) { found[needle] = true } } access.Unlock() } }() do() time.Sleep(200 * time.Millisecond) _ = cmd.Process.Signal(os.Interrupt) <-readerDone 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) { 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(network, address) 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(network, 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) } ================================================ FILE: common/tlsspoof/integration_tls_test.go ================================================ //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") } } ================================================ FILE: common/tlsspoof/integration_unix_test.go ================================================ //go:build linux || darwin package tlsspoof import ( "encoding/hex" "io" "net" "runtime" "testing" "time" "github.com/stretchr/testify/require" ) func TestIntegrationSpoofer(t *testing.T) { requireRoot(t) 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. 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") } // 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) 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, "letsencrypt.org") require.NoError(t, err) 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") } } ================================================ FILE: common/tlsspoof/integration_windows_test.go ================================================ //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) rawSpoofer { t.Helper() s, err := newRawSpoofer(conn, method) require.NoError(t, err) return s } // 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 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) 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() }) wrapped, err := NewConn(client, MethodWrongSequence, "letsencrypt.org") require.NoError(t, err) 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 // raw 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() }) 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) _ = 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") } } ================================================ FILE: common/tlsspoof/packet.go ================================================ //go:build linux || darwin || (windows && (amd64 || 386)) package tlsspoof import ( "encoding/binary" "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 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, packetInfo spoofPacketInfo, payload []byte, ) []byte { if src.Addr().Is4() != dst.Addr().Is4() { panic("tlsspoof: mixed IPv4/IPv6 address family") } var ( frame []byte ipHeaderLen int ) ipPayloadLen := tcpHeaderLen + len(packetInfo.options) + len(payload) if src.Addr().Is4() { ipHeaderLen = header.IPv4MinimumSize frame = make([]byte, ipHeaderLen+ipPayloadLen) 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+ipPayloadLen) ip := header.IPv6(frame[:ipHeaderLen]) ip.Encode(&header.IPv6Fields{ PayloadLength: uint16(ipPayloadLen), TransportProtocol: header.TCPProtocolNumber, HopLimit: defaultTTL, SrcAddr: src.Addr(), DstAddr: dst.Addr(), }) } encodeTCP(frame, ipHeaderLen, src, dst, packetInfo, payload) return frame } func encodeTCP(frame []byte, ipHeaderLen int, src, dst netip.AddrPort, packetInfo spoofPacketInfo, payload []byte) { tcp := header.TCP(frame[ipHeaderLen:]) 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: packetInfo.seqNum, AckNum: packetInfo.ackNum, DataOffset: uint8(tcpHeaderLen + optionsLen), Flags: header.TCPFlagAck | header.TCPFlagPsh, WindowSize: defaultWindowSize, }) applyTCPChecksum(tcp, src.Addr(), dst.Addr(), payload, packetInfo.corrupt) } 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, packetInfo, payload), nil } func resolveSpoofPacketInfo(method Method, sendNext, receiveNext, timestamp uint32, tcpOptions, payload []byte) (spoofPacketInfo, error) { packetInfo := spoofPacketInfo{seqNum: sendNext, ackNum: receiveNext} switch method { case MethodWrongSequence: packetInfo.seqNum = sendNext - uint32(len(payload)) case MethodWrongChecksum: 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 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 := 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)) if corrupt { tcpChecksum ^= 0xFFFF } tcp.SetChecksum(tcpChecksum) } ================================================ FILE: common/tlsspoof/packet_darwin.go ================================================ package tlsspoof import "net/netip" // 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, 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(packetInfo.options)+len(payload)) encodeTCP(segment, 0, src, dst, packetInfo, payload) return segment, nil } ================================================ FILE: common/tlsspoof/raw_darwin.go ================================================ package tlsspoof import ( "encoding/binary" "net" "net/netip" "strconv" "strings" "sync" "syscall" "github.com/sagernet/sing-tun/gtcpip/header" 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 ) // 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, nil } return 384, nil }) 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) (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 } fd, sockaddr, err := openDarwinRawSocket(src, 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, 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] 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(src, dst netip.AddrPort) (int, unix.Sockaddr, error) { if dst.Addr().Is4() { 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, 0, 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, 0, nil, 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. 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") } return nil } func (s *darwinSpoofer) Close() error { if s.rawFD < 0 { return nil } err := unix.Close(s.rawFD) s.rawFD = -1 return err } ================================================ FILE: common/tlsspoof/raw_linux.go ================================================ 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 timestamp uint32 } func newRawSpoofer(conn net.Conn, method Method) (rawSpoofer, 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") } // 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) (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)") } 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 { 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, s.timestamp, nil, 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 } ================================================ FILE: common/tlsspoof/raw_stub.go ================================================ //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) (rawSpoofer, error) { return nil, E.New("tls_spoof: unsupported platform") } ================================================ FILE: common/tlsspoof/raw_unix.go ================================================ //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 } ================================================ FILE: common/tlsspoof/raw_windows.go ================================================ //go:build windows && (amd64 || 386) package tlsspoof import ( "errors" "net" "net/netip" "slices" "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 // 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 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) (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 } divertH, err := windivert.Open(filter, windivert.LayerNetwork, 0, 0) if err != nil { return nil, E.Cause(err, "tls_spoof: open WinDivert") } s := &windowsSpoofer{ method: method, src: src, dst: dst, divertH: divertH, 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 } }) 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, 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 // 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. _, 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 } 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 } 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.divertH.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 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, nil, 0, false } ip := header.IPv6(pkt) if ip.TransportProtocol() != header.TCPProtocolNumber { 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, nil, 0, false } total := header.IPv6MinimumSize + int(ip.PayloadLength()) if total == header.IPv6MinimumSize || total > len(pkt) { total = len(pkt) } 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, nil, 0, false } ip := header.IPv4(pkt) if ip.Protocol() != uint8(header.TCPProtocolNumber) { 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, nil, 0, false } tcp := header.TCP(pkt[ihl:]) tcpHdr := int(tcp.DataOffset()) if tcpHdr < header.TCPMinimumSize || ihl+tcpHdr > len(pkt) { return 0, 0, nil, 0, false } total := int(ip.TotalLength()) if total == 0 || total > len(pkt) { total = len(pkt) } if total < ihl+tcpHdr { return 0, 0, nil, 0, false } return tcp.SequenceNumber(), tcp.AckNumber(), slices.Clone(tcp.Options()), total - ihl - tcpHdr, true } ================================================ FILE: common/tlsspoof/spoof.go ================================================ package tlsspoof import ( "net" E "github.com/sagernet/sing/common/exceptions" ) type Method int const ( MethodWrongSequence Method = iota MethodWrongChecksum MethodWrongAcknowledgment MethodWrongMD5Sig MethodWrongTimestamp ) const ( 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) } } func (m Method) String() string { switch m { case MethodWrongSequence: return MethodNameWrongSequence case MethodWrongChecksum: return MethodNameWrongChecksum case MethodWrongAcknowledgment: return MethodNameWrongAcknowledgment case MethodWrongMD5Sig: return MethodNameWrongMD5Sig case MethodWrongTimestamp: return MethodNameWrongTimestamp default: return "unknown" } } type rawSpoofer interface { Inject(payload []byte) error Close() error } type Conn struct { net.Conn spoofer rawSpoofer fakeHello []byte injected bool } 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 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 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") } 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, "tls_spoof: 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 } ================================================ FILE: common/tlsspoof/testdata_test.go ================================================ //go:build linux || darwin || (windows && (amd64 || 386)) package tlsspoof // realClientHello is a captured Chrome ClientHello for github.com. const realClientHello = "16030105f8010005f403036e35de7389a679c54029cf452611f2211c70d9ac3897271de589ab6155f8e4ab20637d225f1ef969ad87ed78bfb9d171300bcb1703b6f314ccefb964f79b7d0961002a0a0a130213031301c02cc02bcca9c030c02fcca8c00ac009c014c013009d009c0035002fc008c012000a01000581baba00000000000f000d00000a6769746875622e636f6d00170000ff01000100000a000e000c3a3a11ec001d001700180019000b000201000010000e000c02683208687474702f312e31000500050100000000000d00160014040308040401050308050805050108060601020100120000003304ef04ed3a3a00010011ec04c0aeb2250c092a3463161cccb29d9183331a424964248579507ed23a180b0ceab2a5f5d9ce41547e497a89055471ea572867ba3a1fc3c9e45025274a20f60c6b60e62476b6afed0403af59ab83660ef4112ae20386a602010d0a5d454c0ed34c84ed4423e750213e6a2baab1bf9c4367a6007ab40a33d95220c2dcaa44f257024a5626b545db0510f4311b1a60714154909c6a61fdfca011fb2626d657aeb6070bf078508babe3b584555013e34acc56198ed4663742b3155a664a9901794c4586820a7dc162c01827291f3792e1237f801a8d1ef096013c181c4a58d2f6859ba75022d18cc4418bd4f351d5c18f83a58857d05af860c4b9ac018a5b63f17184e591532c6bc2cf2215d4a282c8a8a4f6f7aee110422c8bc9ebd3b1d609c568523aaae555db320e6c269473d87af38c256cbb9febc20aea6380c32a8916f7a373c8b1e37554e3260bf6621f6b804ee80b3c516b1d01985bf4c603b6daa9a5991de6a7a29f3a7122b8afb843a7660110fce62b43c615f5bcc2db688ba012649c0952b0a2c031e732d2b454c6b2968683cb8d244be2c9a7fa163222979eaf92722b92b862d81a3d94450c2b60c318421ebb4307c42d1f0473592a5c30e42039cc68cda9721e61aa63f49def17c15221680ed444896340133bbee67556f56b9f9d78a4df715f926a12add0cc9c862e46ea8b7316ae468282c18601b2771c9c9322f982228cf93effaacd3f80cbd12bce5fc36f56e2a3caf91e578a5fae00c9b23a8ed1a66764f4433c3628a70b8f0a6196adc60a4cb4226f07ba4c6b363fe9065563bfc1347452946386bab488686e837ab979c64f9047417fca635fe1bb4f074f256cc8af837c7b455e280426547755af90a61640169ef180aea3a77e662bb6dac1b6c3696027129b1a5edf495314e9c7f4b6110e16378ec893fa24642330a40aba1a85326101acb97c620fd8d71389e69eaed7bdb01bbe1fd428d66191150c7b2cd1ad4257391676a82ba8ce07fb2667c3b289f159003a7c7bc31d361b7b7f49a802961739d950dfcc0fa1c7abce5abdd2245101da391151490862028110465950b9e9c03d08a90998ab83267838d2e74a0593bc81f74cdf734519a05b351c0e5488c68dd810e6e9142ccc1e2f4a7f464297eb340e27acc6b9d64e12e38cce8492b3d939140b5a9e149a75597f10a23874c84323a07cdd657274378f887c85c4259b9c04cd33ba58ed630ef2a744f8e19dd34843dff331d2a6be7e2332c599289cd248a611c73d7481cd4a9bd43449a3836f14b2af18a1739e17999e4c67e85cc5bcecabb14185e5bcaff3c96098f03dc5aba819f29587758f49f940585354a2a780830528d68ccd166920dadcaa25cab5fc1907272a826aba3f08bc6b88757776812ecb6c7cec69a223ec0a13a7b62a2349a0f63ed7a27a3b15ba21d71fe6864ec6e089ae17cadd433fa3138f7ee24353c11365818f8fc34f43a05542d18efaac24bfccc1f748a0cc1a67ad379468b76fd34973dba785f5c91d618333cd810fe0700d1bbc8422029782628070a624c52c5309a4a64d625b11f8033ab28df34a1add297517fcc06b92b6817b3c5144438cf260867c57bde68c8c4b82e6a135ef676a52fbae5708002a404e6189a60e2836de565ad1b29e3819e5ed49f6810bcb28e1bd6de57306f94b79d9dae1cc4624d2a068499beef81cd5fe4b76dcbfff2a2008001d002001976128c6d5a934533f28b9914d2480aab2a8c1ab03d212529ce8b27640a716002d00020101002b000706caca03040303001b00030200015a5a000100" ================================================ FILE: common/uot/router.go ================================================ package uot import ( "context" "net" "net/netip" "github.com/sagernet/sing-box/adapter" 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/common/uot" ) var _ adapter.ConnectionRouterEx = (*Router)(nil) type Router struct { router adapter.ConnectionRouterEx logger logger.ContextLogger } func NewRouter(router adapter.ConnectionRouterEx, logger logger.ContextLogger) *Router { return &Router{router, logger} } func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { switch metadata.Destination.Fqdn { case uot.MagicAddress: request, err := uot.ReadRequest(conn) if err != nil { return E.Cause(err, "read UoT request") } if request.IsConnect { r.logger.InfoContext(ctx, "inbound UoT connect connection to ", request.Destination) } else { r.logger.InfoContext(ctx, "inbound UoT connection to ", request.Destination) } metadata.Domain = metadata.Destination.Fqdn metadata.Destination = request.Destination return r.router.RoutePacketConnection(ctx, uot.NewConn(conn, *request), metadata) case uot.LegacyMagicAddress: r.logger.InfoContext(ctx, "inbound legacy UoT connection") metadata.Domain = metadata.Destination.Fqdn metadata.Destination = M.Socksaddr{Addr: netip.IPv4Unspecified()} return r.RoutePacketConnection(ctx, uot.NewConn(conn, uot.Request{}), metadata) } return r.router.RouteConnection(ctx, conn, metadata) } func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { return r.router.RoutePacketConnection(ctx, conn, metadata) } func (r *Router) RouteConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { switch metadata.Destination.Fqdn { case uot.MagicAddress: request, err := uot.ReadRequest(conn) if err != nil { err = E.Cause(err, "UoT read request") r.logger.ErrorContext(ctx, "process connection from ", metadata.Source, ": ", err) N.CloseOnHandshakeFailure(conn, onClose, err) return } if request.IsConnect { r.logger.InfoContext(ctx, "inbound UoT connect connection to ", request.Destination) } else { r.logger.InfoContext(ctx, "inbound UoT connection to ", request.Destination) } metadata.Domain = metadata.Destination.Fqdn metadata.Destination = request.Destination r.router.RoutePacketConnectionEx(ctx, uot.NewConn(conn, *request), metadata, onClose) return case uot.LegacyMagicAddress: r.logger.InfoContext(ctx, "inbound legacy UoT connection") metadata.Domain = metadata.Destination.Fqdn metadata.Destination = M.Socksaddr{Addr: netip.IPv4Unspecified()} r.RoutePacketConnectionEx(ctx, uot.NewConn(conn, uot.Request{}), metadata, onClose) return } r.router.RouteConnectionEx(ctx, conn, metadata, onClose) } func (r *Router) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { r.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } ================================================ FILE: common/urltest/urltest.go ================================================ package urltest import ( "context" "crypto/tls" "net" "net/http" "net/url" "sync" "time" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" 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/observable" ) var _ adapter.URLTestHistoryStorage = (*HistoryStorage)(nil) type HistoryStorage struct { access sync.RWMutex delayHistory map[string]*adapter.URLTestHistory updateHook *observable.Subscriber[struct{}] } func NewHistoryStorage() *HistoryStorage { return &HistoryStorage{ delayHistory: make(map[string]*adapter.URLTestHistory), } } func (s *HistoryStorage) SetHook(hook *observable.Subscriber[struct{}]) { s.updateHook = hook } func (s *HistoryStorage) LoadURLTestHistory(tag string) *adapter.URLTestHistory { if s == nil { return nil } s.access.RLock() defer s.access.RUnlock() return s.delayHistory[tag] } func (s *HistoryStorage) DeleteURLTestHistory(tag string) { s.access.Lock() delete(s.delayHistory, tag) s.notifyUpdated() s.access.Unlock() } func (s *HistoryStorage) StoreURLTestHistory(tag string, history *adapter.URLTestHistory) { s.access.Lock() s.delayHistory[tag] = history s.notifyUpdated() s.access.Unlock() } func (s *HistoryStorage) notifyUpdated() { updateHook := s.updateHook if updateHook != nil { updateHook.Emit(struct{}{}) } } func (s *HistoryStorage) Close() error { s.access.Lock() defer s.access.Unlock() s.updateHook = nil return nil } func URLTest(ctx context.Context, link string, detour N.Dialer) (t uint16, err error) { if link == "" { link = "https://www.gstatic.com/generate_204" } linkURL, err := url.Parse(link) if err != nil { return } hostname := linkURL.Hostname() port := linkURL.Port() if port == "" { switch linkURL.Scheme { case "http": port = "80" case "https": port = "443" } } start := time.Now() instance, err := detour.DialContext(ctx, "tcp", M.ParseSocksaddrHostPortStr(hostname, port)) if err != nil { return } defer instance.Close() if N.NeedHandshakeForWrite(instance) { start = time.Now() } req, err := http.NewRequest(http.MethodHead, link, nil) if err != nil { return } client := http.Client{ Transport: &http.Transport{ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { return instance, nil }, TLSClientConfig: &tls.Config{ Time: ntp.TimeFuncFromContext(ctx), RootCAs: adapter.RootPoolFromContext(ctx), }, }, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, Timeout: C.TCPTimeout, } defer client.CloseIdleConnections() resp, err := client.Do(req.WithContext(ctx)) if err != nil { return } resp.Body.Close() t = uint16(time.Since(start) / time.Millisecond) return } ================================================ FILE: common/windivert/address_test.go ================================================ 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. ================================================ FILE: common/windivert/assets_386.go ================================================ //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" } ================================================ FILE: common/windivert/assets_amd64.go ================================================ //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" } ================================================ FILE: common/windivert/assets_unsupported.go ================================================ //go:build windows && !amd64 && !386 package windivert func assetFiles() []assetFile { return nil } func driverSysName() string { return "" } ================================================ FILE: common/windivert/driver_windows.go ================================================ //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 } ================================================ FILE: common/windivert/filter.go ================================================ 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 } ================================================ FILE: common/windivert/filter_test.go ================================================ 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) } } ================================================ FILE: common/windivert/handle_windows.go ================================================ //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") } 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 } 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 } ================================================ FILE: common/windivert/handle_windows_test.go ================================================ //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)) 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))) } ================================================ FILE: common/windivert/integration_windows_test.go ================================================ //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 range 2 { go func() { h, err := Open(nil, LayerNetwork, 0, FlagSendOnly) handles <- h errCh <- err }() } for range 2 { err := <-errCh h := <-handles require.NoError(t, err) require.NoError(t, h.Close()) } } ================================================ FILE: common/windivert/windivert.go ================================================ // 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 ( // 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 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 _ [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< 0 { err = server.Send(&ConnectionEvents{Events: pendingEvents}) if err != nil { return err } } case <-ticker.C: protoEvents := s.buildTrafficUpdates(trafficManager, connectionSnapshots) if len(protoEvents) == 0 { continue } err = server.Send(&ConnectionEvents{Events: protoEvents}) if err != nil { return err } } } } type connectionSnapshot struct { uplink int64 downlink int64 hadTraffic bool } func (s *StartedService) buildInitialConnectionState(manager *trafficontrol.Manager, snapshots map[uuid.UUID]connectionSnapshot) []*ConnectionEvent { var events []*ConnectionEvent for _, metadata := range manager.Connections() { events = append(events, &ConnectionEvent{ Type: ConnectionEventType_CONNECTION_EVENT_NEW, Id: metadata.ID.String(), Connection: buildConnectionProto(metadata), }) snapshots[metadata.ID] = connectionSnapshot{ uplink: metadata.Upload.Load(), downlink: metadata.Download.Load(), } } for _, metadata := range manager.ClosedConnections() { conn := buildConnectionProto(metadata) conn.ClosedAt = metadata.ClosedAt.UnixMilli() events = append(events, &ConnectionEvent{ Type: ConnectionEventType_CONNECTION_EVENT_NEW, Id: metadata.ID.String(), Connection: conn, }) } return events } func (s *StartedService) applyConnectionEvent(event trafficontrol.ConnectionEvent, snapshots map[uuid.UUID]connectionSnapshot) *ConnectionEvent { switch event.Type { case trafficontrol.ConnectionEventNew: if _, exists := snapshots[event.ID]; exists { return nil } snapshots[event.ID] = connectionSnapshot{ uplink: event.Metadata.Upload.Load(), downlink: event.Metadata.Download.Load(), } return &ConnectionEvent{ Type: ConnectionEventType_CONNECTION_EVENT_NEW, Id: event.ID.String(), Connection: buildConnectionProto(event.Metadata), } case trafficontrol.ConnectionEventClosed: delete(snapshots, event.ID) protoEvent := &ConnectionEvent{ Type: ConnectionEventType_CONNECTION_EVENT_CLOSED, Id: event.ID.String(), } closedAt := event.ClosedAt if closedAt.IsZero() && !event.Metadata.ClosedAt.IsZero() { closedAt = event.Metadata.ClosedAt } if closedAt.IsZero() { closedAt = time.Now() } protoEvent.ClosedAt = closedAt.UnixMilli() if event.Metadata.ID != uuid.Nil { conn := buildConnectionProto(event.Metadata) conn.ClosedAt = protoEvent.ClosedAt protoEvent.Connection = conn } return protoEvent default: return nil } } func (s *StartedService) buildTrafficUpdates(manager *trafficontrol.Manager, snapshots map[uuid.UUID]connectionSnapshot) []*ConnectionEvent { activeConnections := manager.Connections() activeIndex := make(map[uuid.UUID]*trafficontrol.TrackerMetadata, len(activeConnections)) var events []*ConnectionEvent for _, metadata := range activeConnections { activeIndex[metadata.ID] = metadata currentUpload := metadata.Upload.Load() currentDownload := metadata.Download.Load() snapshot, exists := snapshots[metadata.ID] if !exists { snapshots[metadata.ID] = connectionSnapshot{ uplink: currentUpload, downlink: currentDownload, } events = append(events, &ConnectionEvent{ Type: ConnectionEventType_CONNECTION_EVENT_NEW, Id: metadata.ID.String(), Connection: buildConnectionProto(metadata), }) continue } uplinkDelta := currentUpload - snapshot.uplink downlinkDelta := currentDownload - snapshot.downlink if uplinkDelta < 0 || downlinkDelta < 0 { if snapshot.hadTraffic { events = append(events, &ConnectionEvent{ Type: ConnectionEventType_CONNECTION_EVENT_UPDATE, Id: metadata.ID.String(), UplinkDelta: 0, DownlinkDelta: 0, }) } snapshot.uplink = currentUpload snapshot.downlink = currentDownload snapshot.hadTraffic = false snapshots[metadata.ID] = snapshot continue } if uplinkDelta > 0 || downlinkDelta > 0 { snapshot.uplink = currentUpload snapshot.downlink = currentDownload snapshot.hadTraffic = true snapshots[metadata.ID] = snapshot events = append(events, &ConnectionEvent{ Type: ConnectionEventType_CONNECTION_EVENT_UPDATE, Id: metadata.ID.String(), UplinkDelta: uplinkDelta, DownlinkDelta: downlinkDelta, }) continue } if snapshot.hadTraffic { snapshot.uplink = currentUpload snapshot.downlink = currentDownload snapshot.hadTraffic = false snapshots[metadata.ID] = snapshot events = append(events, &ConnectionEvent{ Type: ConnectionEventType_CONNECTION_EVENT_UPDATE, Id: metadata.ID.String(), UplinkDelta: 0, DownlinkDelta: 0, }) } } var closedIndex map[uuid.UUID]*trafficontrol.TrackerMetadata for id := range snapshots { if _, exists := activeIndex[id]; exists { continue } if closedIndex == nil { closedIndex = make(map[uuid.UUID]*trafficontrol.TrackerMetadata) for _, metadata := range manager.ClosedConnections() { closedIndex[metadata.ID] = metadata } } closedAt := time.Now() var conn *Connection if metadata, ok := closedIndex[id]; ok { if !metadata.ClosedAt.IsZero() { closedAt = metadata.ClosedAt } conn = buildConnectionProto(metadata) conn.ClosedAt = closedAt.UnixMilli() } events = append(events, &ConnectionEvent{ Type: ConnectionEventType_CONNECTION_EVENT_CLOSED, Id: id.String(), ClosedAt: closedAt.UnixMilli(), Connection: conn, }) delete(snapshots, id) } return events } func buildConnectionProto(metadata *trafficontrol.TrackerMetadata) *Connection { var rule string if metadata.Rule != nil { rule = metadata.Rule.String() } uplinkTotal := metadata.Upload.Load() downlinkTotal := metadata.Download.Load() var processInfo *ProcessInfo if metadata.Metadata.ProcessInfo != nil { processInfo = &ProcessInfo{ ProcessId: metadata.Metadata.ProcessInfo.ProcessID, UserId: metadata.Metadata.ProcessInfo.UserId, UserName: metadata.Metadata.ProcessInfo.UserName, ProcessPath: metadata.Metadata.ProcessInfo.ProcessPath, PackageNames: metadata.Metadata.ProcessInfo.AndroidPackageNames, } } return &Connection{ Id: metadata.ID.String(), Inbound: metadata.Metadata.Inbound, InboundType: metadata.Metadata.InboundType, IpVersion: int32(metadata.Metadata.IPVersion), Network: metadata.Metadata.Network, Source: metadata.Metadata.Source.String(), Destination: metadata.Metadata.Destination.String(), Domain: metadata.Metadata.Domain, Protocol: metadata.Metadata.Protocol, User: metadata.Metadata.User, FromOutbound: metadata.Metadata.Outbound, CreatedAt: metadata.CreatedAt.UnixMilli(), UplinkTotal: uplinkTotal, DownlinkTotal: downlinkTotal, Rule: rule, Outbound: metadata.Outbound, OutboundType: metadata.OutboundType, ChainList: metadata.Chain, ProcessInfo: processInfo, } } func (s *StartedService) CloseConnection(ctx context.Context, request *CloseConnectionRequest) (*emptypb.Empty, error) { s.serviceAccess.RLock() switch s.serviceStatus.Status { case ServiceStatus_STARTING, ServiceStatus_STARTED: default: s.serviceAccess.RUnlock() return nil, os.ErrInvalid } boxService := s.instance s.serviceAccess.RUnlock() targetConn := boxService.clashServer.(*clashapi.Server).TrafficManager().Connection(uuid.FromStringOrNil(request.Id)) if targetConn != nil { targetConn.Close() } return &emptypb.Empty{}, nil } func (s *StartedService) CloseAllConnections(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) { s.serviceAccess.RLock() nowService := s.instance s.serviceAccess.RUnlock() if nowService != nil && nowService.connectionManager != nil { nowService.connectionManager.CloseAll() } return &emptypb.Empty{}, nil } func (s *StartedService) GetDeprecatedWarnings(ctx context.Context, empty *emptypb.Empty) (*DeprecatedWarnings, error) { s.serviceAccess.RLock() if s.serviceStatus.Status != ServiceStatus_STARTED { s.serviceAccess.RUnlock() return nil, os.ErrInvalid } boxService := s.instance s.serviceAccess.RUnlock() notes := service.FromContext[deprecated.Manager](boxService.ctx).(*deprecatedManager).Get() return &DeprecatedWarnings{ Warnings: common.Map(notes, func(it deprecated.Note) *DeprecatedWarning { return &DeprecatedWarning{ Message: it.Message(), Impending: it.Impending(), MigrationLink: it.MigrationLink, Description: it.Description, DeprecatedVersion: it.DeprecatedVersion, ScheduledVersion: it.ScheduledVersion, } }), }, nil } func (s *StartedService) GetStartedAt(ctx context.Context, empty *emptypb.Empty) (*StartedAt, error) { s.serviceAccess.RLock() defer s.serviceAccess.RUnlock() return &StartedAt{StartedAt: s.startedAt.UnixMilli()}, 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 var list OutboundList for _, ob := range boxService.instance.Outbound().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) } 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 } 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) 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.TailscaleEndpoint } var endpoints []tailscaleEndpoint for _, endpoint := range endpointManager.Endpoints() { if endpoint.Type() != C.TypeTailscale { continue } provider, loaded := endpoint.(adapter.TailscaleEndpoint) 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.TailscaleEndpoint) { 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) }() 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 := range tags { protoEndpoints = append(protoEndpoints, tailscaleEndpointStatusToProto(tag, statuses[tag])) } sendErr := server.Send(&TailscaleStatusUpdate{ Endpoints: protoEndpoints, }) if sendErr != nil { return sendErr } } return nil } func tailscaleEndpointStatusToProto(tag string, s *adapter.TailscaleEndpointStatus) *TailscaleEndpointStatus { 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) } userGroups[i] = &TailscaleUserGroup{ UserID: group.UserID, LoginName: group.LoginName, DisplayName: group.DisplayName, ProfilePicURL: group.ProfilePicURL, Peers: peers, } } 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) 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() { } func (s *StartedService) WriteMessage(level log.Level, message string) { item := &log.Entry{Level: level, Message: message} s.logAccess.Lock() s.logLines.PushBack(item) if s.logLines.Len() > s.logMaxLines { s.logLines.Remove(s.logLines.Front()) } s.logAccess.Unlock() s.logSubscriber.Emit(item) if s.debug { s.handler.WriteDebugMessage(message) } } func (s *StartedService) Instance() *Instance { s.serviceAccess.RLock() defer s.serviceAccess.RUnlock() return s.instance } ================================================ FILE: daemon/started_service.pb.go ================================================ package daemon import ( reflect "reflect" sync "sync" unsafe "unsafe" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" emptypb "google.golang.org/protobuf/types/known/emptypb" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) type LogLevel int32 const ( LogLevel_PANIC LogLevel = 0 LogLevel_FATAL LogLevel = 1 LogLevel_ERROR LogLevel = 2 LogLevel_WARN LogLevel = 3 LogLevel_INFO LogLevel = 4 LogLevel_DEBUG LogLevel = 5 LogLevel_TRACE LogLevel = 6 ) // Enum value maps for LogLevel. var ( LogLevel_name = map[int32]string{ 0: "PANIC", 1: "FATAL", 2: "ERROR", 3: "WARN", 4: "INFO", 5: "DEBUG", 6: "TRACE", } LogLevel_value = map[string]int32{ "PANIC": 0, "FATAL": 1, "ERROR": 2, "WARN": 3, "INFO": 4, "DEBUG": 5, "TRACE": 6, } ) func (x LogLevel) Enum() *LogLevel { p := new(LogLevel) *p = x return p } func (x LogLevel) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (LogLevel) Descriptor() protoreflect.EnumDescriptor { return file_daemon_started_service_proto_enumTypes[0].Descriptor() } func (LogLevel) Type() protoreflect.EnumType { return &file_daemon_started_service_proto_enumTypes[0] } func (x LogLevel) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use LogLevel.Descriptor instead. func (LogLevel) EnumDescriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{0} } type ConnectionEventType int32 const ( ConnectionEventType_CONNECTION_EVENT_NEW ConnectionEventType = 0 ConnectionEventType_CONNECTION_EVENT_UPDATE ConnectionEventType = 1 ConnectionEventType_CONNECTION_EVENT_CLOSED ConnectionEventType = 2 ) // Enum value maps for ConnectionEventType. var ( ConnectionEventType_name = map[int32]string{ 0: "CONNECTION_EVENT_NEW", 1: "CONNECTION_EVENT_UPDATE", 2: "CONNECTION_EVENT_CLOSED", } ConnectionEventType_value = map[string]int32{ "CONNECTION_EVENT_NEW": 0, "CONNECTION_EVENT_UPDATE": 1, "CONNECTION_EVENT_CLOSED": 2, } ) func (x ConnectionEventType) Enum() *ConnectionEventType { p := new(ConnectionEventType) *p = x return p } func (x ConnectionEventType) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (ConnectionEventType) Descriptor() protoreflect.EnumDescriptor { return file_daemon_started_service_proto_enumTypes[1].Descriptor() } func (ConnectionEventType) Type() protoreflect.EnumType { return &file_daemon_started_service_proto_enumTypes[1] } func (x ConnectionEventType) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use ConnectionEventType.Descriptor instead. func (ConnectionEventType) EnumDescriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{1} } type ServiceStatus_Type int32 const ( ServiceStatus_IDLE ServiceStatus_Type = 0 ServiceStatus_STARTING ServiceStatus_Type = 1 ServiceStatus_STARTED ServiceStatus_Type = 2 ServiceStatus_STOPPING ServiceStatus_Type = 3 ServiceStatus_FATAL ServiceStatus_Type = 4 ) // Enum value maps for ServiceStatus_Type. var ( ServiceStatus_Type_name = map[int32]string{ 0: "IDLE", 1: "STARTING", 2: "STARTED", 3: "STOPPING", 4: "FATAL", } ServiceStatus_Type_value = map[string]int32{ "IDLE": 0, "STARTING": 1, "STARTED": 2, "STOPPING": 3, "FATAL": 4, } ) func (x ServiceStatus_Type) Enum() *ServiceStatus_Type { p := new(ServiceStatus_Type) *p = x return p } func (x ServiceStatus_Type) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (ServiceStatus_Type) Descriptor() protoreflect.EnumDescriptor { return file_daemon_started_service_proto_enumTypes[2].Descriptor() } func (ServiceStatus_Type) Type() protoreflect.EnumType { return &file_daemon_started_service_proto_enumTypes[2] } func (x ServiceStatus_Type) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use ServiceStatus_Type.Descriptor instead. 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"` ErrorMessage string `protobuf:"bytes,2,opt,name=errorMessage,proto3" json:"errorMessage,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ServiceStatus) Reset() { *x = ServiceStatus{} mi := &file_daemon_started_service_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ServiceStatus) String() string { return protoimpl.X.MessageStringOf(x) } func (*ServiceStatus) ProtoMessage() {} func (x *ServiceStatus) ProtoReflect() protoreflect.Message { mi := &file_daemon_started_service_proto_msgTypes[0] 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 ServiceStatus.ProtoReflect.Descriptor instead. func (*ServiceStatus) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{0} } func (x *ServiceStatus) GetStatus() ServiceStatus_Type { if x != nil { return x.Status } return ServiceStatus_IDLE } func (x *ServiceStatus) GetErrorMessage() string { if x != nil { return x.ErrorMessage } return "" } type ReloadServiceRequest struct { state protoimpl.MessageState `protogen:"open.v1"` NewProfileContent string `protobuf:"bytes,1,opt,name=newProfileContent,proto3" json:"newProfileContent,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ReloadServiceRequest) Reset() { *x = ReloadServiceRequest{} mi := &file_daemon_started_service_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ReloadServiceRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*ReloadServiceRequest) ProtoMessage() {} func (x *ReloadServiceRequest) ProtoReflect() protoreflect.Message { mi := &file_daemon_started_service_proto_msgTypes[1] 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 ReloadServiceRequest.ProtoReflect.Descriptor instead. func (*ReloadServiceRequest) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{1} } func (x *ReloadServiceRequest) GetNewProfileContent() string { if x != nil { return x.NewProfileContent } return "" } type SubscribeStatusRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Interval int64 `protobuf:"varint,1,opt,name=interval,proto3" json:"interval,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SubscribeStatusRequest) Reset() { *x = SubscribeStatusRequest{} mi := &file_daemon_started_service_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SubscribeStatusRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*SubscribeStatusRequest) ProtoMessage() {} func (x *SubscribeStatusRequest) ProtoReflect() protoreflect.Message { mi := &file_daemon_started_service_proto_msgTypes[2] 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 SubscribeStatusRequest.ProtoReflect.Descriptor instead. func (*SubscribeStatusRequest) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{2} } func (x *SubscribeStatusRequest) GetInterval() int64 { if x != nil { return x.Interval } return 0 } type Log struct { state protoimpl.MessageState `protogen:"open.v1"` Messages []*Log_Message `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"` Reset_ bool `protobuf:"varint,2,opt,name=reset,proto3" json:"reset,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Log) Reset() { *x = Log{} mi := &file_daemon_started_service_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Log) String() string { return protoimpl.X.MessageStringOf(x) } func (*Log) ProtoMessage() {} func (x *Log) ProtoReflect() protoreflect.Message { mi := &file_daemon_started_service_proto_msgTypes[3] 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 Log.ProtoReflect.Descriptor instead. func (*Log) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{3} } func (x *Log) GetMessages() []*Log_Message { if x != nil { return x.Messages } return nil } func (x *Log) GetReset_() bool { if x != nil { return x.Reset_ } return false } type DefaultLogLevel struct { state protoimpl.MessageState `protogen:"open.v1"` Level LogLevel `protobuf:"varint,1,opt,name=level,proto3,enum=daemon.LogLevel" json:"level,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DefaultLogLevel) Reset() { *x = DefaultLogLevel{} mi := &file_daemon_started_service_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *DefaultLogLevel) String() string { return protoimpl.X.MessageStringOf(x) } func (*DefaultLogLevel) ProtoMessage() {} func (x *DefaultLogLevel) ProtoReflect() protoreflect.Message { mi := &file_daemon_started_service_proto_msgTypes[4] 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 DefaultLogLevel.ProtoReflect.Descriptor instead. func (*DefaultLogLevel) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{4} } func (x *DefaultLogLevel) GetLevel() LogLevel { if x != nil { return x.Level } return LogLevel_PANIC } type Status struct { state protoimpl.MessageState `protogen:"open.v1"` Memory uint64 `protobuf:"varint,1,opt,name=memory,proto3" json:"memory,omitempty"` Goroutines int32 `protobuf:"varint,2,opt,name=goroutines,proto3" json:"goroutines,omitempty"` ConnectionsIn int32 `protobuf:"varint,3,opt,name=connectionsIn,proto3" json:"connectionsIn,omitempty"` ConnectionsOut int32 `protobuf:"varint,4,opt,name=connectionsOut,proto3" json:"connectionsOut,omitempty"` TrafficAvailable bool `protobuf:"varint,5,opt,name=trafficAvailable,proto3" json:"trafficAvailable,omitempty"` Uplink int64 `protobuf:"varint,6,opt,name=uplink,proto3" json:"uplink,omitempty"` Downlink int64 `protobuf:"varint,7,opt,name=downlink,proto3" json:"downlink,omitempty"` UplinkTotal int64 `protobuf:"varint,8,opt,name=uplinkTotal,proto3" json:"uplinkTotal,omitempty"` DownlinkTotal int64 `protobuf:"varint,9,opt,name=downlinkTotal,proto3" json:"downlinkTotal,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Status) Reset() { *x = Status{} mi := &file_daemon_started_service_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Status) String() string { return protoimpl.X.MessageStringOf(x) } func (*Status) ProtoMessage() {} func (x *Status) ProtoReflect() protoreflect.Message { mi := &file_daemon_started_service_proto_msgTypes[5] 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 Status.ProtoReflect.Descriptor instead. func (*Status) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{5} } func (x *Status) GetMemory() uint64 { if x != nil { return x.Memory } return 0 } func (x *Status) GetGoroutines() int32 { if x != nil { return x.Goroutines } return 0 } func (x *Status) GetConnectionsIn() int32 { if x != nil { return x.ConnectionsIn } return 0 } func (x *Status) GetConnectionsOut() int32 { if x != nil { return x.ConnectionsOut } return 0 } func (x *Status) GetTrafficAvailable() bool { if x != nil { return x.TrafficAvailable } return false } func (x *Status) GetUplink() int64 { if x != nil { return x.Uplink } return 0 } func (x *Status) GetDownlink() int64 { if x != nil { return x.Downlink } return 0 } func (x *Status) GetUplinkTotal() int64 { if x != nil { return x.UplinkTotal } return 0 } func (x *Status) GetDownlinkTotal() int64 { if x != nil { return x.DownlinkTotal } return 0 } type Groups struct { state protoimpl.MessageState `protogen:"open.v1"` Group []*Group `protobuf:"bytes,1,rep,name=group,proto3" json:"group,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Groups) Reset() { *x = Groups{} mi := &file_daemon_started_service_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Groups) String() string { return protoimpl.X.MessageStringOf(x) } func (*Groups) ProtoMessage() {} func (x *Groups) ProtoReflect() protoreflect.Message { mi := &file_daemon_started_service_proto_msgTypes[6] 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 Groups.ProtoReflect.Descriptor instead. func (*Groups) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{6} } func (x *Groups) GetGroup() []*Group { if x != nil { return x.Group } return nil } type Group struct { state protoimpl.MessageState `protogen:"open.v1"` Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` Selectable bool `protobuf:"varint,3,opt,name=selectable,proto3" json:"selectable,omitempty"` Selected string `protobuf:"bytes,4,opt,name=selected,proto3" json:"selected,omitempty"` IsExpand bool `protobuf:"varint,5,opt,name=isExpand,proto3" json:"isExpand,omitempty"` Items []*GroupItem `protobuf:"bytes,6,rep,name=items,proto3" json:"items,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Group) Reset() { *x = Group{} mi := &file_daemon_started_service_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Group) String() string { return protoimpl.X.MessageStringOf(x) } func (*Group) ProtoMessage() {} func (x *Group) ProtoReflect() protoreflect.Message { mi := &file_daemon_started_service_proto_msgTypes[7] 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 Group.ProtoReflect.Descriptor instead. func (*Group) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{7} } func (x *Group) GetTag() string { if x != nil { return x.Tag } return "" } func (x *Group) GetType() string { if x != nil { return x.Type } return "" } func (x *Group) GetSelectable() bool { if x != nil { return x.Selectable } return false } func (x *Group) GetSelected() string { if x != nil { return x.Selected } return "" } func (x *Group) GetIsExpand() bool { if x != nil { return x.IsExpand } return false } func (x *Group) GetItems() []*GroupItem { if x != nil { return x.Items } return nil } type GroupItem struct { state protoimpl.MessageState `protogen:"open.v1"` Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` UrlTestTime int64 `protobuf:"varint,3,opt,name=urlTestTime,proto3" json:"urlTestTime,omitempty"` UrlTestDelay int32 `protobuf:"varint,4,opt,name=urlTestDelay,proto3" json:"urlTestDelay,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *GroupItem) Reset() { *x = GroupItem{} mi := &file_daemon_started_service_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *GroupItem) String() string { return protoimpl.X.MessageStringOf(x) } func (*GroupItem) ProtoMessage() {} func (x *GroupItem) ProtoReflect() protoreflect.Message { mi := &file_daemon_started_service_proto_msgTypes[8] 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 GroupItem.ProtoReflect.Descriptor instead. func (*GroupItem) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{8} } func (x *GroupItem) GetTag() string { if x != nil { return x.Tag } return "" } func (x *GroupItem) GetType() string { if x != nil { return x.Type } return "" } func (x *GroupItem) GetUrlTestTime() int64 { if x != nil { return x.UrlTestTime } return 0 } func (x *GroupItem) GetUrlTestDelay() int32 { if x != nil { return x.UrlTestDelay } return 0 } type URLTestRequest struct { state protoimpl.MessageState `protogen:"open.v1"` OutboundTag string `protobuf:"bytes,1,opt,name=outboundTag,proto3" json:"outboundTag,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *URLTestRequest) Reset() { *x = URLTestRequest{} mi := &file_daemon_started_service_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *URLTestRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*URLTestRequest) ProtoMessage() {} func (x *URLTestRequest) ProtoReflect() protoreflect.Message { mi := &file_daemon_started_service_proto_msgTypes[9] 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 URLTestRequest.ProtoReflect.Descriptor instead. func (*URLTestRequest) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{9} } func (x *URLTestRequest) GetOutboundTag() string { if x != nil { return x.OutboundTag } return "" } type SelectOutboundRequest struct { state protoimpl.MessageState `protogen:"open.v1"` GroupTag string `protobuf:"bytes,1,opt,name=groupTag,proto3" json:"groupTag,omitempty"` OutboundTag string `protobuf:"bytes,2,opt,name=outboundTag,proto3" json:"outboundTag,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SelectOutboundRequest) Reset() { *x = SelectOutboundRequest{} mi := &file_daemon_started_service_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SelectOutboundRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*SelectOutboundRequest) ProtoMessage() {} func (x *SelectOutboundRequest) ProtoReflect() protoreflect.Message { mi := &file_daemon_started_service_proto_msgTypes[10] 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 SelectOutboundRequest.ProtoReflect.Descriptor instead. func (*SelectOutboundRequest) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{10} } func (x *SelectOutboundRequest) GetGroupTag() string { if x != nil { return x.GroupTag } return "" } func (x *SelectOutboundRequest) GetOutboundTag() string { if x != nil { return x.OutboundTag } return "" } type SetGroupExpandRequest struct { state protoimpl.MessageState `protogen:"open.v1"` GroupTag string `protobuf:"bytes,1,opt,name=groupTag,proto3" json:"groupTag,omitempty"` IsExpand bool `protobuf:"varint,2,opt,name=isExpand,proto3" json:"isExpand,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SetGroupExpandRequest) Reset() { *x = SetGroupExpandRequest{} mi := &file_daemon_started_service_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SetGroupExpandRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*SetGroupExpandRequest) ProtoMessage() {} func (x *SetGroupExpandRequest) ProtoReflect() protoreflect.Message { mi := &file_daemon_started_service_proto_msgTypes[11] 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 SetGroupExpandRequest.ProtoReflect.Descriptor instead. func (*SetGroupExpandRequest) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{11} } func (x *SetGroupExpandRequest) GetGroupTag() string { if x != nil { return x.GroupTag } return "" } func (x *SetGroupExpandRequest) GetIsExpand() bool { if x != nil { return x.IsExpand } return false } type ClashMode struct { state protoimpl.MessageState `protogen:"open.v1"` Mode string `protobuf:"bytes,3,opt,name=mode,proto3" json:"mode,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ClashMode) Reset() { *x = ClashMode{} mi := &file_daemon_started_service_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ClashMode) String() string { return protoimpl.X.MessageStringOf(x) } func (*ClashMode) ProtoMessage() {} func (x *ClashMode) ProtoReflect() protoreflect.Message { mi := &file_daemon_started_service_proto_msgTypes[12] 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 ClashMode.ProtoReflect.Descriptor instead. func (*ClashMode) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{12} } func (x *ClashMode) GetMode() string { if x != nil { return x.Mode } return "" } type ClashModeStatus struct { state protoimpl.MessageState `protogen:"open.v1"` ModeList []string `protobuf:"bytes,1,rep,name=modeList,proto3" json:"modeList,omitempty"` CurrentMode string `protobuf:"bytes,2,opt,name=currentMode,proto3" json:"currentMode,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ClashModeStatus) Reset() { *x = ClashModeStatus{} mi := &file_daemon_started_service_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ClashModeStatus) String() string { return protoimpl.X.MessageStringOf(x) } func (*ClashModeStatus) ProtoMessage() {} func (x *ClashModeStatus) ProtoReflect() protoreflect.Message { mi := &file_daemon_started_service_proto_msgTypes[13] 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 ClashModeStatus.ProtoReflect.Descriptor instead. func (*ClashModeStatus) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{13} } func (x *ClashModeStatus) GetModeList() []string { if x != nil { return x.ModeList } return nil } func (x *ClashModeStatus) GetCurrentMode() string { if x != nil { return x.CurrentMode } return "" } type SystemProxyStatus struct { state protoimpl.MessageState `protogen:"open.v1"` Available bool `protobuf:"varint,1,opt,name=available,proto3" json:"available,omitempty"` Enabled bool `protobuf:"varint,2,opt,name=enabled,proto3" json:"enabled,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SystemProxyStatus) Reset() { *x = SystemProxyStatus{} mi := &file_daemon_started_service_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SystemProxyStatus) String() string { return protoimpl.X.MessageStringOf(x) } func (*SystemProxyStatus) ProtoMessage() {} func (x *SystemProxyStatus) ProtoReflect() protoreflect.Message { mi := &file_daemon_started_service_proto_msgTypes[14] 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 SystemProxyStatus.ProtoReflect.Descriptor instead. func (*SystemProxyStatus) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{14} } func (x *SystemProxyStatus) GetAvailable() bool { if x != nil { return x.Available } return false } func (x *SystemProxyStatus) GetEnabled() bool { if x != nil { return x.Enabled } return false } type SetSystemProxyEnabledRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SetSystemProxyEnabledRequest) Reset() { *x = SetSystemProxyEnabledRequest{} mi := &file_daemon_started_service_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SetSystemProxyEnabledRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*SetSystemProxyEnabledRequest) ProtoMessage() {} func (x *SetSystemProxyEnabledRequest) ProtoReflect() protoreflect.Message { mi := &file_daemon_started_service_proto_msgTypes[15] 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 SetSystemProxyEnabledRequest.ProtoReflect.Descriptor instead. func (*SetSystemProxyEnabledRequest) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{15} } func (x *SetSystemProxyEnabledRequest) GetEnabled() bool { if x != nil { return x.Enabled } 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"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SubscribeConnectionsRequest) Reset() { *x = SubscribeConnectionsRequest{} mi := &file_daemon_started_service_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SubscribeConnectionsRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*SubscribeConnectionsRequest) ProtoMessage() {} func (x *SubscribeConnectionsRequest) ProtoReflect() protoreflect.Message { mi := &file_daemon_started_service_proto_msgTypes[17] 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 SubscribeConnectionsRequest.ProtoReflect.Descriptor instead. func (*SubscribeConnectionsRequest) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{17} } func (x *SubscribeConnectionsRequest) GetInterval() int64 { if x != nil { return x.Interval } return 0 } type ConnectionEvent struct { state protoimpl.MessageState `protogen:"open.v1"` Type ConnectionEventType `protobuf:"varint,1,opt,name=type,proto3,enum=daemon.ConnectionEventType" json:"type,omitempty"` Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"` Connection *Connection `protobuf:"bytes,3,opt,name=connection,proto3" json:"connection,omitempty"` UplinkDelta int64 `protobuf:"varint,4,opt,name=uplinkDelta,proto3" json:"uplinkDelta,omitempty"` DownlinkDelta int64 `protobuf:"varint,5,opt,name=downlinkDelta,proto3" json:"downlinkDelta,omitempty"` ClosedAt int64 `protobuf:"varint,6,opt,name=closedAt,proto3" json:"closedAt,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ConnectionEvent) Reset() { *x = ConnectionEvent{} mi := &file_daemon_started_service_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ConnectionEvent) String() string { return protoimpl.X.MessageStringOf(x) } func (*ConnectionEvent) ProtoMessage() {} func (x *ConnectionEvent) ProtoReflect() protoreflect.Message { mi := &file_daemon_started_service_proto_msgTypes[18] 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 ConnectionEvent.ProtoReflect.Descriptor instead. func (*ConnectionEvent) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{18} } func (x *ConnectionEvent) GetType() ConnectionEventType { if x != nil { return x.Type } return ConnectionEventType_CONNECTION_EVENT_NEW } func (x *ConnectionEvent) GetId() string { if x != nil { return x.Id } return "" } func (x *ConnectionEvent) GetConnection() *Connection { if x != nil { return x.Connection } return nil } func (x *ConnectionEvent) GetUplinkDelta() int64 { if x != nil { return x.UplinkDelta } return 0 } func (x *ConnectionEvent) GetDownlinkDelta() int64 { if x != nil { return x.DownlinkDelta } return 0 } func (x *ConnectionEvent) GetClosedAt() int64 { if x != nil { return x.ClosedAt } return 0 } type ConnectionEvents struct { state protoimpl.MessageState `protogen:"open.v1"` Events []*ConnectionEvent `protobuf:"bytes,1,rep,name=events,proto3" json:"events,omitempty"` Reset_ bool `protobuf:"varint,2,opt,name=reset,proto3" json:"reset,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ConnectionEvents) Reset() { *x = ConnectionEvents{} mi := &file_daemon_started_service_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ConnectionEvents) String() string { return protoimpl.X.MessageStringOf(x) } func (*ConnectionEvents) ProtoMessage() {} func (x *ConnectionEvents) ProtoReflect() protoreflect.Message { mi := &file_daemon_started_service_proto_msgTypes[19] 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 ConnectionEvents.ProtoReflect.Descriptor instead. func (*ConnectionEvents) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{19} } func (x *ConnectionEvents) GetEvents() []*ConnectionEvent { if x != nil { return x.Events } return nil } func (x *ConnectionEvents) GetReset_() bool { if x != nil { return x.Reset_ } return false } type Connection struct { state protoimpl.MessageState `protogen:"open.v1"` Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` Inbound string `protobuf:"bytes,2,opt,name=inbound,proto3" json:"inbound,omitempty"` InboundType string `protobuf:"bytes,3,opt,name=inboundType,proto3" json:"inboundType,omitempty"` IpVersion int32 `protobuf:"varint,4,opt,name=ipVersion,proto3" json:"ipVersion,omitempty"` Network string `protobuf:"bytes,5,opt,name=network,proto3" json:"network,omitempty"` Source string `protobuf:"bytes,6,opt,name=source,proto3" json:"source,omitempty"` Destination string `protobuf:"bytes,7,opt,name=destination,proto3" json:"destination,omitempty"` Domain string `protobuf:"bytes,8,opt,name=domain,proto3" json:"domain,omitempty"` Protocol string `protobuf:"bytes,9,opt,name=protocol,proto3" json:"protocol,omitempty"` User string `protobuf:"bytes,10,opt,name=user,proto3" json:"user,omitempty"` FromOutbound string `protobuf:"bytes,11,opt,name=fromOutbound,proto3" json:"fromOutbound,omitempty"` CreatedAt int64 `protobuf:"varint,12,opt,name=createdAt,proto3" json:"createdAt,omitempty"` ClosedAt int64 `protobuf:"varint,13,opt,name=closedAt,proto3" json:"closedAt,omitempty"` Uplink int64 `protobuf:"varint,14,opt,name=uplink,proto3" json:"uplink,omitempty"` Downlink int64 `protobuf:"varint,15,opt,name=downlink,proto3" json:"downlink,omitempty"` UplinkTotal int64 `protobuf:"varint,16,opt,name=uplinkTotal,proto3" json:"uplinkTotal,omitempty"` DownlinkTotal int64 `protobuf:"varint,17,opt,name=downlinkTotal,proto3" json:"downlinkTotal,omitempty"` Rule string `protobuf:"bytes,18,opt,name=rule,proto3" json:"rule,omitempty"` Outbound string `protobuf:"bytes,19,opt,name=outbound,proto3" json:"outbound,omitempty"` OutboundType string `protobuf:"bytes,20,opt,name=outboundType,proto3" json:"outboundType,omitempty"` ChainList []string `protobuf:"bytes,21,rep,name=chainList,proto3" json:"chainList,omitempty"` ProcessInfo *ProcessInfo `protobuf:"bytes,22,opt,name=processInfo,proto3" json:"processInfo,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Connection) Reset() { *x = Connection{} mi := &file_daemon_started_service_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Connection) String() string { return protoimpl.X.MessageStringOf(x) } func (*Connection) ProtoMessage() {} func (x *Connection) ProtoReflect() protoreflect.Message { mi := &file_daemon_started_service_proto_msgTypes[20] 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 Connection.ProtoReflect.Descriptor instead. func (*Connection) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{20} } func (x *Connection) GetId() string { if x != nil { return x.Id } return "" } func (x *Connection) GetInbound() string { if x != nil { return x.Inbound } return "" } func (x *Connection) GetInboundType() string { if x != nil { return x.InboundType } return "" } func (x *Connection) GetIpVersion() int32 { if x != nil { return x.IpVersion } return 0 } func (x *Connection) GetNetwork() string { if x != nil { return x.Network } return "" } func (x *Connection) GetSource() string { if x != nil { return x.Source } return "" } func (x *Connection) GetDestination() string { if x != nil { return x.Destination } return "" } func (x *Connection) GetDomain() string { if x != nil { return x.Domain } return "" } func (x *Connection) GetProtocol() string { if x != nil { return x.Protocol } return "" } func (x *Connection) GetUser() string { if x != nil { return x.User } return "" } func (x *Connection) GetFromOutbound() string { if x != nil { return x.FromOutbound } return "" } func (x *Connection) GetCreatedAt() int64 { if x != nil { return x.CreatedAt } return 0 } func (x *Connection) GetClosedAt() int64 { if x != nil { return x.ClosedAt } return 0 } func (x *Connection) GetUplink() int64 { if x != nil { return x.Uplink } return 0 } func (x *Connection) GetDownlink() int64 { if x != nil { return x.Downlink } return 0 } func (x *Connection) GetUplinkTotal() int64 { if x != nil { return x.UplinkTotal } return 0 } func (x *Connection) GetDownlinkTotal() int64 { if x != nil { return x.DownlinkTotal } return 0 } func (x *Connection) GetRule() string { if x != nil { return x.Rule } return "" } func (x *Connection) GetOutbound() string { if x != nil { return x.Outbound } return "" } func (x *Connection) GetOutboundType() string { if x != nil { return x.OutboundType } return "" } func (x *Connection) GetChainList() []string { if x != nil { return x.ChainList } return nil } func (x *Connection) GetProcessInfo() *ProcessInfo { if x != nil { return x.ProcessInfo } return nil } type ProcessInfo struct { state protoimpl.MessageState `protogen:"open.v1"` ProcessId uint32 `protobuf:"varint,1,opt,name=processId,proto3" json:"processId,omitempty"` UserId int32 `protobuf:"varint,2,opt,name=userId,proto3" json:"userId,omitempty"` UserName string `protobuf:"bytes,3,opt,name=userName,proto3" json:"userName,omitempty"` ProcessPath string `protobuf:"bytes,4,opt,name=processPath,proto3" json:"processPath,omitempty"` PackageNames []string `protobuf:"bytes,5,rep,name=packageNames,proto3" json:"packageNames,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ProcessInfo) Reset() { *x = ProcessInfo{} mi := &file_daemon_started_service_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *ProcessInfo) String() string { return protoimpl.X.MessageStringOf(x) } func (*ProcessInfo) ProtoMessage() {} func (x *ProcessInfo) ProtoReflect() protoreflect.Message { mi := &file_daemon_started_service_proto_msgTypes[21] 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 ProcessInfo.ProtoReflect.Descriptor instead. func (*ProcessInfo) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{21} } func (x *ProcessInfo) GetProcessId() uint32 { if x != nil { return x.ProcessId } return 0 } func (x *ProcessInfo) GetUserId() int32 { if x != nil { return x.UserId } return 0 } func (x *ProcessInfo) GetUserName() string { if x != nil { return x.UserName } return "" } func (x *ProcessInfo) GetProcessPath() string { if x != nil { return x.ProcessPath } return "" } func (x *ProcessInfo) GetPackageNames() []string { if x != nil { return x.PackageNames } return nil } type CloseConnectionRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CloseConnectionRequest) Reset() { *x = CloseConnectionRequest{} mi := &file_daemon_started_service_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *CloseConnectionRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*CloseConnectionRequest) ProtoMessage() {} func (x *CloseConnectionRequest) ProtoReflect() protoreflect.Message { mi := &file_daemon_started_service_proto_msgTypes[22] 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 CloseConnectionRequest.ProtoReflect.Descriptor instead. func (*CloseConnectionRequest) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{22} } func (x *CloseConnectionRequest) GetId() string { if x != nil { return x.Id } return "" } type DeprecatedWarnings struct { state protoimpl.MessageState `protogen:"open.v1"` Warnings []*DeprecatedWarning `protobuf:"bytes,1,rep,name=warnings,proto3" json:"warnings,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *DeprecatedWarnings) Reset() { *x = DeprecatedWarnings{} mi := &file_daemon_started_service_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *DeprecatedWarnings) String() string { return protoimpl.X.MessageStringOf(x) } func (*DeprecatedWarnings) ProtoMessage() {} func (x *DeprecatedWarnings) ProtoReflect() protoreflect.Message { mi := &file_daemon_started_service_proto_msgTypes[23] 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 DeprecatedWarnings.ProtoReflect.Descriptor instead. func (*DeprecatedWarnings) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{23} } func (x *DeprecatedWarnings) GetWarnings() []*DeprecatedWarning { if x != nil { return x.Warnings } return nil } 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"` 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() { *x = DeprecatedWarning{} mi := &file_daemon_started_service_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *DeprecatedWarning) String() string { return protoimpl.X.MessageStringOf(x) } func (*DeprecatedWarning) ProtoMessage() {} func (x *DeprecatedWarning) ProtoReflect() protoreflect.Message { mi := &file_daemon_started_service_proto_msgTypes[24] 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 DeprecatedWarning.ProtoReflect.Descriptor instead. func (*DeprecatedWarning) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{24} } func (x *DeprecatedWarning) GetMessage() string { if x != nil { return x.Message } return "" } func (x *DeprecatedWarning) GetImpending() bool { if x != nil { return x.Impending } return false } func (x *DeprecatedWarning) GetMigrationLink() string { if x != nil { return x.MigrationLink } 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"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *StartedAt) Reset() { *x = StartedAt{} mi := &file_daemon_started_service_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *StartedAt) String() string { return protoimpl.X.MessageStringOf(x) } func (*StartedAt) ProtoMessage() {} func (x *StartedAt) ProtoReflect() protoreflect.Message { mi := &file_daemon_started_service_proto_msgTypes[25] 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 StartedAt.ProtoReflect.Descriptor instead. func (*StartedAt) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{25} } func (x *StartedAt) GetStartedAt() int64 { if x != nil { return x.StartedAt } 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 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 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"` Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Log_Message) Reset() { *x = Log_Message{} mi := &file_daemon_started_service_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Log_Message) String() string { return protoimpl.X.MessageStringOf(x) } func (*Log_Message) ProtoMessage() {} func (x *Log_Message) ProtoReflect() protoreflect.Message { mi := &file_daemon_started_service_proto_msgTypes[37] 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 Log_Message.ProtoReflect.Descriptor instead. func (*Log_Message) Descriptor() ([]byte, []int) { return file_daemon_started_service_proto_rawDescGZIP(), []int{3, 0} } func (x *Log_Message) GetLevel() LogLevel { if x != nil { return x.Level } return LogLevel_PANIC } func (x *Log_Message) GetMessage() string { if x != nil { return x.Message } return "" } var File_daemon_started_service_proto protoreflect.FileDescriptor const file_daemon_started_service_proto_rawDesc = "" + "\n" + "\x1cdaemon/started_service.proto\x12\x06daemon\x1a\x1bgoogle/protobuf/empty.proto\"\xad\x01\n" + "\rServiceStatus\x122\n" + "\x06status\x18\x01 \x01(\x0e2\x1a.daemon.ServiceStatus.TypeR\x06status\x12\"\n" + "\ferrorMessage\x18\x02 \x01(\tR\ferrorMessage\"D\n" + "\x04Type\x12\b\n" + "\x04IDLE\x10\x00\x12\f\n" + "\bSTARTING\x10\x01\x12\v\n" + "\aSTARTED\x10\x02\x12\f\n" + "\bSTOPPING\x10\x03\x12\t\n" + "\x05FATAL\x10\x04\"D\n" + "\x14ReloadServiceRequest\x12,\n" + "\x11newProfileContent\x18\x01 \x01(\tR\x11newProfileContent\"4\n" + "\x16SubscribeStatusRequest\x12\x1a\n" + "\binterval\x18\x01 \x01(\x03R\binterval\"\x99\x01\n" + "\x03Log\x12/\n" + "\bmessages\x18\x01 \x03(\v2\x13.daemon.Log.MessageR\bmessages\x12\x14\n" + "\x05reset\x18\x02 \x01(\bR\x05reset\x1aK\n" + "\aMessage\x12&\n" + "\x05level\x18\x01 \x01(\x0e2\x10.daemon.LogLevelR\x05level\x12\x18\n" + "\amessage\x18\x02 \x01(\tR\amessage\"9\n" + "\x0fDefaultLogLevel\x12&\n" + "\x05level\x18\x01 \x01(\x0e2\x10.daemon.LogLevelR\x05level\"\xb6\x02\n" + "\x06Status\x12\x16\n" + "\x06memory\x18\x01 \x01(\x04R\x06memory\x12\x1e\n" + "\n" + "goroutines\x18\x02 \x01(\x05R\n" + "goroutines\x12$\n" + "\rconnectionsIn\x18\x03 \x01(\x05R\rconnectionsIn\x12&\n" + "\x0econnectionsOut\x18\x04 \x01(\x05R\x0econnectionsOut\x12*\n" + "\x10trafficAvailable\x18\x05 \x01(\bR\x10trafficAvailable\x12\x16\n" + "\x06uplink\x18\x06 \x01(\x03R\x06uplink\x12\x1a\n" + "\bdownlink\x18\a \x01(\x03R\bdownlink\x12 \n" + "\vuplinkTotal\x18\b \x01(\x03R\vuplinkTotal\x12$\n" + "\rdownlinkTotal\x18\t \x01(\x03R\rdownlinkTotal\"-\n" + "\x06Groups\x12#\n" + "\x05group\x18\x01 \x03(\v2\r.daemon.GroupR\x05group\"\xae\x01\n" + "\x05Group\x12\x10\n" + "\x03tag\x18\x01 \x01(\tR\x03tag\x12\x12\n" + "\x04type\x18\x02 \x01(\tR\x04type\x12\x1e\n" + "\n" + "selectable\x18\x03 \x01(\bR\n" + "selectable\x12\x1a\n" + "\bselected\x18\x04 \x01(\tR\bselected\x12\x1a\n" + "\bisExpand\x18\x05 \x01(\bR\bisExpand\x12'\n" + "\x05items\x18\x06 \x03(\v2\x11.daemon.GroupItemR\x05items\"w\n" + "\tGroupItem\x12\x10\n" + "\x03tag\x18\x01 \x01(\tR\x03tag\x12\x12\n" + "\x04type\x18\x02 \x01(\tR\x04type\x12 \n" + "\vurlTestTime\x18\x03 \x01(\x03R\vurlTestTime\x12\"\n" + "\furlTestDelay\x18\x04 \x01(\x05R\furlTestDelay\"2\n" + "\x0eURLTestRequest\x12 \n" + "\voutboundTag\x18\x01 \x01(\tR\voutboundTag\"U\n" + "\x15SelectOutboundRequest\x12\x1a\n" + "\bgroupTag\x18\x01 \x01(\tR\bgroupTag\x12 \n" + "\voutboundTag\x18\x02 \x01(\tR\voutboundTag\"O\n" + "\x15SetGroupExpandRequest\x12\x1a\n" + "\bgroupTag\x18\x01 \x01(\tR\bgroupTag\x12\x1a\n" + "\bisExpand\x18\x02 \x01(\bR\bisExpand\"\x1f\n" + "\tClashMode\x12\x12\n" + "\x04mode\x18\x03 \x01(\tR\x04mode\"O\n" + "\x0fClashModeStatus\x12\x1a\n" + "\bmodeList\x18\x01 \x03(\tR\bmodeList\x12 \n" + "\vcurrentMode\x18\x02 \x01(\tR\vcurrentMode\"K\n" + "\x11SystemProxyStatus\x12\x1c\n" + "\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\"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" + "\x04type\x18\x01 \x01(\x0e2\x1b.daemon.ConnectionEventTypeR\x04type\x12\x0e\n" + "\x02id\x18\x02 \x01(\tR\x02id\x122\n" + "\n" + "connection\x18\x03 \x01(\v2\x12.daemon.ConnectionR\n" + "connection\x12 \n" + "\vuplinkDelta\x18\x04 \x01(\x03R\vuplinkDelta\x12$\n" + "\rdownlinkDelta\x18\x05 \x01(\x03R\rdownlinkDelta\x12\x1a\n" + "\bclosedAt\x18\x06 \x01(\x03R\bclosedAt\"Y\n" + "\x10ConnectionEvents\x12/\n" + "\x06events\x18\x01 \x03(\v2\x17.daemon.ConnectionEventR\x06events\x12\x14\n" + "\x05reset\x18\x02 \x01(\bR\x05reset\"\x95\x05\n" + "\n" + "Connection\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x18\n" + "\ainbound\x18\x02 \x01(\tR\ainbound\x12 \n" + "\vinboundType\x18\x03 \x01(\tR\vinboundType\x12\x1c\n" + "\tipVersion\x18\x04 \x01(\x05R\tipVersion\x12\x18\n" + "\anetwork\x18\x05 \x01(\tR\anetwork\x12\x16\n" + "\x06source\x18\x06 \x01(\tR\x06source\x12 \n" + "\vdestination\x18\a \x01(\tR\vdestination\x12\x16\n" + "\x06domain\x18\b \x01(\tR\x06domain\x12\x1a\n" + "\bprotocol\x18\t \x01(\tR\bprotocol\x12\x12\n" + "\x04user\x18\n" + " \x01(\tR\x04user\x12\"\n" + "\ffromOutbound\x18\v \x01(\tR\ffromOutbound\x12\x1c\n" + "\tcreatedAt\x18\f \x01(\x03R\tcreatedAt\x12\x1a\n" + "\bclosedAt\x18\r \x01(\x03R\bclosedAt\x12\x16\n" + "\x06uplink\x18\x0e \x01(\x03R\x06uplink\x12\x1a\n" + "\bdownlink\x18\x0f \x01(\x03R\bdownlink\x12 \n" + "\vuplinkTotal\x18\x10 \x01(\x03R\vuplinkTotal\x12$\n" + "\rdownlinkTotal\x18\x11 \x01(\x03R\rdownlinkTotal\x12\x12\n" + "\x04rule\x18\x12 \x01(\tR\x04rule\x12\x1a\n" + "\boutbound\x18\x13 \x01(\tR\boutbound\x12\"\n" + "\foutboundType\x18\x14 \x01(\tR\foutboundType\x12\x1c\n" + "\tchainList\x18\x15 \x03(\tR\tchainList\x125\n" + "\vprocessInfo\x18\x16 \x01(\v2\x13.daemon.ProcessInfoR\vprocessInfo\"\xa5\x01\n" + "\vProcessInfo\x12\x1c\n" + "\tprocessId\x18\x01 \x01(\rR\tprocessId\x12\x16\n" + "\x06userId\x18\x02 \x01(\x05R\x06userId\x12\x1a\n" + "\buserName\x18\x03 \x01(\tR\buserName\x12 \n" + "\vprocessPath\x18\x04 \x01(\tR\vprocessPath\x12\"\n" + "\fpackageNames\x18\x05 \x03(\tR\fpackageNames\"(\n" + "\x16CloseConnectionRequest\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\"K\n" + "\x12DeprecatedWarnings\x125\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\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\"?\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\"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\"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" + "\x05ERROR\x10\x02\x12\b\n" + "\x04WARN\x10\x03\x12\b\n" + "\x04INFO\x10\x04\x12\t\n" + "\x05DEBUG\x10\x05\x12\t\n" + "\x05TRACE\x10\x06*i\n" + "\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\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" + "\x16SubscribeServiceStatus\x12\x16.google.protobuf.Empty\x1a\x15.daemon.ServiceStatus\"\x000\x01\x127\n" + "\fSubscribeLog\x12\x16.google.protobuf.Empty\x1a\v.daemon.Log\"\x000\x01\x12G\n" + "\x12GetDefaultLogLevel\x12\x16.google.protobuf.Empty\x1a\x17.daemon.DefaultLogLevel\"\x00\x12=\n" + "\tClearLogs\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12E\n" + "\x0fSubscribeStatus\x12\x1e.daemon.SubscribeStatusRequest\x1a\x0e.daemon.Status\"\x000\x01\x12=\n" + "\x0fSubscribeGroups\x12\x16.google.protobuf.Empty\x1a\x0e.daemon.Groups\"\x000\x01\x12G\n" + "\x12GetClashModeStatus\x12\x16.google.protobuf.Empty\x1a\x17.daemon.ClashModeStatus\"\x00\x12C\n" + "\x12SubscribeClashMode\x12\x16.google.protobuf.Empty\x1a\x11.daemon.ClashMode\"\x000\x01\x12;\n" + "\fSetClashMode\x12\x11.daemon.ClashMode\x1a\x16.google.protobuf.Empty\"\x00\x12;\n" + "\aURLTest\x12\x16.daemon.URLTestRequest\x1a\x16.google.protobuf.Empty\"\x00\x12I\n" + "\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\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" + "\x15GetDeprecatedWarnings\x12\x16.google.protobuf.Empty\x1a\x1a.daemon.DeprecatedWarnings\"\x00\x12;\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\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 file_daemon_started_service_proto_rawDescData []byte ) func file_daemon_started_service_proto_rawDescGZIP() []byte { file_daemon_started_service_proto_rawDescOnce.Do(func() { file_daemon_started_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_daemon_started_service_proto_rawDesc), len(file_daemon_started_service_proto_rawDesc))) }) return file_daemon_started_service_proto_rawDescData } var ( file_daemon_started_service_proto_enumTypes = make([]protoimpl.EnumInfo, 4) 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 (ServiceStatus_Type)(0), // 2: daemon.ServiceStatus.Type (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 (*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 (*TailscaleStatusUpdate)(nil), // 35: daemon.TailscaleStatusUpdate (*TailscaleEndpointStatus)(nil), // 36: daemon.TailscaleEndpointStatus (*TailscaleUserGroup)(nil), // 37: daemon.TailscaleUserGroup (*TailscalePeer)(nil), // 38: daemon.TailscalePeer (*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 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 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 12, // 11: daemon.OutboundList.outbounds:type_name -> daemon.GroupItem 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 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 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 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 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 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 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 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 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 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.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 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() } func file_daemon_started_service_proto_init() { if File_daemon_started_service_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ 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: 4, NumMessages: 38, NumExtensions: 0, NumServices: 1, }, GoTypes: file_daemon_started_service_proto_goTypes, DependencyIndexes: file_daemon_started_service_proto_depIdxs, EnumInfos: file_daemon_started_service_proto_enumTypes, MessageInfos: file_daemon_started_service_proto_msgTypes, }.Build() File_daemon_started_service_proto = out.File file_daemon_started_service_proto_goTypes = nil file_daemon_started_service_proto_depIdxs = nil } ================================================ FILE: daemon/started_service.proto ================================================ syntax = "proto3"; package daemon; option go_package = "github.com/sagernet/sing-box/daemon"; import "google/protobuf/empty.proto"; service StartedService { rpc StopService(google.protobuf.Empty) returns (google.protobuf.Empty); rpc ReloadService(google.protobuf.Empty) returns (google.protobuf.Empty); rpc SubscribeServiceStatus(google.protobuf.Empty) returns(stream ServiceStatus) {} rpc SubscribeLog(google.protobuf.Empty) returns(stream Log) {} rpc GetDefaultLogLevel(google.protobuf.Empty) returns(DefaultLogLevel) {} rpc ClearLogs(google.protobuf.Empty) returns(google.protobuf.Empty) {} rpc SubscribeStatus(SubscribeStatusRequest) returns(stream Status) {} rpc SubscribeGroups(google.protobuf.Empty) returns(stream Groups) {} rpc GetClashModeStatus(google.protobuf.Empty) returns(ClashModeStatus) {} rpc SubscribeClashMode(google.protobuf.Empty) returns(stream ClashMode) {} rpc SetClashMode(ClashMode) returns(google.protobuf.Empty) {} rpc URLTest(URLTestRequest) returns(google.protobuf.Empty) {} rpc SelectOutbound(SelectOutboundRequest) returns (google.protobuf.Empty) {} rpc SetGroupExpand(SetGroupExpandRequest) returns (google.protobuf.Empty) {} 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) {} rpc CloseAllConnections(google.protobuf.Empty) returns(google.protobuf.Empty) {} rpc GetDeprecatedWarnings(google.protobuf.Empty) returns(DeprecatedWarnings) {} rpc GetStartedAt(google.protobuf.Empty) returns(StartedAt) {} 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 { enum Type { IDLE = 0; STARTING = 1; STARTED = 2; STOPPING = 3; FATAL = 4; } Type status = 1; string errorMessage = 2; } message ReloadServiceRequest { string newProfileContent = 1; } message SubscribeStatusRequest { int64 interval = 1; } enum LogLevel { PANIC = 0; FATAL = 1; ERROR = 2; WARN = 3; INFO = 4; DEBUG = 5; TRACE = 6; } message Log { repeated Message messages = 1; bool reset = 2; message Message { LogLevel level = 1; string message = 2; } } message DefaultLogLevel { LogLevel level = 1; } message Status { uint64 memory = 1; int32 goroutines = 2; int32 connectionsIn = 3; int32 connectionsOut = 4; bool trafficAvailable = 5; int64 uplink = 6; int64 downlink = 7; int64 uplinkTotal = 8; int64 downlinkTotal = 9; } message Groups { repeated Group group = 1; } message Group { string tag = 1; string type = 2; bool selectable = 3; string selected = 4; bool isExpand = 5; repeated GroupItem items = 6; } message GroupItem { string tag = 1; string type = 2; int64 urlTestTime = 3; int32 urlTestDelay = 4; } message URLTestRequest { string outboundTag = 1; } message SelectOutboundRequest { string groupTag = 1; string outboundTag = 2; } message SetGroupExpandRequest { string groupTag = 1; bool isExpand = 2; } message ClashMode { string mode = 3; } message ClashModeStatus { repeated string modeList = 1; string currentMode = 2; } message SystemProxyStatus { bool available = 1; bool enabled = 2; } message SetSystemProxyEnabledRequest { bool enabled = 1; } message DebugCrashRequest { enum Type { GO = 0; NATIVE = 1; } Type type = 1; } message SubscribeConnectionsRequest { int64 interval = 1; } enum ConnectionEventType { CONNECTION_EVENT_NEW = 0; CONNECTION_EVENT_UPDATE = 1; CONNECTION_EVENT_CLOSED = 2; } message ConnectionEvent { ConnectionEventType type = 1; string id = 2; Connection connection = 3; int64 uplinkDelta = 4; int64 downlinkDelta = 5; int64 closedAt = 6; } message ConnectionEvents { repeated ConnectionEvent events = 1; bool reset = 2; } message Connection { string id = 1; string inbound = 2; string inboundType = 3; int32 ipVersion = 4; string network = 5; string source = 6; string destination = 7; string domain = 8; string protocol = 9; string user = 10; string fromOutbound = 11; int64 createdAt = 12; int64 closedAt = 13; int64 uplink = 14; int64 downlink = 15; int64 uplinkTotal = 16; int64 downlinkTotal = 17; string rule = 18; string outbound = 19; string outboundType = 20; repeated string chainList = 21; ProcessInfo processInfo = 22; } message ProcessInfo { uint32 processId = 1; int32 userId = 2; string userName = 3; string processPath = 4; repeated string packageNames = 5; } message CloseConnectionRequest { string id = 1; } message DeprecatedWarnings { repeated DeprecatedWarning warnings = 1; } message DeprecatedWarning { string message = 1; bool impending = 2; string migrationLink = 3; string description = 4; string deprecatedVersion = 5; string scheduledVersion = 6; } 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; } 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; } 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; } ================================================ FILE: daemon/started_service_grpc.pb.go ================================================ package daemon import ( context "context" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" emptypb "google.golang.org/protobuf/types/known/emptypb" ) // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. // Requires gRPC-Go v1.64.0 or later. 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_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. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type StartedServiceClient interface { StopService(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) ReloadService(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) SubscribeServiceStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ServiceStatus], error) SubscribeLog(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Log], error) GetDefaultLogLevel(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DefaultLogLevel, error) ClearLogs(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) SubscribeStatus(ctx context.Context, in *SubscribeStatusRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Status], error) SubscribeGroups(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Groups], error) GetClashModeStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ClashModeStatus, error) SubscribeClashMode(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ClashMode], error) SetClashMode(ctx context.Context, in *ClashMode, opts ...grpc.CallOption) (*emptypb.Empty, error) URLTest(ctx context.Context, in *URLTestRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) SelectOutbound(ctx context.Context, in *SelectOutboundRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) 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) GetDeprecatedWarnings(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DeprecatedWarnings, error) GetStartedAt(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StartedAt, 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 { cc grpc.ClientConnInterface } func NewStartedServiceClient(cc grpc.ClientConnInterface) StartedServiceClient { return &startedServiceClient{cc} } func (c *startedServiceClient) StopService(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_StopService_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *startedServiceClient) ReloadService(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_ReloadService_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *startedServiceClient) SubscribeServiceStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ServiceStatus], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[0], StartedService_SubscribeServiceStatus_FullMethodName, cOpts...) if err != nil { return nil, err } x := &grpc.GenericClientStream[emptypb.Empty, ServiceStatus]{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_SubscribeServiceStatusClient = grpc.ServerStreamingClient[ServiceStatus] func (c *startedServiceClient) SubscribeLog(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Log], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[1], StartedService_SubscribeLog_FullMethodName, cOpts...) if err != nil { return nil, err } x := &grpc.GenericClientStream[emptypb.Empty, Log]{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_SubscribeLogClient = grpc.ServerStreamingClient[Log] func (c *startedServiceClient) GetDefaultLogLevel(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DefaultLogLevel, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(DefaultLogLevel) err := c.cc.Invoke(ctx, StartedService_GetDefaultLogLevel_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *startedServiceClient) ClearLogs(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_ClearLogs_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *startedServiceClient) SubscribeStatus(ctx context.Context, in *SubscribeStatusRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Status], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[2], StartedService_SubscribeStatus_FullMethodName, cOpts...) if err != nil { return nil, err } x := &grpc.GenericClientStream[SubscribeStatusRequest, Status]{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_SubscribeStatusClient = grpc.ServerStreamingClient[Status] func (c *startedServiceClient) SubscribeGroups(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Groups], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[3], StartedService_SubscribeGroups_FullMethodName, cOpts...) if err != nil { return nil, err } x := &grpc.GenericClientStream[emptypb.Empty, Groups]{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_SubscribeGroupsClient = grpc.ServerStreamingClient[Groups] func (c *startedServiceClient) GetClashModeStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ClashModeStatus, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ClashModeStatus) err := c.cc.Invoke(ctx, StartedService_GetClashModeStatus_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *startedServiceClient) SubscribeClashMode(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ClashMode], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[4], StartedService_SubscribeClashMode_FullMethodName, cOpts...) if err != nil { return nil, err } x := &grpc.GenericClientStream[emptypb.Empty, ClashMode]{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_SubscribeClashModeClient = grpc.ServerStreamingClient[ClashMode] func (c *startedServiceClient) SetClashMode(ctx context.Context, in *ClashMode, opts ...grpc.CallOption) (*emptypb.Empty, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(emptypb.Empty) err := c.cc.Invoke(ctx, StartedService_SetClashMode_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *startedServiceClient) URLTest(ctx context.Context, in *URLTestRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(emptypb.Empty) err := c.cc.Invoke(ctx, StartedService_URLTest_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *startedServiceClient) SelectOutbound(ctx context.Context, in *SelectOutboundRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(emptypb.Empty) err := c.cc.Invoke(ctx, StartedService_SelectOutbound_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *startedServiceClient) SetGroupExpand(ctx context.Context, in *SetGroupExpandRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(emptypb.Empty) err := c.cc.Invoke(ctx, StartedService_SetGroupExpand_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *startedServiceClient) GetSystemProxyStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SystemProxyStatus, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(SystemProxyStatus) err := c.cc.Invoke(ctx, StartedService_GetSystemProxyStatus_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *startedServiceClient) SetSystemProxyEnabled(ctx context.Context, in *SetSystemProxyEnabledRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(emptypb.Empty) err := c.cc.Invoke(ctx, StartedService_SetSystemProxyEnabled_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } 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...) if err != nil { return nil, err } x := &grpc.GenericClientStream[SubscribeConnectionsRequest, ConnectionEvents]{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_SubscribeConnectionsClient = grpc.ServerStreamingClient[ConnectionEvents] func (c *startedServiceClient) CloseConnection(ctx context.Context, in *CloseConnectionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(emptypb.Empty) err := c.cc.Invoke(ctx, StartedService_CloseConnection_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *startedServiceClient) CloseAllConnections(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_CloseAllConnections_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *startedServiceClient) GetDeprecatedWarnings(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DeprecatedWarnings, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(DeprecatedWarnings) err := c.cc.Invoke(ctx, StartedService_GetDeprecatedWarnings_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *startedServiceClient) GetStartedAt(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StartedAt, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(StartedAt) err := c.cc.Invoke(ctx, StartedService_GetStartedAt_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] 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] 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. type StartedServiceServer interface { StopService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) ReloadService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) SubscribeServiceStatus(*emptypb.Empty, grpc.ServerStreamingServer[ServiceStatus]) error SubscribeLog(*emptypb.Empty, grpc.ServerStreamingServer[Log]) error GetDefaultLogLevel(context.Context, *emptypb.Empty) (*DefaultLogLevel, error) ClearLogs(context.Context, *emptypb.Empty) (*emptypb.Empty, error) SubscribeStatus(*SubscribeStatusRequest, grpc.ServerStreamingServer[Status]) error SubscribeGroups(*emptypb.Empty, grpc.ServerStreamingServer[Groups]) error GetClashModeStatus(context.Context, *emptypb.Empty) (*ClashModeStatus, error) SubscribeClashMode(*emptypb.Empty, grpc.ServerStreamingServer[ClashMode]) error SetClashMode(context.Context, *ClashMode) (*emptypb.Empty, error) URLTest(context.Context, *URLTestRequest) (*emptypb.Empty, error) SelectOutbound(context.Context, *SelectOutboundRequest) (*emptypb.Empty, error) 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) GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error) GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, 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() } // UnimplementedStartedServiceServer must be embedded to have // forward compatible implementations. // // NOTE: this should be embedded by value instead of pointer to avoid a nil // pointer dereference when methods are called. type UnimplementedStartedServiceServer struct{} func (UnimplementedStartedServiceServer) StopService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { return nil, status.Error(codes.Unimplemented, "method StopService not implemented") } func (UnimplementedStartedServiceServer) ReloadService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { return nil, status.Error(codes.Unimplemented, "method ReloadService not implemented") } func (UnimplementedStartedServiceServer) SubscribeServiceStatus(*emptypb.Empty, grpc.ServerStreamingServer[ServiceStatus]) error { return status.Error(codes.Unimplemented, "method SubscribeServiceStatus not implemented") } func (UnimplementedStartedServiceServer) SubscribeLog(*emptypb.Empty, grpc.ServerStreamingServer[Log]) error { return status.Error(codes.Unimplemented, "method SubscribeLog not implemented") } func (UnimplementedStartedServiceServer) GetDefaultLogLevel(context.Context, *emptypb.Empty) (*DefaultLogLevel, error) { return nil, status.Error(codes.Unimplemented, "method GetDefaultLogLevel not implemented") } func (UnimplementedStartedServiceServer) ClearLogs(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { return nil, status.Error(codes.Unimplemented, "method ClearLogs not implemented") } func (UnimplementedStartedServiceServer) SubscribeStatus(*SubscribeStatusRequest, grpc.ServerStreamingServer[Status]) error { return status.Error(codes.Unimplemented, "method SubscribeStatus not implemented") } func (UnimplementedStartedServiceServer) SubscribeGroups(*emptypb.Empty, grpc.ServerStreamingServer[Groups]) error { return status.Error(codes.Unimplemented, "method SubscribeGroups not implemented") } func (UnimplementedStartedServiceServer) GetClashModeStatus(context.Context, *emptypb.Empty) (*ClashModeStatus, error) { return nil, status.Error(codes.Unimplemented, "method GetClashModeStatus not implemented") } func (UnimplementedStartedServiceServer) SubscribeClashMode(*emptypb.Empty, grpc.ServerStreamingServer[ClashMode]) error { return status.Error(codes.Unimplemented, "method SubscribeClashMode not implemented") } func (UnimplementedStartedServiceServer) SetClashMode(context.Context, *ClashMode) (*emptypb.Empty, error) { return nil, status.Error(codes.Unimplemented, "method SetClashMode not implemented") } func (UnimplementedStartedServiceServer) URLTest(context.Context, *URLTestRequest) (*emptypb.Empty, error) { return nil, status.Error(codes.Unimplemented, "method URLTest not implemented") } func (UnimplementedStartedServiceServer) SelectOutbound(context.Context, *SelectOutboundRequest) (*emptypb.Empty, error) { return nil, status.Error(codes.Unimplemented, "method SelectOutbound not implemented") } func (UnimplementedStartedServiceServer) SetGroupExpand(context.Context, *SetGroupExpandRequest) (*emptypb.Empty, error) { return nil, status.Error(codes.Unimplemented, "method SetGroupExpand not implemented") } func (UnimplementedStartedServiceServer) GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error) { return nil, status.Error(codes.Unimplemented, "method GetSystemProxyStatus not implemented") } func (UnimplementedStartedServiceServer) SetSystemProxyEnabled(context.Context, *SetSystemProxyEnabledRequest) (*emptypb.Empty, error) { 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") } func (UnimplementedStartedServiceServer) CloseConnection(context.Context, *CloseConnectionRequest) (*emptypb.Empty, error) { return nil, status.Error(codes.Unimplemented, "method CloseConnection not implemented") } func (UnimplementedStartedServiceServer) CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { return nil, status.Error(codes.Unimplemented, "method CloseAllConnections not implemented") } func (UnimplementedStartedServiceServer) GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error) { return nil, status.Error(codes.Unimplemented, "method GetDeprecatedWarnings not implemented") } func (UnimplementedStartedServiceServer) GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, error) { return nil, status.Error(codes.Unimplemented, "method GetStartedAt 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) 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() {} // UnsafeStartedServiceServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to StartedServiceServer will // result in compilation errors. type UnsafeStartedServiceServer interface { mustEmbedUnimplementedStartedServiceServer() } func RegisterStartedServiceServer(s grpc.ServiceRegistrar, srv StartedServiceServer) { // If the following call panics, it indicates UnimplementedStartedServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { t.testEmbeddedByValue() } s.RegisterService(&StartedService_ServiceDesc, srv) } func _StartedService_StopService_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).StopService(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: StartedService_StopService_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(StartedServiceServer).StopService(ctx, req.(*emptypb.Empty)) } return interceptor(ctx, in, info, handler) } func _StartedService_ReloadService_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).ReloadService(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: StartedService_ReloadService_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(StartedServiceServer).ReloadService(ctx, req.(*emptypb.Empty)) } return interceptor(ctx, in, info, handler) } func _StartedService_SubscribeServiceStatus_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(emptypb.Empty) if err := stream.RecvMsg(m); err != nil { return err } return srv.(StartedServiceServer).SubscribeServiceStatus(m, &grpc.GenericServerStream[emptypb.Empty, ServiceStatus]{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_SubscribeServiceStatusServer = grpc.ServerStreamingServer[ServiceStatus] func _StartedService_SubscribeLog_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(emptypb.Empty) if err := stream.RecvMsg(m); err != nil { return err } return srv.(StartedServiceServer).SubscribeLog(m, &grpc.GenericServerStream[emptypb.Empty, Log]{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_SubscribeLogServer = grpc.ServerStreamingServer[Log] func _StartedService_GetDefaultLogLevel_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).GetDefaultLogLevel(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: StartedService_GetDefaultLogLevel_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(StartedServiceServer).GetDefaultLogLevel(ctx, req.(*emptypb.Empty)) } return interceptor(ctx, in, info, handler) } func _StartedService_ClearLogs_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).ClearLogs(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: StartedService_ClearLogs_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(StartedServiceServer).ClearLogs(ctx, req.(*emptypb.Empty)) } return interceptor(ctx, in, info, handler) } func _StartedService_SubscribeStatus_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(SubscribeStatusRequest) if err := stream.RecvMsg(m); err != nil { return err } return srv.(StartedServiceServer).SubscribeStatus(m, &grpc.GenericServerStream[SubscribeStatusRequest, Status]{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_SubscribeStatusServer = grpc.ServerStreamingServer[Status] func _StartedService_SubscribeGroups_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(emptypb.Empty) if err := stream.RecvMsg(m); err != nil { return err } return srv.(StartedServiceServer).SubscribeGroups(m, &grpc.GenericServerStream[emptypb.Empty, Groups]{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_SubscribeGroupsServer = grpc.ServerStreamingServer[Groups] func _StartedService_GetClashModeStatus_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).GetClashModeStatus(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: StartedService_GetClashModeStatus_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(StartedServiceServer).GetClashModeStatus(ctx, req.(*emptypb.Empty)) } return interceptor(ctx, in, info, handler) } func _StartedService_SubscribeClashMode_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(emptypb.Empty) if err := stream.RecvMsg(m); err != nil { return err } return srv.(StartedServiceServer).SubscribeClashMode(m, &grpc.GenericServerStream[emptypb.Empty, ClashMode]{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_SubscribeClashModeServer = grpc.ServerStreamingServer[ClashMode] func _StartedService_SetClashMode_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ClashMode) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(StartedServiceServer).SetClashMode(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: StartedService_SetClashMode_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(StartedServiceServer).SetClashMode(ctx, req.(*ClashMode)) } return interceptor(ctx, in, info, handler) } func _StartedService_URLTest_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(URLTestRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(StartedServiceServer).URLTest(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: StartedService_URLTest_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(StartedServiceServer).URLTest(ctx, req.(*URLTestRequest)) } return interceptor(ctx, in, info, handler) } func _StartedService_SelectOutbound_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(SelectOutboundRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(StartedServiceServer).SelectOutbound(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: StartedService_SelectOutbound_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(StartedServiceServer).SelectOutbound(ctx, req.(*SelectOutboundRequest)) } return interceptor(ctx, in, info, handler) } func _StartedService_SetGroupExpand_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(SetGroupExpandRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(StartedServiceServer).SetGroupExpand(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: StartedService_SetGroupExpand_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(StartedServiceServer).SetGroupExpand(ctx, req.(*SetGroupExpandRequest)) } return interceptor(ctx, in, info, handler) } func _StartedService_GetSystemProxyStatus_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).GetSystemProxyStatus(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: StartedService_GetSystemProxyStatus_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(StartedServiceServer).GetSystemProxyStatus(ctx, req.(*emptypb.Empty)) } return interceptor(ctx, in, info, handler) } func _StartedService_SetSystemProxyEnabled_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(SetSystemProxyEnabledRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(StartedServiceServer).SetSystemProxyEnabled(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: StartedService_SetSystemProxyEnabled_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(StartedServiceServer).SetSystemProxyEnabled(ctx, req.(*SetSystemProxyEnabledRequest)) } 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 { return err } return srv.(StartedServiceServer).SubscribeConnections(m, &grpc.GenericServerStream[SubscribeConnectionsRequest, ConnectionEvents]{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_SubscribeConnectionsServer = grpc.ServerStreamingServer[ConnectionEvents] func _StartedService_CloseConnection_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(CloseConnectionRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(StartedServiceServer).CloseConnection(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: StartedService_CloseConnection_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(StartedServiceServer).CloseConnection(ctx, req.(*CloseConnectionRequest)) } return interceptor(ctx, in, info, handler) } func _StartedService_CloseAllConnections_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).CloseAllConnections(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: StartedService_CloseAllConnections_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(StartedServiceServer).CloseAllConnections(ctx, req.(*emptypb.Empty)) } return interceptor(ctx, in, info, handler) } func _StartedService_GetDeprecatedWarnings_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).GetDeprecatedWarnings(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: StartedService_GetDeprecatedWarnings_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(StartedServiceServer).GetDeprecatedWarnings(ctx, req.(*emptypb.Empty)) } return interceptor(ctx, in, info, handler) } func _StartedService_GetStartedAt_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).GetStartedAt(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: StartedService_GetStartedAt_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(StartedServiceServer).GetStartedAt(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] 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] 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) var StartedService_ServiceDesc = grpc.ServiceDesc{ ServiceName: "daemon.StartedService", HandlerType: (*StartedServiceServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "StopService", Handler: _StartedService_StopService_Handler, }, { MethodName: "ReloadService", Handler: _StartedService_ReloadService_Handler, }, { MethodName: "GetDefaultLogLevel", Handler: _StartedService_GetDefaultLogLevel_Handler, }, { MethodName: "ClearLogs", Handler: _StartedService_ClearLogs_Handler, }, { MethodName: "GetClashModeStatus", Handler: _StartedService_GetClashModeStatus_Handler, }, { MethodName: "SetClashMode", Handler: _StartedService_SetClashMode_Handler, }, { MethodName: "URLTest", Handler: _StartedService_URLTest_Handler, }, { MethodName: "SelectOutbound", Handler: _StartedService_SelectOutbound_Handler, }, { MethodName: "SetGroupExpand", Handler: _StartedService_SetGroupExpand_Handler, }, { MethodName: "GetSystemProxyStatus", Handler: _StartedService_GetSystemProxyStatus_Handler, }, { MethodName: "SetSystemProxyEnabled", Handler: _StartedService_SetSystemProxyEnabled_Handler, }, { MethodName: "TriggerDebugCrash", Handler: _StartedService_TriggerDebugCrash_Handler, }, { MethodName: "TriggerOOMReport", Handler: _StartedService_TriggerOOMReport_Handler, }, { MethodName: "CloseConnection", Handler: _StartedService_CloseConnection_Handler, }, { MethodName: "CloseAllConnections", Handler: _StartedService_CloseAllConnections_Handler, }, { MethodName: "GetDeprecatedWarnings", Handler: _StartedService_GetDeprecatedWarnings_Handler, }, { MethodName: "GetStartedAt", Handler: _StartedService_GetStartedAt_Handler, }, }, Streams: []grpc.StreamDesc{ { StreamName: "SubscribeServiceStatus", Handler: _StartedService_SubscribeServiceStatus_Handler, ServerStreams: true, }, { StreamName: "SubscribeLog", Handler: _StartedService_SubscribeLog_Handler, ServerStreams: true, }, { StreamName: "SubscribeStatus", Handler: _StartedService_SubscribeStatus_Handler, ServerStreams: true, }, { StreamName: "SubscribeGroups", Handler: _StartedService_SubscribeGroups_Handler, ServerStreams: true, }, { StreamName: "SubscribeClashMode", Handler: _StartedService_SubscribeClashMode_Handler, ServerStreams: true, }, { StreamName: "SubscribeConnections", 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, }, { StreamName: "SubscribeTailscaleStatus", Handler: _StartedService_SubscribeTailscaleStatus_Handler, ServerStreams: true, }, { StreamName: "StartTailscalePing", Handler: _StartedService_StartTailscalePing_Handler, ServerStreams: true, }, }, Metadata: "daemon/started_service.proto", } ================================================ FILE: debug.go ================================================ package box import ( "runtime/debug" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" ) func applyDebugOptions(options option.DebugOptions) error { applyDebugListenOption(options) if options.GCPercent != nil { debug.SetGCPercent(*options.GCPercent) } if options.MaxStack != nil { debug.SetMaxStack(*options.MaxStack) } if options.MaxThreads != nil { debug.SetMaxThreads(*options.MaxThreads) } if options.PanicOnFault != nil { debug.SetPanicOnFault(*options.PanicOnFault) } if options.TraceBack != "" { debug.SetTraceback(options.TraceBack) } if options.MemoryLimit.Value() != 0 { debug.SetMemoryLimit(int64(float64(options.MemoryLimit.Value()) / 1.5)) } if options.OOMKiller != nil { return E.New("legacy oom_killer in debug options is removed, use oom-killer service instead") } return nil } ================================================ FILE: debug_http.go ================================================ package box import ( "net/http" "net/http/pprof" "runtime" "runtime/debug" "strings" "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/json" "github.com/sagernet/sing/common/json/badjson" "github.com/go-chi/chi/v5" ) var debugHTTPServer *http.Server func applyDebugListenOption(options option.DebugOptions) { if debugHTTPServer != nil { debugHTTPServer.Close() debugHTTPServer = nil } if options.Listen == "" { return } r := chi.NewMux() r.Route("/debug", func(r chi.Router) { r.Get("/gc", func(writer http.ResponseWriter, request *http.Request) { writer.WriteHeader(http.StatusNoContent) go debug.FreeOSMemory() }) r.Get("/memory", func(writer http.ResponseWriter, request *http.Request) { var memStats runtime.MemStats runtime.ReadMemStats(&memStats) var memObject badjson.JSONObject memObject.Put("heap", byteformats.FormatMemoryBytes(memStats.HeapInuse)) memObject.Put("stack", byteformats.FormatMemoryBytes(memStats.StackInuse)) memObject.Put("idle", byteformats.FormatMemoryBytes(memStats.HeapIdle-memStats.HeapReleased)) memObject.Put("goroutines", runtime.NumGoroutine()) memObject.Put("rss", rusageMaxRSS()) encoder := json.NewEncoder(writer) encoder.SetIndent("", " ") encoder.Encode(&memObject) }) r.Route("/pprof", func(r chi.Router) { r.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { if !strings.HasSuffix(request.URL.Path, "/") { http.Redirect(writer, request, request.URL.Path+"/", http.StatusMovedPermanently) } else { pprof.Index(writer, request) } }) r.HandleFunc("/*", pprof.Index) r.HandleFunc("/cmdline", pprof.Cmdline) r.HandleFunc("/profile", pprof.Profile) r.HandleFunc("/symbol", pprof.Symbol) r.HandleFunc("/trace", pprof.Trace) }) }) debugHTTPServer = &http.Server{ Addr: options.Listen, Handler: r, } go func() { err := debugHTTPServer.ListenAndServe() if err != nil && !E.IsClosed(err) { log.Error(E.Cause(err, "serve debug HTTP server")) } }() } ================================================ FILE: debug_stub.go ================================================ //go:build !(linux || darwin) package box func rusageMaxRSS() float64 { return -1 } ================================================ FILE: debug_unix.go ================================================ //go:build linux || darwin package box import ( "runtime" "syscall" ) func rusageMaxRSS() float64 { ru := syscall.Rusage{} err := syscall.Getrusage(syscall.RUSAGE_SELF, &ru) if err != nil { return 0 } rss := float64(ru.Maxrss) if runtime.GOOS == "darwin" || runtime.GOOS == "ios" { rss /= 1 << 20 // ru_maxrss is bytes on darwin } else { // ru_maxrss is kilobytes elsewhere (linux, openbsd, etc) rss /= 1 << 10 } return rss } ================================================ FILE: dns/client.go ================================================ package dns import ( "context" "errors" "net" "net/netip" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/compatible" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/task" "github.com/sagernet/sing/contrab/freelru" "github.com/sagernet/sing/contrab/maphash" "github.com/miekg/dns" ) var ( ErrNoRawSupport = E.New("no raw query support by current transport") ErrNotCached = E.New("not cached") ErrResponseRejected = E.New("response rejected") ErrResponseRejectedCached = E.Extend(ErrResponseRejected, "cached") ) var _ adapter.DNSClient = (*Client)(nil) type Client 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 { 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 := max(options.CacheCapacity, 1024) client := &Client{ 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 } if !client.disableCache && client.initDNSCacheFunc == nil { client.initializeMemoryCache() } return client } type dnsCacheKey struct { dns.Question transportTag string } 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) { for _, record := range response.Ns { if soa, isSOA := record.(*dns.SOA); isSOA { soaTTL := soa.Header().Ttl soaMinimum := soa.Minttl if soaTTL < soaMinimum { return soaTTL, true } return soaMinimum, true } } 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 { c.logger.WarnContext(ctx, "bad question size: ", len(message.Question)) } return FixedResponseStatus(message, dns.RcodeFormatError), nil } question := message.Question[0] if question.Qtype == dns.TypeA && options.Strategy == C.DomainStrategyIPv6Only || question.Qtype == dns.TypeAAAA && options.Strategy == C.DomainStrategyIPv4Only { if c.logger != nil { c.logger.DebugContext(ctx, "strategy rejected") } return FixedResponseStatus(message, dns.RcodeSuccess), nil } message = c.prepareExchangeMessage(message, options) isSimpleRequest := len(message.Question) == 1 && len(message.Ns) == 0 && (len(message.Extra) == 0 || len(message.Extra) == 1 && message.Extra[0].Header().Rrtype == dns.TypeOPT && message.Extra[0].Header().Class > 0 && message.Extra[0].Header().Ttl == 0 && len(message.Extra[0].(*dns.OPT).Option) == 0) && !options.ClientSubnet.IsValid() disableCache := !isSimpleRequest || c.disableCache || options.DisableCache if !disableCache { 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, isStale := c.loadResponse(question, transport) if 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 } } } messageId := message.Id contextTransport, clientSubnetLoaded := transportTagFromContext(ctx) if clientSubnetLoaded && transport.Tag() == contextTransport { return nil, E.New("DNS query loopback in transport[", contextTransport, "]") } ctx = contextWithTransportTag(ctx, transport.Tag()) if !disableCache && responseChecker != nil && c.rdrc != nil { rejected := c.rdrc.LoadRDRC(transport.Tag(), question.Name, question.Qtype) if rejected { return nil, ErrResponseRejectedCached } } response, err := c.exchangeToTransport(ctx, transport, message, options.Timeout) if err != nil { return nil, err } disableCache = disableCache || (response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError) if responseChecker != nil { var rejected bool if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError { rejected = true } else { rejected = !responseChecker(response) } if rejected { if !disableCache && c.rdrc != nil { c.rdrc.SaveRDRCAsync(transport.Tag(), question.Name, question.Qtype, c.logger) } logRejectedResponse(c.logger, ctx, response) return response, ErrResponseRejected } } timeToLive := applyResponseOptions(question, response, options) if !disableCache { c.storeCache(transport, question, response, timeToLive) } response.Id = messageId requestEDNSOpt := message.IsEdns0() responseEDNSOpt := response.IsEdns0() if responseEDNSOpt != nil && (requestEDNSOpt == nil || requestEDNSOpt.Version() < responseEDNSOpt.Version()) { response.Extra = common.Filter(response.Extra, func(it dns.RR) bool { return it.Header().Rrtype != dns.TypeOPT }) if requestEDNSOpt != nil { response.SetEdns0(responseEDNSOpt.UDPSize(), responseEDNSOpt.Do()) } } logExchangedResponse(c.logger, ctx, response, timeToLive) return response, nil } 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 if options.LookupStrategy != C.DomainStrategyAsIS { strategy = options.LookupStrategy } else { strategy = options.Strategy } lookupOptions := options if options.LookupStrategy != C.DomainStrategyAsIS { lookupOptions.Strategy = strategy } switch strategy { case C.DomainStrategyIPv4Only: return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, lookupOptions, responseChecker) case C.DomainStrategyIPv6Only: return c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, lookupOptions, responseChecker) } var response4 []netip.Addr var response6 []netip.Addr var group task.Group group.Append("exchange4", func(ctx context.Context) error { response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, lookupOptions, responseChecker) if err != nil { return err } response4 = response return nil }) group.Append("exchange6", func(ctx context.Context) error { response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, lookupOptions, responseChecker) if err != nil { return err } response6 = response return nil }) err := group.Run(ctx) if len(response4) == 0 && len(response6) == 0 { return nil, err } return sortAddresses(response4, response6, strategy), nil } func (c *Client) ClearCache() { if c.cache != nil { c.cache.Purge() } if c.dnsCache != nil { err := c.dnsCache.ClearDNSCache() if err != nil && c.logger != nil { c.logger.Warn("clear DNS cache: ", err) } } } func sortAddresses(response4 []netip.Addr, response6 []netip.Addr, strategy C.DomainStrategy) []netip.Addr { if strategy == C.DomainStrategyPreferIPv6 { return append(response6, response4...) } else { return append(response4, response6...) } } func (c *Client) storeCache(transport adapter.DNSTransport, question dns.Question, message *dns.Msg, timeToLive uint32) { if timeToLive == 0 { return } 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 { c.cache.AddWithLifetime(key, message.Copy(), time.Second*time.Duration(timeToLive)) } } 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, Qclass: dns.ClassINET, } 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 } if response.Rcode != dns.RcodeSuccess { return nil, RcodeError(response.Rcode) } return MessageToAddresses(response), nil } 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, 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 { response, loaded := c.cache.Get(key) if !loaded { return nil, 0, false } 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 } c.cache.Remove(key) return nil, 0, false } nowTTL := max(int(expireAt.Sub(timeNow).Seconds()), 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 } return nil, 0, false } nowTTL := max(int(expireAt.Sub(timeNow).Seconds()), 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 } 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 } } 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, options.Timeout) if err != nil { if c.logger != nil { c.logger.DebugContext(ctx, "optimistic refresh failed for ", FqdnToDomain(question.Name), ": ", err) } return } if responseChecker != nil { var rejected bool if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError { rejected = true } else { 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) } return } } else if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError { return } timeToLive := applyResponseOptions(question, response, options) c.storeCache(transport, question, response, timeToLive) logRefreshedResponse(c.logger, ctx, 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 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, 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 { stripDNSPadding(response) 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 { return adapter.DNSResponseAddresses(response) } 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 } func FixedResponseStatus(message *dns.Msg, rcode int) *dns.Msg { return &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: message.Id, Response: true, Authoritative: true, RecursionDesired: true, RecursionAvailable: true, Rcode: rcode, }, Question: message.Question, } } func FixedResponse(id uint16, question dns.Question, addresses []netip.Addr, timeToLive uint32) *dns.Msg { response := dns.Msg{ MsgHdr: dns.MsgHdr{ Id: id, Response: true, Authoritative: true, RecursionDesired: true, RecursionAvailable: true, Rcode: dns.RcodeSuccess, }, Question: []dns.Question{question}, } for _, address := range addresses { if address.Is4() && question.Qtype == dns.TypeA { response.Answer = append(response.Answer, &dns.A{ Hdr: dns.RR_Header{ Name: question.Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: timeToLive, }, A: address.AsSlice(), }) } else if address.Is6() && question.Qtype == dns.TypeAAAA { response.Answer = append(response.Answer, &dns.AAAA{ Hdr: dns.RR_Header{ Name: question.Name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: timeToLive, }, AAAA: address.AsSlice(), }) } } return &response } func FixedResponseCNAME(id uint16, question dns.Question, record string, timeToLive uint32) *dns.Msg { response := dns.Msg{ MsgHdr: dns.MsgHdr{ Id: id, Response: true, Authoritative: true, RecursionDesired: true, RecursionAvailable: true, Rcode: dns.RcodeSuccess, }, Question: []dns.Question{question}, Answer: []dns.RR{ &dns.CNAME{ Hdr: dns.RR_Header{ Name: question.Name, Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: timeToLive, }, Target: record, }, }, } return &response } func FixedResponseTXT(id uint16, question dns.Question, records []string, timeToLive uint32) *dns.Msg { response := dns.Msg{ MsgHdr: dns.MsgHdr{ Id: id, Response: true, Authoritative: true, RecursionDesired: true, RecursionAvailable: true, Rcode: dns.RcodeSuccess, }, Question: []dns.Question{question}, Answer: []dns.RR{ &dns.TXT{ Hdr: dns.RR_Header{ Name: question.Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: timeToLive, }, Txt: records, }, }, } return &response } func FixedResponseMX(id uint16, question dns.Question, records []*net.MX, timeToLive uint32) *dns.Msg { response := dns.Msg{ MsgHdr: dns.MsgHdr{ Id: id, Response: true, Authoritative: true, RecursionDesired: true, RecursionAvailable: true, Rcode: dns.RcodeSuccess, }, Question: []dns.Question{question}, } for _, record := range records { response.Answer = append(response.Answer, &dns.MX{ Hdr: dns.RR_Header{ Name: question.Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: timeToLive, }, Preference: record.Pref, Mx: record.Host, }) } return &response } ================================================ FILE: dns/client_log.go ================================================ package dns import ( "context" "strings" "github.com/sagernet/sing/common/logger" "github.com/miekg/dns" ) func logCachedResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg, ttl int) { if logger == nil || len(response.Question) == 0 { return } domain := FqdnToDomain(response.Question[0].Name) logger.DebugContext(ctx, "cached ", domain, " ", dns.RcodeToString[response.Rcode], " ", ttl) for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { for _, record := range recordList { logger.InfoContext(ctx, "cached ", dns.Type(record.Header().Rrtype).String(), " ", FormatQuestion(record.String())) } } } 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 } domain := FqdnToDomain(response.Question[0].Name) logger.DebugContext(ctx, "exchanged ", domain, " ", dns.RcodeToString[response.Rcode], " ", ttl) for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { for _, record := range recordList { logger.InfoContext(ctx, "exchanged ", dns.Type(record.Header().Rrtype).String(), " ", FormatQuestion(record.String())) } } } 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 } for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { for _, record := range recordList { logger.InfoContext(ctx, "rejected ", dns.Type(record.Header().Rrtype).String(), " ", FormatQuestion(record.String())) } } } func FqdnToDomain(fqdn string) string { if dns.IsFqdn(fqdn) { return fqdn[:len(fqdn)-1] } return fqdn } func FormatQuestion(string string) string { for strings.HasPrefix(string, ";") { string = string[1:] } string = strings.ReplaceAll(string, "\t", " ") string = strings.ReplaceAll(string, "\n", " ") string = strings.ReplaceAll(string, ";; ", " ") string = strings.ReplaceAll(string, "; ", " ") for strings.Contains(string, " ") { string = strings.ReplaceAll(string, " ", " ") } return strings.TrimSpace(string) } ================================================ FILE: dns/client_truncate.go ================================================ package dns import ( "github.com/sagernet/sing/common/buf" "github.com/miekg/dns" ) func TruncateDNSMessage(request *dns.Msg, response *dns.Msg, headroom int) (*buf.Buffer, error) { maxLen := 512 if edns0Option := request.IsEdns0(); edns0Option != nil { if udpSize := int(edns0Option.UDPSize()); udpSize > 512 { maxLen = udpSize } } responseLen := response.Len() if responseLen > maxLen { response = response.Copy() response.Truncate(maxLen) } buffer := buf.NewSize(headroom*2 + 1 + responseLen) buffer.Resize(headroom, 0) rawMessage, err := response.PackBuffer(buffer.FreeBytes()) if err != nil { buffer.Release() return nil, err } buffer.Truncate(len(rawMessage)) return buffer, nil } ================================================ FILE: dns/extension_edns0_subnet.go ================================================ package dns import ( "net/netip" "github.com/miekg/dns" ) func SetClientSubnet(message *dns.Msg, clientSubnet netip.Prefix) *dns.Msg { return setClientSubnet(message, clientSubnet, true) } func setClientSubnet(message *dns.Msg, clientSubnet netip.Prefix, clone bool) *dns.Msg { var ( optRecord *dns.OPT subnetOption *dns.EDNS0_SUBNET ) findExists: for _, record := range message.Extra { var isOPTRecord bool if optRecord, isOPTRecord = record.(*dns.OPT); isOPTRecord { for _, option := range optRecord.Option { var isEDNS0Subnet bool subnetOption, isEDNS0Subnet = option.(*dns.EDNS0_SUBNET) if isEDNS0Subnet { break findExists } } } } if optRecord == nil { exMessage := *message message = &exMessage optRecord = &dns.OPT{ Hdr: dns.RR_Header{ Name: ".", Rrtype: dns.TypeOPT, }, } message.Extra = append(message.Extra, optRecord) } else if clone { return setClientSubnet(message.Copy(), clientSubnet, false) } if subnetOption == nil { subnetOption = new(dns.EDNS0_SUBNET) subnetOption.Code = dns.EDNS0SUBNET optRecord.Option = append(optRecord.Option, subnetOption) } if clientSubnet.Addr().Is4() { subnetOption.Family = 1 } else { subnetOption.Family = 2 } subnetOption.SourceNetmask = uint8(clientSubnet.Bits()) subnetOption.Address = clientSubnet.Addr().AsSlice() return message } ================================================ FILE: dns/rcode.go ================================================ package dns import ( mDNS "github.com/miekg/dns" ) const ( RcodeSuccess RcodeError = mDNS.RcodeSuccess RcodeServerFailure RcodeError = mDNS.RcodeServerFailure RcodeFormatError RcodeError = mDNS.RcodeFormatError RcodeNameError RcodeError = mDNS.RcodeNameError RcodeRefused RcodeError = mDNS.RcodeRefused ) type RcodeError int func (e RcodeError) Error() string { return mDNS.RcodeToString[int(e)] } ================================================ FILE: dns/repro_test.go ================================================ 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)) } ================================================ FILE: dns/router.go ================================================ package dns import ( "context" "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" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" 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" mDNS "github.com/miekg/dns" ) var ( _ adapter.DNSRouter = (*Router)(nil) _ adapter.DNSRuleSetUpdateValidator = (*Router)(nil) ) type Router struct { ctx context.Context logger logger.ContextLogger 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, error) { router := &Router{ ctx: ctx, 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), } 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{ Context: ctx, Timeout: time.Duration(options.DNSClientOptions.Timeout), 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 { return nil } if !cacheFile.StoreRDRC() { return nil } 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, nil } func (r *Router) Initialize(rules []option.DNSRule) error { r.rawRules = append(r.rawRules[:0], rules...) newRules, _, _, err := r.buildRules(false) if err != nil { return err } closeRules(newRules) return nil } func (r *Router) Start(stage adapter.StartStage) error { monitor := taskmonitor.New(r.logger, C.StartTimeout) switch stage { case adapter.StartStateStart: monitor.Start("initialize DNS client") r.client.Start() monitor.Finish() 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 { 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(router, r.rawRules, nil) 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(router, r.rawRules, overrides) } return nil } candidateLegacyDNSMode, flags, err := resolveLegacyDNSMode(router, r.rawRules, overrides) if err != nil { return err } if legacyDNSMode { if !candidateLegacyDNSMode && flags.disabled { err := validateLegacyDNSModeDisabledRules(router, r.rawRules, overrides) if err != nil { return err } return E.New(deprecated.OptionLegacyDNSAddressFilter.MessageWithLink()) } return nil } if candidateLegacyDNSMode { return E.New(deprecated.OptionLegacyDNSAddressFilter.MessageWithLink()) } 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) { metadata := adapter.ContextFrom(ctx) if metadata == nil { panic("no context") } var currentRuleIndex int if ruleIndex != -1 { currentRuleIndex = ruleIndex + 1 } for ; currentRuleIndex < len(rules); currentRuleIndex++ { currentRule := rules[currentRuleIndex] if currentRule.WithAddressLimit() && !isAddressQuery { continue } metadata.ResetRuleCache() 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[", currentRuleIndex, "] => ", currentRule.Action()) } switch action := currentRule.Action().(type) { case *R.RuleActionDNSRoute: transport, loaded := r.transport.Transport(action.Server) if !loaded { r.logger.ErrorContext(ctx, "transport not found: ", action.Server) continue } isFakeIP := transport.Type() == C.DNSTypeFakeIP if isFakeIP && !allowFakeIP { continue } if action.Strategy != C.DomainStrategyAsIS { options.Strategy = action.Strategy } if isFakeIP || action.DisableCache { options.DisableCache = true } if action.RewriteTTL != nil { options.RewriteTTL = action.RewriteTTL } if action.Timeout > 0 { options.Timeout = action.Timeout } if action.ClientSubnet.IsValid() { options.ClientSubnet = action.ClientSubnet } return transport, currentRule, currentRuleIndex case *R.RuleActionDNSRouteOptions: if action.Strategy != C.DomainStrategyAsIS { options.Strategy = action.Strategy } if action.DisableCache { options.DisableCache = true } if action.RewriteTTL != nil { options.RewriteTTL = action.RewriteTTL } if action.Timeout > 0 { options.Timeout = action.Timeout } if action.ClientSubnet.IsValid() { options.ClientSubnet = action.ClientSubnet } case *R.RuleActionReject: return nil, currentRule, currentRuleIndex case *R.RuleActionPredefined: return nil, currentRule, currentRuleIndex } } } transport := r.transport.Default() 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.DisableOptimisticCache { options.DisableOptimisticCache = true } if routeOptions.RewriteTTL != nil { options.RewriteTTL = routeOptions.RewriteTTL } if routeOptions.Timeout > 0 { options.Timeout = routeOptions.Timeout } 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 } 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), } } } 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) { if len(message.Question) != 1 { r.logger.WarnContext(ctx, "bad question size: ", len(message.Question)) responseMessage := mDNS.Msg{ MsgHdr: mDNS.MsgHdr{ Id: message.Id, Response: true, Rcode: mDNS.RcodeFormatError, }, Question: message.Question, } return &responseMessage, nil } r.rulesAccess.RLock() 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 transport adapter.DNSTransport err error ) var metadata *adapter.InboundContext 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 case mDNS.TypeAAAA: metadata.IPVersion = 6 } metadata.Domain = FqdnToDomain(message.Question[0].Name) if options.Transport != nil { transport = options.Transport 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 ruleIndex int ) ruleIndex = -1 for { dnsCtx := adapter.OverrideContext(ctx) dnsOptions := options transport, rule, ruleIndex = r.matchDNS(ctx, rules, true, ruleIndex, isAddressQuery(message), &dnsOptions) if rule != nil { switch action := rule.Action().(type) { case *R.RuleActionReject: switch action.Method { case C.RuleActionRejectMethodDefault: return &mDNS.Msg{ MsgHdr: mDNS.MsgHdr{ Id: message.Id, Rcode: mDNS.RcodeRefused, Response: true, }, Question: []mDNS.Question{message.Question[0]}, }, nil case C.RuleActionRejectMethodDrop: return nil, tun.ErrDrop } case *R.RuleActionPredefined: err = nil response = action.Response(message) goto done } } responseCheck := addressLimitResponseCheck(rule, metadata) if dnsOptions.Strategy == C.DomainStrategyAsIS { dnsOptions.Strategy = r.defaultDomainStrategy } response, err = r.client.Exchange(dnsCtx, transport, message, dnsOptions, responseCheck) var rejected bool if err != nil { if errors.Is(err, ErrResponseRejectedCached) { rejected = true r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())), " (cached)") } else if errors.Is(err, ErrResponseRejected) { rejected = true r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String()))) } else if len(message.Question) > 0 { r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String()))) } else { r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ")) } } if responseCheck != nil && rejected { continue } break } } done: if err != nil { return nil, err } if r.dnsReverseMapping != nil && len(message.Question) > 0 && response != nil && len(response.Answer) > 0 { if transport == nil || transport.Type() != C.DNSTypeFakeIP { for _, answer := range response.Answer { switch record := answer.(type) { case *mDNS.A: r.dnsReverseMapping.AddWithLifetime(M.AddrFromIP(record.A), FqdnToDomain(record.Hdr.Name), time.Duration(record.Hdr.Ttl)*time.Second) case *mDNS.AAAA: r.dnsReverseMapping.AddWithLifetime(M.AddrFromIP(record.AAAA), FqdnToDomain(record.Hdr.Name), time.Duration(record.Hdr.Ttl)*time.Second) } } } } return response, nil } func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) { r.rulesAccess.RLock() 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 ) printResult := func() { if err == nil && len(responseAddrs) == 0 { err = E.New("empty result") } if err != nil { if errors.Is(err, ErrResponseRejectedCached) { 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)) } } if err != nil { err = E.Cause(err, "lookup ", domain) } } r.logger.DebugContext(ctx, "lookup domain ", domain) 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 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 rule adapter.DNSRule ruleIndex int ) ruleIndex = -1 for { dnsCtx := adapter.OverrideContext(ctx) dnsOptions := options transport, rule, ruleIndex = r.matchDNS(ctx, rules, false, ruleIndex, true, &dnsOptions) if rule != nil { switch action := rule.Action().(type) { case *R.RuleActionReject: return nil, &R.RejectedError{Cause: action.Error(ctx)} case *R.RuleActionPredefined: responseAddrs = nil if action.Rcode != mDNS.RcodeSuccess { err = RcodeError(action.Rcode) } else { err = nil for _, answer := range action.Answer { switch record := answer.(type) { case *mDNS.A: responseAddrs = append(responseAddrs, M.AddrFromIP(record.A)) case *mDNS.AAAA: responseAddrs = append(responseAddrs, M.AddrFromIP(record.AAAA)) } } } goto response } } responseCheck := addressLimitResponseCheck(rule, metadata) if dnsOptions.Strategy == C.DomainStrategyAsIS { dnsOptions.Strategy = r.defaultDomainStrategy } responseAddrs, err = r.client.Lookup(dnsCtx, transport, domain, dnsOptions, responseCheck) if responseCheck == nil || err == nil { break } printResult() } } response: printResult() if len(responseAddrs) > 0 { r.logger.InfoContext(ctx, "lookup succeed for ", domain, ": ", strings.Join(F.MapToString(responseAddrs), " ")) } return responseAddrs, err } func isAddressQuery(message *mDNS.Msg) bool { for _, question := range message.Question { if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA || question.Qtype == mDNS.TypeHTTPS { return true } } return false } func addressLimitResponseCheck(rule adapter.DNSRule, metadata *adapter.InboundContext) func(response *mDNS.Msg) bool { if rule == nil || !rule.WithAddressLimit() { return nil } responseMetadata := *metadata return func(response *mDNS.Msg) bool { checkMetadata := responseMetadata return rule.MatchAddressLimit(&checkMetadata, response) } } func (r *Router) ClearCache() { r.client.ClearCache() if r.platformInterface != nil { r.platformInterface.ClearDNSCache() } if r.dnsReverseMapping != nil { r.dnsReverseMapping.Purge() } } func (r *Router) LookupReverseMapping(ip netip.Addr) (string, bool) { if r.dnsReverseMapping == nil { return "", false } domain, loaded := r.dnsReverseMapping.Get(ip) return domain, loaded } func (r *Router) ResetNetwork() { r.ClearCache() for _, transport := range r.transport.Transports() { transport.Reset() } } func defaultRuleNeedsLegacyDNSModeFromAddressFilter(rule option.DefaultDNSRule) bool { if rule.RuleSetIPCIDRAcceptEmpty { //nolint:staticcheck return true } return !rule.MatchResponse && (rule.IPAcceptAny || 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 || dnsRuleActionDisablesLegacyDNSMode(rule.LogicalOptions.DNSRuleAction), 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) || dnsRuleActionDisablesLegacyDNSMode(rule.DNSRuleAction), 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 validateLegacyDNSModeDisabledRules(router adapter.Router, rules []option.DNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) error { var seenEvaluate bool for i, rule := range rules { requiresPriorEvaluate, err := validateLegacyDNSModeDisabledRuleTree(router, rule, metadataOverrides) 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(router adapter.Router, rule option.DNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) (bool, error) { switch rule.Type { case "", C.RuleTypeDefault: return validateLegacyDNSModeDisabledDefaultRule(router, rule.DefaultOptions, metadataOverrides) case C.RuleTypeLogical: requiresPriorEvaluate := dnsRuleActionType(rule) == C.RuleActionTypeRespond for i, subRule := range rule.LogicalOptions.Rules { subRequiresPriorEvaluate, err := validateLegacyDNSModeDisabledRuleTree(router, subRule, metadataOverrides) if err != nil { return false, E.Cause(err, "sub rule[", i, "]") } requiresPriorEvaluate = requiresPriorEvaluate || subRequiresPriorEvaluate } return requiresPriorEvaluate, nil default: return false, nil } } 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") } // 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()) } 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: 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 "" } } ================================================ FILE: dns/router_test.go ================================================ package dns import ( "context" "net" "net/netip" "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) 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) 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 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, ContainsNonIPCIDRRule: 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 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() 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) } ================================================ FILE: dns/transport/conn_pool.go ================================================ package transport import ( "context" "net" "sync" "github.com/sagernet/sing/common/x/list" "golang.org/x/sync/semaphore" ) type ConnPoolMode int const ( ConnPoolSingle ConnPoolMode = iota ConnPoolOrdered ) type ConnPoolOptions[T comparable] struct { 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] } type connPoolState[T comparable] struct { ctx context.Context cancel context.CancelCauseFunc all map[T]struct{} idle list.List[T] idleElements map[T]*list.Element[T] shared T hasShared bool sharedClaimed bool sharedCtx context.Context sharedCancel context.CancelCauseFunc connecting *connPoolConnect[T] } type connPoolConnect[T comparable] struct { done chan struct{} err error } func NewConnPool[T comparable](options ConnPoolOptions[T]) *ConnPool[T] { p := &ConnPool[T]{ options: options, } 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] { ctx, cancel := context.WithCancelCause(context.Background()) state := &connPoolState[T]{ ctx: ctx, cancel: cancel, all: make(map[T]struct{}), } if mode == ConnPoolOrdered { state.idleElements = make(map[T]*list.Element[T]) } return state } func (p *ConnPool[T]) Acquire(ctx context.Context, dial func(context.Context) (T, error)) (T, bool, error) { switch p.options.Mode { case ConnPoolSingle: conn, _, created, err := p.acquireShared(ctx, dial) return conn, created, err case ConnPoolOrdered: return p.acquireOrdered(ctx, dial) default: var zero T return zero, false, net.ErrClosed } } func (p *ConnPool[T]) AcquireShared(ctx context.Context, dial func(context.Context) (T, error)) (T, context.Context, bool, error) { if p.options.Mode != ConnPoolSingle { var zero T return zero, nil, false, net.ErrClosed } return p.acquireShared(ctx, dial) } func (p *ConnPool[T]) Release(conn T, reuse bool) { p.access.Lock() if p.closed { p.access.Unlock() p.options.Close(conn, net.ErrClosed) return } state := p.state if _, tracked := state.all[conn]; !tracked { p.access.Unlock() p.options.Close(conn, net.ErrClosed) return } if !reuse || !p.options.IsAlive(conn) { p.removeConn(state, conn, net.ErrClosed) p.access.Unlock() p.options.Close(conn, net.ErrClosed) return } if p.options.Mode == ConnPoolOrdered { if _, idle := state.idleElements[conn]; !idle { state.idleElements[conn] = state.idle.PushBack(conn) } } p.access.Unlock() } func (p *ConnPool[T]) Invalidate(conn T, cause error) { p.access.Lock() if p.closed { p.access.Unlock() p.options.Close(conn, cause) return } 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) } 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) switch p.options.Mode { case ConnPoolSingle: if state.hasShared && state.shared == conn { var zero T 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 := state.idleElements[conn]; loaded { state.idle.Remove(element) delete(state.idleElements, conn) } } } func (p *ConnPool[T]) Reset() { p.access.Lock() if p.closed { p.access.Unlock() return } oldState := p.state p.state = newConnPoolState[T](p.options.Mode) p.access.Unlock() p.closeState(oldState, net.ErrClosed) } func (p *ConnPool[T]) Close() error { p.access.Lock() if p.closed { p.access.Unlock() return nil } p.closed = true oldState := p.state p.state = nil p.access.Unlock() p.closeState(oldState, net.ErrClosed) return nil } func (p *ConnPool[T]) acquireOrdered(ctx context.Context, dial func(context.Context) (T, error)) (T, bool, error) { var zero T for { p.access.Lock() if p.closed { p.access.Unlock() return zero, false, net.ErrClosed } current := p.state if element := current.idle.Front(); element != nil { idleConn := current.idle.Remove(element) delete(current.idleElements, idleConn) if p.options.IsAlive(idleConn) { p.access.Unlock() return idleConn, false, nil } delete(current.all, idleConn) p.access.Unlock() p.options.Close(idleConn, net.ErrClosed) continue } p.access.Unlock() return p.dialAndInstall(ctx, current, dial) } } 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 { 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.Unlock() return conn, true, nil } func (p *ConnPool[T]) acquireShared(ctx context.Context, dial func(context.Context) (T, error)) (T, context.Context, bool, error) { var zero T for { p.access.Lock() if p.closed { p.access.Unlock() return zero, nil, false, net.ErrClosed } current := p.state if current.hasShared { conn := current.shared if p.options.IsAlive(conn) { created := !current.sharedClaimed current.sharedClaimed = true connCtx := current.sharedCtx p.access.Unlock() return conn, connCtx, created, nil } p.removeConn(current, conn, net.ErrClosed) p.access.Unlock() p.options.Close(conn, net.ErrClosed) continue } startDial := current.connecting == nil if startDial { current.connecting = &connPoolConnect[T]{done: make(chan struct{})} } state := current.connecting p.access.Unlock() if startDial { go p.connectSingle(current, state, ctx, dial) } select { case <-state.done: conn, connCtx, created, retry, err := p.collectShared(current, state, startDial) if retry { continue } return conn, connCtx, created, err case <-ctx.Done(): return zero, nil, false, ctx.Err() case <-current.ctx.Done(): p.access.Lock() closed := p.closed p.access.Unlock() if closed { return zero, nil, false, net.ErrClosed } } } } func (p *ConnPool[T]) connectSingle(current *connPoolState[T], state *connPoolConnect[T], ctx context.Context, dial func(context.Context) (T, error)) { 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) } err = dialErr } var closeErr error p.access.Lock() 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 = net.ErrClosed state.err = closeErr } else { sharedCtx, sharedCancel := context.WithCancelCause(current.ctx) current.shared = conn current.hasShared = true current.sharedCtx = sharedCtx current.sharedCancel = sharedCancel current.all[conn] = struct{}{} } p.access.Unlock() if closeErr != nil { p.options.Close(conn, closeErr) } close(state.done) } func (p *ConnPool[T]) collectShared(current *connPoolState[T], state *connPoolConnect[T], startDial bool) (T, context.Context, bool, bool, error) { var zero T p.access.Lock() if state.err != nil { err := state.err p.access.Unlock() if startDial { return zero, nil, false, false, err } return zero, nil, false, true, nil } if p.closed { p.access.Unlock() return zero, nil, false, false, net.ErrClosed } if p.state != current { p.access.Unlock() return zero, nil, false, false, net.ErrClosed } if !current.hasShared { p.access.Unlock() return zero, nil, false, true, nil } conn := current.shared if !p.options.IsAlive(conn) { p.removeConn(current, conn, net.ErrClosed) p.access.Unlock() p.options.Close(conn, net.ErrClosed) return zero, nil, false, true, nil } created := !current.sharedClaimed current.sharedClaimed = true connCtx := current.sharedCtx p.access.Unlock() return conn, connCtx, created, false, nil } func (p *ConnPool[T]) closeState(state *connPoolState[T], cause error) { state.cancel(cause) for conn := range state.all { p.options.Close(conn, cause) } } ================================================ FILE: dns/transport/dhcp/dhcp.go ================================================ package dhcp import ( "context" "errors" "io" "net" "runtime" "strings" "sync" "syscall" "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-tun" "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/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/task" "github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/service" "github.com/insomniacslk/dhcp/dhcpv4" mDNS "github.com/miekg/dns" "golang.org/x/exp/slices" ) func RegisterTransport(registry *dns.TransportRegistry) { dns.RegisterTransport[option.DHCPDNSServerOptions](registry, C.DNSTypeDHCP, NewTransport) } var _ adapter.DNSTransport = (*Transport)(nil) type Transport struct { dns.TransportAdapter ctx context.Context dialer N.Dialer logger logger.ContextLogger networkManager adapter.NetworkManager interfaceName string interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback] transportLock sync.RWMutex updatedAt time.Time lastError error servers []M.Socksaddr search []string ndots int attempts int } func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.DHCPDNSServerOptions) (adapter.DNSTransport, error) { transportDialer, err := dns.NewLocalDialer(ctx, options.LocalDNSServerOptions) if err != nil { return nil, err } return &Transport{ TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeDHCP, tag, options.LocalDNSServerOptions), ctx: ctx, dialer: transportDialer, logger: logger, networkManager: service.FromContext[adapter.NetworkManager](ctx), interfaceName: options.Interface, ndots: 1, attempts: 2, }, nil } func NewRawTransport(transportAdapter dns.TransportAdapter, ctx context.Context, dialer N.Dialer, logger log.ContextLogger) *Transport { return &Transport{ TransportAdapter: transportAdapter, ctx: ctx, dialer: dialer, logger: logger, networkManager: service.FromContext[adapter.NetworkManager](ctx), ndots: 1, attempts: 2, } } func (t *Transport) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } if t.interfaceName == "" { t.interfaceCallback = t.networkManager.InterfaceMonitor().RegisterCallback(t.interfaceUpdated) } go func() { _, err := t.fetch() if err != nil { t.logger.Error(E.Cause(err, "fetch DNS servers")) } }() return nil } func (t *Transport) Close() error { if t.interfaceCallback != nil { t.networkManager.InterfaceMonitor().UnregisterCallback(t.interfaceCallback) } return nil } func (t *Transport) Reset() { t.transportLock.Lock() t.updatedAt = time.Time{} t.servers = nil t.transportLock.Unlock() } func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { servers, err := t.fetch() if err != nil { return nil, err } if len(servers) == 0 { return nil, E.New("dhcp: empty DNS servers from response") } return t.Exchange0(ctx, message, servers) } func (t *Transport) Exchange0(ctx context.Context, message *mDNS.Msg, servers []M.Socksaddr) (*mDNS.Msg, error) { question := message.Question[0] domain := dns.FqdnToDomain(question.Name) if len(servers) == 1 || !(message.Question[0].Qtype == mDNS.TypeA || message.Question[0].Qtype == mDNS.TypeAAAA) { return t.exchangeSingleRequest(ctx, servers, message, domain) } else { return t.exchangeParallel(ctx, servers, message, domain) } } func (t *Transport) Fetch() []M.Socksaddr { servers, _ := t.fetch() return servers } func (t *Transport) fetch() ([]M.Socksaddr, error) { t.transportLock.RLock() updatedAt := t.updatedAt lastError := t.lastError servers := t.servers t.transportLock.RUnlock() if lastError != nil { return nil, lastError } if time.Since(updatedAt) < C.DHCPTTL { return servers, nil } t.transportLock.Lock() defer t.transportLock.Unlock() if time.Since(t.updatedAt) < C.DHCPTTL { return t.servers, nil } err := t.updateServers() if err != nil { return servers, err } return t.servers, nil } func (t *Transport) fetchInterface() (*control.Interface, error) { if t.interfaceName == "" { if t.networkManager.InterfaceMonitor() == nil { return nil, E.New("missing monitor for auto DHCP, set route.auto_detect_interface") } defaultInterface := t.networkManager.InterfaceMonitor().DefaultInterface() if defaultInterface == nil { return nil, E.New("missing default interface") } return defaultInterface, nil } else { return t.networkManager.InterfaceFinder().ByName(t.interfaceName) } } func (t *Transport) updateServers() error { iface, err := t.fetchInterface() if err != nil { return E.Cause(err, "dhcp: prepare interface") } t.logger.Info("dhcp: query DNS servers on ", iface.Name) fetchCtx, cancel := context.WithTimeout(t.ctx, C.DHCPTimeout) err = t.fetchServers0(fetchCtx, iface) cancel() t.updatedAt = time.Now() if err != nil { t.lastError = err return err } else if len(t.servers) == 0 { t.lastError = E.New("dhcp: empty DNS servers response") return t.lastError } else { t.lastError = nil return nil } } func (t *Transport) interfaceUpdated(defaultInterface *control.Interface, flags int) { err := t.updateServers() if err != nil { t.logger.Error("update servers: ", err) } } func (t *Transport) fetchServers0(ctx context.Context, iface *control.Interface) error { var listener net.ListenConfig listener.Control = control.Append(listener.Control, control.BindToInterface(t.networkManager.InterfaceFinder(), iface.Name, iface.Index)) listener.Control = control.Append(listener.Control, control.ReuseAddr()) listenAddr := "0.0.0.0:68" if runtime.GOOS == "linux" || runtime.GOOS == "android" { listenAddr = "255.255.255.255:68" } var ( packetConn net.PacketConn err error ) for range 5 { packetConn, err = listener.ListenPacket(t.ctx, "udp4", listenAddr) if err == nil || !errors.Is(err, syscall.EADDRINUSE) { break } time.Sleep(time.Second) } if err != nil { return err } defer packetConn.Close() discovery, err := dhcpv4.NewDiscovery(iface.HardwareAddr, dhcpv4.WithBroadcast(true), dhcpv4.WithRequestedOptions( dhcpv4.OptionDomainName, dhcpv4.OptionDomainNameServer, dhcpv4.OptionDNSDomainSearchList, )) if err != nil { return err } _, err = packetConn.WriteTo(discovery.ToBytes(), &net.UDPAddr{IP: net.IPv4bcast, Port: 67}) if err != nil { return err } var group task.Group group.Append0(func(ctx context.Context) error { return t.fetchServersResponse(iface, packetConn, discovery.TransactionID) }) group.Cleanup(func() { packetConn.Close() }) return group.Run(ctx) } func (t *Transport) fetchServersResponse(iface *control.Interface, packetConn net.PacketConn, transactionID dhcpv4.TransactionID) error { buffer := buf.NewSize(dhcpv4.MaxMessageSize) defer buffer.Release() for { buffer.Reset() _, _, err := buffer.ReadPacketFrom(packetConn) if err != nil { if errors.Is(err, io.ErrShortBuffer) { continue } return err } dhcpPacket, err := dhcpv4.FromBytes(buffer.Bytes()) if err != nil { t.logger.Trace("dhcp: parse DHCP response: ", err) return err } if dhcpPacket.MessageType() != dhcpv4.MessageTypeOffer { t.logger.Trace("dhcp: expected OFFER response, but got ", dhcpPacket.MessageType()) continue } if dhcpPacket.TransactionID != transactionID { t.logger.Trace("dhcp: expected transaction ID ", transactionID, ", but got ", dhcpPacket.TransactionID) continue } return t.recreateServers(iface, dhcpPacket) } } func (t *Transport) recreateServers(iface *control.Interface, dhcpPacket *dhcpv4.DHCPv4) error { searchList := dhcpPacket.DomainSearch() if searchList != nil && len(searchList.Labels) > 0 { t.search = searchList.Labels } else if dhcpPacket.DomainName() != "" { t.search = []string{dhcpPacket.DomainName()} } serverAddrs := common.Map(dhcpPacket.DNS(), func(it net.IP) M.Socksaddr { return M.SocksaddrFrom(M.AddrFromIP(it), 53) }) if len(serverAddrs) > 0 && !slices.Equal(t.servers, serverAddrs) { t.logger.Info("dhcp: updated DNS servers from ", iface.Name, ": [", strings.Join(common.Map(serverAddrs, M.Socksaddr.String), ","), "], search: [", strings.Join(t.search, ","), "]") } t.servers = serverAddrs return nil } ================================================ FILE: dns/transport/dhcp/dhcp_shared.go ================================================ package dhcp import ( "context" "errors" "math/rand" "strings" "syscall" "github.com/sagernet/sing-box/dns/transport" "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" mDNS "github.com/miekg/dns" ) func (t *Transport) exchangeSingleRequest(ctx context.Context, servers []M.Socksaddr, message *mDNS.Msg, domain string) (*mDNS.Msg, error) { var lastErr error for _, fqdn := range t.nameList(domain) { response, err := t.tryOneName(ctx, servers, fqdn, message) if err != nil { lastErr = err continue } return response, nil } return nil, lastErr } func (t *Transport) exchangeParallel(ctx context.Context, servers []M.Socksaddr, message *mDNS.Msg, domain string) (*mDNS.Msg, error) { returned := make(chan struct{}) defer close(returned) type queryResult struct { response *mDNS.Msg err error } results := make(chan queryResult) startRacer := func(ctx context.Context, fqdn string) { response, err := t.tryOneName(ctx, servers, fqdn, message) select { case results <- queryResult{response, err}: case <-returned: } } queryCtx, queryCancel := context.WithCancel(ctx) defer queryCancel() var nameCount int for _, fqdn := range t.nameList(domain) { nameCount++ go startRacer(queryCtx, fqdn) } var errors []error for { select { case <-ctx.Done(): return nil, ctx.Err() case result := <-results: if result.err == nil { return result.response, nil } errors = append(errors, result.err) if len(errors) == nameCount { return nil, E.Errors(errors...) } } } } func (t *Transport) tryOneName(ctx context.Context, servers []M.Socksaddr, fqdn string, message *mDNS.Msg) (*mDNS.Msg, error) { sLen := len(servers) var lastErr error for i := 0; i < t.attempts; i++ { for j := range sLen { server := servers[j] question := message.Question[0] question.Name = fqdn response, err := t.exchangeOne(ctx, server, question) if err != nil { lastErr = err continue } return response, nil } } return nil, E.Cause(lastErr, fqdn) } func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, question mDNS.Question) (*mDNS.Msg, error) { if server.Port == 0 { server.Port = 53 } request := &mDNS.Msg{ MsgHdr: mDNS.MsgHdr{ Id: uint16(rand.Uint32()), RecursionDesired: true, AuthenticatedData: true, }, Question: []mDNS.Question{question}, Compress: true, } request.SetEdns0(buf.UDPBufferSize, false) return t.exchangeUDP(ctx, server, request) } func (t *Transport) exchangeUDP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg) (*mDNS.Msg, error) { conn, err := t.dialer.DialContext(ctx, N.NetworkUDP, server) if err != nil { return nil, err } defer conn.Close() if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() { conn.SetDeadline(deadline) } buffer := buf.Get(buf.UDPBufferSize) defer buf.Put(buffer) rawMessage, err := request.PackBuffer(buffer) if err != nil { return nil, E.Cause(err, "pack request") } _, err = conn.Write(rawMessage) if err != nil { if errors.Is(err, syscall.EMSGSIZE) { return t.exchangeTCP(ctx, server, request) } return nil, E.Cause(err, "write request") } n, err := conn.Read(buffer) if err != nil { if errors.Is(err, syscall.EMSGSIZE) { return t.exchangeTCP(ctx, server, request) } return nil, E.Cause(err, "read response") } var response mDNS.Msg err = response.Unpack(buffer[:n]) if err != nil { return nil, E.Cause(err, "unpack response") } if response.Truncated { return t.exchangeTCP(ctx, server, request) } return &response, nil } func (t *Transport) exchangeTCP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg) (*mDNS.Msg, error) { conn, err := t.dialer.DialContext(ctx, N.NetworkTCP, server) if err != nil { return nil, err } defer conn.Close() if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() { conn.SetDeadline(deadline) } err = transport.WriteMessage(conn, 0, request) if err != nil { return nil, err } return transport.ReadMessage(conn) } func (t *Transport) nameList(name string) []string { l := len(name) rooted := l > 0 && name[l-1] == '.' if l > 254 || l == 254 && !rooted { return nil } if rooted { if avoidDNS(name) { return nil } return []string{name} } hasNdots := strings.Count(name, ".") >= t.ndots name += "." // l++ names := make([]string, 0, 1+len(t.search)) if hasNdots && !avoidDNS(name) { names = append(names, name) } for _, suffix := range t.search { fqdn := name + suffix if !avoidDNS(fqdn) && len(fqdn) <= 254 { names = append(names, fqdn) } } if !hasNdots && !avoidDNS(name) { names = append(names, name) } return names } func avoidDNS(name string) bool { if name == "" { return true } if name[len(name)-1] == '.' { name = name[:len(name)-1] } return strings.HasSuffix(name, ".onion") } ================================================ FILE: dns/transport/fakeip/fakeip.go ================================================ package fakeip import ( "context" "net/netip" "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" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" mDNS "github.com/miekg/dns" ) func RegisterTransport(registry *dns.TransportRegistry) { dns.RegisterTransport[option.FakeIPDNSServerOptions](registry, C.DNSTypeFakeIP, NewTransport) } var _ adapter.FakeIPTransport = (*Transport)(nil) type Transport struct { dns.TransportAdapter logger logger.ContextLogger store adapter.FakeIPStore inet4Enabled bool inet6Enabled bool } func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.FakeIPDNSServerOptions) (adapter.DNSTransport, error) { inet4Range := options.Inet4Range.Build(netip.Prefix{}) inet6Range := options.Inet6Range.Build(netip.Prefix{}) if !inet4Range.IsValid() && !inet6Range.IsValid() { return nil, E.New("at least one of inet4_range or inet6_range must be set") } store := NewStore(ctx, logger, inet4Range, inet6Range) return &Transport{ TransportAdapter: dns.NewTransportAdapter(C.DNSTypeFakeIP, tag, nil), logger: logger, store: store, inet4Enabled: inet4Range.IsValid(), inet6Enabled: inet6Range.IsValid(), }, nil } func (t *Transport) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } return t.store.Start() } func (t *Transport) Close() error { return t.store.Close() } func (t *Transport) 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 { return nil, E.New("only IP queries are supported by fakeip") } if question.Qtype == mDNS.TypeA && !t.inet4Enabled || question.Qtype == mDNS.TypeAAAA && !t.inet6Enabled { return dns.FixedResponseStatus(message, mDNS.RcodeSuccess), nil } address, err := t.store.Create(dns.FqdnToDomain(question.Name), question.Qtype == mDNS.TypeAAAA) if err != nil { return nil, err } return dns.FixedResponse(message.Id, question, []netip.Addr{address}, C.DefaultDNSTTL), nil } func (t *Transport) Store() adapter.FakeIPStore { return t.store } ================================================ FILE: dns/transport/fakeip/memory.go ================================================ package fakeip import ( "net/netip" "sync" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing/common/logger" ) var _ adapter.FakeIPStorage = (*MemoryStorage)(nil) type MemoryStorage struct { addressAccess sync.RWMutex domainAccess sync.RWMutex addressCache map[netip.Addr]string domainCache4 map[string]netip.Addr domainCache6 map[string]netip.Addr } func NewMemoryStorage() *MemoryStorage { return &MemoryStorage{ addressCache: make(map[netip.Addr]string), domainCache4: make(map[string]netip.Addr), domainCache6: make(map[string]netip.Addr), } } func (s *MemoryStorage) FakeIPMetadata() *adapter.FakeIPMetadata { return nil } func (s *MemoryStorage) FakeIPSaveMetadata(metadata *adapter.FakeIPMetadata) error { return nil } func (s *MemoryStorage) FakeIPSaveMetadataAsync(metadata *adapter.FakeIPMetadata) { } func (s *MemoryStorage) FakeIPStore(address netip.Addr, domain string) error { s.addressAccess.Lock() s.domainAccess.Lock() if oldDomain, loaded := s.addressCache[address]; loaded { if address.Is4() { delete(s.domainCache4, oldDomain) } else { delete(s.domainCache6, oldDomain) } } s.addressCache[address] = domain if address.Is4() { s.domainCache4[domain] = address } else { s.domainCache6[domain] = address } s.domainAccess.Unlock() s.addressAccess.Unlock() return nil } func (s *MemoryStorage) FakeIPStoreAsync(address netip.Addr, domain string, logger logger.Logger) { _ = s.FakeIPStore(address, domain) } func (s *MemoryStorage) FakeIPLoad(address netip.Addr) (string, bool) { s.addressAccess.RLock() defer s.addressAccess.RUnlock() domain, loaded := s.addressCache[address] return domain, loaded } func (s *MemoryStorage) FakeIPLoadDomain(domain string, isIPv6 bool) (netip.Addr, bool) { s.domainAccess.RLock() defer s.domainAccess.RUnlock() if !isIPv6 { address, loaded := s.domainCache4[domain] return address, loaded } else { address, loaded := s.domainCache6[domain] return address, loaded } } func (s *MemoryStorage) FakeIPReset() error { s.addressAccess.Lock() s.domainAccess.Lock() s.addressCache = make(map[netip.Addr]string) s.domainCache4 = make(map[string]netip.Addr) s.domainCache6 = make(map[string]netip.Addr) s.domainAccess.Unlock() s.addressAccess.Unlock() return nil } ================================================ FILE: dns/transport/fakeip/store.go ================================================ package fakeip import ( "context" "net/netip" "sync" "github.com/sagernet/sing-box/adapter" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/service" ) var _ adapter.FakeIPStore = (*Store)(nil) type Store struct { ctx context.Context logger logger.Logger inet4Range netip.Prefix inet6Range netip.Prefix inet4Last netip.Addr inet6Last netip.Addr storage adapter.FakeIPStorage addressAccess sync.Mutex inet4Current netip.Addr inet6Current netip.Addr } func NewStore(ctx context.Context, logger logger.Logger, inet4Range netip.Prefix, inet6Range netip.Prefix) *Store { store := &Store{ ctx: ctx, logger: logger, inet4Range: inet4Range, inet6Range: inet6Range, } if inet4Range.IsValid() { store.inet4Last = broadcastAddress(inet4Range) } if inet6Range.IsValid() { store.inet6Last = broadcastAddress(inet6Range) } return store } func broadcastAddress(prefix netip.Prefix) netip.Addr { addr := prefix.Addr() raw := addr.As16() bits := prefix.Bits() if addr.Is4() { bits += 96 } for i := bits; i < 128; i++ { raw[i/8] |= 1 << (7 - i%8) } if addr.Is4() { return netip.AddrFrom4([4]byte(raw[12:])) } return netip.AddrFrom16(raw) } func (s *Store) Start() error { var storage adapter.FakeIPStorage cacheFile := service.FromContext[adapter.CacheFile](s.ctx) if cacheFile != nil && cacheFile.StoreFakeIP() { storage = cacheFile } if storage == nil { storage = NewMemoryStorage() } metadata := storage.FakeIPMetadata() if metadata != nil && metadata.Inet4Range == s.inet4Range && metadata.Inet6Range == s.inet6Range { s.inet4Current = metadata.Inet4Current s.inet6Current = metadata.Inet6Current } else { if s.inet4Range.IsValid() { s.inet4Current = s.inet4Range.Addr().Next() } if s.inet6Range.IsValid() { s.inet6Current = s.inet6Range.Addr().Next() } _ = storage.FakeIPReset() } s.storage = storage return nil } func (s *Store) Contains(address netip.Addr) bool { return s.inet4Range.Contains(address) || s.inet6Range.Contains(address) } func (s *Store) Close() error { if s.storage == nil { return nil } s.addressAccess.Lock() metadata := &adapter.FakeIPMetadata{ Inet4Range: s.inet4Range, Inet6Range: s.inet6Range, Inet4Current: s.inet4Current, Inet6Current: s.inet6Current, } s.addressAccess.Unlock() return s.storage.FakeIPSaveMetadata(metadata) } func (s *Store) Create(domain string, isIPv6 bool) (netip.Addr, error) { if address, loaded := s.storage.FakeIPLoadDomain(domain, isIPv6); loaded { return address, nil } s.addressAccess.Lock() defer s.addressAccess.Unlock() // Double-check after acquiring lock if address, loaded := s.storage.FakeIPLoadDomain(domain, isIPv6); loaded { return address, nil } var address netip.Addr if !isIPv6 { if !s.inet4Current.IsValid() { return netip.Addr{}, E.New("missing IPv4 fakeip address range") } nextAddress := s.inet4Current.Next() if nextAddress == s.inet4Last || !s.inet4Range.Contains(nextAddress) { nextAddress = s.inet4Range.Addr().Next().Next() } s.inet4Current = nextAddress address = nextAddress } else { if !s.inet6Current.IsValid() { return netip.Addr{}, E.New("missing IPv6 fakeip address range") } nextAddress := s.inet6Current.Next() if nextAddress == s.inet6Last || !s.inet6Range.Contains(nextAddress) { nextAddress = s.inet6Range.Addr().Next().Next() } s.inet6Current = nextAddress address = nextAddress } err := s.storage.FakeIPStore(address, domain) if err != nil { s.logger.Warn("save FakeIP cache: ", err) } s.storage.FakeIPSaveMetadataAsync(&adapter.FakeIPMetadata{ Inet4Range: s.inet4Range, Inet6Range: s.inet6Range, Inet4Current: s.inet4Current, Inet6Current: s.inet6Current, }) return address, nil } func (s *Store) Lookup(address netip.Addr) (string, bool) { return s.storage.FakeIPLoad(address) } func (s *Store) Reset() error { return s.storage.FakeIPReset() } ================================================ FILE: dns/transport/hosts/hosts.go ================================================ package hosts import ( "context" "net/netip" "os" "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/service/filemanager" mDNS "github.com/miekg/dns" ) func RegisterTransport(registry *dns.TransportRegistry) { dns.RegisterTransport[option.HostsDNSServerOptions](registry, C.DNSTypeHosts, NewTransport) } var ( _ adapter.DNSTransport = (*Transport)(nil) _ adapter.DNSTransportWithPreferredDomain = (*Transport)(nil) ) type Transport struct { dns.TransportAdapter files []*File predefined map[string][]netip.Addr } func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.HostsDNSServerOptions) (adapter.DNSTransport, error) { var ( files []*File predefined = make(map[string][]netip.Addr) ) if len(options.Path) == 0 { 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)))) } } if options.Predefined != nil { for _, entry := range options.Predefined.Entries() { predefined[mDNS.CanonicalName(entry.Key)] = entry.Value } } return &Transport{ TransportAdapter: dns.NewTransportAdapter(C.DNSTypeHosts, tag, nil), files: files, predefined: predefined, }, nil } 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 { 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) if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA { if addresses, ok := t.predefined[domain]; ok { return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil } for _, file := range t.files { addresses := file.Lookup(domain) if len(addresses) > 0 { return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil } } } return &mDNS.Msg{ MsgHdr: mDNS.MsgHdr{ Id: message.Id, Rcode: mDNS.RcodeNameError, Response: true, }, Question: []mDNS.Question{question}, }, nil } ================================================ FILE: dns/transport/hosts/hosts_file.go ================================================ package hosts import ( "bufio" "errors" "io" "net/netip" "os" "strings" "sync" "time" E "github.com/sagernet/sing/common/exceptions" "github.com/miekg/dns" ) const cacheMaxAge = 5 * time.Second type File struct { path string access sync.Mutex byName map[string][]netip.Addr expire time.Time modTime time.Time size int64 } func NewFile(path string) *File { return &File{ path: path, } } 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() f.update() return f.byName[dns.CanonicalName(name)] } func (f *File) update() { now := time.Now() if now.Before(f.expire) && len(f.byName) > 0 { return } stat, err := os.Stat(f.path) if err != nil { return } if f.modTime.Equal(stat.ModTime()) && f.size == stat.Size() { f.expire = now.Add(cacheMaxAge) return } byName := make(map[string][]netip.Addr) file, err := os.Open(f.path) if err != nil { return } defer file.Close() reader := bufio.NewReader(file) var ( prefix []byte line []byte isPrefix bool ) for { line, isPrefix, err = reader.ReadLine() if err != nil { if errors.Is(err, io.EOF) { break } return } if isPrefix { prefix = append(prefix, line...) continue } else if len(prefix) > 0 { line = append(prefix, line...) prefix = nil } commentIndex := strings.IndexRune(string(line), '#') if commentIndex != -1 { line = line[:commentIndex] } fields := strings.Fields(string(line)) if len(fields) < 2 { continue } var addr netip.Addr addr, err = netip.ParseAddr(fields[0]) if err != nil { continue } for index := 1; index < len(fields); index++ { canonicalName := dns.CanonicalName(fields[index]) byName[canonicalName] = append(byName[canonicalName], addr) } } f.expire = now.Add(cacheMaxAge) f.modTime = stat.ModTime() f.size = stat.Size() f.byName = byName } ================================================ FILE: dns/transport/hosts/hosts_test.go ================================================ package hosts import ( "net/netip" "os" "runtime" "testing" 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()}, 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)) } } } ================================================ FILE: dns/transport/hosts/hosts_unix.go ================================================ //go:build !windows package hosts func defaultPath() (string, error) { return "/etc/hosts", nil } ================================================ FILE: dns/transport/hosts/hosts_windows.go ================================================ package hosts import ( "path/filepath" "sync" "golang.org/x/sys/windows" ) var defaultPath = sync.OnceValues(func() (string, error) { systemDirectory, err := windows.GetSystemDirectory() if err != nil { return "", err } return filepath.Join(systemDirectory, "Drivers", "etc", "hosts"), nil }) ================================================ FILE: dns/transport/hosts/testdata/hosts ================================================ 127.0.0.1 localhost ::1 localhost ================================================ FILE: dns/transport/https.go ================================================ package transport import ( "bytes" "context" "errors" "io" "net" "net/http" "net/url" "strconv" "sync" "time" "github.com/sagernet/sing-box/adapter" "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/dns" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" 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" sHTTP "github.com/sagernet/sing/protocol/http" mDNS "github.com/miekg/dns" "golang.org/x/net/http2" ) const MimeType = "application/dns-message" var _ adapter.DNSTransport = (*HTTPSTransport)(nil) func RegisterHTTPS(registry *dns.TransportRegistry) { dns.RegisterTransport[option.RemoteHTTPSDNSServerOptions](registry, C.DNSTypeHTTPS, NewHTTPS) } type HTTPSTransport struct { dns.TransportAdapter logger logger.ContextLogger dialer N.Dialer destination *url.URL headers http.Header transportAccess sync.Mutex transport *HTTPSTransportWrapper transportResetAt time.Time } func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) { transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions) if err != nil { return nil, err } tlsOptions := common.PtrValueOrDefault(options.TLS) tlsOptions.Enabled = true tlsConfig, err := tls.NewClient(ctx, logger, options.Server, tlsOptions) if err != nil { return nil, err } if len(tlsConfig.NextProtos()) == 0 { tlsConfig.SetNextProtos([]string{http2.NextProtoTLS, "http/1.1"}) } headers := options.Headers.Build() host := headers.Get("Host") if host != "" { headers.Del("Host") } else { if tlsConfig.ServerName() != "" { host = tlsConfig.ServerName() } else { host = options.Server } } destinationURL := url.URL{ Scheme: "https", Host: host, } if destinationURL.Host == "" { destinationURL.Host = options.Server } if options.ServerPort != 0 && options.ServerPort != 443 { destinationURL.Host = net.JoinHostPort(destinationURL.Host, strconv.Itoa(int(options.ServerPort))) } path := options.Path if path == "" { path = "/dns-query" } err = sHTTP.URLSetPath(&destinationURL, path) if err != nil { return nil, err } serverAddr := options.DNSServerAddressOptions.Build() if serverAddr.Port == 0 { serverAddr.Port = 443 } if !serverAddr.IsValid() { return nil, E.New("invalid server address: ", serverAddr) } return NewHTTPSRaw( dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTPS, tag, options.RemoteDNSServerOptions), logger, transportDialer, &destinationURL, headers, serverAddr, tlsConfig, ), nil } func NewHTTPSRaw( adapter dns.TransportAdapter, logger log.ContextLogger, dialer N.Dialer, destination *url.URL, headers http.Header, serverAddr M.Socksaddr, tlsConfig tls.Config, ) *HTTPSTransport { return &HTTPSTransport{ TransportAdapter: adapter, logger: logger, dialer: dialer, destination: destination, headers: headers, transport: NewHTTPSTransportWrapper(tls.NewDialer(dialer, tlsConfig), serverAddr), } } func (t *HTTPSTransport) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } return dialer.InitializeDetour(t.dialer) } func (t *HTTPSTransport) Close() error { t.Reset() return nil } func (t *HTTPSTransport) Reset() { t.transportAccess.Lock() defer t.transportAccess.Unlock() t.transport.CloseIdleConnections() t.transport = t.transport.Clone() } func (t *HTTPSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { startAt := time.Now() response, err := t.exchange(ctx, message) if err != nil { if errors.Is(err, context.DeadlineExceeded) { t.transportAccess.Lock() defer t.transportAccess.Unlock() if t.transportResetAt.After(startAt) { return nil, err } t.transport.CloseIdleConnections() t.transport = t.transport.Clone() t.transportResetAt = time.Now() } return nil, err } return response, nil } func (t *HTTPSTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { exMessage := *message exMessage.Id = 0 exMessage.Compress = true requestBuffer := buf.NewSize(1 + message.Len()) rawMessage, err := exMessage.PackBuffer(requestBuffer.FreeBytes()) if err != nil { requestBuffer.Release() return nil, err } request, err := http.NewRequestWithContext(ctx, http.MethodPost, t.destination.String(), bytes.NewReader(rawMessage)) if err != nil { requestBuffer.Release() return nil, err } request.Header = t.headers.Clone() request.Header.Set("Content-Type", MimeType) request.Header.Set("Accept", MimeType) t.transportAccess.Lock() currentTransport := t.transport t.transportAccess.Unlock() response, err := currentTransport.RoundTrip(request) requestBuffer.Release() if err != nil { return nil, err } defer response.Body.Close() if response.StatusCode != http.StatusOK { return nil, E.New("unexpected status: ", response.Status) } var responseMessage mDNS.Msg if response.ContentLength > 0 { responseBuffer := buf.NewSize(int(response.ContentLength)) defer responseBuffer.Release() _, err = responseBuffer.ReadFullFrom(response.Body, int(response.ContentLength)) if err != nil { return nil, err } err = responseMessage.Unpack(responseBuffer.Bytes()) } else { rawMessage, err = io.ReadAll(response.Body) if err != nil { return nil, err } err = responseMessage.Unpack(rawMessage) } if err != nil { return nil, err } return &responseMessage, nil } ================================================ FILE: dns/transport/https_transport.go ================================================ package transport import ( "context" "errors" "net" "net/http" "sync/atomic" "github.com/sagernet/sing-box/common/tls" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" "golang.org/x/net/http2" ) var errFallback = E.New("fallback to HTTP/1.1") type HTTPSTransportWrapper struct { http2Transport *http2.Transport httpTransport *http.Transport fallback *atomic.Bool } func NewHTTPSTransportWrapper(dialer tls.Dialer, serverAddr M.Socksaddr) *HTTPSTransportWrapper { var fallback atomic.Bool return &HTTPSTransportWrapper{ http2Transport: &http2.Transport{ DialTLSContext: func(ctx context.Context, _, _ string, _ *tls.STDConfig) (net.Conn, error) { tlsConn, err := dialer.DialTLSContext(ctx, serverAddr) if err != nil { return nil, err } state := tlsConn.ConnectionState() if state.NegotiatedProtocol == http2.NextProtoTLS { return tlsConn, nil } tlsConn.Close() fallback.Store(true) return nil, errFallback }, }, httpTransport: &http.Transport{ DialTLSContext: func(ctx context.Context, _, _ string) (net.Conn, error) { return dialer.DialTLSContext(ctx, serverAddr) }, }, fallback: &fallback, } } func (h *HTTPSTransportWrapper) RoundTrip(request *http.Request) (*http.Response, error) { if h.fallback.Load() { return h.httpTransport.RoundTrip(request) } else { response, err := h.http2Transport.RoundTrip(request) if err != nil { if errors.Is(err, errFallback) { return h.httpTransport.RoundTrip(request) } return nil, err } return response, nil } } func (h *HTTPSTransportWrapper) CloseIdleConnections() { h.http2Transport.CloseIdleConnections() h.httpTransport.CloseIdleConnections() } func (h *HTTPSTransportWrapper) Clone() *HTTPSTransportWrapper { return &HTTPSTransportWrapper{ httpTransport: h.httpTransport, http2Transport: &http2.Transport{ DialTLSContext: h.http2Transport.DialTLSContext, }, fallback: h.fallback, } } ================================================ FILE: dns/transport/local/local.go ================================================ 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/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" 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 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 { 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 } 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 && isSystemdResolvedManaged() { resolvedResolver, err := NewResolvedResolver(t.ctx, t.logger) if err == nil { err = resolvedResolver.Start() if err == nil { 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 { 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 { if len(t.hosts.Lookup(dns.FqdnToDomain(domain))) > 0 { return true } } return t.hasNeighborHost(domain) || mdns.IsLocalDomain(domain) } func (t *Transport) Exchange(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, C.DefaultDNSTTL), nil } } response := t.lookupNeighbor(message) 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) } ================================================ FILE: dns/transport/local/local_darwin_cgo.go ================================================ //go:build darwin package local /* #include #include #include 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); 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; } */ import "C" import ( "context" "errors" "unsafe" "github.com/sagernet/sing-box/dns" E "github.com/sagernet/sing/common/exceptions" mDNS "github.com/miekg/dns" ) const ( darwinResolverHostNotFound = 1 darwinResolverTryAgain = 2 darwinResolverNoRecovery = 3 darwinResolverNoData = 4 ) func darwinLookupSystemDNS(name string, class, qtype int) (*mDNS.Msg, error) { cName := C.CString(name) defer C.free(unsafe.Pointer(cName)) 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)) } var response mDNS.Msg err := response.Unpack(answer[:int(n)]) if err != nil { return nil, E.Cause(err, "unpack dns_search response") } return &response, nil } func darwinResolverHErrno(name string, hErrno int) error { switch hErrno { case darwinResolverHostNotFound: return dns.RcodeNameError case darwinResolverNoData: return dns.RcodeSuccess case darwinResolverTryAgain, darwinResolverNoRecovery: return dns.RcodeServerFailure default: return E.New("dns_search: unknown h_errno ", hErrno, " for ", name) } } func (t *Transport) systemExchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { question := message.Question[0] type resolvResult struct { response *mDNS.Msg err error } resultCh := make(chan resolvResult, 1) go func() { response, err := darwinLookupSystemDNS(question.Name, int(question.Qclass), int(question.Qtype)) 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 // 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 } ================================================ FILE: dns/transport/local/local_darwin_stun.go ================================================ //go:build darwin && !cgo package local import ( "context" E "github.com/sagernet/sing/common/exceptions" mDNS "github.com/miekg/dns" ) func (t *Transport) systemExchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { return nil, E.New(`local DNS server requires CGO on darwin, rebuild with CGO_ENABLED=1`) } ================================================ FILE: dns/transport/local/local_dhcp.go ================================================ //go:build with_dhcp package local import ( "context" "github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/dns/transport/dhcp" "github.com/sagernet/sing-box/log" N "github.com/sagernet/sing/common/network" ) func newDHCPTransport(transportAdapter dns.TransportAdapter, ctx context.Context, dialer N.Dialer, logger log.ContextLogger) dhcpTransport { return dhcp.NewRawTransport(transportAdapter, ctx, dialer, logger) } ================================================ FILE: dns/transport/local/local_neighbor.go ================================================ 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 (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) { continue } host := canonical[:len(canonical)-len(suffix)] if !strings.ContainsRune(host, '.') { return host } } return "" } ================================================ FILE: dns/transport/local/local_nodhcp.go ================================================ //go:build !with_dhcp package local import ( "context" "github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/log" N "github.com/sagernet/sing/common/network" ) func newDHCPTransport(transportAdapter dns.TransportAdapter, ctx context.Context, dialer N.Dialer, logger log.ContextLogger) dhcpTransport { return nil } ================================================ FILE: dns/transport/local/local_other.go ================================================ //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 } ================================================ FILE: dns/transport/local/local_resolved.go ================================================ package local import ( "context" mDNS "github.com/miekg/dns" ) type ResolvedResolver interface { Start() error Close() error Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) } ================================================ FILE: dns/transport/local/local_resolved_linux.go ================================================ package local import ( "bufio" "context" "errors" "net/netip" "os" "strings" "sync" "sync/atomic" "github.com/sagernet/sing-box/adapter" "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/dns" dnsTransport "github.com/sagernet/sing-box/dns/transport" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/service/resolved" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/control" 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/common/x/list" "github.com/sagernet/sing/service" "github.com/godbus/dbus/v5" mDNS "github.com/miekg/dns" ) func isSystemdResolvedManaged() bool { resolvContent, err := os.Open("/etc/resolv.conf") if err != nil { return false } defer resolvContent.Close() scanner := bufio.NewScanner(resolvContent) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || line[0] != '#' { return false } if strings.Contains(line, "systemd-resolved") { return true } } return false } type DBusResolvedResolver struct { ctx context.Context logger logger.ContextLogger interfaceMonitor tun.DefaultInterfaceMonitor interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback] systemBus *dbus.Conn savedServerSet atomic.Pointer[resolvedServerSet] closeOnce sync.Once } type resolvedServerSet struct { servers []resolvedServer } type resolvedServer struct { primaryTransport adapter.DNSTransport fallbackTransport adapter.DNSTransport } type resolvedServerSpecification struct { address netip.Addr port uint16 serverName string } func NewResolvedResolver(ctx context.Context, logger logger.ContextLogger) (ResolvedResolver, error) { interfaceMonitor := service.FromContext[adapter.NetworkManager](ctx).InterfaceMonitor() if interfaceMonitor == nil { return nil, os.ErrInvalid } systemBus, err := dbus.SystemBus() if err != nil { return nil, err } return &DBusResolvedResolver{ ctx: ctx, logger: logger, interfaceMonitor: interfaceMonitor, systemBus: systemBus, }, nil } func (t *DBusResolvedResolver) Start() error { t.updateStatus() t.interfaceCallback = t.interfaceMonitor.RegisterCallback(t.updateDefaultInterface) err := t.systemBus.BusObject().AddMatchSignal( "org.freedesktop.DBus", "NameOwnerChanged", dbus.WithMatchSender("org.freedesktop.DBus"), dbus.WithMatchArg(0, "org.freedesktop.resolve1"), ).Err if err != nil { return E.Cause(err, "configure resolved restart listener") } err = t.systemBus.BusObject().AddMatchSignal( "org.freedesktop.DBus.Properties", "PropertiesChanged", dbus.WithMatchSender("org.freedesktop.resolve1"), dbus.WithMatchArg(0, "org.freedesktop.resolve1.Manager"), ).Err if err != nil { return E.Cause(err, "configure resolved properties listener") } go t.loopUpdateStatus() return nil } func (t *DBusResolvedResolver) Close() error { var closeErr error t.closeOnce.Do(func() { serverSet := t.savedServerSet.Swap(nil) if serverSet != nil { closeErr = serverSet.Close() } if t.interfaceCallback != nil { t.interfaceMonitor.UnregisterCallback(t.interfaceCallback) } if t.systemBus != nil { _ = t.systemBus.Close() } }) return closeErr } func (t *DBusResolvedResolver) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { serverSet := t.savedServerSet.Load() if serverSet == nil { var err error serverSet, err = t.checkResolved(context.Background()) if err != nil { return nil, err } previousServerSet := t.savedServerSet.Swap(serverSet) if previousServerSet != nil { _ = previousServerSet.Close() } } response, err := t.exchangeServerSet(ctx, message, serverSet) if err == nil { return response, nil } t.updateStatus() refreshedServerSet := t.savedServerSet.Load() if refreshedServerSet == nil || refreshedServerSet == serverSet { return nil, err } return t.exchangeServerSet(ctx, message, refreshedServerSet) } func (t *DBusResolvedResolver) loopUpdateStatus() { signalChan := make(chan *dbus.Signal, 1) t.systemBus.Signal(signalChan) for signal := range signalChan { switch signal.Name { case "org.freedesktop.DBus.NameOwnerChanged": if len(signal.Body) != 3 { continue } newOwner, loaded := signal.Body[2].(string) if !loaded || newOwner == "" { continue } t.updateStatus() case "org.freedesktop.DBus.Properties.PropertiesChanged": if !shouldUpdateResolvedServerSet(signal) { continue } t.updateStatus() } } } func (t *DBusResolvedResolver) updateStatus() { serverSet, err := t.checkResolved(context.Background()) oldServerSet := t.savedServerSet.Swap(serverSet) if oldServerSet != nil { _ = oldServerSet.Close() } if err != nil { var dbusErr dbus.Error if !errors.As(err, &dbusErr) || dbusErr.Name != "org.freedesktop.DBus.Error.NameHasNoOwner" { t.logger.Debug(E.Cause(err, "systemd-resolved service unavailable")) } if oldServerSet != nil { t.logger.Debug("systemd-resolved service is gone") } return } else if oldServerSet == nil { t.logger.Debug("using systemd-resolved service as resolver") } } func (t *DBusResolvedResolver) exchangeServerSet(ctx context.Context, message *mDNS.Msg, serverSet *resolvedServerSet) (*mDNS.Msg, error) { if serverSet == nil || len(serverSet.servers) == 0 { return nil, E.New("link has no DNS servers configured") } var lastError error for _, server := range serverSet.servers { response, err := server.primaryTransport.Exchange(ctx, message) if err != nil && server.fallbackTransport != nil { response, err = server.fallbackTransport.Exchange(ctx, message) } if err != nil { lastError = err continue } return response, nil } return nil, lastError } func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*resolvedServerSet, error) { dbusObject := t.systemBus.Object("org.freedesktop.resolve1", "/org/freedesktop/resolve1") err := dbusObject.Call("org.freedesktop.DBus.Peer.Ping", 0).Err if err != nil { return nil, err } defaultInterface := t.interfaceMonitor.DefaultInterface() if defaultInterface == nil { return nil, E.New("missing default interface") } call := dbusObject.(*dbus.Object).CallWithContext( ctx, "org.freedesktop.resolve1.Manager.GetLink", 0, int32(defaultInterface.Index), ) if call.Err != nil { return nil, call.Err } var linkPath dbus.ObjectPath err = call.Store(&linkPath) if err != nil { return nil, err } linkObject := t.systemBus.Object("org.freedesktop.resolve1", linkPath) if linkObject == nil { return nil, E.New("missing link object for default interface") } dnsOverTLSMode, err := loadResolvedLinkDNSOverTLS(linkObject) if err != nil { return nil, err } linkDNSEx, err := loadResolvedLinkDNSEx(linkObject) if err != nil { return nil, err } linkDNS, err := loadResolvedLinkDNS(linkObject) if err != nil { return nil, err } if len(linkDNSEx) == 0 && len(linkDNS) == 0 { for _, inbound := range service.FromContext[adapter.InboundManager](t.ctx).Inbounds() { if inbound.Type() == C.TypeTun { return nil, E.New("No appropriate name servers or networks for name found") } } return nil, E.New("link has no DNS servers configured") } serverDialer, err := dialer.NewDefault(t.ctx, option.DialerOptions{ BindInterface: defaultInterface.Name, UDPFragmentDefault: true, }) if err != nil { return nil, err } var serverSpecifications []resolvedServerSpecification if len(linkDNSEx) > 0 { for _, entry := range linkDNSEx { serverSpecification, loaded := buildResolvedServerSpecification(defaultInterface.Name, entry.Address, entry.Port, entry.Name) if !loaded { continue } serverSpecifications = append(serverSpecifications, serverSpecification) } } else { for _, entry := range linkDNS { serverSpecification, loaded := buildResolvedServerSpecification(defaultInterface.Name, entry.Address, 0, "") if !loaded { continue } serverSpecifications = append(serverSpecifications, serverSpecification) } } if len(serverSpecifications) == 0 { return nil, E.New("no valid DNS servers on link") } serverSet := &resolvedServerSet{ servers: make([]resolvedServer, 0, len(serverSpecifications)), } for _, serverSpecification := range serverSpecifications { server, createErr := t.createResolvedServer(serverDialer, dnsOverTLSMode, serverSpecification) if createErr != nil { _ = serverSet.Close() return nil, createErr } serverSet.servers = append(serverSet.servers, server) } return serverSet, nil } func (t *DBusResolvedResolver) createResolvedServer(serverDialer N.Dialer, dnsOverTLSMode string, serverSpecification resolvedServerSpecification) (resolvedServer, error) { if dnsOverTLSMode == "yes" { primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, true) if err != nil { return resolvedServer{}, err } return resolvedServer{ primaryTransport: primaryTransport, }, nil } if dnsOverTLSMode == "opportunistic" { primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, true) if err != nil { return resolvedServer{}, err } fallbackTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, false) if err != nil { _ = primaryTransport.Close() return resolvedServer{}, err } return resolvedServer{ primaryTransport: primaryTransport, fallbackTransport: fallbackTransport, }, nil } primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, false) if err != nil { return resolvedServer{}, err } return resolvedServer{ primaryTransport: primaryTransport, }, nil } func (t *DBusResolvedResolver) createResolvedTransport(serverDialer N.Dialer, serverSpecification resolvedServerSpecification, useTLS bool) (adapter.DNSTransport, error) { serverAddress := M.SocksaddrFrom(serverSpecification.address, resolvedServerPort(serverSpecification.port, useTLS)) if useTLS { tlsAddress := serverSpecification.address if tlsAddress.Zone() != "" { tlsAddress = tlsAddress.WithZone("") } serverName := serverSpecification.serverName if serverName == "" { serverName = tlsAddress.String() } tlsConfig, err := tls.NewClient(t.ctx, t.logger, tlsAddress.String(), option.OutboundTLSOptions{ Enabled: true, ServerName: serverName, }) if err != nil { return nil, err } serverTransport := dnsTransport.NewTLSRaw(t.logger, dns.NewTransportAdapter(C.DNSTypeTLS, "", nil), serverDialer, serverAddress, tlsConfig) err = serverTransport.Start(adapter.StartStateStart) if err != nil { _ = serverTransport.Close() return nil, err } return serverTransport, nil } serverTransport := dnsTransport.NewUDPRaw(t.logger, dns.NewTransportAdapter(C.DNSTypeUDP, "", nil), serverDialer, serverAddress) err := serverTransport.Start(adapter.StartStateStart) if err != nil { _ = serverTransport.Close() return nil, err } return serverTransport, nil } func (s *resolvedServerSet) Close() error { var errors []error for _, server := range s.servers { errors = append(errors, server.primaryTransport.Close()) if server.fallbackTransport != nil { errors = append(errors, server.fallbackTransport.Close()) } } return E.Errors(errors...) } func buildResolvedServerSpecification(interfaceName string, rawAddress []byte, port uint16, serverName string) (resolvedServerSpecification, bool) { address, loaded := netip.AddrFromSlice(rawAddress) if !loaded { return resolvedServerSpecification{}, false } if address.Is6() && address.IsLinkLocalUnicast() && address.Zone() == "" { address = address.WithZone(interfaceName) } return resolvedServerSpecification{ address: address, port: port, serverName: serverName, }, true } func resolvedServerPort(port uint16, useTLS bool) uint16 { if port > 0 { return port } if useTLS { return 853 } return 53 } func loadResolvedLinkDNS(linkObject dbus.BusObject) ([]resolved.LinkDNS, error) { dnsProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNS") if err != nil { if isResolvedUnknownPropertyError(err) { return nil, nil } return nil, err } var linkDNS []resolved.LinkDNS err = dnsProperty.Store(&linkDNS) if err != nil { return nil, err } return linkDNS, nil } func loadResolvedLinkDNSEx(linkObject dbus.BusObject) ([]resolved.LinkDNSEx, error) { dnsProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNSEx") if err != nil { if isResolvedUnknownPropertyError(err) { return nil, nil } return nil, err } var linkDNSEx []resolved.LinkDNSEx err = dnsProperty.Store(&linkDNSEx) if err != nil { return nil, err } return linkDNSEx, nil } func loadResolvedLinkDNSOverTLS(linkObject dbus.BusObject) (string, error) { dnsOverTLSProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNSOverTLS") if err != nil { if isResolvedUnknownPropertyError(err) { return "", nil } return "", err } var dnsOverTLSMode string err = dnsOverTLSProperty.Store(&dnsOverTLSMode) if err != nil { return "", err } return dnsOverTLSMode, nil } func isResolvedUnknownPropertyError(err error) bool { var dbusError dbus.Error return errors.As(err, &dbusError) && dbusError.Name == "org.freedesktop.DBus.Error.UnknownProperty" } func shouldUpdateResolvedServerSet(signal *dbus.Signal) bool { if len(signal.Body) != 3 { return true } changedProperties, loaded := signal.Body[1].(map[string]dbus.Variant) if !loaded { return true } for propertyName := range changedProperties { switch propertyName { case "DNS", "DNSEx", "DNSOverTLS": return true } } invalidatedProperties, loaded := signal.Body[2].([]string) if !loaded { return true } for _, propertyName := range invalidatedProperties { switch propertyName { case "DNS", "DNSEx", "DNSOverTLS": return true } } return false } func (t *DBusResolvedResolver) updateDefaultInterface(defaultInterface *control.Interface, flags int) { t.updateStatus() } ================================================ FILE: dns/transport/local/local_resolved_stub.go ================================================ //go:build !linux //nolint:unused package local import ( "context" "os" "github.com/sagernet/sing/common/logger" ) func isSystemdResolvedManaged() bool { return false } func NewResolvedResolver(ctx context.Context, logger logger.ContextLogger) (ResolvedResolver, error) { return nil, os.ErrInvalid } ================================================ FILE: dns/transport/local/local_shared.go ================================================ package local import ( "context" "errors" "math/rand" "syscall" "time" "github.com/sagernet/sing-box/dns/transport" "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" mDNS "github.com/miekg/dns" ) func (t *Transport) exchange(ctx context.Context, message *mDNS.Msg, domain string) (*mDNS.Msg, error) { systemConfig := getSystemDNSConfig(t.ctx) if systemConfig.singleRequest || !(message.Question[0].Qtype == mDNS.TypeA || message.Question[0].Qtype == mDNS.TypeAAAA) { return t.exchangeSingleRequest(ctx, systemConfig, message, domain) } else { return t.exchangeParallel(ctx, systemConfig, message, domain) } } func (t *Transport) exchangeSingleRequest(ctx context.Context, systemConfig *dnsConfig, message *mDNS.Msg, domain string) (*mDNS.Msg, error) { var lastErr error for _, fqdn := range systemConfig.nameList(domain) { response, err := t.tryOneName(ctx, systemConfig, fqdn, message) if err != nil { lastErr = err continue } return response, nil } return nil, lastErr } func (t *Transport) exchangeParallel(ctx context.Context, systemConfig *dnsConfig, message *mDNS.Msg, domain string) (*mDNS.Msg, error) { returned := make(chan struct{}) defer close(returned) type queryResult struct { response *mDNS.Msg err error } results := make(chan queryResult) startRacer := func(ctx context.Context, fqdn string) { response, err := t.tryOneName(ctx, systemConfig, fqdn, message) select { case results <- queryResult{response, err}: case <-returned: } } queryCtx, queryCancel := context.WithCancel(ctx) defer queryCancel() var nameCount int for _, fqdn := range systemConfig.nameList(domain) { nameCount++ go startRacer(queryCtx, fqdn) } var errors []error for { select { case <-ctx.Done(): return nil, ctx.Err() case result := <-results: if result.err == nil { return result.response, nil } errors = append(errors, result.err) if len(errors) == nameCount { return nil, E.Errors(errors...) } } } } func (t *Transport) tryOneName(ctx context.Context, config *dnsConfig, fqdn string, message *mDNS.Msg) (*mDNS.Msg, error) { serverOffset := config.serverOffset() sLen := uint32(len(config.servers)) var lastErr error for i := 0; i < config.attempts; i++ { for j := range sLen { server := config.servers[(serverOffset+j)%sLen] question := message.Question[0] question.Name = fqdn response, err := t.exchangeOne(ctx, M.ParseSocksaddr(server), question, config.timeout, config.useTCP, config.trustAD) if err != nil { lastErr = err continue } return response, nil } } return nil, E.Cause(lastErr, fqdn) } func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, question mDNS.Question, timeout time.Duration, useTCP, ad bool) (*mDNS.Msg, error) { if server.Port == 0 { server.Port = 53 } request := &mDNS.Msg{ MsgHdr: mDNS.MsgHdr{ Id: uint16(rand.Uint32()), RecursionDesired: true, AuthenticatedData: ad, }, Question: []mDNS.Question{question}, Compress: true, } request.SetEdns0(buf.UDPBufferSize, false) if !useTCP { return t.exchangeUDP(ctx, server, request, timeout) } else { return t.exchangeTCP(ctx, server, request, timeout) } } func (t *Transport) exchangeUDP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg, timeout time.Duration) (*mDNS.Msg, error) { conn, err := t.dialer.DialContext(ctx, N.NetworkUDP, server) if err != nil { return nil, err } defer conn.Close() if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() { newDeadline := time.Now().Add(timeout) if deadline.After(newDeadline) { deadline = newDeadline } conn.SetDeadline(deadline) } buffer := buf.Get(buf.UDPBufferSize) defer buf.Put(buffer) rawMessage, err := request.PackBuffer(buffer) if err != nil { return nil, E.Cause(err, "pack request") } _, err = conn.Write(rawMessage) if err != nil { if errors.Is(err, syscall.EMSGSIZE) { return t.exchangeTCP(ctx, server, request, timeout) } return nil, E.Cause(err, "write request") } n, err := conn.Read(buffer) if err != nil { if errors.Is(err, syscall.EMSGSIZE) { return t.exchangeTCP(ctx, server, request, timeout) } return nil, E.Cause(err, "read response") } var response mDNS.Msg err = response.Unpack(buffer[:n]) if err != nil { return nil, E.Cause(err, "unpack response") } if response.Truncated { return t.exchangeTCP(ctx, server, request, timeout) } return &response, nil } func (t *Transport) exchangeTCP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg, timeout time.Duration) (*mDNS.Msg, error) { conn, err := t.dialer.DialContext(ctx, N.NetworkTCP, server) if err != nil { return nil, err } defer conn.Close() if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() { newDeadline := time.Now().Add(timeout) if deadline.After(newDeadline) { deadline = newDeadline } conn.SetDeadline(deadline) } err = transport.WriteMessage(conn, 0, request) if err != nil { return nil, err } return transport.ReadMessage(conn) } ================================================ FILE: dns/transport/local/resolv.go ================================================ //nolint:unused package local import ( "context" "os" "runtime" "strings" "sync" "sync/atomic" "time" ) type resolverConfig struct { initOnce sync.Once ch chan struct{} lastChecked time.Time dnsConfig atomic.Pointer[dnsConfig] } var resolvConf resolverConfig func getSystemDNSConfig(ctx context.Context) *dnsConfig { resolvConf.tryUpdate(ctx, "/etc/resolv.conf") return resolvConf.dnsConfig.Load() } func (conf *resolverConfig) init(ctx context.Context) { conf.dnsConfig.Store(dnsReadConfig(ctx, "/etc/resolv.conf")) conf.lastChecked = time.Now() conf.ch = make(chan struct{}, 1) } func (conf *resolverConfig) tryUpdate(ctx context.Context, name string) { conf.initOnce.Do(func() { conf.init(ctx) }) if conf.dnsConfig.Load().noReload { return } if !conf.tryAcquireSema() { return } defer conf.releaseSema() now := time.Now() if conf.lastChecked.After(now.Add(-5 * time.Second)) { return } conf.lastChecked = now if runtime.GOOS != "windows" { var mtime time.Time if fi, err := os.Stat(name); err == nil { mtime = fi.ModTime() } if mtime.Equal(conf.dnsConfig.Load().mtime) { return } } dnsConf := dnsReadConfig(ctx, name) conf.dnsConfig.Store(dnsConf) } func (conf *resolverConfig) tryAcquireSema() bool { select { case conf.ch <- struct{}{}: return true default: return false } } func (conf *resolverConfig) releaseSema() { <-conf.ch } type dnsConfig struct { servers []string search []string ndots int timeout time.Duration attempts int rotate bool unknownOpt bool lookup []string err error mtime time.Time soffset uint32 singleRequest bool useTCP bool trustAD bool noReload bool } func (c *dnsConfig) serverOffset() uint32 { if c.rotate { return atomic.AddUint32(&c.soffset, 1) - 1 // return 0 to start } return 0 } func (c *dnsConfig) nameList(name string) []string { l := len(name) rooted := l > 0 && name[l-1] == '.' if l > 254 || l == 254 && !rooted { return nil } if rooted { if avoidDNS(name) { return nil } return []string{name} } hasNdots := strings.Count(name, ".") >= c.ndots name += "." // l++ names := make([]string, 0, 1+len(c.search)) if hasNdots && !avoidDNS(name) { names = append(names, name) } for _, suffix := range c.search { fqdn := name + suffix if !avoidDNS(fqdn) && len(fqdn) <= 254 { names = append(names, fqdn) } } if !hasNdots && !avoidDNS(name) { names = append(names, name) } return names } func avoidDNS(name string) bool { if name == "" { return true } if name[len(name)-1] == '.' { name = name[:len(name)-1] } return strings.HasSuffix(name, ".onion") } ================================================ FILE: dns/transport/local/resolv_default.go ================================================ //nolint:unused package local import ( "os" "strings" _ "unsafe" "github.com/miekg/dns" ) //go:linkname defaultNS net.defaultNS var defaultNS []string func dnsDefaultSearch() []string { hn, err := os.Hostname() if err != nil { return nil } if i := strings.IndexRune(hn, '.'); i >= 0 && i < len(hn)-1 { return []string{dns.Fqdn(hn[i+1:])} } return nil } ================================================ FILE: dns/transport/local/resolv_test.go ================================================ package local import ( "context" "testing" "github.com/stretchr/testify/require" ) func TestDNSReadConfig(t *testing.T) { t.Parallel() require.NoError(t, dnsReadConfig(context.Background(), "/etc/resolv.conf").err) } ================================================ FILE: dns/transport/local/resolv_unix.go ================================================ //go:build !windows package local import ( "bufio" "context" "net" "net/netip" "os" "strings" "time" "github.com/miekg/dns" ) func dnsReadConfig(_ context.Context, name string) *dnsConfig { conf := &dnsConfig{ ndots: 1, timeout: 5 * time.Second, attempts: 2, } file, err := os.Open(name) if err != nil { conf.servers = defaultNS conf.search = dnsDefaultSearch() conf.err = err return conf } defer file.Close() fi, err := file.Stat() if err == nil { conf.mtime = fi.ModTime() } else { conf.servers = defaultNS conf.search = dnsDefaultSearch() conf.err = err return conf } reader := bufio.NewReader(file) var ( prefix []byte line []byte isPrefix bool ) for { line, isPrefix, err = reader.ReadLine() if err != nil { break } if isPrefix { prefix = append(prefix, line...) continue } else if len(prefix) > 0 { line = append(prefix, line...) prefix = nil } if len(line) > 0 && (line[0] == ';' || line[0] == '#') { continue } f := strings.Fields(string(line)) if len(f) < 1 { continue } switch f[0] { case "nameserver": if len(f) > 1 && len(conf.servers) < 3 { if _, err := netip.ParseAddr(f[1]); err == nil { conf.servers = append(conf.servers, net.JoinHostPort(f[1], "53")) } } case "domain": if len(f) > 1 { conf.search = []string{dns.Fqdn(f[1])} } case "search": conf.search = make([]string, 0, len(f)-1) for i := 1; i < len(f); i++ { name := dns.Fqdn(f[i]) if name == "." { continue } conf.search = append(conf.search, name) } case "options": for _, s := range f[1:] { switch { case strings.HasPrefix(s, "ndots:"): n, _, _ := dtoi(s[6:]) if n < 0 { n = 0 } else if n > 15 { n = 15 } conf.ndots = n case strings.HasPrefix(s, "timeout:"): n, _, _ := dtoi(s[8:]) if n < 1 { n = 1 } conf.timeout = time.Duration(n) * time.Second case strings.HasPrefix(s, "attempts:"): n, _, _ := dtoi(s[9:]) if n < 1 { n = 1 } conf.attempts = n case s == "rotate": conf.rotate = true case s == "single-request" || s == "single-request-reopen": conf.singleRequest = true case s == "use-vc" || s == "usevc" || s == "tcp": conf.useTCP = true case s == "trust-ad": conf.trustAD = true case s == "edns0": case s == "no-reload": conf.noReload = true default: conf.unknownOpt = true } } case "lookup": conf.lookup = f[1:] default: conf.unknownOpt = true } } if len(conf.servers) == 0 { conf.servers = defaultNS } if len(conf.search) == 0 { conf.search = dnsDefaultSearch() } return conf } const big = 0xFFFFFF func dtoi(s string) (n int, i int, ok bool) { n = 0 for i = 0; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ { n = n*10 + int(s[i]-'0') if n >= big { return big, i, false } } if i == 0 { return 0, 0, false } return n, i, true } ================================================ FILE: dns/transport/local/resolv_windows.go ================================================ package local import ( "context" "net" "net/netip" "os" "strconv" "syscall" "time" "unsafe" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing/service" "golang.org/x/sys/windows" ) func dnsReadConfig(ctx context.Context, _ string) *dnsConfig { conf := &dnsConfig{ ndots: 1, timeout: 5 * time.Second, attempts: 2, } defer func() { if len(conf.servers) == 0 { conf.servers = defaultNS } }() addresses, err := adapterAddresses() if err != nil { return nil } var dnsAddresses []struct { ifName string netip.Addr } for _, address := range addresses { if address.OperStatus != windows.IfOperStatusUp { continue } if address.IfType == windows.IF_TYPE_TUNNEL { continue } if address.FirstGatewayAddress == nil { continue } for dnsServerAddress := address.FirstDnsServerAddress; dnsServerAddress != nil; dnsServerAddress = dnsServerAddress.Next { rawSockaddr, err := dnsServerAddress.Address.Sockaddr.Sockaddr() if err != nil { continue } var dnsServerAddr netip.Addr switch sockaddr := rawSockaddr.(type) { case *syscall.SockaddrInet4: dnsServerAddr = netip.AddrFrom4(sockaddr.Addr) case *syscall.SockaddrInet6: if sockaddr.Addr[0] == 0xfe && sockaddr.Addr[1] == 0xc0 { // fec0/10 IPv6 addresses are site local anycast DNS // addresses Microsoft sets by default if no other // IPv6 DNS address is set. Site local anycast is // deprecated since 2004, see // https://datatracker.ietf.org/doc/html/rfc3879 continue } dnsServerAddr = netip.AddrFrom16(sockaddr.Addr) if sockaddr.ZoneId != 0 { dnsServerAddr = dnsServerAddr.WithZone(strconv.FormatInt(int64(sockaddr.ZoneId), 10)) } default: // Unexpected type. continue } dnsAddresses = append(dnsAddresses, struct { ifName string netip.Addr }{ifName: windows.UTF16PtrToString(address.FriendlyName), Addr: dnsServerAddr}) } } var myInterface string if networkManager := service.FromContext[adapter.NetworkManager](ctx); networkManager != nil { myInterface = networkManager.InterfaceMonitor().MyInterface() } for _, address := range dnsAddresses { if address.ifName == myInterface { continue } conf.servers = append(conf.servers, net.JoinHostPort(address.String(), "53")) } return conf } func adapterAddresses() ([]*windows.IpAdapterAddresses, error) { var b []byte l := uint32(15000) // recommended initial size for { b = make([]byte, l) const flags = windows.GAA_FLAG_INCLUDE_PREFIX | windows.GAA_FLAG_INCLUDE_GATEWAYS err := windows.GetAdaptersAddresses(syscall.AF_UNSPEC, flags, 0, (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])), &l) if err == nil { if l == 0 { return nil, nil } break } if err.(syscall.Errno) != syscall.ERROR_BUFFER_OVERFLOW { return nil, os.NewSyscallError("getadaptersaddresses", err) } if l <= uint32(len(b)) { return nil, os.NewSyscallError("getadaptersaddresses", err) } } var aas []*windows.IpAdapterAddresses for aa := (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])); aa != nil; aa = aa.Next { aas = append(aas, aa) } return aas, nil } ================================================ FILE: dns/transport/mdns/mdns.go ================================================ 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 } ================================================ FILE: dns/transport/quic/http3.go ================================================ package quic import ( "bytes" "context" "io" "net" "net/http" "net/url" "strconv" "sync" "github.com/sagernet/quic-go" "github.com/sagernet/quic-go/http3" "github.com/sagernet/sing-box/adapter" "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/dns" "github.com/sagernet/sing-box/dns/transport" "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/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" sHTTP "github.com/sagernet/sing/protocol/http" mDNS "github.com/miekg/dns" ) var _ adapter.DNSTransport = (*HTTP3Transport)(nil) func RegisterHTTP3Transport(registry *dns.TransportRegistry) { dns.RegisterTransport[option.RemoteHTTPSDNSServerOptions](registry, C.DNSTypeHTTP3, NewHTTP3) } type HTTP3Transport struct { dns.TransportAdapter logger logger.ContextLogger dialer N.Dialer destination *url.URL headers http.Header serverAddr M.Socksaddr tlsConfig *tls.STDConfig transportAccess sync.Mutex transport *http3.Transport } func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) { transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions) if err != nil { return nil, err } tlsOptions := common.PtrValueOrDefault(options.TLS) tlsOptions.Enabled = true tlsConfig, err := tls.NewClient(ctx, logger, options.Server, tlsOptions) if err != nil { return nil, err } stdConfig, err := tlsConfig.STDConfig() if err != nil { return nil, err } headers := options.Headers.Build() host := headers.Get("Host") if host != "" { headers.Del("Host") } else { if tlsConfig.ServerName() != "" { host = tlsConfig.ServerName() } else { host = options.Server } } destinationURL := url.URL{ Scheme: "https", Host: host, } if destinationURL.Host == "" { destinationURL.Host = options.Server } if options.ServerPort != 0 && options.ServerPort != 443 { destinationURL.Host = net.JoinHostPort(destinationURL.Host, strconv.Itoa(int(options.ServerPort))) } path := options.Path if path == "" { path = "/dns-query" } err = sHTTP.URLSetPath(&destinationURL, path) if err != nil { return nil, err } serverAddr := options.DNSServerAddressOptions.Build() if serverAddr.Port == 0 { serverAddr.Port = 443 } if !serverAddr.IsValid() { return nil, E.New("invalid server address: ", serverAddr) } t := &HTTP3Transport{ TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTP3, tag, options.RemoteDNSServerOptions), logger: logger, dialer: transportDialer, destination: &destinationURL, headers: headers, serverAddr: serverAddr, tlsConfig: stdConfig, } t.transport = t.newTransport() return t, nil } func (t *HTTP3Transport) newTransport() *http3.Transport { return &http3.Transport{ Dial: func(ctx context.Context, addr string, tlsCfg *tls.STDConfig, cfg *quic.Config) (*quic.Conn, error) { conn, dialErr := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr) if dialErr != nil { return nil, dialErr } quicConn, dialErr := quic.DialEarly(ctx, bufio.NewUnbindPacketConn(conn), conn.RemoteAddr(), tlsCfg, cfg) if dialErr != nil { conn.Close() return nil, dialErr } return quicConn, nil }, TLSClientConfig: t.tlsConfig, } } func (t *HTTP3Transport) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } return dialer.InitializeDetour(t.dialer) } func (t *HTTP3Transport) Close() error { t.transportAccess.Lock() defer t.transportAccess.Unlock() return t.transport.Close() } func (t *HTTP3Transport) Reset() { t.transportAccess.Lock() defer t.transportAccess.Unlock() t.transport.Close() t.transport = t.newTransport() } func (t *HTTP3Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { exMessage := *message exMessage.Id = 0 exMessage.Compress = true requestBuffer := buf.NewSize(1 + message.Len()) rawMessage, err := exMessage.PackBuffer(requestBuffer.FreeBytes()) if err != nil { requestBuffer.Release() return nil, err } request, err := http.NewRequestWithContext(ctx, http.MethodPost, t.destination.String(), bytes.NewReader(rawMessage)) if err != nil { requestBuffer.Release() return nil, err } request.Header = t.headers.Clone() request.Header.Set("Content-Type", transport.MimeType) request.Header.Set("Accept", transport.MimeType) t.transportAccess.Lock() currentTransport := t.transport t.transportAccess.Unlock() response, err := currentTransport.RoundTrip(request) requestBuffer.Release() if err != nil { return nil, err } defer response.Body.Close() if response.StatusCode != http.StatusOK { return nil, E.New("unexpected status: ", response.Status) } var responseMessage mDNS.Msg if response.ContentLength > 0 { responseBuffer := buf.NewSize(int(response.ContentLength)) defer responseBuffer.Release() _, err = responseBuffer.ReadFullFrom(response.Body, int(response.ContentLength)) if err != nil { return nil, err } err = responseMessage.Unpack(responseBuffer.Bytes()) } else { rawMessage, err = io.ReadAll(response.Body) if err != nil { return nil, err } err = responseMessage.Unpack(rawMessage) } if err != nil { return nil, err } return &responseMessage, nil } ================================================ FILE: dns/transport/quic/quic.go ================================================ package quic import ( "context" "errors" "os" "github.com/sagernet/quic-go" "github.com/sagernet/sing-box/adapter" "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/dns" "github.com/sagernet/sing-box/dns/transport" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" sQUIC "github.com/sagernet/sing-quic" "github.com/sagernet/sing/common" "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" mDNS "github.com/miekg/dns" ) var _ adapter.DNSTransport = (*Transport)(nil) func RegisterTransport(registry *dns.TransportRegistry) { dns.RegisterTransport[option.RemoteTLSDNSServerOptions](registry, C.DNSTypeQUIC, NewQUIC) } type Transport struct { dns.TransportAdapter dialer N.Dialer serverAddr M.Socksaddr tlsConfig tls.Config connection *transport.ConnPool[*quic.Conn] } func NewQUIC(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteTLSDNSServerOptions) (adapter.DNSTransport, error) { transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions) if err != nil { return nil, err } tlsOptions := common.PtrValueOrDefault(options.TLS) tlsOptions.Enabled = true tlsConfig, err := tls.NewClient(ctx, logger, options.Server, tlsOptions) if err != nil { return nil, err } if len(tlsConfig.NextProtos()) == 0 { tlsConfig.SetNextProtos([]string{"doq"}) } serverAddr := options.DNSServerAddressOptions.Build() if serverAddr.Port == 0 { serverAddr.Port = 853 } if !serverAddr.IsValid() { return nil, E.New("invalid server address: ", serverAddr) } return &Transport{ TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeQUIC, tag, options.RemoteDNSServerOptions), dialer: transportDialer, serverAddr: serverAddr, tlsConfig: tlsConfig, connection: transport.NewConnPool(transport.ConnPoolOptions[*quic.Conn]{ Mode: transport.ConnPoolSingle, IsAlive: func(conn *quic.Conn) bool { return conn != nil && !common.Done(conn.Context()) }, Close: func(conn *quic.Conn, _ error) { conn.CloseWithError(0, "") }, }), }, nil } func (t *Transport) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } return dialer.InitializeDetour(t.dialer) } func (t *Transport) Close() error { return t.connection.Close() } func (t *Transport) Reset() { t.connection.Reset() } func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { var ( conn *quic.Conn err error response *mDNS.Msg ) for range 2 { conn, _, err = t.connection.Acquire(ctx, func(ctx context.Context) (*quic.Conn, error) { rawConn, err := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr) if err != nil { return nil, E.Cause(err, "dial UDP connection") } earlyConnection, err := sQUIC.DialEarly( ctx, bufio.NewUnbindPacketConn(rawConn), t.serverAddr.UDPAddr(), t.tlsConfig, nil, ) if err != nil { rawConn.Close() return nil, E.Cause(err, "establish QUIC connection") } return earlyConnection, nil }) if err != nil { return nil, err } response, err = t.exchange(ctx, message, conn) if err == nil { t.connection.Release(conn, true) return response, nil } else if !isQUICRetryError(err) { t.connection.Release(conn, true) return nil, err } else { t.connection.Release(conn, true) t.Reset() continue } } return nil, err } func (t *Transport) exchange(ctx context.Context, message *mDNS.Msg, conn *quic.Conn) (*mDNS.Msg, error) { stream, err := conn.OpenStreamSync(ctx) if err != nil { return nil, E.Cause(err, "open stream") } defer stream.CancelRead(0) err = transport.WriteMessage(stream, 0, message) if err != nil { stream.Close() return nil, E.Cause(err, "write request") } stream.Close() response, err := transport.ReadMessage(stream) if err != nil { return nil, E.Cause(err, "read response") } return response, nil } // https://github.com/AdguardTeam/dnsproxy/blob/fd1868577652c639cce3da00e12ca548f421baf1/upstream/upstream_quic.go#L394 func isQUICRetryError(err error) (ok bool) { if errors.Is(err, os.ErrClosed) { return true } var qAppErr *quic.ApplicationError if errors.As(err, &qAppErr) && qAppErr.ErrorCode == 0 { return true } var qIdleErr *quic.IdleTimeoutError if errors.As(err, &qIdleErr) { return true } var resetErr *quic.StatelessResetError if errors.As(err, &resetErr) { return true } var qTransportError *quic.TransportError if errors.As(err, &qTransportError) && qTransportError.ErrorCode == quic.NoError { return true } if errors.Is(err, quic.Err0RTTRejected) { return true } return false } ================================================ FILE: dns/transport/tcp.go ================================================ package transport import ( "context" "encoding/binary" "io" "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/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/bufio/deadline" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" mDNS "github.com/miekg/dns" ) var _ adapter.DNSTransport = (*TCPTransport)(nil) func RegisterTCP(registry *dns.TransportRegistry) { dns.RegisterTransport[option.RemoteDNSServerOptions](registry, C.DNSTypeTCP, NewTCP) } type TCPTransport struct { dns.TransportAdapter dialer N.Dialer serverAddr M.Socksaddr } func NewTCP(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteDNSServerOptions) (adapter.DNSTransport, error) { transportDialer, err := dns.NewRemoteDialer(ctx, options) if err != nil { return nil, err } serverAddr := options.DNSServerAddressOptions.Build() if serverAddr.Port == 0 { serverAddr.Port = 53 } if !serverAddr.IsValid() { return nil, E.New("invalid server address: ", serverAddr) } return &TCPTransport{ TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTCP, tag, options), dialer: transportDialer, serverAddr: serverAddr, }, nil } func (t *TCPTransport) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } return dialer.InitializeDetour(t.dialer) } func (t *TCPTransport) Close() error { return nil } func (t *TCPTransport) Reset() { } func (t *TCPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { conn, err := t.dialer.DialContext(ctx, N.NetworkTCP, t.serverAddr) if err != nil { 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") } response, err := ReadMessage(conn) if err != nil { return nil, E.Cause(err, "read response") } 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) if err != nil { return nil, err } if responseLen < 10 { return nil, mDNS.ErrShortRead } buffer := buf.NewSize(int(responseLen)) defer buffer.Release() _, err = buffer.ReadFullFrom(reader, int(responseLen)) if err != nil { return nil, err } var message mDNS.Msg err = message.Unpack(buffer.Bytes()) return &message, err } func WriteMessage(writer io.Writer, messageId uint16, message *mDNS.Msg) error { requestLen := message.Len() buffer := buf.NewSize(3 + requestLen) defer buffer.Release() common.Must(binary.Write(buffer, binary.BigEndian, uint16(requestLen))) exMessage := *message exMessage.Id = messageId exMessage.Compress = true rawMessage, err := exMessage.PackBuffer(buffer.FreeBytes()) if err != nil { return err } buffer.Truncate(2 + len(rawMessage)) return common.Error(writer.Write(buffer.Bytes())) } ================================================ FILE: dns/transport/tls.go ================================================ package transport import ( "context" "github.com/sagernet/sing-box/adapter" "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/dns" "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" N "github.com/sagernet/sing/common/network" mDNS "github.com/miekg/dns" ) var _ adapter.DNSTransport = (*TLSTransport)(nil) const tlsDNSMaxInflight = 8 func RegisterTLS(registry *dns.TransportRegistry) { dns.RegisterTransport[option.RemoteTLSDNSServerOptions](registry, C.DNSTypeTLS, NewTLS) } type TLSTransport struct { dns.TransportAdapter logger logger.ContextLogger dialer tls.Dialer serverAddr M.Socksaddr tlsConfig tls.Config connections *ConnPool[*tlsDNSConn] } type tlsDNSConn struct { tls.Conn queryId uint16 needDeadlineClose bool } func NewTLS(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteTLSDNSServerOptions) (adapter.DNSTransport, error) { transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions) if err != nil { return nil, err } tlsOptions := common.PtrValueOrDefault(options.TLS) tlsOptions.Enabled = true tlsConfig, err := tls.NewClient(ctx, logger, options.Server, tlsOptions) if err != nil { return nil, err } serverAddr := options.DNSServerAddressOptions.Build() if serverAddr.Port == 0 { serverAddr.Port = 853 } if !serverAddr.IsValid() { return nil, E.New("invalid server address: ", serverAddr) } return NewTLSRaw(logger, dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTLS, tag, options.RemoteDNSServerOptions), transportDialer, serverAddr, tlsConfig), nil } func NewTLSRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer N.Dialer, serverAddr M.Socksaddr, tlsConfig tls.Config) *TLSTransport { return &TLSTransport{ TransportAdapter: adapter, logger: logger, dialer: tls.NewDialer(dialer, tlsConfig), serverAddr: serverAddr, tlsConfig: tlsConfig, connections: NewConnPool(ConnPoolOptions[*tlsDNSConn]{ Mode: ConnPoolOrdered, MaxInflight: tlsDNSMaxInflight, IsAlive: func(conn *tlsDNSConn) bool { return conn != nil }, Close: func(conn *tlsDNSConn, _ error) { conn.Close() }, }), } } func (t *TLSTransport) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } return dialer.InitializeDetour(t.dialer) } func (t *TLSTransport) Close() error { return t.connections.Close() } func (t *TLSTransport) Reset() { t.connections.Reset() } func (t *TLSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { var lastErr error for range 2 { conn, created, err := t.connections.Acquire(ctx, func(ctx context.Context) (*tlsDNSConn, error) { tlsConn, err := t.dialer.DialTLSContext(ctx, t.serverAddr) if err != nil { return nil, E.Cause(err, "dial TLS connection") } return &tlsDNSConn{ Conn: tlsConn, needDeadlineClose: deadline.NeedAdditionalReadDeadline(tlsConn.NetConn()), }, nil }) if err != nil { return nil, err } response, err := t.exchange(ctx, message, conn) if err == nil { t.connections.Release(conn, true) return response, nil } lastErr = err t.logger.DebugContext(ctx, "discarded pooled connection: ", err) t.connections.Release(conn, false) if created { return nil, err } } return nil, lastErr } func (t *TLSTransport) exchange(ctx context.Context, message *mDNS.Msg, conn *tlsDNSConn) (*mDNS.Msg, error) { defer setConnDeadline(ctx, conn, conn.needDeadlineClose)() conn.queryId++ err := WriteMessage(conn, conn.queryId, message) if err != nil { return nil, E.Cause(err, "write request") } response, err := ReadMessage(conn) if err != nil { return nil, E.Cause(err, "read response") } return response, nil } ================================================ FILE: dns/transport/udp.go ================================================ package transport import ( "context" "net" "sync" "sync/atomic" "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/dns" "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" N "github.com/sagernet/sing/common/network" mDNS "github.com/miekg/dns" ) var _ adapter.DNSTransport = (*UDPTransport)(nil) func RegisterUDP(registry *dns.TransportRegistry) { dns.RegisterTransport[option.RemoteDNSServerOptions](registry, C.DNSTypeUDP, NewUDP) } type UDPTransport struct { dns.TransportAdapter logger logger.ContextLogger dialer N.Dialer serverAddr M.Socksaddr udpSize atomic.Int32 connection *ConnPool[net.Conn] callbackAccess sync.RWMutex queryId uint16 callbacks map[uint16]*udpCallback } type udpCallback struct { access sync.Mutex response *mDNS.Msg done chan struct{} } func NewUDP(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteDNSServerOptions) (adapter.DNSTransport, error) { transportDialer, err := dns.NewRemoteDialer(ctx, options) if err != nil { return nil, err } serverAddr := options.DNSServerAddressOptions.Build() if serverAddr.Port == 0 { serverAddr.Port = 53 } if !serverAddr.IsValid() { return nil, E.New("invalid server address: ", serverAddr) } return NewUDPRaw(logger, dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeUDP, tag, options), transportDialer, serverAddr), nil } func NewUDPRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialerInstance N.Dialer, serverAddr M.Socksaddr) *UDPTransport { t := &UDPTransport{ TransportAdapter: adapter, logger: logger, dialer: dialerInstance, serverAddr: serverAddr, callbacks: make(map[uint16]*udpCallback), connection: NewConnPool(ConnPoolOptions[net.Conn]{ Mode: ConnPoolSingle, IsAlive: func(conn net.Conn) bool { return conn != nil }, Close: func(conn net.Conn, cause error) { conn.Close() }, }), } t.udpSize.Store(2048) return t } func (t *UDPTransport) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } return dialer.InitializeDetour(t.dialer) } func (t *UDPTransport) Close() error { return t.connection.Close() } func (t *UDPTransport) Reset() { t.connection.Reset() } func (t *UDPTransport) nextAvailableQueryId() (uint16, error) { start := t.queryId for { t.queryId++ if _, exists := t.callbacks[t.queryId]; !exists { return t.queryId, nil } if t.queryId == start { return 0, E.New("no available query ID") } } } func (t *UDPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { response, err := t.exchange(ctx, message) if err != nil { return nil, err } if response.Truncated { t.logger.InfoContext(ctx, "response truncated, retrying with TCP") return t.exchangeTCP(ctx, message) } return response, nil } func (t *UDPTransport) exchangeTCP(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { conn, err := t.dialer.DialContext(ctx, N.NetworkTCP, t.serverAddr) if err != nil { 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") } response, err := ReadMessage(conn) if err != nil { return nil, E.Cause(err, "read response") } return response, nil } func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { if edns0Opt := message.IsEdns0(); edns0Opt != nil { udpSize := int32(edns0Opt.UDPSize()) for { current := t.udpSize.Load() if udpSize <= current { break } if t.udpSize.CompareAndSwap(current, udpSize) { t.Reset() break } } } conn, connCtx, created, err := t.connection.AcquireShared(ctx, func(ctx context.Context) (net.Conn, error) { rawConn, err := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr) if err != nil { return nil, E.Cause(err, "dial UDP connection") } return rawConn, nil }) if err != nil { return nil, err } if created { go t.recvLoop(conn) } callback := &udpCallback{ done: make(chan struct{}), } t.callbackAccess.Lock() queryId, err := t.nextAvailableQueryId() if err != nil { t.callbackAccess.Unlock() t.connection.Release(conn, true) return nil, err } t.callbacks[queryId] = callback t.callbackAccess.Unlock() defer func() { t.callbackAccess.Lock() delete(t.callbacks, queryId) t.callbackAccess.Unlock() }() buffer := buf.NewSize(1 + message.Len()) defer buffer.Release() exMessage := *message exMessage.Compress = true originalId := message.Id exMessage.Id = queryId rawMessage, err := exMessage.PackBuffer(buffer.FreeBytes()) if err != nil { return nil, err } _, err = conn.Write(rawMessage) if err != nil { t.connection.Invalidate(conn, err) return nil, E.Cause(err, "write request") } select { case <-callback.done: t.connection.Release(conn, true) callback.response.Id = originalId return callback.response, nil case <-connCtx.Done(): return nil, context.Cause(connCtx) case <-ctx.Done(): t.connection.Release(conn, true) return nil, ctx.Err() } } func (t *UDPTransport) recvLoop(conn net.Conn) { for { buffer := buf.NewSize(int(t.udpSize.Load())) _, err := buffer.ReadOnceFrom(conn) if err != nil { buffer.Release() t.connection.Invalidate(conn, err) return } var message mDNS.Msg err = message.Unpack(buffer.Bytes()) buffer.Release() if err != nil { t.logger.Debug("discarded malformed UDP response: ", err) continue } t.callbackAccess.RLock() callback, loaded := t.callbacks[message.Id] t.callbackAccess.RUnlock() if !loaded { continue } callback.access.Lock() select { case <-callback.done: default: callback.response = &message close(callback.done) } callback.access.Unlock() } } ================================================ FILE: dns/transport_adapter.go ================================================ package dns import ( "github.com/sagernet/sing-box/option" ) type TransportAdapter struct { transportType string transportTag string dependencies []string } func NewTransportAdapter(transportType string, transportTag string, dependencies []string) TransportAdapter { return TransportAdapter{ transportType: transportType, transportTag: transportTag, dependencies: dependencies, } } func NewTransportAdapterWithLocalOptions(transportType string, transportTag string, localOptions option.LocalDNSServerOptions) TransportAdapter { var dependencies []string if localOptions.DomainResolver != nil && localOptions.DomainResolver.Server != "" { dependencies = append(dependencies, localOptions.DomainResolver.Server) } return TransportAdapter{ transportType: transportType, transportTag: transportTag, dependencies: dependencies, } } func NewTransportAdapterWithRemoteOptions(transportType string, transportTag string, remoteOptions option.RemoteDNSServerOptions) TransportAdapter { var dependencies []string if remoteOptions.DomainResolver != nil && remoteOptions.DomainResolver.Server != "" { dependencies = append(dependencies, remoteOptions.DomainResolver.Server) } return TransportAdapter{ transportType: transportType, transportTag: transportTag, dependencies: dependencies, } } func (a *TransportAdapter) Type() string { return a.transportType } func (a *TransportAdapter) Tag() string { return a.transportTag } func (a *TransportAdapter) Dependencies() []string { return a.dependencies } ================================================ FILE: dns/transport_dialer.go ================================================ package dns import ( "context" "github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/option" N "github.com/sagernet/sing/common/network" ) func NewLocalDialer(ctx context.Context, options option.LocalDNSServerOptions) (N.Dialer, error) { return dialer.NewWithOptions(dialer.Options{ Context: ctx, Options: options.DialerOptions, DirectResolver: true, }) } 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, }) } ================================================ FILE: dns/transport_manager.go ================================================ package dns import ( "context" "io" "os" "strings" "sync" "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" "github.com/sagernet/sing/common/logger" ) var _ adapter.DNSTransportManager = (*TransportManager)(nil) type TransportManager struct { logger log.ContextLogger registry adapter.DNSTransportRegistry outbound adapter.OutboundManager defaultTag string access sync.RWMutex started bool stage adapter.StartStage transports []adapter.DNSTransport transportByTag map[string]adapter.DNSTransport dependByTag map[string][]string defaultTransport adapter.DNSTransport defaultTransportFallback func() (adapter.DNSTransport, error) fakeIPTransport adapter.FakeIPTransport } func NewTransportManager(logger logger.ContextLogger, registry adapter.DNSTransportRegistry, outbound adapter.OutboundManager, defaultTag string) *TransportManager { return &TransportManager{ logger: logger, registry: registry, outbound: outbound, defaultTag: defaultTag, transportByTag: make(map[string]adapter.DNSTransport), dependByTag: make(map[string][]string), } } func (m *TransportManager) Initialize(defaultTransportFallback func() (adapter.DNSTransport, error)) { m.defaultTransportFallback = defaultTransportFallback } func (m *TransportManager) Start(stage adapter.StartStage) error { m.access.Lock() if m.started && m.stage >= stage { panic("already started") } m.started = true m.stage = stage if stage == adapter.StartStateStart { if m.defaultTag != "" && m.defaultTransport == nil { m.access.Unlock() return E.New("default DNS server not found: ", m.defaultTag) } if m.defaultTransport == nil { defaultTransport, err := m.defaultTransportFallback() if err != nil { m.access.Unlock() return E.Cause(err, "default DNS server fallback") } m.transports = append(m.transports, defaultTransport) m.transportByTag[defaultTransport.Tag()] = defaultTransport m.defaultTransport = defaultTransport } transports := m.transports m.access.Unlock() return m.startTransports(transports) } else { transports := m.transports m.access.Unlock() for _, outbound := range transports { err := adapter.LegacyStart(outbound, stage) if err != nil { return E.Cause(err, stage, " dns/", outbound.Type(), "[", outbound.Tag(), "]") } } } return nil } func (m *TransportManager) startTransports(transports []adapter.DNSTransport) error { monitor := taskmonitor.New(m.logger, C.StartTimeout) started := make(map[string]bool) for { canContinue := false startOne: for _, transportToStart := range transports { transportTag := transportToStart.Tag() if started[transportTag] { continue } dependencies := transportToStart.Dependencies() for _, dependency := range dependencies { if !started[dependency] { continue startOne } } started[transportTag] = true canContinue = true if starter, isStarter := transportToStart.(adapter.Lifecycle); isStarter { monitor.Start("start dns/", transportToStart.Type(), "[", transportTag, "]") err := starter.Start(adapter.StartStateStart) monitor.Finish() if err != nil { return E.Cause(err, "start dns/", transportToStart.Type(), "[", transportTag, "]") } } } if len(started) == len(transports) { break } if canContinue { continue } currentTransport := common.Find(transports, func(it adapter.DNSTransport) bool { return !started[it.Tag()] }) var lintTransport func(oTree []string, oCurrent adapter.DNSTransport) error lintTransport = func(oTree []string, oCurrent adapter.DNSTransport) error { problemTransportTag := common.Find(oCurrent.Dependencies(), func(it string) bool { return !started[it] }) if common.Contains(oTree, problemTransportTag) { return E.New("circular server dependency: ", strings.Join(oTree, " -> "), " -> ", problemTransportTag) } m.access.Lock() problemTransport := m.transportByTag[problemTransportTag] m.access.Unlock() if problemTransport == nil { return E.New("dependency[", problemTransportTag, "] not found for server[", oCurrent.Tag(), "]") } return lintTransport(append(oTree, problemTransportTag), problemTransport) } return lintTransport([]string{currentTransport.Tag()}, currentTransport) } return nil } func (m *TransportManager) Close() error { monitor := taskmonitor.New(m.logger, C.StopTimeout) m.access.Lock() if !m.started { m.access.Unlock() return nil } m.started = false transports := m.transports m.transports = nil m.access.Unlock() var err error for _, transport := range transports { if closer, isCloser := transport.(io.Closer); isCloser { monitor.Start("close server/", transport.Type(), "[", transport.Tag(), "]") err = E.Append(err, closer.Close(), func(err error) error { return E.Cause(err, "close server/", transport.Type(), "[", transport.Tag(), "]") }) monitor.Finish() } } return nil } func (m *TransportManager) Transports() []adapter.DNSTransport { m.access.RLock() defer m.access.RUnlock() return m.transports } func (m *TransportManager) Transport(tag string) (adapter.DNSTransport, bool) { m.access.RLock() outbound, found := m.transportByTag[tag] m.access.RUnlock() return outbound, found } func (m *TransportManager) Default() adapter.DNSTransport { m.access.RLock() defer m.access.RUnlock() return m.defaultTransport } func (m *TransportManager) FakeIP() adapter.FakeIPTransport { m.access.RLock() defer m.access.RUnlock() return m.fakeIPTransport } func (m *TransportManager) Remove(tag string) error { m.access.Lock() defer m.access.Unlock() transport, found := m.transportByTag[tag] if !found { return os.ErrInvalid } delete(m.transportByTag, tag) index := common.Index(m.transports, func(it adapter.DNSTransport) bool { return it == transport }) if index == -1 { panic("invalid inbound index") } m.transports = append(m.transports[:index], m.transports[index+1:]...) started := m.started if m.defaultTransport == transport { if len(m.transports) > 0 { nextTransport := m.transports[0] if nextTransport.Type() != C.DNSTypeFakeIP { return E.New("default server cannot be fakeip") } m.defaultTransport = nextTransport m.logger.Info("updated default server to ", m.defaultTransport.Tag()) } else { m.defaultTransport = nil } } dependBy := m.dependByTag[tag] if len(dependBy) > 0 { return E.New("server[", tag, "] is depended by ", strings.Join(dependBy, ", ")) } dependencies := transport.Dependencies() for _, dependency := range dependencies { if len(m.dependByTag[dependency]) == 1 { delete(m.dependByTag, dependency) } else { m.dependByTag[dependency] = common.Filter(m.dependByTag[dependency], func(it string) bool { return it != tag }) } } if started { transport.Close() } return nil } func (m *TransportManager) Create(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) error { if tag == "" { return os.ErrInvalid } transport, err := m.registry.CreateDNSTransport(ctx, logger, tag, transportType, options) if err != nil { return err } m.access.Lock() defer m.access.Unlock() if m.started { for _, stage := range adapter.ListStartStages { err = adapter.LegacyStart(transport, stage) if err != nil { return E.Cause(err, stage, " dns/", transport.Type(), "[", transport.Tag(), "]") } } } if existsTransport, loaded := m.transportByTag[tag]; loaded { if m.started { err = common.Close(existsTransport) if err != nil { return E.Cause(err, "close dns/", existsTransport.Type(), "[", existsTransport.Tag(), "]") } } existsIndex := common.Index(m.transports, func(it adapter.DNSTransport) bool { return it == existsTransport }) if existsIndex == -1 { panic("invalid inbound index") } m.transports = append(m.transports[:existsIndex], m.transports[existsIndex+1:]...) } m.transports = append(m.transports, transport) m.transportByTag[tag] = transport dependencies := transport.Dependencies() for _, dependency := range dependencies { m.dependByTag[dependency] = append(m.dependByTag[dependency], tag) } if tag == m.defaultTag || (m.defaultTag == "" && m.defaultTransport == nil) { if transport.Type() == C.DNSTypeFakeIP { return E.New("default server cannot be fakeip") } m.defaultTransport = transport if m.started { m.logger.Info("updated default server to ", transport.Tag()) } } if transport.Type() == C.DNSTypeFakeIP { if m.fakeIPTransport != nil { return E.New("multiple fakeip server are not supported") } m.fakeIPTransport = transport.(adapter.FakeIPTransport) } return nil } ================================================ FILE: dns/transport_registry.go ================================================ package dns 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 TransportConstructorFunc[T any] func(ctx context.Context, logger log.ContextLogger, tag string, options T) (adapter.DNSTransport, error) func RegisterTransport[Options any](registry *TransportRegistry, transportType string, constructor TransportConstructorFunc[Options]) { registry.register(transportType, func() any { return new(Options) }, func(ctx context.Context, logger log.ContextLogger, tag string, rawOptions any) (adapter.DNSTransport, error) { var options *Options if rawOptions != nil { options = rawOptions.(*Options) } return constructor(ctx, logger, tag, common.PtrValueOrDefault(options)) }) } var _ adapter.DNSTransportRegistry = (*TransportRegistry)(nil) type ( optionsConstructorFunc func() any constructorFunc func(ctx context.Context, logger log.ContextLogger, tag string, options any) (adapter.DNSTransport, error) ) type TransportRegistry struct { access sync.Mutex optionsType map[string]optionsConstructorFunc constructors map[string]constructorFunc } func NewTransportRegistry() *TransportRegistry { return &TransportRegistry{ optionsType: make(map[string]optionsConstructorFunc), constructors: make(map[string]constructorFunc), } } func (r *TransportRegistry) CreateOptions(transportType string) (any, bool) { r.access.Lock() defer r.access.Unlock() optionsConstructor, loaded := r.optionsType[transportType] if !loaded { return nil, false } return optionsConstructor(), true } func (r *TransportRegistry) CreateDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) (adapter.DNSTransport, error) { r.access.Lock() defer r.access.Unlock() constructor, loaded := r.constructors[transportType] if !loaded { return nil, E.New("transport type not found: " + transportType) } return constructor(ctx, logger, tag, options) } func (r *TransportRegistry) register(transportType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) { r.access.Lock() defer r.access.Unlock() r.optionsType[transportType] = optionsConstructor r.constructors[transportType] = constructor } ================================================ FILE: docs/CNAME ================================================ sing-box.sagernet.org ================================================ FILE: docs/changelog.md ================================================ --- icon: material/alert-decagram --- #### 1.14.0-alpha.24 * Fixes and improvement #### 1.13.12 * Update naiveproxy to v148.0.7778.96-1 * Fixes and improvements #### 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** * 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 * 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.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** * 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.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 #### 1.13.1 * Fixes and improvements #### 1.12.14 * Backport fixes #### 1.13.0 Important changes since 1.12: * Add NaiveProxy outbound **1** * Add pre-match support for `auto_redirect` **2** * Improve `auto_redirect` **3** * Add Chrome Root Store certificate option **4** * Add new options for ACME DNS-01 challenge providers **5** * Add Wi-Fi state support for Linux and Windows **6** * Add curve preferences, pinned public key SHA256, mTLS and ECH `query_server_name` for TLS options **7** * Add kTLS support **8** * Add ICMP echo (ping) proxy support **9** * Add `interface_address`, `network_interface_address` and `default_interface_address` rule items **10** * Add `preferred_by` route rule item **11** * Improve `local` DNS server **12** * Add `disable_tcp_keep_alive`, `tcp_keep_alive` and `tcp_keep_alive_interval` options for listen and dial fields **13** * Add `bind_address_no_port` option for dial fields **14** * Add system interface, relay server and advertise tags options for Tailscale endpoint **15** * Add Claude Code Multiplexer service **16** * Add OpenAI Codex Multiplexer service **17** * Apple/Android: Refactor GUI * Apple/Android: Add support for sharing configurations via [QRS](https://github.com/qifi-dev/qrs) * Android: Add support for resisting VPN detection via Xposed * Drop support for go1.23 **18** * Drop support for Android 5.0 **19** * Update uTLS to v1.8.2 **20** * Update quic-go to v0.59.0 * Update gVisor to v20250811 * Update Tailscale to v1.92.4 **1**: NaiveProxy outbound now supports QUIC, ECH, UDP over TCP, and configurable QUIC congestion control. Only available on Apple platforms, Android, Windows and some Linux architectures. Each Windows release includes `libcronet.dll` — ensure this file is in the same directory as `sing-box.exe` or in a directory listed in `PATH`. See [NaiveProxy outbound](/configuration/outbound/naive/). **2**: `auto_redirect` now allows you to bypass sing-box for connections based on routing rules. A new rule action `bypass` is introduced to support this feature. When matched during pre-match, the connection will bypass sing-box and connect directly. This feature requires Linux with `auto_redirect` enabled. See [Pre-match](/configuration/shared/pre-match/) and [Rule Action](/configuration/route/rule_action/#bypass). **3**: `auto_redirect` now rejects MPTCP connections by default to fix compatibility issues. You can change it to bypass sing-box via the new `exclude_mptcp` option. Adds a fallback iproute2 rule checked after system default rules (32766: main, 32767: default), ensuring traffic is routed to the sing-box table when no route is found in system tables. The rule index can be customized via `auto_redirect_iproute2_fallback_rule_index` (default: 32768). See [TUN](/configuration/inbound/tun/#exclude_mptcp). **4**: Adds `chrome` as a new certificate store option alongside `mozilla`. Both stores filter out China-based CA certificates. See [Certificate](/configuration/certificate/#store). **5**: See [DNS-01 Challenge](/configuration/shared/dns01_challenge/). **6**: sing-box can now monitor Wi-Fi state on Linux and Windows to enable routing rules based on `wifi_ssid` and `wifi_bssid`. See [Wi-Fi State](/configuration/shared/wifi-state/). **7**: See [TLS](/configuration/shared/tls/). **8**: Adds `kernel_tx` and `kernel_rx` options for TLS inbound. Enables kernel-level TLS offloading via `splice(2)` on Linux 5.1+ with TLS 1.3. See [TLS](/configuration/shared/tls/). **9**: sing-box can now proxy ICMP echo (ping) requests. A new `icmp` network type is available for route rules. Supported from TUN, WireGuard and Tailscale inbounds to Direct, WireGuard and Tailscale outbounds. The `reject` action can also reply to ICMP echo requests. **10**: New rule items for matching based on interface IP addresses, available in route rules, DNS rules and rule-sets. **11**: Matches outbounds' preferred routes. For Tailscale: MagicDNS domains and peers' allowed IPs. For WireGuard: peers' allowed IPs. **12**: The `local` DNS server now uses platform-native resolution: `getaddrinfo`/libresolv on Apple platforms, systemd-resolved DBus on Linux. A new `prefer_go` option is available to opt out. See [Local DNS](/configuration/dns/server/local/). **13**: The default TCP keep-alive initial period has been updated from 10 minutes to 5 minutes. See [Dial Fields](/configuration/shared/dial/#tcp_keep_alive). **14**: Adds the Linux socket option `IP_BIND_ADDRESS_NO_PORT` support when explicitly binding to a source address. This allows reusing the same source port for multiple connections, improving scalability for high-concurrency proxy scenarios. See [Dial Fields](/configuration/shared/dial/#bind_address_no_port). **15**: Tailscale endpoint can now create a system TUN interface to handle traffic directly. New `relay_server_port` and `relay_server_static_endpoints` options for incoming relay connections. New `advertise_tags` option for ACL tag advertisement. See [Tailscale endpoint](/configuration/endpoint/tailscale/). **16**: CCM (Claude Code Multiplexer) service allows you to access your local Claude Code subscription remotely through custom tokens, eliminating the need for OAuth authentication on remote clients. See [CCM](/configuration/service/ccm). **17**: See [OCM](/configuration/service/ocm). **18**: Due to maintenance difficulties, sing-box 1.13.0 requires at least Go 1.24 to compile. **19**: Due to maintenance difficulties, sing-box 1.13.0 will be the last version to support Android 5.0, and only through a separate legacy build (with `-legacy-android-5` suffix). For standalone binaries, the minimum Android version has been raised to Android 6.0, since Termux requires Android 7.0 or later. **20**: This update fixes missing padding extension for Chrome 120+ fingerprints. Also, documentation has been updated with a warning about uTLS fingerprinting vulnerabilities. uTLS is not recommended for censorship circumvention due to fundamental architectural limitations; use NaiveProxy instead for TLS fingerprint resistance. #### 1.12.23 * Fixes and improvements #### 1.13.0-rc.5 * Add `mipsle`, `mips64le`, `riscv64` and `loong64` support for NaiveProxy outbound #### 1.12.22 * Fixes and improvements #### 1.13.0-rc.3 * Fixes and improvements #### 1.12.21 * Fixes and improvements #### 1.13.0-rc.2 * Fixes and improvements #### 1.12.20 * Fixes and improvements #### 1.13.0-rc.1 * Fixes and improvements #### 1.12.19 * Fixes and improvements #### 1.13.0-beta.8 * Add fallback routing rule for `auto_redirect` **1** * Fixes and improvements **1**: Adds a fallback iproute2 rule checked after system default rules (32766: main, 32767: default), ensuring traffic is routed to the sing-box table when no route is found in system tables. The rule index can be customized via `auto_redirect_iproute2_fallback_rule_index` (default: 32768). #### 1.12.18 * Add fallback routing rule for `auto_redirect` **1** * Fixes and improvements **1**: Adds a fallback iproute2 rule checked after system default rules (32766: main, 32767: default), ensuring traffic is routed to the sing-box table when no route is found in system tables. The rule index can be customized via `auto_redirect_iproute2_fallback_rule_index` (default: 32768). #### 1.13.0-beta.6 * Update uTLS to v1.8.2 **1** * Fixes and improvements **1**: This update fixes missing padding extension for Chrome 120+ fingerprints. Also, documentation has been updated with a warning about uTLS fingerprinting vulnerabilities. uTLS is not recommended for censorship circumvention due to fundamental architectural limitations; use NaiveProxy instead for TLS fingerprint resistance. #### 1.12.17 * Update uTLS to v1.8.2 **1** * Fixes and improvements **1**: This update fixes missing padding extension for Chrome 120+ fingerprints. Also, documentation has been updated with a warning about uTLS fingerprinting vulnerabilities. uTLS is not recommended for censorship circumvention due to fundamental architectural limitations; use NaiveProxy instead for TLS fingerprint resistance. #### 1.13.0-beta.5 * Fixes and improvements #### 1.12.16 * Fixes and improvements #### 1.13.0-beta.4 * Apple/Android: Add support for sharing configurations via [QRS](https://github.com/qifi-dev/qrs) * Android: Add support for resisting VPN detection via Xposed * Update quic-go to v0.59.0 * Fixes and improvements #### 1.13.0-beta.2 * Add `bind_address_no_port` option for dial fields **1** * Fixes and improvements **1**: Adds the Linux socket option `IP_BIND_ADDRESS_NO_PORT` support when explicitly binding to a source address. This allows reusing the same source port for multiple connections, improving scalability for high-concurrency proxy scenarios. See [Dial Fields](/configuration/shared/dial/#bind_address_no_port). #### 1.13.0-beta.1 * Add system interface support for Tailscale endpoint **1** * Fixes and improvements **1**: Tailscale endpoint can now create a system TUN interface to handle traffic directly. See [Tailscale endpoint](/configuration/endpoint/tailscale/#system_interface). #### 1.12.15 * Fixes and improvements #### 1.13.0-alpha.36 * Downgrade quic-go to v0.57.1 * Fixes and improvements #### 1.13.0-alpha.35 * Add pre-match support for `auto_redirect` **1** * Fixes and improvements **1**: `auto_redirect` now allows you to bypass sing-box for connections based on routing rules. A new rule action `bypass` is introduced to support this feature. When matched during pre-match, the connection will bypass sing-box and connect directly. This feature requires Linux with `auto_redirect` enabled. See [Pre-match](/configuration/shared/pre-match/) and [Rule Action](/configuration/route/rule_action/#bypass). #### 1.13.0-alpha.34 * Add Chrome Root Store certificate option **1** * Add new options for ACME DNS-01 challenge providers **2** * Add Wi-Fi state support for Linux and Windows **3** * Update naiveproxy to 143.0.7499.109 * Update quic-go to v0.58.0 * Update tailscale to v1.92.4 * Drop support for go1.23 **4** * Drop support for Android 5.0 **5** **1**: Adds `chrome` as a new certificate store option alongside `mozilla`. Both stores filter out China-based CA certificates. See [Certificate](/configuration/certificate/#store). **2**: See [DNS-01 Challenge](/configuration/shared/dns01_challenge/). **3**: sing-box can now monitor Wi-Fi state on Linux and Windows to enable routing rules based on `wifi_ssid` and `wifi_bssid`. See [Wi-Fi State](/configuration/shared/wifi-state/). **4**: Due to maintenance difficulties, sing-box 1.13.0 requires at least Go 1.24 to compile. **5**: Due to maintenance difficulties, sing-box 1.13.0 will be the last version to support Android 5.0, and only through a separate legacy build (with `-legacy-android-5` suffix). For standalone binaries, the minimum Android version has been raised to Android 6.0, since Termux requires Android 7.0 or later. #### 1.12.14 * Fixes and improvements #### 1.13.0-alpha.33 * Fixes and improvements #### 1.13.0-alpha.32 * Remove `certificate_public_key_sha256` option for NaiveProxy outbound **1** * Fixes and improvements **1**: Self-signed certificates change traffic behavior significantly, which defeats the purpose of NaiveProxy's design to resist traffic analysis. For this reason, and due to maintenance costs, there is no reason to continue supporting `certificate_public_key_sha256`, which was designed to simplify the use of self-signed certificates. #### 1.13.0-alpha.31 * Add QUIC support for NaiveProxy outbound **1** * Add QUIC congestion control option for NaiveProxy **2** * Fixes and improvements **1**: NaiveProxy outbound now supports QUIC. See [NaiveProxy outbound](/configuration/outbound/naive/#quic). **2**: NaiveProxy inbound and outbound now supports configurable QUIC congestion control algorithms, including BBR and BBRv2. See [NaiveProxy inbound](/configuration/inbound/naive/#quic_congestion_control) and [NaiveProxy outbound](/configuration/outbound/naive/#quic_congestion_control). #### 1.13.0-alpha.30 * Add ECH support for NaiveProxy outbound **1** * Add `tls.ech.query_server_name` option **2** * Fix NaiveProxy outbound on Windows **3** * Add OpenAI Codex Multiplexer service **4** * Fixes and improvements **1**: See [NaiveProxy outbound](/configuration/outbound/naive/#tls). **2**: See [TLS](/configuration/shared/tls/#query_server_name). **3**: Each Windows release now includes `libcronet.dll`. Ensure this file is in the same directory as `sing-box.exe` or in a directory listed in `PATH`. **4**: See [OCM](/configuration/service/ocm). #### 1.13.0-alpha.29 * Add UDP over TCP support for naiveproxy outbound **1** * Fixes and improvements **1**: See [NaiveProxy outbound](/configuration/outbound/naive/#udp_over_tcp). #### 1.13.0-alpha.28 * Add naiveproxy outbound **1** * Add `disable_tcp_keep_alive`, `tcp_keep_alive` and `tcp_keep_alive_interval` options for dial fields **2** * Update default TCP keep-alive initial period from 10 minutes to 5 minutes * Update quic-go to v0.57.1 * Fixes and improvements **1**: Only available on Apple platforms, Android, Windows and some Linux architectures. See [NaiveProxy outbound](/configuration/outbound/naive/). **2**: See [Dial Fields](/configuration/shared/dial/#tcp_keep_alive). * __Unfortunately, for non-technical reasons, we are currently unable to notarize the standalone version of the macOS client: because system extensions require signatures to function, we have had to temporarily halt its release.__ __We plan to fix the App Store release issue and launch a new standalone desktop client, but until then, only clients on TestFlight will be available (unless you have an Apple Developer Program and compile from source code).__ #### 1.12.13 * Fix naive inbound * Fixes and improvements __Unfortunately, for non-technical reasons, we are currently unable to notarize the standalone version of the macOS client: because system extensions require signatures to function, we have had to temporarily halt its release.__ __We plan to fix the App Store release issue and launch a new standalone desktop client, but until then, only clients on TestFlight will be available (unless you have an Apple Developer Program and compile from source code).__ #### 1.12.12 * Fixes and improvements #### 1.13.0-alpha.26 * Update quic-go to v0.55.0 * Fix memory leak in hysteria2 * Fixes and improvements #### 1.12.11 * Fixes and improvements #### 1.13.0-alpha.24 * Add Claude Code Multiplexer service **1** * Fixes and improvements **1**: CCM (Claude Code Multiplexer) service allows you to access your local Claude Code subscription remotely through custom tokens, eliminating the need for OAuth authentication on remote clients. See [CCM](/configuration/service/ccm). #### 1.13.0-alpha.23 * Fix compatibility with MPTCP **1** * Fixes and improvements **1**: `auto_redirect` now rejects MPTCP connections by default to fix compatibility issues, but you can change it to bypass the sing-box via the new `exclude_mptcp` option. See [TUN](/configuration/inbound/tun/#exclude_mptcp). #### 1.13.0-alpha.22 * Update uTLS to v1.8.1 **1** * Fixes and improvements **1**: This update fixes an critical issue that could cause simulated Chrome fingerprints to be detected, see https://github.com/refraction-networking/utls/pull/375. #### 1.12.10 * Update uTLS to v1.8.1 **1** * Fixes and improvements **1**: This update fixes an critical issue that could cause simulated Chrome fingerprints to be detected, see https://github.com/refraction-networking/utls/pull/375. #### 1.13.0-alpha.21 * Fix missing mTLS support in client options **1** * Fixes and improvements See [TLS](/configuration/shared/tls/). #### 1.12.9 * Fixes and improvements #### 1.13.0-alpha.16 * Add curve preferences, pinned public key SHA256 and mTLS for TLS options **1** * Fixes and improvements See [TLS](/configuration/shared/tls/). #### 1.13.0-alpha.15 * Update quic-go to v0.54.0 * Update gVisor to v20250811 * Update Tailscale to v1.86.5 * Fixes and improvements #### 1.12.8 * Fixes and improvements #### 1.13.0-alpha.11 * Fixes and improvements #### 1.12.5 * Fixes and improvements #### 1.13.0-alpha.10 * Improve kTLS support **1** * Fixes and improvements **1**: kTLS is now compatible with custom TLS implementations other than uTLS. #### 1.12.4 * Fixes and improvements #### 1.12.3 * Fixes and improvements #### 1.12.2 * Fixes and improvements #### 1.12.1 * Fixes and improvements #### 1.12.0 * Refactor DNS servers **1** * Add domain resolver options**2** * Add TLS fragment/record fragment support to route options and outbound TLS options **3** * Add certificate options **4** * Add Tailscale endpoint and DNS server **5** * Drop support for go1.22 **6** * Add AnyTLS protocol **7** * Migrate to stdlib ECH implementation **8** * Add NTP sniffer **9** * Add wildcard SNI support for ShadowTLS inbound **10** * Improve `auto_redirect` **11** * Add control options for listeners **12** * Add DERP service **13** * Add Resolved service and DNS server **14** * Add SSM API service **15** * Add loopback address support for tun **16** * Improve tun performance on Apple platforms **17** * Update quic-go to v0.52.0 * Update gVisor to 20250319.0 * Update the status of graphical clients in stores **18** **1**: 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-server-formats). Compatibility for old formats will be removed in sing-box 1.14.0. **2**: Legacy `outbound` DNS rules are deprecated and can be replaced by the new `domain_resolver` option. See [Dial Fields](/configuration/shared/dial/#domain_resolver) and [Route](/configuration/route/#default_domain_resolver). For migration, see [Migrate outbound DNS rule items to domain resolver](/migration/#migrate-outbound-dns-rule-items-to-domain-resolver). **3**: See [Route Action](/configuration/route/rule_action/#tls_fragment) and [TLS](/configuration/shared/tls/). **4**: New certificate options allow you to manage the default list of trusted X509 CA certificates. For the system certificate list, fixed Go not reading Android trusted certificates correctly. You can also use the Mozilla Included List instead, or add trusted certificates yourself. See [Certificate](/configuration/certificate/). **5**: See [Tailscale](/configuration/endpoint/tailscale/). **6**: Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to compile. For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches from [MetaCubeX/go](https://github.com/MetaCubeX/go). **7**: The new AnyTLS protocol claims to mitigate TLS proxy traffic characteristics and comes with a new multiplexing scheme. See [AnyTLS Inbound](/configuration/inbound/anytls/) and [AnyTLS Outbound](/configuration/outbound/anytls/). **8**: See [TLS](/configuration/shared/tls). The build tag `with_ech` is no longer needed and has been removed. **9**: See [Protocol Sniff](/configuration/route/sniff/). **10**: See [ShadowTLS](/configuration/inbound/shadowtls/#wildcard_sni). **11**: Now `auto_redirect` fixes compatibility issues between tun and Docker bridge networks, see [Tun](/configuration/inbound/tun/#auto_redirect). **12**: You can now set `bind_interface`, `routing_mark` and `reuse_addr` in Listen Fields. See [Listen Fields](/configuration/shared/listen/). **13**: DERP service is a Tailscale DERP server, similar to [derper](https://pkg.go.dev/tailscale.com/cmd/derper). See [DERP Service](/configuration/service/derp/). **14**: Resolved service is a fake systemd-resolved DBUS service to receive DNS settings from other programs (e.g. NetworkManager) and provide DNS resolution. See [Resolved Service](/configuration/service/resolved/) and [Resolved DNS Server](/configuration/dns/server/resolved/). **15**: SSM API service is a RESTful API server for managing Shadowsocks servers. See [SSM API Service](/configuration/service/ssm-api/). **16**: TUN now implements SideStore's StosVPN. See [Tun](/configuration/inbound/tun/#loopback_address). **17**: We have significantly improved the performance of tun inbound on Apple platforms, especially in the gVisor stack. The following data was tested using [tun_bench](https://github.com/SagerNet/sing-box/blob/dev-next/cmd/internal/tun_bench/main.go) on M4 MacBook pro. | Version | Stack | MTU | Upload | Download | |-------------|--------|-------|--------|----------| | 1.11.15 | gvisor | 1500 | 852M | 2.57G | | 1.12.0-rc.4 | gvisor | 1500 | 2.90G | 4.68G | | 1.11.15 | gvisor | 4064 | 2.31G | 6.34G | | 1.12.0-rc.4 | gvisor | 4064 | 7.54G | 12.2G | | 1.11.15 | gvisor | 65535 | 27.6G | 18.1G | | 1.12.0-rc.4 | gvisor | 65535 | 39.8G | 34.7G | | 1.11.15 | system | 1500 | 664M | 706M | | 1.12.0-rc.4 | system | 1500 | 2.44G | 2.51G | | 1.11.15 | system | 4064 | 1.88G | 1.94G | | 1.12.0-rc.4 | system | 4064 | 6.45G | 6.27G | | 1.11.15 | system | 65535 | 26.2G | 17.4G | | 1.12.0-rc.4 | system | 65535 | 17.6G | 21.0G | **18**: We continue to experience issues updating our sing-box apps on the App Store and Play Store. Until we rewrite and resubmit the apps, they are considered irrecoverable. Therefore, after this release, we will not be repeating this notice unless there is new information. ### 1.11.15 * Fixes and improvements _We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected)._ #### 1.12.0-beta.32 * Improve tun performance on Apple platforms **1** * Fixes and improvements **1**: We have significantly improved the performance of tun inbound on Apple platforms, especially in the gVisor stack. ### 1.11.14 * Fixes and improvements _We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected)._ #### 1.12.0-beta.24 * Allow `tls_fragment` and `tls_record_fragment` to be enabled together **1** * Also add fragment options for TLS client configuration **2** * Fixes and improvements **1**: For debugging only, it is recommended to disable if record fragmentation works. See [Route Action](/configuration/route/rule_action/#tls_fragment). **2**: See [TLS](/configuration/shared/tls/). #### 1.12.0-beta.23 * Add loopback address support for tun **1** * Add cache support for ssm-api **2** * Fixes and improvements **1**: TUN now implements SideStore's StosVPN. See [Tun](/configuration/inbound/tun/#loopback_address). **2**: See [SSM API Service](/configuration/service/ssm-api/#cache_path). #### 1.12.0-beta.21 * Fix missing `home` option for DERP service **1** * Fixes and improvements **1**: You can now choose what the DERP home page shows, just like with derper's `-home` flag. See [DERP](/configuration/service/derp/#home). ### 1.11.13 * Fixes and improvements _We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected)._ #### 1.12.0-beta.17 * Update quic-go to v0.52.0 * Fixes and improvements #### 1.12.0-beta.15 * Add DERP service **1** * Add Resolved service and DNS server **2** * Add SSM API service **3** * Fixes and improvements **1**: DERP service is a Tailscale DERP server, similar to [derper](https://pkg.go.dev/tailscale.com/cmd/derper). See [DERP Service](/configuration/service/derp/). **2**: Resolved service is a fake systemd-resolved DBUS service to receive DNS settings from other programs (e.g. NetworkManager) and provide DNS resolution. See [Resolved Service](/configuration/service/resolved/) and [Resolved DNS Server](/configuration/dns/server/resolved/). **3**: SSM API service is a RESTful API server for managing Shadowsocks servers. See [SSM API Service](/configuration/service/ssm-api/). ### 1.11.11 * Fixes and improvements _We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected)._ #### 1.12.0-beta.13 * Add TLS record fragment route options **1** * Add missing `accept_routes` option for Tailscale **2** * Fixes and improvements **1**: See [Route Action](/configuration/route/rule_action/#tls_record_fragment). **2**: See [Tailscale](/configuration/endpoint/tailscale/#accept_routes). #### 1.12.0-beta.10 * Add control options for listeners **1** * Fixes and improvements **1**: You can now set `bind_interface`, `routing_mark` and `reuse_addr` in Listen Fields. See [Listen Fields](/configuration/shared/listen/). ### 1.11.10 * Undeprecate the `block` outbound **1** * Fixes and improvements **1**: Since we don’t have a replacement for using the `block` outbound in selectors yet, we decided to temporarily undeprecate the `block` outbound until a replacement is available in the future. _We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected)._ #### 1.12.0-beta.9 * Update quic-go to v0.51.0 * Fixes and improvements ### 1.11.9 * Fixes and improvements _We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected)._ #### 1.12.0-beta.5 * Fixes and improvements ### 1.11.8 * Improve `auto_redirect` **1** * Fixes and improvements **1**: Now `auto_redirect` fixes compatibility issues between TUN and Docker bridge networks, see [Tun](/configuration/inbound/tun/#auto_redirect). _We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected)._ #### 1.12.0-beta.3 * Fixes and improvements ### 1.11.7 * Fixes and improvements _We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected)._ #### 1.12.0-beta.1 * Fixes and improvements **1**: Now `auto_redirect` fixes compatibility issues between tun and Docker bridge networks, see [Tun](/configuration/inbound/tun/#auto_redirect). ### 1.11.6 * Fixes and improvements _We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected)._ #### 1.12.0-alpha.19 * Update gVisor to 20250319.0 * Fixes and improvements #### 1.12.0-alpha.18 * Add wildcard SNI support for ShadowTLS inbound **1** * Fixes and improvements **1**: See [ShadowTLS](/configuration/inbound/shadowtls/#wildcard_sni). #### 1.12.0-alpha.17 * Add NTP sniffer **1** * Fixes and improvements **1**: See [Protocol Sniff](/configuration/route/sniff/). #### 1.12.0-alpha.16 * Update `domain_resolver` behavior **1** * Fixes and improvements **1**: `route.default_domain_resolver` or `outbound.domain_resolver` is now optional when only one DNS server is configured. See [Dial Fields](/configuration/shared/dial/#domain_resolver). ### 1.11.5 * Fixes and improvements _We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected)._ #### 1.12.0-alpha.13 * Move `predefined` DNS server to DNS rule action **1** * Fixes and improvements **1**: See [DNS Rule Action](/configuration/dns/rule_action/#predefined). ### 1.11.4 * Fixes and improvements #### 1.12.0-alpha.11 * Fixes and improvements #### 1.12.0-alpha.10 * Add AnyTLS protocol **1** * Improve `resolve` route action **2** * Migrate to stdlib ECH implementation **3** * Fixes and improvements **1**: The new AnyTLS protocol claims to mitigate TLS proxy traffic characteristics and comes with a new multiplexing scheme. See [AnyTLS Inbound](/configuration/inbound/anytls/) and [AnyTLS Outbound](/configuration/outbound/anytls/). **2**: `resolve` route action now accepts `disable_cache` and other options like in DNS route actions, see [Route Action](/configuration/route/rule_action). **3**: See [TLS](/configuration/shared/tls). The build tag `with_ech` is no longer needed and has been removed. #### 1.12.0-alpha.7 * Add Tailscale DNS server **1** * Fixes and improvements **1**: See [Tailscale](/configuration/dns/server/tailscale/). #### 1.12.0-alpha.6 * Add Tailscale endpoint **1** * Drop support for go1.22 **2** * Fixes and improvements **1**: See [Tailscale](/configuration/endpoint/tailscale/). **2**: Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to compile. For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches from [MetaCubeX/go](https://github.com/MetaCubeX/go). ### 1.11.3 * Fixes and improvements _This version overwrites 1.11.2, as incorrect binaries were released due to a bug in the continuous integration process._ #### 1.12.0-alpha.5 * Fixes and improvements ### 1.11.1 * Fixes and improvements #### 1.12.0-alpha.2 * Update quic-go to v0.49.0 * Fixes and improvements #### 1.12.0-alpha.1 * Refactor DNS servers **1** * Add domain resolver options**2** * Add TLS fragment route options **3** * Add certificate options **4** **1**: 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-server-formats). Compatibility for old formats will be removed in sing-box 1.14.0. **2**: Legacy `outbound` DNS rules are deprecated and can be replaced by the new `domain_resolver` option. See [Dial Fields](/configuration/shared/dial/#domain_resolver) and [Route](/configuration/route/#default_domain_resolver). For migration, see [Migrate outbound DNS rule items to domain resolver](/migration/#migrate-outbound-dns-rule-items-to-domain-resolver). **3**: The new TLS fragment route options allow you to fragment TLS handshakes to bypass firewalls. This feature is intended to circumvent simple firewalls based on **plaintext packet matching**, and should not be used to circumvent real censorship. Since it is not designed for performance, it should not be applied to all connections, but only to server names that are known to be blocked. See [Route Action](/configuration/route/rule_action/#tls_fragment). **4**: New certificate options allow you to manage the default list of trusted X509 CA certificates. For the system certificate list, fixed Go not reading Android trusted certificates correctly. You can also use the Mozilla Included List instead, or add trusted certificates yourself. See [Certificate](/configuration/certificate/). ### 1.11.0 Important changes since 1.10: * Introducing rule actions **1** * Improve tun compatibility **3** * Merge route options to route actions **4** * Add `network_type`, `network_is_expensive` and `network_is_constrainted` rule items **5** * Add multi network dialing **6** * Add `cache_capacity` DNS option **7** * Add `override_address` and `override_port` route options **8** * Upgrade WireGuard outbound to endpoint **9** * Add UDP GSO support for WireGuard * Make GSO adaptive **10** * Add UDP timeout route option **11** * Add more masquerade options for hysteria2 **12** * Add `rule-set merge` command * Add port hopping support for Hysteria2 **13** * Hysteria2 `ignore_client_bandwidth` behavior update **14** **1**: New rule actions replace legacy inbound fields and special outbound fields, and can be used for pre-matching **2**. See [Rule](/configuration/route/rule/), [Rule Action](/configuration/route/rule_action/), [DNS Rule](/configuration/dns/rule/) and [DNS Rule Action](/configuration/dns/rule_action/). For migration, see [Migrate legacy special outbounds to rule actions](/migration/#migrate-legacy-special-outbounds-to-rule-actions), [Migrate legacy inbound fields to rule actions](/migration/#migrate-legacy-inbound-fields-to-rule-actions) and [Migrate legacy DNS route options to rule actions](/migration/#migrate-legacy-dns-route-options-to-rule-actions). **2**: Similar to Surge's pre-matching. Specifically, new rule actions allow you to reject connections with TCP RST (for TCP connections) and ICMP port unreachable (for UDP packets) before connection established to improve tun's compatibility. See [Rule Action](/configuration/route/rule_action/). **3**: When `gvisor` tun stack is enabled, even if the request passes routing, if the outbound connection establishment fails, the connection still does not need to be established and a TCP RST is replied. **4**: Route options in DNS route actions will no longer be considered deprecated, see [DNS Route Action](/configuration/dns/rule_action/). Also, now `udp_disable_domain_unmapping` and `udp_connect` can also be configured in route action, see [Route Action](/configuration/route/rule_action/). **5**: When using in graphical clients, new routing rule items allow you to match on network type (WIFI, cellular, etc.), whether the network is expensive, and whether Low Data Mode is enabled. See [Route Rule](/configuration/route/rule/), [DNS Route Rule](/configuration/dns/rule/) and [Headless Rule](/configuration/rule-set/headless-rule/). **6**: Similar to Surge's strategy. New options allow you to connect using multiple network interfaces, prefer or only use one type of interface, and configure a timeout to fallback to other interfaces. See [Dial Fields](/configuration/shared/dial/#network_strategy), [Rule Action](/configuration/route/rule_action/#network_strategy) and [Route](/configuration/route/#default_network_strategy). **7**: See [DNS](/configuration/dns/#cache_capacity). **8**: See [Rule Action](/configuration/route/#override_address) and [Migrate destination override fields to route options](/migration/#migrate-destination-override-fields-to-route-options). **9**: The new WireGuard endpoint combines inbound and outbound capabilities, and the old outbound will be removed in sing-box 1.13.0. See [Endpoint](/configuration/endpoint/), [WireGuard Endpoint](/configuration/endpoint/wireguard/) and [Migrate WireGuard outbound fields to route options](/migration/#migrate-wireguard-outbound-to-endpoint). **10**: For WireGuard outbound and endpoint, GSO will be automatically enabled when available, see [WireGuard Outbound](/configuration/outbound/wireguard/#gso). For TUN, GSO has been removed, see [Deprecated](/deprecated/#gso-option-in-tun). **11**: See [Rule Action](/configuration/route/rule_action/#udp_timeout). **12**: See [Hysteria2](/configuration/inbound/hysteria2/#masquerade). **13**: See [Hysteria2](/configuration/outbound/hysteria2/). **14**: When `up_mbps` and `down_mbps` are set, `ignore_client_bandwidth` instead denies clients from using BBR CC. ### 1.10.7 * Fixes and improvements #### 1.11.0-beta.20 * Hysteria2 `ignore_client_bandwidth` behavior update **1** * Fixes and improvements **1**: When `up_mbps` and `down_mbps` are set, `ignore_client_bandwidth` instead denies clients from using BBR CC. See [Hysteria2](/configuration/inbound/hysteria2/#ignore_client_bandwidth). #### 1.11.0-beta.17 * Add port hopping support for Hysteria2 **1** * Fixes and improvements **1**: See [Hysteria2](/configuration/outbound/hysteria2/). #### 1.11.0-beta.14 * Allow adding route (exclude) address sets to routes **1** * Fixes and improvements **1**: When `auto_redirect` is not enabled, directly add `route[_exclude]_address_set` to tun routes (equivalent to `route[_exclude]_address`). Note that it **doesn't work on the Android graphical client** due to the Android VpnService not being able to handle a large number of routes (DeadSystemException), but otherwise it works fine on all command line clients and Apple platforms. See [route_address_set](/configuration/inbound/tun/#route_address_set) and [route_exclude_address_set](/configuration/inbound/tun/#route_exclude_address_set). #### 1.11.0-beta.12 * Add `rule-set merge` command * Fixes and improvements #### 1.11.0-beta.3 * Add more masquerade options for hysteria2 **1** * Fixes and improvements **1**: See [Hysteria2](/configuration/inbound/hysteria2/#masquerade). #### 1.11.0-alpha.25 * Update quic-go to v0.48.2 * Fixes and improvements #### 1.11.0-alpha.22 * Add UDP timeout route option **1** * Fixes and improvements **1**: See [Rule Action](/configuration/route/rule_action/#udp_timeout). #### 1.11.0-alpha.20 * Add UDP GSO support for WireGuard * Make GSO adaptive **1** **1**: For WireGuard outbound and endpoint, GSO will be automatically enabled when available, see [WireGuard Outbound](/configuration/outbound/wireguard/#gso). For TUN, GSO has been removed, see [Deprecated](/deprecated/#gso-option-in-tun). #### 1.11.0-alpha.19 * Upgrade WireGuard outbound to endpoint **1** * Fixes and improvements **1**: The new WireGuard endpoint combines inbound and outbound capabilities, and the old outbound will be removed in sing-box 1.13.0. See [Endpoint](/configuration/endpoint/), [WireGuard Endpoint](/configuration/endpoint/wireguard/) and [Migrate WireGuard outbound fields to route options](/migration/#migrate-wireguard-outbound-to-endpoint). ### 1.10.2 * Add deprecated warnings * Fix proxying websocket connections in HTTP/mixed inbounds * Fixes and improvements #### 1.11.0-alpha.18 * Fixes and improvements #### 1.11.0-alpha.16 * Add `cache_capacity` DNS option **1** * Add `override_address` and `override_port` route options **2** * Fixes and improvements **1**: See [DNS](/configuration/dns/#cache_capacity). **2**: See [Rule Action](/configuration/route/#override_address) and [Migrate destination override fields to route options](/migration/#migrate-destination-override-fields-to-route-options). #### 1.11.0-alpha.15 * Improve multi network dialing **1** * Fixes and improvements **1**: New options allow you to configure the network strategy flexibly. See [Dial Fields](/configuration/shared/dial/#network_strategy), [Rule Action](/configuration/route/rule_action/#network_strategy) and [Route](/configuration/route/#default_network_strategy). #### 1.11.0-alpha.14 * Add multi network dialing **1** * Fixes and improvements **1**: Similar to Surge's strategy. New options allow you to connect using multiple network interfaces, prefer or only use one type of interface, and configure a timeout to fallback to other interfaces. See [Dial Fields](/configuration/shared/dial/#network_strategy), [Rule Action](/configuration/route/rule_action/#network_strategy) and [Route](/configuration/route/#default_network_strategy). #### 1.11.0-alpha.13 * Fixes and improvements #### 1.11.0-alpha.12 * Merge route options to route actions **1** * Add `network_type`, `network_is_expensive` and `network_is_constrainted` rule items **2** * Fixes and improvements **1**: Route options in DNS route actions will no longer be considered deprecated, see [DNS Route Action](/configuration/dns/rule_action/). Also, now `udp_disable_domain_unmapping` and `udp_connect` can also be configured in route action, see [Route Action](/configuration/route/rule_action/). **2**: When using in graphical clients, new routing rule items allow you to match on network type (WIFI, cellular, etc.), whether the network is expensive, and whether Low Data Mode is enabled. See [Route Rule](/configuration/route/rule/), [DNS Route Rule](/configuration/dns/rule/) and [Headless Rule](/configuration/rule-set/headless-rule/). #### 1.11.0-alpha.9 * Improve tun compatibility **1** * Fixes and improvements **1**: When `gvisor` tun stack is enabled, even if the request passes routing, if the outbound connection establishment fails, the connection still does not need to be established and a TCP RST is replied. #### 1.11.0-alpha.7 * Introducing rule actions **1** **1**: New rule actions replace legacy inbound fields and special outbound fields, and can be used for pre-matching **2**. See [Rule](/configuration/route/rule/), [Rule Action](/configuration/route/rule_action/), [DNS Rule](/configuration/dns/rule/) and [DNS Rule Action](/configuration/dns/rule_action/). For migration, see [Migrate legacy special outbounds to rule actions](/migration/#migrate-legacy-special-outbounds-to-rule-actions), [Migrate legacy inbound fields to rule actions](/migration/#migrate-legacy-inbound-fields-to-rule-actions) and [Migrate legacy DNS route options to rule actions](/migration/#migrate-legacy-dns-route-options-to-rule-actions). **2**: Similar to Surge's pre-matching. Specifically, new rule actions allow you to reject connections with TCP RST (for TCP connections) and ICMP port unreachable (for UDP packets) before connection established to improve tun's compatibility. See [Rule Action](/configuration/route/rule_action/). #### 1.11.0-alpha.6 * Update quic-go to v0.48.1 * Set gateway for tun correctly * Fixes and improvements #### 1.11.0-alpha.2 * Add warnings for usage of deprecated features * Fixes and improvements #### 1.11.0-alpha.1 * Update quic-go to v0.48.0 * Fixes and improvements ### 1.10.1 * Fixes and improvements ### 1.10.0 Important changes since 1.9: * Introducing auto-redirect **1** * Add AdGuard DNS Filter support **2** * TUN address fields are merged **3** * Add custom options for `auto-route` and `auto-redirect` **4** * Drop support for go1.18 and go1.19 **5** * Add tailing comma support in JSON configuration * Improve sniffers **6** * Add new `inline` rule-set type **7** * Add access control options for Clash API **8** * Add `rule_set_ip_cidr_accept_empty` DNS address filter rule item **9** * Add auto reload support for local rule-set * Update fsnotify usages **10** * Add IP address support for `rule-set match` command * Add `rule-set decompile` command * Add `process_path_regex` rule item * Update uTLS to v1.6.7 **11** * Optimize memory usages of rule-sets **12** **1**: The new auto-redirect feature allows TUN to automatically configure connection redirection to improve proxy performance. When auto-redirect is enabled, new route address set options will allow you to automatically configure destination IP CIDR rules from a specified rule set to the firewall. Specified or unspecified destinations will bypass the sing-box routes to get better performance (for example, keep hardware offloading of direct traffics on the router). See [TUN](/configuration/inbound/tun). **2**: The new feature allows you to use AdGuard DNS Filter lists in a sing-box without AdGuard Home. See [AdGuard DNS Filter](/configuration/rule-set/adguard/). **3**: See [Migration](/migration/#tun-address-fields-are-merged). **4**: See [iproute2_table_index](/configuration/inbound/tun/#iproute2_table_index), [iproute2_rule_index](/configuration/inbound/tun/#iproute2_rule_index), [auto_redirect_input_mark](/configuration/inbound/tun/#auto_redirect_input_mark) and [auto_redirect_output_mark](/configuration/inbound/tun/#auto_redirect_output_mark). **5**: Due to maintenance difficulties, sing-box 1.10.0 requires at least Go 1.20 to compile. **6**: BitTorrent, DTLS, RDP, SSH sniffers are added. Now the QUIC sniffer can correctly extract the server name from Chromium requests and can identify common QUIC clients, including Chromium, Safari, Firefox, quic-go (including uquic disguised as Chrome). **7**: The new [rule-set](/configuration/rule-set/) type inline (which also becomes the default type) allows you to write headless rules directly without creating a rule-set file. **8**: With new access control options, not only can you allow Clash dashboards to access the Clash API on your local network, you can also manually limit the websites that can access the API instead of allowing everyone. See [Clash API](/configuration/experimental/clash-api/). **9**: See [DNS Rule](/configuration/dns/rule/#rule_set_ip_cidr_accept_empty). **10**: sing-box now uses fsnotify correctly and will not cancel watching if the target file is deleted or recreated via rename (e.g. `mv`). This affects all path options that support reload, including `tls.certificate_path`, `tls.key_path`, `tls.ech.key_path` and `rule_set.path`. **11**: Some legacy chrome fingerprints have been removed and will fallback to chrome, see [utls](/configuration/shared/tls#utls). **12**: See [Source Format](/configuration/rule-set/source-format/#version). ### 1.9.7 * Fixes and improvements #### 1.10.0-beta.11 * Update uTLS to v1.6.7 **1** **1**: Some legacy chrome fingerprints have been removed and will fallback to chrome, see [utls](/configuration/shared/tls#utls). #### 1.10.0-beta.10 * Add `process_path_regex` rule item * Fixes and improvements _The macOS standalone versions of sing-box (>=1.9.5/<1.10.0-beta.11) now silently fail and require manual granting of the **Full Disk Access** permission to system extension to start, probably due to Apple's changed security policy. We will prompt users about this in feature versions._ ### 1.9.6 * Fixes and improvements ### 1.9.5 * Update quic-go to v0.47.0 * Fix direct dialer not resolving domain * Fix no error return when empty DNS cache retrieved * Fix build with go1.23 * Fix stream sniffer * Fix bad redirect in clash-api * Fix wireguard events chan leak * Fix cached conn eats up read deadlines * Fix disconnected interface selected as default in windows * Update Bundle Identifiers for Apple platform clients **1** **1**: See [Migration](/migration/#bundle-identifier-updates-in-apple-platform-clients). We are still working on getting all sing-box apps back on the App Store, which should be completed within a week (SFI on the App Store and others on TestFlight are already available). #### 1.10.0-beta.8 * Fixes and improvements _With the help of a netizen, we are in the process of getting sing-box apps back on the App Store, which should be completed within a month (TestFlight is already available)._ #### 1.10.0-beta.7 * Update quic-go to v0.47.0 * Fixes and improvements #### 1.10.0-beta.6 * Add RDP sniffer * Fixes and improvements #### 1.10.0-beta.5 * Add PNA support for [Clash API](/configuration/experimental/clash-api/) * Fixes and improvements #### 1.10.0-beta.3 * Add SSH sniffer * Fixes and improvements #### 1.10.0-beta.2 * Build with go1.23 * Fixes and improvements ### 1.9.4 * Update quic-go to v0.46.0 * Update Hysteria2 BBR congestion control * Filter HTTPS ipv4hint/ipv6hint with domain strategy * Fix crash on Android when using process rules * Fix non-IP queries accepted by address filter rules * Fix UDP server for shadowsocks AEAD multi-user inbounds * Fix default next protos for v2ray QUIC transport * Fix default end value of port range configuration options * Fix reset v2ray transports * Fix panic caused by rule-set generation of duplicate keys for `domain_suffix` * Fix UDP connnection leak when sniffing * Fixes and improvements _Due to problems with our Apple developer account, sing-box apps on Apple platforms are temporarily unavailable for download or update. If your company or organization is willing to help us return to the App Store, please [contact us](mailto:contact@sagernet.org)._ #### 1.10.0-alpha.29 * Update quic-go to v0.46.0 * Fixes and improvements #### 1.10.0-alpha.25 * Add AdGuard DNS Filter support **1** **1**: The new feature allows you to use AdGuard DNS Filter lists in a sing-box without AdGuard Home. See [AdGuard DNS Filter](/configuration/rule-set/adguard/). #### 1.10.0-alpha.23 * Add Chromium support for QUIC sniffer * Add client type detect support for QUIC sniffer **1** * Fixes and improvements **1**: Now the QUIC sniffer can correctly extract the server name from Chromium requests and can identify common QUIC clients, including Chromium, Safari, Firefox, quic-go (including uquic disguised as Chrome). See [Protocol Sniff](/configuration/route/sniff/) and [Route Rule](/configuration/route/rule/#client). #### 1.10.0-alpha.22 * Optimize memory usages of rule-sets **1** * Fixes and improvements **1**: See [Source Format](/configuration/rule-set/source-format/#version). #### 1.10.0-alpha.20 * Add DTLS sniffer * Fixes and improvements #### 1.10.0-alpha.19 * Add `rule-set decompile` command * Add IP address support for `rule-set match` command * Fixes and improvements #### 1.10.0-alpha.18 * Add new `inline` rule-set type **1** * Add auto reload support for local rule-set * Update fsnotify usages **2** * Fixes and improvements **1**: The new [rule-set](/configuration/rule-set/) type inline (which also becomes the default type) allows you to write headless rules directly without creating a rule-set file. **2**: sing-box now uses fsnotify correctly and will not cancel watching if the target file is deleted or recreated via rename (e.g. `mv`). This affects all path options that support reload, including `tls.certificate_path`, `tls.key_path`, `tls.ech.key_path` and `rule_set.path`. #### 1.10.0-alpha.17 * Some chaotic changes **1** * `rule_set_ipcidr_match_source` rule items are renamed **2** * Add `rule_set_ip_cidr_accept_empty` DNS address filter rule item **3** * Update quic-go to v0.45.1 * Fixes and improvements **1**: Something may be broken, please actively report problems with this version. **2**: `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. **3**: See [DNS Rule](/configuration/dns/rule/#rule_set_ip_cidr_accept_empty). #### 1.10.0-alpha.16 * Add custom options for `auto-route` and `auto-redirect` **1** * Fixes and improvements **1**: See [iproute2_table_index](/configuration/inbound/tun/#iproute2_table_index), [iproute2_rule_index](/configuration/inbound/tun/#iproute2_rule_index), [auto_redirect_input_mark](/configuration/inbound/tun/#auto_redirect_input_mark) and [auto_redirect_output_mark](/configuration/inbound/tun/#auto_redirect_output_mark). #### 1.10.0-alpha.13 * TUN address fields are merged **1** * Add route address set support for auto-redirect **2** **1**: See [Migration](/migration/#tun-address-fields-are-merged). **2**: The new feature will allow you to configure the destination IP CIDR rules in the specified rule-sets to the firewall automatically. Specified or unspecified destinations will bypass the sing-box routes to get better performance (for example, keep hardware offloading of direct traffics on the router). See [route_address_set](/configuration/inbound/tun/#route_address_set) and [route_exclude_address_set](/configuration/inbound/tun/#route_exclude_address_set). #### 1.10.0-alpha.12 * Fix auto-redirect not configuring nftables forward chain correctly * Fixes and improvements ### 1.9.3 * Fixes and improvements #### 1.10.0-alpha.10 * Fixes and improvements ### 1.9.2 * Fixes and improvements #### 1.10.0-alpha.8 * Drop support for go1.18 and go1.19 **1** * Update quic-go to v0.45.0 * Update Hysteria2 BBR congestion control * Fixes and improvements **1**: Due to maintenance difficulties, sing-box 1.10.0 requires at least Go 1.20 to compile. ### 1.9.1 * Fixes and improvements #### 1.10.0-alpha.7 * Fixes and improvements #### 1.10.0-alpha.5 * Improve auto-redirect **1** **1**: nftables support and DNS hijacking has been added. Tun inbounds with `auto_route` and `auto_redirect` now works as expected on routers **without intervention**. #### 1.10.0-alpha.4 * Fix auto-redirect **1** * Improve auto-route on linux **2** **1**: Tun inbounds with `auto_route` and `auto_redirect` now works as expected on routers. **2**: Tun inbounds with `auto_route` and `strict_route` now works as expected on routers and servers, but the usages of [exclude_interface](/configuration/inbound/tun/#exclude_interface) need to be updated. #### 1.10.0-alpha.2 * Move auto-redirect to Tun **1** * Fixes and improvements **1**: Linux support are added. See [Tun](/configuration/inbound/tun/#auto_redirect). #### 1.10.0-alpha.1 * Add tailing comma support in JSON configuration * Add simple auto-redirect for Android **1** * Add BitTorrent sniffer **2** **1**: It allows you to use redirect inbound in the sing-box Android client and automatically configures IPv4 TCP redirection via su. This may alleviate the symptoms of some OCD patients who think that redirect can effectively save power compared to the system HTTP Proxy. See [Redirect](/configuration/inbound/redirect/). **2**: See [Protocol Sniff](/configuration/route/sniff/). ### 1.9.0 * Fixes and improvements Important changes since 1.8: * `domain_suffix` behavior update **1** * `process_path` format update on Windows **2** * Add address filter DNS rule items **3** * Add support for `client-subnet` DNS options **4** * Add rejected DNS response cache support **5** * Add `bypass_domain` and `search_domain` platform HTTP proxy options **6** * Fix missing `rule_set_ipcidr_match_source` item in DNS rules **7** * Handle Windows power events * Always disable cache for fake-ip DNS transport if `dns.independent_cache` disabled * Improve DNS truncate behavior * Update Hysteria protocol * Update quic-go to v0.43.1 * Update gVisor to 20240422.0 * Mitigating TunnelVision attacks **8** **1**: See [Migration](/migration/#domain_suffix-behavior-update). **2**: See [Migration](/migration/#process_path-format-update-on-windows). **3**: 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 [Legacy Address Filter Fields](/configuration/dns/rule#legacy-address-filter-fields). [Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) updated. **4**: See [DNS](/configuration/dns), [DNS Server](/configuration/dns/server) and [DNS Rules](/configuration/dns/rule). Since this feature makes the scenario mentioned in `alpha.1` no longer leak DNS requests, the [Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) has been updated. **5**: The new feature allows you to cache the check results of [Legacy Address Filter Fields](/configuration/dns/rule/#legacy-address-filter-fields) until expiration. **6**: See [TUN](/configuration/inbound/tun) inbound. **7**: See [DNS Rule](/configuration/dns/rule/). **8**: See [TunnelVision](/manual/misc/tunnelvision). #### 1.9.0-rc.22 * Fixes and improvements #### 1.9.0-rc.20 * Prioritize `*_route_address` in linux auto-route * Fix `*_route_address` in darwin auto-route #### 1.8.14 * Fix hysteria2 panic * Fixes and improvements #### 1.9.0-rc.18 * Add custom prefix support in EDNS0 client subnet options * Fix hysteria2 crash * Fix `store_rdrc` corrupted * Update quic-go to v0.43.1 * Fixes and improvements #### 1.9.0-rc.16 * Mitigating TunnelVision attacks **1** * Fixes and improvements **1**: See [TunnelVision](/manual/misc/tunnelvision). #### 1.9.0-rc.15 * Fixes and improvements #### 1.8.13 * Fix fake-ip mapping * Fixes and improvements #### 1.9.0-rc.14 * Fixes and improvements #### 1.9.0-rc.13 * Update Hysteria protocol * Update quic-go to v0.43.0 * Update gVisor to 20240422.0 * Fixes and improvements #### 1.8.12 * Now we have official APT and DNF repositories **1** * Fix packet MTU for QUIC protocols * Fixes and improvements **1**: Including stable and beta versions, see https://sing-box.sagernet.org/installation/package-manager/ #### 1.9.0-rc.11 * Fixes and improvements #### 1.8.11 * Fixes and improvements #### 1.8.10 * Fixes and improvements #### 1.9.0-beta.17 * Update `quic-go` to v0.42.0 * Fixes and improvements #### 1.9.0-beta.16 * Fixes and improvements _Our Testflight distribution has been temporarily blocked by Apple (possibly due to too many beta versions) and you cannot join the test, install or update the sing-box beta app right now. Please wait patiently for processing._ #### 1.9.0-beta.14 * Update gVisor to 20240212.0-65-g71212d503 * Fixes and improvements #### 1.8.9 * Fixes and improvements #### 1.8.8 * Fixes and improvements #### 1.9.0-beta.7 * Fixes and improvements #### 1.9.0-beta.6 * Fix address filter DNS rule items **1** * Fix DNS outbound responding with wrong data * Fixes and improvements **1**: Fixed an issue where address filter DNS rule was incorrectly rejected under certain circumstances. If you have enabled `store_rdrc` to save results, consider clearing the cache file. #### 1.8.7 * Fixes and improvements #### 1.9.0-alpha.15 * Fixes and improvements #### 1.9.0-alpha.14 * Improve DNS truncate behavior * Fixes and improvements #### 1.9.0-alpha.13 * Fixes and improvements #### 1.8.6 * Fixes and improvements #### 1.9.0-alpha.12 * Handle Windows power events * Always disable cache for fake-ip DNS transport if `dns.independent_cache` disabled * Fixes and improvements #### 1.9.0-alpha.11 * Fix missing `rule_set_ipcidr_match_source` item in DNS rules **1** * Fixes and improvements **1**: See [DNS Rule](/configuration/dns/rule/). #### 1.9.0-alpha.10 * Add `bypass_domain` and `search_domain` platform HTTP proxy options **1** * Fixes and improvements **1**: See [TUN](/configuration/inbound/tun) inbound. #### 1.9.0-alpha.8 * Add rejected DNS response cache support **1** * Fixes and improvements **1**: The new feature allows you to cache the check results of [Legacy Address Filter Fields](/configuration/dns/rule/#legacy-address-filter-fields) until expiration. #### 1.9.0-alpha.7 * Update gVisor to 20240206.0 * Fixes and improvements #### 1.9.0-alpha.6 * Fixes and improvements #### 1.9.0-alpha.3 * Update `quic-go` to v0.41.0 * Fixes and improvements #### 1.9.0-alpha.2 * Add support for `client-subnet` DNS options **1** * Fixes and improvements **1**: See [DNS](/configuration/dns), [DNS Server](/configuration/dns/server) and [DNS Rules](/configuration/dns/rule). Since this feature makes the scenario mentioned in `alpha.1` no longer leak DNS requests, the [Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) has been updated. #### 1.9.0-alpha.1 * `domain_suffix` behavior update **1** * `process_path` format update on Windows **2** * Add address filter DNS rule items **3** **1**: See [Migration](/migration/#domain_suffix-behavior-update). **2**: See [Migration](/migration/#process_path-format-update-on-windows). **3**: 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 [Legacy Address Filter Fields](/configuration/dns/rule#legacy-address-filter-fields). [Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) updated. #### 1.8.5 * Fixes and improvements #### 1.8.4 * Fixes and improvements #### 1.8.2 * Fixes and improvements #### 1.8.1 * Fixes and improvements ### 1.8.0 * Fixes and improvements Important changes since 1.7: * Migrate cache file from Clash API to independent options **1** * Introducing [rule-set](/configuration/rule-set/) **2** * Add `sing-box geoip`, `sing-box geosite` and `sing-box rule-set` commands **3** * Allow nested logical rules **4** * Independent `source_ip_is_private` and `ip_is_private` rules **5** * Add context to JSON decode error message **6** * Reject internal fake-ip queries **7** * Add GSO support for TUN and WireGuard system interface **8** * Add `idle_timeout` for URLTest outbound **9** * Add simple loopback detect * Optimize memory usage of idle connections * Update uTLS to 1.5.4 **10** * Update dependencies **11** **1**: See [Cache File](/configuration/experimental/cache-file/) and [Migration](/migration/#migrate-cache-file-from-clash-api-to-independent-options). **2**: rule-set is independent collections of rules that can be compiled into binaries to improve performance. Compared to legacy GeoIP and Geosite resources, it can include more types of rules, load faster, use less memory, and update automatically. See [Route#rule_set](/configuration/route/#rule_set), [Route Rule](/configuration/route/rule/), [DNS Rule](/configuration/dns/rule/), [rule-set](/configuration/rule-set/), [Source Format](/configuration/rule-set/source-format/) and [Headless Rule](/configuration/rule-set/headless-rule/). For GEO resources migration, see [Migrate GeoIP to rule-sets](/migration/#migrate-geoip-to-rule-sets) and [Migrate Geosite to rule-sets](/migration/#migrate-geosite-to-rule-sets). **3**: New commands manage GeoIP, Geosite and rule-set resources, and help you migrate GEO resources to rule-sets. **4**: Logical rules in route rules, DNS rules, and the new headless rule now allow nesting of logical rules. **5**: The `private` GeoIP country never existed and was actually implemented inside V2Ray. Since GeoIP was deprecated, we made this rule independent, see [Migration](/migration/#migrate-geoip-to-rule-sets). **6**: JSON parse errors will now include the current key path. Only takes effect when compiled with Go 1.21+. **7**: All internal DNS queries now skip DNS rules with `server` type `fakeip`, and the default DNS server can no longer be `fakeip`. This change is intended to break incorrect usage and essentially requires no action. **8**: See [TUN](/configuration/inbound/tun/) inbound and [WireGuard](/configuration/outbound/wireguard/) outbound. **9**: When URLTest is idle for a certain period of time, the scheduled delay test will be paused. **10**: Added some new [fingerprints](/configuration/shared/tls#utls). Also, starting with this release, uTLS requires at least Go 1.20. **11**: Updated `cloudflare-tls`, `gomobile`, `smux`, `tfo-go` and `wireguard-go` to latest, `quic-go` to `0.40.1` and `gvisor` to `20231204.0` #### 1.8.0-rc.11 * Fixes and improvements #### 1.7.8 * Fixes and improvements #### 1.8.0-rc.10 * Fixes and improvements #### 1.7.7 * Fix V2Ray transport `path` validation behavior **1** * Fixes and improvements **1**: See [V2Ray transport](/configuration/shared/v2ray-transport/). #### 1.8.0-rc.7 * Fixes and improvements #### 1.8.0-rc.3 * Fix V2Ray transport `path` validation behavior **1** * Fixes and improvements **1**: See [V2Ray transport](/configuration/shared/v2ray-transport/). #### 1.7.6 * Fixes and improvements #### 1.8.0-rc.1 * Fixes and improvements #### 1.8.0-beta.9 * Add simple loopback detect * Fixes and improvements #### 1.7.5 * Fixes and improvements #### 1.8.0-alpha.17 * Add GSO support for TUN and WireGuard system interface **1** * Update uTLS to 1.5.4 **2** * Update dependencies **3** * Fixes and improvements **1**: See [TUN](/configuration/inbound/tun/) inbound and [WireGuard](/configuration/outbound/wireguard/) outbound. **2**: Added some new [fingerprints](/configuration/shared/tls#utls). Also, starting with this release, uTLS requires at least Go 1.20. **3**: Updated `cloudflare-tls`, `gomobile`, `smux`, `tfo-go` and `wireguard-go` to latest, and `gvisor` to `20231204.0` This may break something, good luck! #### 1.7.4 * Fixes and improvements _Due to the long waiting time, this version is no longer waiting for approval by the Apple App Store, so updates to Apple Platforms will be delayed._ #### 1.8.0-alpha.16 * Fixes and improvements #### 1.8.0-alpha.15 * Some chaotic changes **1** * Fixes and improvements **1**: Designed to optimize memory usage of idle connections, may take effect on the following protocols: | Protocol | TCP | UDP | |------------------------------------------------------|------------------|------------------| | HTTP proxy server | :material-check: | / | | SOCKS5 | :material-close: | :material-check: | | Shadowsocks none/AEAD/AEAD2022 | :material-check: | :material-check: | | Trojan | / | :material-check: | | TUIC/Hysteria/Hysteria2 | :material-close: | :material-check: | | Multiplex | :material-close: | :material-check: | | Plain TLS (Trojan/VLESS without extra sub-protocols) | :material-check: | / | | Other protocols | :material-close: | :material-close: | At the same time, everything existing may be broken, please actively report problems with this version. #### 1.8.0-alpha.13 * Fixes and improvements #### 1.8.0-alpha.10 * Add `idle_timeout` for URLTest outbound **1** * Fixes and improvements **1**: When URLTest is idle for a certain period of time, the scheduled delay test will be paused. #### 1.7.2 * Fixes and improvements #### 1.8.0-alpha.8 * Add context to JSON decode error message **1** * Reject internal fake-ip queries **2** * Fixes and improvements **1**: JSON parse errors will now include the current key path. Only takes effect when compiled with Go 1.21+. **2**: All internal DNS queries now skip DNS rules with `server` type `fakeip`, and the default DNS server can no longer be `fakeip`. This change is intended to break incorrect usage and essentially requires no action. #### 1.8.0-alpha.7 * Fixes and improvements #### 1.7.1 * Fixes and improvements #### 1.8.0-alpha.6 * Fix rule-set matching logic **1** * Fixes and improvements **1**: Now the rules in the `rule_set` rule item can be logically considered to be merged into the rule using rule-sets, rather than completely following the AND logic. #### 1.8.0-alpha.5 * Parallel rule-set initialization * Independent `source_ip_is_private` and `ip_is_private` rules **1** **1**: The `private` GeoIP country never existed and was actually implemented inside V2Ray. Since GeoIP was deprecated, we made this rule independent, see [Migration](/migration/#migrate-geoip-to-rule-sets). #### 1.8.0-alpha.1 * Migrate cache file from Clash API to independent options **1** * Introducing [rule-set](/configuration/rule-set/) **2** * Add `sing-box geoip`, `sing-box geosite` and `sing-box rule-set` commands **3** * Allow nested logical rules **4** **1**: See [Cache File](/configuration/experimental/cache-file/) and [Migration](/migration/#migrate-cache-file-from-clash-api-to-independent-options). **2**: rule-set is independent collections of rules that can be compiled into binaries to improve performance. Compared to legacy GeoIP and Geosite resources, it can include more types of rules, load faster, use less memory, and update automatically. See [Route#rule_set](/configuration/route/#rule_set), [Route Rule](/configuration/route/rule/), [DNS Rule](/configuration/dns/rule/), [rule-set](/configuration/rule-set/), [Source Format](/configuration/rule-set/source-format/) and [Headless Rule](/configuration/rule-set/headless-rule/). For GEO resources migration, see [Migrate GeoIP to rule-sets](/migration/#migrate-geoip-to-rule-sets) and [Migrate Geosite to rule-sets](/migration/#migrate-geosite-to-rule-sets). **3**: New commands manage GeoIP, Geosite and rule-set resources, and help you migrate GEO resources to rule-sets. **4**: Logical rules in route rules, DNS rules, and the new headless rule now allow nesting of logical rules. ### 1.7.0 * Fixes and improvements Important changes since 1.6: * Add [exclude route support](/configuration/inbound/tun/) for TUN inbound * Add `udp_disable_domain_unmapping` [inbound listen option](/configuration/shared/listen/) **1** * Add [HTTPUpgrade V2Ray transport](/configuration/shared/v2ray-transport#HTTPUpgrade) support **2** * Migrate multiplex and UoT server to inbound **3** * Add TCP Brutal support for multiplex **4** * Add `wifi_ssid` and `wifi_bssid` route and DNS rules **5** * Update quic-go to v0.40.0 * Update gVisor to 20231113.0 **1**: If enabled, for UDP proxy requests addressed to a domain, the original packet address will be sent in the response instead of the mapped domain. This option is used for compatibility with clients that do not support receiving UDP packets with domain addresses, such as Surge. **2**: Introduced in V2Ray 5.10.0. The new HTTPUpgrade transport has better performance than WebSocket and is better suited for CDN abuse. **3**: Starting in 1.7.0, multiplexing support is no longer enabled by default and needs to be turned on explicitly in inbound options. **4** Hysteria Brutal Congestion Control Algorithm in TCP. A kernel module needs to be installed on the Linux server, see [TCP Brutal](/configuration/shared/tcp-brutal/) for details. **5**: Only supported in graphical clients on Android and Apple platforms. #### 1.7.0-rc.3 * Fixes and improvements #### 1.6.7 * macOS: Add button for uninstall SystemExtension in the standalone graphical client * Fix missing UDP user context on TUIC/Hysteria2 inbounds * Fixes and improvements #### 1.7.0-rc.2 * Fix missing UDP user context on TUIC/Hysteria2 inbounds * macOS: Add button for uninstall SystemExtension in the standalone graphical client #### 1.6.6 * Fixes and improvements #### 1.7.0-rc.1 * Fixes and improvements #### 1.7.0-beta.5 * Update gVisor to 20231113.0 * Fixes and improvements #### 1.7.0-beta.4 * Add `wifi_ssid` and `wifi_bssid` route and DNS rules **1** * Fixes and improvements **1**: Only supported in graphical clients on Android and Apple platforms. #### 1.7.0-beta.3 * Fix zero TTL was incorrectly reset * Fixes and improvements #### 1.6.5 * Fix crash if TUIC inbound authentication failed * Fixes and improvements #### 1.7.0-beta.2 * Fix crash if TUIC inbound authentication failed * Update quic-go to v0.40.0 * Fixes and improvements #### 1.6.4 * Fixes and improvements #### 1.7.0-beta.1 * Fixes and improvements #### 1.6.3 * iOS/Android: Fix profile auto update * Fixes and improvements #### 1.7.0-alpha.11 * iOS/Android: Fix profile auto update * Fixes and improvements #### 1.7.0-alpha.10 * Fix tcp-brutal not working with TLS * Fix Android client not closing in some cases * Fixes and improvements #### 1.6.2 * Fixes and improvements #### 1.6.1 * Our [Android client](/installation/clients/sfa/) is now available in the Google Play Store ▶️ * Fixes and improvements #### 1.7.0-alpha.6 * Fixes and improvements #### 1.7.0-alpha.4 * Migrate multiplex and UoT server to inbound **1** * Add TCP Brutal support for multiplex **2** **1**: Starting in 1.7.0, multiplexing support is no longer enabled by default and needs to be turned on explicitly in inbound options. **2** Hysteria Brutal Congestion Control Algorithm in TCP. A kernel module needs to be installed on the Linux server, see [TCP Brutal](/configuration/shared/tcp-brutal/) for details. #### 1.7.0-alpha.3 * Add [HTTPUpgrade V2Ray transport](/configuration/shared/v2ray-transport#HTTPUpgrade) support **1** * Fixes and improvements **1**: Introduced in V2Ray 5.10.0. The new HTTPUpgrade transport has better performance than WebSocket and is better suited for CDN abuse. ### 1.6.0 * Fixes and improvements Important changes since 1.5: * Our [Apple tvOS client](/installation/clients/sft/) is now available in the App Store 🍎 * Update BBR congestion control for TUIC and Hysteria2 **1** * Update brutal congestion control for Hysteria2 * Add `brutal_debug` option for Hysteria2 * Update legacy Hysteria protocol **2** * Add TLS self sign key pair generate command * Remove [Deprecated Features](/deprecated/) by agreement **1**: None of the existing Golang BBR congestion control implementations have been reviewed or unit tested. This update is intended to address the multi-send defects of the old implementation and may introduce new issues. **2** Based on discussions with the original author, the brutal CC and QUIC protocol parameters of the old protocol (Hysteria 1) have been updated to be consistent with Hysteria 2 #### 1.7.0-alpha.2 * Fix bugs introduced in 1.7.0-alpha.1 #### 1.7.0-alpha.1 * Add [exclude route support](/configuration/inbound/tun/) for TUN inbound * Add `udp_disable_domain_unmapping` [inbound listen option](/configuration/shared/listen/) **1** * Fixes and improvements **1**: If enabled, for UDP proxy requests addressed to a domain, the original packet address will be sent in the response instead of the mapped domain. This option is used for compatibility with clients that do not support receiving UDP packets with domain addresses, such as Surge. #### 1.5.5 * Fix IPv6 `auto_route` for Linux **1** * Add legacy builds for old Windows and macOS systems **2** * Fixes and improvements **1**: When `auto_route` is enabled and `strict_route` is disabled, the device can now be reached from external IPv6 addresses. **2**: Built using Go 1.20, the last version that will run on Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High Sierra, 10.14 Mojave. #### 1.6.0-rc.4 * Fixes and improvements #### 1.6.0-rc.1 * Add legacy builds for old Windows and macOS systems **1** * Fixes and improvements **1**: Built using Go 1.20, the last version that will run on Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High Sierra, 10.14 Mojave. #### 1.6.0-beta.4 * Fix IPv6 `auto_route` for Linux **1** * Fixes and improvements **1**: When `auto_route` is enabled and `strict_route` is disabled, the device can now be reached from external IPv6 addresses. #### 1.5.4 * Fix Clash cache crash on arm32 devices * Fixes and improvements #### 1.6.0-beta.3 * Update the legacy Hysteria protocol **1** * Fixes and improvements **1** Based on discussions with the original author, the brutal CC and QUIC protocol parameters of the old protocol (Hysteria 1) have been updated to be consistent with Hysteria 2 #### 1.6.0-beta.2 * Add TLS self sign key pair generate command * Update brutal congestion control for Hysteria2 * Fix Clash cache crash on arm32 devices * Update golang.org/x/net to v0.17.0 * Fixes and improvements #### 1.6.0-beta.3 * Update the legacy Hysteria protocol **1** * Fixes and improvements **1** Based on discussions with the original author, the brutal CC and QUIC protocol parameters of the old protocol (Hysteria 1) have been updated to be consistent with Hysteria 2 #### 1.6.0-beta.2 * Add TLS self sign key pair generate command * Update brutal congestion control for Hysteria2 * Fix Clash cache crash on arm32 devices * Update golang.org/x/net to v0.17.0 * Fixes and improvements #### 1.5.3 * Fix compatibility with Android 14 * Fixes and improvements #### 1.6.0-beta.1 * Fixes and improvements #### 1.6.0-alpha.5 * Fix compatibility with Android 14 * Update BBR congestion control for TUIC and Hysteria2 **1** * Fixes and improvements **1**: None of the existing Golang BBR congestion control implementations have been reviewed or unit tested. This update is intended to fix a memory leak flaw in the new implementation introduced in 1.6.0-alpha.1 and may introduce new issues. #### 1.6.0-alpha.4 * Add `brutal_debug` option for Hysteria2 * Fixes and improvements #### 1.5.2 * Our [Apple tvOS client](/installation/clients/sft/) is now available in the App Store 🍎 * Fixes and improvements #### 1.6.0-alpha.3 * Fixes and improvements #### 1.6.0-alpha.2 * Fixes and improvements #### 1.5.1 * Fixes and improvements #### 1.6.0-alpha.1 * Update BBR congestion control for TUIC and Hysteria2 **1** * Update quic-go to v0.39.0 * Update gVisor to 20230814.0 * Remove [Deprecated Features](/deprecated/) by agreement * Fixes and improvements **1**: None of the existing Golang BBR congestion control implementations have been reviewed or unit tested. This update is intended to address the multi-send defects of the old implementation and may introduce new issues. ### 1.5.0 * Fixes and improvements Important changes since 1.4: * Add TLS [ECH server](/configuration/shared/tls/) support * Improve TLS TCH client configuration * Add TLS ECH key pair generator **1** * Add TLS ECH support for QUIC based protocols **2** * Add KDE support for the `set_system_proxy` option in HTTP inbound * Add Hysteria2 protocol support **3** * Add `interrupt_exist_connections` option for `Selector` and `URLTest` outbounds **4** * Add DNS01 challenge support for ACME TLS certificate issuer **5** * Add `merge` command **6** * Mark [Deprecated Features](/deprecated/) **1**: Command: `sing-box generate ech-keypair [--pq-signature-schemes-enabled]` **2**: All inbounds and outbounds are supported, including `Naiveproxy`, `Hysteria[/2]`, `TUIC` and `V2ray QUIC transport`. **3**: See [Hysteria2 inbound](/configuration/inbound/hysteria2/) and [Hysteria2 outbound](/configuration/outbound/hysteria2/) For protocol description, please refer to [https://v2.hysteria.network](https://v2.hysteria.network) **4**: Interrupt existing connections when the selected outbound has changed. Only inbound connections are affected by this setting, internal connections will always be interrupted. **5**: Only `Alibaba Cloud DNS` and `Cloudflare` are supported, see [ACME Fields](/configuration/shared/tls#acme-fields) and [DNS01 Challenge Fields](/configuration/shared/dns01_challenge/). **6**: This command also parses path resources that appear in the configuration file and replaces them with embedded configuration, such as TLS certificates or SSH private keys. #### 1.5.0-rc.6 * Fixes and improvements #### 1.4.6 * Fixes and improvements #### 1.5.0-rc.5 * Fixed an improper authentication vulnerability in the SOCKS5 inbound * Fixes and improvements **Security Advisory** This update fixes an improper authentication vulnerability in the sing-box SOCKS inbound. This vulnerability allows an attacker to craft special requests to bypass user authentication. All users exposing SOCKS servers with user authentication in an insecure environment are advised to update immediately. 此更新修复了 sing-box SOCKS 入站中的一个不正确身份验证漏洞。 该漏洞允许攻击者制作特殊请求来绕过用户身份验证。建议所有将使用用户认证的 SOCKS 服务器暴露在不安全环境下的用户立更新。 #### 1.4.5 * Fixed an improper authentication vulnerability in the SOCKS5 inbound * Fixes and improvements **Security Advisory** This update fixes an improper authentication vulnerability in the sing-box SOCKS inbound. This vulnerability allows an attacker to craft special requests to bypass user authentication. All users exposing SOCKS servers with user authentication in an insecure environment are advised to update immediately. 此更新修复了 sing-box SOCKS 入站中的一个不正确身份验证漏洞。 该漏洞允许攻击者制作特殊请求来绕过用户身份验证。建议所有将使用用户认证的 SOCKS 服务器暴露在不安全环境下的用户立更新。 #### 1.5.0-rc.3 * Fixes and improvements #### 1.5.0-beta.12 * Add `merge` command **1** * Fixes and improvements **1**: This command also parses path resources that appear in the configuration file and replaces them with embedded configuration, such as TLS certificates or SSH private keys. ``` Merge configurations Usage: sing-box merge [output] [flags] Flags: -h, --help help for merge Global Flags: -c, --config stringArray set configuration file path -C, --config-directory stringArray set configuration directory path -D, --directory string set working directory --disable-color disable color output ``` #### 1.5.0-beta.11 * Add DNS01 challenge support for ACME TLS certificate issuer **1** * Fixes and improvements **1**: Only `Alibaba Cloud DNS` and `Cloudflare` are supported, see [ACME Fields](/configuration/shared/tls#acme-fields) and [DNS01 Challenge Fields](/configuration/shared/dns01_challenge/). #### 1.5.0-beta.10 * Add `interrupt_exist_connections` option for `Selector` and `URLTest` outbounds **1** * Fixes and improvements **1**: Interrupt existing connections when the selected outbound has changed. Only inbound connections are affected by this setting, internal connections will always be interrupted. #### 1.4.3 * Fixes and improvements #### 1.5.0-beta.8 * Fixes and improvements #### 1.4.2 * Fixes and improvements #### 1.5.0-beta.6 * Fix compatibility issues with official Hysteria2 server and client * Fixes and improvements * Mark [deprecated features](/deprecated/) #### 1.5.0-beta.3 * Fixes and improvements * Updated Hysteria2 documentation **1** **1**: Added notes indicating compatibility issues with the official Hysteria2 server and client when using `fastOpen=false` or UDP MTU >= 1200. #### 1.5.0-beta.2 * Add hysteria2 protocol support **1** * Fixes and improvements **1**: See [Hysteria2 inbound](/configuration/inbound/hysteria2/) and [Hysteria2 outbound](/configuration/outbound/hysteria2/) For protocol description, please refer to [https://v2.hysteria.network](https://v2.hysteria.network) #### 1.5.0-beta.1 * Add TLS [ECH server](/configuration/shared/tls/) support * Improve TLS TCH client configuration * Add TLS ECH key pair generator **1** * Add TLS ECH support for QUIC based protocols **2** * Add KDE support for the `set_system_proxy` option in HTTP inbound **1**: Command: `sing-box generate ech-keypair [--pq-signature-schemes-enabled]` **2**: All inbounds and outbounds are supported, including `Naiveproxy`, `Hysteria`, `TUIC` and `V2ray QUIC transport`. #### 1.4.1 * Fixes and improvements ### 1.4.0 * Fix bugs and update dependencies Important changes since 1.3: * Add TUIC support **1** * Add `udp_over_stream` option for TUIC client **2** * Add MultiPath TCP support **3** * Add `include_interface` and `exclude_interface` options for tun inbound * Pause recurring tasks when no network or device idle * Improve Android and Apple platform clients *1*: See [TUIC inbound](/configuration/inbound/tuic/) and [TUIC outbound](/configuration/outbound/tuic/) **2**: This is the TUIC port of the [UDP over TCP protocol](/configuration/shared/udp-over-tcp/), designed to provide a QUIC stream based UDP relay mode that TUIC does not provide. Since it is an add-on protocol, you will need to use sing-box or another program compatible with the protocol as a server. This mode has no positive effect in a proper UDP proxy scenario and should only be applied to relay streaming UDP traffic (basically QUIC streams). *3*: Requires sing-box to be compiled with Go 1.21. #### 1.4.0-rc.3 * Fixes and improvements #### 1.4.0-rc.2 * Fixes and improvements #### 1.4.0-rc.1 * Fix TUIC UDP #### 1.4.0-beta.6 * Add `udp_over_stream` option for TUIC client **1** * Add `include_interface` and `exclude_interface` options for tun inbound * Fixes and improvements **1**: This is the TUIC port of the [UDP over TCP protocol](/configuration/shared/udp-over-tcp/), designed to provide a QUIC stream based UDP relay mode that TUIC does not provide. Since it is an add-on protocol, you will need to use sing-box or another program compatible with the protocol as a server. This mode has no positive effect in a proper UDP proxy scenario and should only be applied to relay streaming UDP traffic (basically QUIC streams). #### 1.4.0-beta.5 * Fixes and improvements #### 1.4.0-beta.4 * Graphical clients: Persistence group expansion state * Fixes and improvements #### 1.4.0-beta.3 * Fixes and improvements #### 1.4.0-beta.2 * Add MultiPath TCP support **1** * Drop QUIC support for Go 1.18 and 1.19 due to upstream changes * Fixes and improvements *1*: Requires sing-box to be compiled with Go 1.21. #### 1.4.0-beta.1 * Add TUIC support **1** * Pause recurring tasks when no network or device idle * Fixes and improvements *1*: See [TUIC inbound](/configuration/inbound/tuic/) and [TUIC outbound](/configuration/outbound/tuic/) #### 1.3.6 * Fixes and improvements #### 1.3.5 * Fixes and improvements * Introducing our [Apple tvOS](/installation/clients/sft/) client applications **1** * Add per app proxy and app installed/updated trigger support for Android client * Add profile sharing support for Android/iOS/macOS clients **1**: Due to the requirement of tvOS 17, the app cannot be submitted to the App Store for the time being, and can only be downloaded through TestFlight. #### 1.3.4 * Fixes and improvements * We're now on the [App Store](https://apps.apple.com/us/app/sing-box/id6451272673), always free! It should be noted that due to stricter and slower review, the release of Store versions will be delayed. * We've made a standalone version of the macOS client (the original Application Extension relies on App Store distribution), which you can download as SFM-version-universal.zip in the release artifacts. #### 1.3.3 * Fixes and improvements #### 1.3.1-rc.1 * Fix bugs and update dependencies #### 1.3.1-beta.3 * Introducing our [new iOS](/installation/clients/sfi/) and [macOS](/installation/clients/sfm/) client applications **1 ** * Fixes and improvements **1**: The old testflight link and app are no longer valid. #### 1.3.1-beta.2 * Fix bugs and update dependencies #### 1.3.1-beta.1 * Fixes and improvements ### 1.3.0 * Fix bugs and update dependencies Important changes since 1.2: * Add [FakeIP](/configuration/dns/fakeip/) support **1** * Improve multiplex **2** * Add [DNS reverse mapping](/configuration/dns#reverse_mapping) support * Add `rewrite_ttl` DNS rule action * Add `store_fakeip` Clash API option * Add multi-peer support for [WireGuard](/configuration/outbound/wireguard#peers) outbound * Add loopback detect * Add Clash.Meta API compatibility for Clash API * Download Yacd-meta by default if the specified Clash `external_ui` directory is empty * Add path and headers option for HTTP outbound * Perform URLTest recheck after network changes * Fix `system` tun stack for ios * Fix network monitor for android/ios * Update VLESS and XUDP protocol * Make splice work with traffic statistics systems like Clash API * Significantly reduces memory usage of idle connections * Improve DNS caching * Add `independent_cache` [option](/configuration/dns#independent_cache) for DNS * Reimplemented shadowsocks client * Add multiplex support for VLESS outbound * Automatically add Windows firewall rules in order for the system tun stack to work * Fix TLS 1.2 support for shadow-tls client * Add `cache_id` [option](/configuration/experimental#cache_id) for Clash cache file * Fix `local` DNS transport for Android *1*: See [FAQ](/faq/fakeip/) for more information. *2*: Added new `h2mux` multiplex protocol and `padding` multiplex option, see [Multiplex](/configuration/shared/multiplex/). #### 1.3-rc2 * Fix `local` DNS transport for Android * Fix bugs and update dependencies #### 1.3-rc1 * Fix bugs and update dependencies #### 1.3-beta14 * Fixes and improvements #### 1.3-beta13 * Fix resolving fakeip domains **1** * Deprecate L3 routing * Fix bugs and update dependencies **1**: If the destination address of the connection is obtained from fakeip, dns rules with server type fakeip will be skipped. #### 1.3-beta12 * Automatically add Windows firewall rules in order for the system tun stack to work * Fix TLS 1.2 support for shadow-tls client * Add `cache_id` [option](/configuration/experimental#cache_id) for Clash cache file * Fixes and improvements #### 1.3-beta11 * Fix bugs and update dependencies #### 1.3-beta10 * Improve direct copy **1** * Improve DNS caching * Add `independent_cache` [option](/configuration/dns#independent_cache) for DNS * Reimplemented shadowsocks client **2** * Add multiplex support for VLESS outbound * Set TCP keepalive for WireGuard gVisor TCP connections * Fixes and improvements **1**: * Make splice work with traffic statistics systems like Clash API * Significantly reduces memory usage of idle connections **2**: Improved performance and reduced memory usage. #### 1.3-beta9 * Improve multiplex **1** * Fixes and improvements *1*: Added new `h2mux` multiplex protocol and `padding` multiplex option, see [Multiplex](/configuration/shared/multiplex/). #### 1.2.6 * Fix bugs and update dependencies #### 1.3-beta8 * Fix `system` tun stack for ios * Fix network monitor for android/ios * Update VLESS and XUDP protocol **1** * Fixes and improvements *1: This is an incompatible update for XUDP in VLESS if vision flow is enabled. #### 1.3-beta7 * Add `path` and `headers` options for HTTP outbound * Add multi-user support for Shadowsocks legacy AEAD inbound * Fixes and improvements #### 1.2.4 * Fixes and improvements #### 1.3-beta6 * Fix WireGuard reconnect * Perform URLTest recheck after network changes * Fix bugs and update dependencies #### 1.3-beta5 * Add Clash.Meta API compatibility for Clash API * Download Yacd-meta by default if the specified Clash `external_ui` directory is empty * Add path and headers option for HTTP outbound * Fixes and improvements #### 1.3-beta4 * Fix bugs #### 1.3-beta2 * Download clash-dashboard if the specified Clash `external_ui` directory is empty * Fix bugs and update dependencies #### 1.3-beta1 * Add [DNS reverse mapping](/configuration/dns#reverse_mapping) support * Add [L3 routing](/configuration/route/ip-rule/) support **1** * Add `rewrite_ttl` DNS rule action * Add [FakeIP](/configuration/dns/fakeip/) support **2** * Add `store_fakeip` Clash API option * Add multi-peer support for [WireGuard](/configuration/outbound/wireguard#peers) outbound * Add loopback detect *1*: It can currently be used to [route connections directly to WireGuard](/examples/wireguard-direct/) or block connections at the IP layer. *2*: See [FAQ](/faq/fakeip/) for more information. #### 1.2.3 * Introducing our [new Android client application](/installation/clients/sfa/) * Improve UDP domain destination NAT * Update reality protocol * Fix TTL calculation for DNS response * Fix v2ray HTTP transport compatibility * Fix bugs and update dependencies #### 1.2.2 * Accept `any` outbound in dns rule **1** * Fix bugs and update dependencies *1*: Now you can use the `any` outbound rule to match server address queries instead of filling in all server domains to `domain` rule. #### 1.2.1 * Fix missing default host in v2ray http transport`s request * Flush DNS cache for macOS when tun start/close * Fix tun's DNS hijacking compatibility with systemd-resolved ### 1.2.0 * Fix bugs and update dependencies Important changes since 1.1: * Introducing our [new iOS client application](/installation/clients/sfi/) * Introducing [UDP over TCP protocol version 2](/configuration/shared/udp-over-tcp/) * Add [platform options](/configuration/inbound/tun#platform) for tun inbound * Add [ShadowTLS protocol v3](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-v3-en.md) * Add [VLESS server](/configuration/inbound/vless/) and [vision](/configuration/outbound/vless#flow) support * Add [reality TLS](/configuration/shared/tls/) support * Add [NTP service](/configuration/ntp/) * Add [DHCP DNS server](/configuration/dns/server/) support * Add SSH [host key validation](/configuration/outbound/ssh/) support * Add [query_type](/configuration/dns/rule/) DNS rule item * Add fallback support for v2ray transport * Add custom TLS server support for http based v2ray transports * Add health check support for http-based v2ray transports * Add multiple configuration support #### 1.2-rc1 * Fix bugs and update dependencies #### 1.2-beta10 * Add multiple configuration support **1** * Fix bugs and update dependencies *1*: Now you can pass the parameter `--config` or `-c` multiple times, or use the new parameter `--config-directory` or `-C` to load all configuration files in a directory. Loaded configuration files are sorted by name. If you want to control the merge order, add a numeric prefix to the file name. #### 1.1.7 * Improve the stability of the VMESS server * Fix `auto_detect_interface` incorrectly identifying the default interface on Windows * Fix bugs and update dependencies #### 1.2-beta9 * Introducing the [UDP over TCP protocol version 2](/configuration/shared/udp-over-tcp/) * Add health check support for http-based v2ray transports * Remove length limit on short_id for reality TLS config * Fix bugs and update dependencies #### 1.2-beta8 * Update reality and uTLS libraries * Fix `auto_detect_interface` incorrectly identifying the default interface on Windows #### 1.2-beta7 * Fix the compatibility issue between VLESS's vision sub-protocol and the Xray-core client * Improve the stability of the VMESS server #### 1.2-beta6 * Introducing our [new iOS client application](/installation/clients/sfi/) * Add [platform options](/configuration/inbound/tun#platform) for tun inbound * Add custom TLS server support for http based v2ray transports * Add generate commands * Enable XUDP by default in VLESS * Update reality server * Update vision protocol * Fixed [user flow in vless server](/configuration/inbound/vless#usersflow) * Bug fixes * Update dependencies #### 1.2-beta5 * Add [VLESS server](/configuration/inbound/vless/) and [vision](/configuration/outbound/vless#flow) support * Add [reality TLS](/configuration/shared/tls/) support * Fix match private address #### 1.1.6 * Improve vmess request * Fix ipv6 redirect on Linux * Fix match geoip private * Fix parse hysteria UDP message * Fix socks connect response * Disable vmess header protection if transport enabled * Update QUIC v2 version number and initial salt #### 1.2-beta4 * Add [NTP service](/configuration/ntp/) * Add Add multiple server names and multi-user support for shadowtls * Add strict mode support for shadowtls v3 * Add uTLS support for shadowtls v3 #### 1.2-beta3 * Update QUIC v2 version number and initial salt * Fix shadowtls v3 implementation #### 1.2-beta2 * Add [ShadowTLS protocol v3](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-v3-en.md) * Add fallback support for v2ray transport * Fix parse hysteria UDP message * Fix socks connect response * Disable vmess header protection if transport enabled #### 1.2-beta1 * Add [DHCP DNS server](/configuration/dns/server/) support * Add SSH [host key validation](/configuration/outbound/ssh/) support * Add [query_type](/configuration/dns/rule/) DNS rule item * Add v2ray [user stats](/configuration/experimental#statsusers) api * Add new clash DNS query api * Improve vmess request * Fix ipv6 redirect on Linux * Fix match geoip private #### 1.1.5 * Add Go 1.20 support * Fix inbound default DF value * Fix auth_user route for naive inbound * Fix gRPC lite header * Ignore domain case in route rules #### 1.1.4 * Fix DNS log * Fix write to h2 conn after closed * Fix create UDP DNS transport from plain IPv6 address #### 1.1.2 * Fix http proxy auth * Fix user from stream packet conn * Fix DNS response TTL * Fix override packet conn * Skip override system proxy bypass list * Improve DNS log #### 1.1.1 * Fix acme config * Fix vmess packet conn * Suppress quic-go set DF error #### 1.1 * Fix close clash cache Important changes since 1.0: * Add support for use with android VPNService * Add tun support for WireGuard outbound * Add system tun stack * Add comment filter for config * Add option for allow optional proxy protocol header * Add Clash mode and persistence support * Add TLS ECH and uTLS support for outbound TLS options * Add internal simple-obfs and v2ray-plugin * Add ShadowsocksR outbound * Add VLESS outbound and XUDP * Skip wait for hysteria tcp handshake response * Add v2ray mux support for all inbound * Add XUDP support for VMess * Improve websocket writer * Refine tproxy write back * Fix DNS leak caused by Windows' ordinary multihomed DNS resolution behavior * Add sniff_timeout listen option * Add custom route support for tun * Add option for custom wireguard reserved bytes * Split bind_address into ipv4 and ipv6 * Add ShadowTLS v1 and v2 support #### 1.1-rc1 * Fix TLS config for h2 server * Fix crash when input bad method in shadowsocks multi-user inbound * Fix listen UDP * Fix check invalid packet on macOS #### 1.1-beta18 * Enhance defense against active probe for shadowtls server **1** **1**: The `fallback_after` option has been removed. #### 1.1-beta17 * Fix shadowtls server **1** *1*: Added [fallback_after](/configuration/inbound/shadowtls#fallback_after) option. #### 1.0.7 * Add support for new x/h2 deadline * Fix copy pipe * Fix decrypt xplus packet * Fix macOS Ventura process name match * Fix smux keepalive * Fix vmess request buffer * Fix h2c transport * Fix tor geoip * Fix udp connect for mux client * Fix default dns transport strategy #### 1.1-beta16 * Improve shadowtls server * Fix default dns transport strategy * Update uTLS to v1.2.0 #### 1.1-beta15 * Add support for new x/h2 deadline * Fix udp connect for mux client * Fix dns buffer * Fix quic dns retry * Fix create TLS config * Fix websocket alpn * Fix tor geoip #### 1.1-beta14 * Add multi-user support for hysteria inbound **1** * Add custom tls client support for std grpc * Fix smux keep alive * Fix vmess request buffer * Fix default local DNS server behavior * Fix h2c transport *1*: The `auth` and `auth_str` fields have been replaced by the `users` field. #### 1.1-beta13 * Add custom worker count option for WireGuard outbound * Split bind_address into ipv4 and ipv6 * Move WFP manipulation to strict route * Fix WireGuard outbound panic when close * Fix macOS Ventura process name match * Fix QUIC connection migration by @HyNetwork * Fix handling QUIC client SNI by @HyNetwork #### 1.1-beta12 * Fix uTLS config * Update quic-go to v0.30.0 * Update cloudflare-tls to go1.18.7 #### 1.1-beta11 * Add option for custom wireguard reserved bytes * Fix shadowtls v2 * Fix h3 dns transport * Fix copy pipe * Fix decrypt xplus packet * Fix v2ray api * Suppress no network error * Improve local dns transport #### 1.1-beta10 * Add [sniff_timeout](/configuration/shared/listen#sniff_timeout) listen option * Add [custom route](/configuration/inbound/tun#inet4_route_address) support for tun **1** * Fix interface monitor * Fix websocket headroom * Fix uTLS handshake * Fix ssh outbound * Fix sniff fragmented quic client hello * Fix DF for hysteria * Fix naive overflow * Check destination before udp connect * Update uTLS to v1.1.5 * Update tfo-go to v2.0.2 * Update fsnotify to v1.6.0 * Update grpc to v1.50.1 *1*: The `strict_route` on windows is removed. #### 1.0.6 * Fix ssh outbound * Fix sniff fragmented quic client hello * Fix naive overflow * Check destination before udp connect #### 1.1-beta9 * Fix windows route **1** * Add [v2ray statistics api](/configuration/experimental#v2ray-api-fields) * Add ShadowTLS v2 support **2** * Fixes and improvements **1**: * Fix DNS leak caused by Windows' [ordinary multihomed DNS resolution behavior](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd197552%28v%3Dws.10%29) * Flush Windows DNS cache when start/close **2**: See [ShadowTLS inbound](/configuration/inbound/shadowtls#version) and [ShadowTLS outbound](/configuration/outbound/shadowtls#version) #### 1.1-beta8 * Fix leaks on close * Improve websocket writer * Refine tproxy write back * Refine 4in6 processing * Fix shadowsocks plugins * Fix missing source address from transport connection * Fix fqdn socks5 outbound connection * Fix read source address from grpc-go #### 1.0.5 * Fix missing source address from transport connection * Fix fqdn socks5 outbound connection * Fix read source address from grpc-go #### 1.1-beta7 * Add v2ray mux and XUDP support for VMess inbound * Add XUDP support for VMess outbound * Disable DF on direct outbound by default * Fix bugs in 1.1-beta6 #### 1.1-beta6 * Add [URLTest outbound](/configuration/outbound/urltest/) * Fix bugs in 1.1-beta5 #### 1.1-beta5 * Print tags in version command * Redirect clash hello to external ui * Move shadowsocksr implementation to clash * Make gVisor optional **1** * Refactor to miekg/dns * Refactor bind control * Fix build on go1.18 * Fix clash store-selected * Fix close grpc conn * Fix port rule match logic * Fix clash api proxy type *1*: The build tag `no_gvisor` is replaced by `with_gvisor`. The default tun stack is changed to system. #### 1.0.4 * Fix close grpc conn * Fix port rule match logic * Fix clash api proxy type #### 1.1-beta4 * Add internal simple-obfs and v2ray-plugin [Shadowsocks plugins](/configuration/outbound/shadowsocks#plugin) * Add [ShadowsocksR outbound](/configuration/outbound/shadowsocksr/) * Add [VLESS outbound and XUDP](/configuration/outbound/vless/) * Skip wait for hysteria tcp handshake response * Fix socks4 client * Fix hysteria inbound * Fix concurrent write #### 1.0.3 * Fix socks4 client * Fix hysteria inbound * Fix concurrent write #### 1.1-beta3 * Fix using custom TLS client in http2 client * Fix bugs in 1.1-beta2 #### 1.1-beta2 * Add Clash mode and persistence support **1** * Add TLS ECH and uTLS support for outbound TLS options **2** * Fix socks4 request * Fix processing empty dns result *1*: Switching modes using the Clash API, and `store-selected` are now supported, see [Experimental](/configuration/experimental/). *2*: ECH (Encrypted Client Hello) is a TLS extension that allows a client to encrypt the first part of its ClientHello message, see [TLS#ECH](/configuration/shared/tls#ech). uTLS is a fork of "crypto/tls", which provides ClientHello fingerprinting resistance, see [TLS#uTLS](/configuration/shared/tls#utls). #### 1.0.2 * Fix socks4 request * Fix processing empty dns result #### 1.1-beta1 * Add support for use with android VPNService **1** * Add tun support for WireGuard outbound **2** * Add system tun stack **3** * Add comment filter for config **4** * Add option for allow optional proxy protocol header * Add half close for smux * Set UDP DF by default **5** * Set default tun mtu to 9000 * Update gVisor to 20220905.0 *1*: In previous versions, Android VPN would not work with tun enabled. The usage of tun over VPN and VPN over tun is now supported, see [Tun Inbound](/configuration/inbound/tun#auto_route). *2*: In previous releases, WireGuard outbound support was backed by the lower performance gVisor virtual interface. It achieves the same performance as wireguard-go by providing automatic system interface support. *3*: It does not depend on gVisor and has better performance in some cases. It is less compatible and may not be available in some environments. *4*: Annotated json configuration files are now supported. *5*: UDP fragmentation is now blocked by default. Including shadowsocks-libev, shadowsocks-rust and quic-go all disable segmentation by default. See [Dial Fields](/configuration/shared/dial#udp_fragment) and [Listen Fields](/configuration/shared/listen#udp_fragment). #### 1.0.1 * Fix match 4in6 address in ip_cidr * Fix clash api log level format error * Fix clash api unknown proxy type #### 1.0 * Fix wireguard reconnect * Fix naive inbound * Fix json format error message * Fix processing vmess termination signal * Fix hysteria stream error * Fix listener close when proxyproto failed #### 1.0-rc1 * Fix write log timestamp * Fix write zero * Fix dial parallel in direct outbound * Fix write trojan udp * Fix DNS routing * Add attribute support for geosite * Update documentation for [Dial Fields](/configuration/shared/dial/) #### 1.0-beta3 * Add [chained inbound](/configuration/shared/listen#detour) support * Add process_path rule item * Add macOS redirect support * Add ShadowTLS [Inbound](/configuration/inbound/shadowtls/), [Outbound](/configuration/outbound/shadowtls/) and [Examples](/examples/shadowtls/) * Fix search android package in non-owner users * Fix socksaddr type condition * Fix smux session status * Refactor inbound and outbound documentation * Minor fixes #### 1.0-beta2 * Add strict_route option for [Tun inbound](/configuration/inbound/tun#strict_route) * Add packetaddr support for [VMess outbound](/configuration/outbound/vmess#packet_addr) * Add better performing alternative gRPC implementation * Add [docker image](https://github.com/SagerNet/sing-box/pkgs/container/sing-box) * Fix sniff override destination #### 1.0-beta1 * Initial release ##### 2022/08/26 * Fix ipv6 route on linux * Fix read DNS message ##### 2022/08/25 * Let vmess use zero instead of auto if TLS enabled * Add trojan fallback for ALPN * Improve ip_cidr rule * Fix format bind_address * Fix http proxy with compressed response * Fix route connections ##### 2022/08/24 * Fix naive padding * Fix unix search path * Fix close non-duplex connections * Add ACME EAB support * Fix early close on windows and catch any * Initial zh-CN document translation ##### 2022/08/23 * Add [V2Ray Transport](/configuration/shared/v2ray-transport/) support for VMess and Trojan * Allow plain http request in Naive inbound (It can now be used with nginx) * Add proxy protocol support * Free memory after start * Parse X-Forward-For in HTTP requests * Handle SIGHUP signal ##### 2022/08/22 * Add strategy setting for each [DNS server](/configuration/dns/server/) * Add bind address to outbound options ##### 2022/08/21 * Add [Tor outbound](/configuration/outbound/tor/) * Add [SSH outbound](/configuration/outbound/ssh/) ##### 2022/08/20 * Attempt to unwrap ip-in-fqdn socksaddr * Fix read packages in android 12 * Fix route on some android devices * Improve linux process searcher * Fix write socks5 username password auth request * Skip bind connection with private destination to interface * Add [Trojan connection fallback](/configuration/inbound/trojan#fallback) ##### 2022/08/19 * Add Hysteria [Inbound](/configuration/inbound/hysteria/) and [Outbund](/configuration/outbound/hysteria/) * Add [ACME TLS certificate issuer](/configuration/shared/tls/) * Allow read config from stdin (-c stdin) * Update gVisor to 20220815.0 ##### 2022/08/18 * Fix find process with lwip stack * Fix crash on shadowsocks server * Fix crash on darwin tun * Fix write log to file ##### 2022/08/17 * Improve async dns transports ##### 2022/08/16 * Add ip_version (route/dns) rule item * Add [WireGuard](/configuration/outbound/wireguard/) outbound ##### 2022/08/15 * Add uid, android user and package rules support in [Tun](/configuration/inbound/tun/) routing. ##### 2022/08/13 * Fix dns concurrent write ##### 2022/08/12 * Performance improvements * Add UoT option for [SOCKS](/configuration/outbound/socks/) outbound ##### 2022/08/11 * Add UoT option for [Shadowsocks](/configuration/outbound/shadowsocks/) outbound, UoT support for all inbounds ##### 2022/08/10 * Add full-featured [Naive](/configuration/inbound/naive/) inbound * Fix default dns server option [#9] by iKirby ##### 2022/08/09 No changelog before. [#9]: https://github.com/SagerNet/sing-box/pull/9 ================================================ FILE: docs/clients/android/features.md ================================================ # :material-decagram: Features #### UI options * Display realtime network speed in the notification #### Service SFA allows you to run sing-box through ForegroundService or VpnService (when TUN is required). #### TUN SFA provides an unprivileged TUN implementation through Android VpnService. | TUN inbound option | Available | Note | |-------------------------------|------------------|--------------------| | `interface_name` | :material-close: | Managed by Android | | `inet4_address` | :material-check: | / | | `inet6_address` | :material-check: | / | | `mtu` | :material-check: | / | | `gso` | :material-close: | No permission | | `auto_route` | :material-check: | / | | `strict_route` | :material-close: | Not implemented | | `inet4_route_address` | :material-check: | / | | `inet6_route_address` | :material-check: | / | | `inet4_route_exclude_address` | :material-check: | / | | `inet6_route_exclude_address` | :material-check: | / | | `endpoint_independent_nat` | :material-check: | / | | `stack` | :material-check: | / | | `include_interface` | :material-close: | No permission | | `exclude_interface` | :material-close: | No permission | | `include_uid` | :material-close: | No permission | | `exclude_uid` | :material-close: | No permission | | `include_android_user` | :material-close: | No permission | | `include_package` | :material-check: | / | | `exclude_package` | :material-check: | / | | `platform` | :material-check: | / | | Route/DNS rule option | Available | Note | |-----------------------|------------------|-----------------------------------| | `process_name` | :material-close: | No permission | | `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 | | `wifi_bssid` | :material-check: | Fine location permission required | ### Override Overrides profile configuration items with platform-specific values. #### Per-app proxy SFA allows you to select a list of Android apps that require proxying or bypassing in the graphical interface to override the `include_package` and `exclude_package` configuration items. In particular, the selector also provides the “China apps” scanning feature, providing Chinese users with an excellent experience to bypass apps that do not require a proxy. Specifically, by scanning China application or SDK characteristics through dex class path and other means, there will be almost no missed reports. ### Chore * The working directory is located at `/sdcard/Android/data/io.nekohasekai.sfa/files` (External files directory) * Crash logs is located in `$working_directory/stderr.log` ================================================ FILE: docs/clients/android/index.md ================================================ --- icon: material/android --- # sing-box for Android SFA allows users to manage and run local or remote sing-box configuration files, and provides platform-specific function implementation, such as TUN transparent proxy implementation. ## :material-graph: Requirements * Android 5.0+ ## :material-download: Download * [Play Store](https://play.google.com/store/apps/details?id=io.nekohasekai.sfa) * [Play Store (Beta)](https://play.google.com/apps/testing/io.nekohasekai.sfa) * [GitHub Releases](https://github.com/SagerNet/sing-box/releases) * [F-Droid](https://f-droid.org/packages/io.nekohasekai.sfa/) (Unified signature via reproducible builds) ## :material-source-repository: Source code * [GitHub](https://github.com/SagerNet/sing-box-for-android) ================================================ FILE: docs/clients/apple/features.md ================================================ # :material-decagram: Features #### UI options * Always On * Include All Networks (Proxy traffic for LAN and cellular services) * (Apple tvOS) Import profile from iPhone/iPad #### Service SFI/SFM/SFT allows you to run sing-box through NetworkExtension with Application Extension or System Extension. #### TUN SFI/SFM/SFT provides an unprivileged TUN implementation through NetworkExtension. | TUN inbound option | Available | Note | |-------------------------------|-------------------|-------------------| | `interface_name` | :material-close:️ | Managed by Darwin | | `inet4_address` | :material-check: | / | | `inet6_address` | :material-check: | / | | `mtu` | :material-check: | / | | `gso` | :material-close: | Not implemented | | `auto_route` | :material-check: | / | | `strict_route` | :material-close:️ | Not implemented | | `inet4_route_address` | :material-check: | / | | `inet6_route_address` | :material-check: | / | | `inet4_route_exclude_address` | :material-check: | / | | `inet6_route_exclude_address` | :material-check: | / | | `endpoint_independent_nat` | :material-check: | / | | `stack` | :material-check: | / | | `include_interface` | :material-close:️ | Not implemented | | `exclude_interface` | :material-close:️ | Not implemented | | `include_uid` | :material-close:️ | Not implemented | | `exclude_uid` | :material-close:️ | Not implemented | | `include_android_user` | :material-close:️ | Not implemented | | `include_package` | :material-close:️ | Not implemented | | `exclude_package` | :material-close:️ | Not implemented | | `platform` | :material-check: | / | | Route/DNS rule option | Available | Note | |-----------------------|------------------|-----------------------| | `process_name` | :material-close: | No permission | | `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 | | `wifi_bssid` | :material-alert: | Only supported on iOS | ### Chore * Crash logs is located in `Settings` -> `View Service Log` ================================================ FILE: docs/clients/apple/index.md ================================================ --- icon: material/apple --- # sing-box for Apple platforms SFI/SFM/SFT allows users to manage and run local or remote sing-box configuration files, and provides platform-specific function implementation, such as TUN transparent proxy implementation. !!! failure "" Due to non-technical reasons, we are temporarily unable to update the sing-box app on the App Store and release the standalone version of the macOS client (TestFlight users are not affected) ## :material-graph: Requirements * iOS 15.0+ / macOS 13.0+ / Apple tvOS 17.0+ * An Apple account outside of mainland China ## :material-download: Download * ~~[App Store](https://apps.apple.com/app/sing-box-vt/id6673731168)~~ * TestFlight (Beta) TestFlight quota is only available to [sponsors](https://github.com/sponsors/nekohasekai) (one-time sponsorships are accepted). Once you donate, you can get an invitation by join our Telegram group for sponsors from [@yet_another_sponsor_bot](https://t.me/yet_another_sponsor_bot) or sending us your Apple ID [via email](mailto:contact@sagernet.org). ## ~~:material-file-download: Download (macOS standalone version)~~ * ~~[Homebrew Cask](https://formulae.brew.sh/cask/sfm)~~ ```bash # brew install sfm ``` * ~~[GitHub Releases](https://github.com/SagerNet/sing-box/releases)~~ ## :material-source-repository: Source code * [GitHub](https://github.com/SagerNet/sing-box-for-apple) ================================================ FILE: docs/clients/general.md ================================================ --- icon: material/pencil-ruler --- # General Describes and explains the functions implemented uniformly by sing-box graphical clients. ### Profile Profile describes a sing-box configuration file and its state. #### Local * Local Profile represents a local sing-box configuration with minimal state * The graphical client must provide an editor to modify configuration content #### iCloud (on iOS and macOS) * iCloud Profile represents a remote sing-box configuration with iCloud as the update source * The configuration file is stored in the sing-box folder under iCloud * The graphical client must provide an editor to modify configuration content #### Remote * Remote Profile represents a remote sing-box configuration with a URL as the update source. * The graphical client should provide a configuration content viewer * The graphical client must implement automatic profile update (default interval is 60 minutes) and HTTP Basic authorization. At the same time, the graphical client must provide support for importing remote profiles through a specific URL Scheme. The URL is defined as follows: ``` sing-box://import-remote-profile?url=urlEncodedURL#urlEncodedName ``` ### Dashboard While the sing-box service is running, the graphical client should provide a Dashboard interface to manage the service. #### Status Dashboard should display status information such as memory, connection, and traffic. #### Mode Dashboard should provide a Mode selector for switching when the configuration uses at least two `clash_mode` values. #### Groups When the configuration includes group outbounds (specifically, Selector or URLTest), the dashboard should provide a Group selector for status display or switching. ### Chore #### Core Graphical clients should provide a Core region: * Display the current sing-box version * Provides a button to clean the working directory * Provides a memory limiter switch ================================================ FILE: docs/clients/index.md ================================================ # :material-cellphone-link: Graphical Clients Maintained by Project S to provide a unified experience and platform-specific functionality. | Platform | Client | |---------------------------------------|------------------------------------------| | :material-android: Android | [sing-box for Android](./android/) | | :material-apple: iOS/macOS/Apple tvOS | [sing-box for Apple platforms](./apple/) | | :material-laptop: Desktop | Working in progress | Some third-party projects that claim to use sing-box or use sing-box as a selling point are not listed here. The core motivation of the maintainers of such projects is to acquire more users, and even though they provide friendly VPN client features, the code is usually of poor quality and contains ads. ================================================ FILE: docs/clients/index.zh.md ================================================ # :material-cellphone-link: 图形界面客户端 由 Project S 维护,提供统一的体验与平台特定的功能。 | 平台 | 客户端 | |---------------------------------------|------------------------------------------| | :material-android: Android | [sing-box for Android](./android/) | | :material-apple: iOS/macOS/Apple tvOS | [sing-box for Apple platforms](./apple/) | | :material-laptop: Desktop | 施工中 | 此处没有列出一些声称使用或以 sing-box 为卖点的第三方项目。此类项目维护者的动机是获得更多用户,即使它们提供友好的商业 VPN 客户端功能, 但代码质量很差且包含广告。 ================================================ FILE: docs/clients/privacy.md ================================================ --- icon: material/security --- # Privacy policy sing-box and official graphics clients do not collect or share personal data, and the data generated by the software is always on your device. ## Android The broad package (App) visibility (QUERY_ALL_PACKAGES) permission is used to provide per-application proxy features for VPN, sing-box will not collect your app list. If your configuration contains `wifi_ssid` or `wifi_bssid` routing rules, sing-box uses the location permission in the background to get information about the connected Wi-Fi network to make them work. ================================================ FILE: docs/configuration/certificate/index.md ================================================ --- icon: material/new-box --- !!! question "Since sing-box 1.12.0" !!! quote "Changes in sing-box 1.13.0" :material-plus: [Chrome Root Store](#store) # Certificate ### Structure ```json { "store": "", "certificate": [], "certificate_path": [], "certificate_directory_path": [] } ``` !!! note "" You can ignore the JSON Array [] tag when the content is only one item ### Fields #### store The default X509 trusted CA certificate list. | Type | Description | |--------------------|----------------------------------------------------------------------------------------------------------------| | `system` (default) | System trusted CA certificates | | `mozilla` | [Mozilla Included List](https://wiki.mozilla.org/CA/Included_Certificates) with China CA certificates removed | | `chrome` | [Chrome Root Store](https://g.co/chrome/root-policy) with China CA certificates removed | | `none` | Empty list | #### certificate The certificate line array to trust, in PEM format. #### certificate_path !!! note "" Will be automatically reloaded if file modified. The paths to certificates to trust, in PEM format. #### certificate_directory_path !!! note "" Will be automatically reloaded if file modified. The directory path to search for certificates to trust,in PEM format. ================================================ FILE: docs/configuration/certificate/index.zh.md ================================================ --- icon: material/new-box --- !!! question "自 sing-box 1.12.0 起" !!! quote "sing-box 1.13.0 中的更改" :material-plus: [Chrome Root Store](#store) # 证书 ### 结构 ```json { "store": "", "certificate": [], "certificate_path": [], "certificate_directory_path": [] } ``` !!! note "" 当内容只有一项时,可以忽略 JSON 数组 [] 标签 ### 字段 #### store 默认的 X509 受信任 CA 证书列表。 | 类型 | 描述 | |-------------------|--------------------------------------------------------------------------------------------| | `system`(默认) | 系统受信任的 CA 证书 | | `mozilla` | [Mozilla 包含列表](https://wiki.mozilla.org/CA/Included_Certificates)(已移除中国 CA 证书) | | `chrome` | [Chrome Root Store](https://g.co/chrome/root-policy)(已移除中国 CA 证书) | | `none` | 空列表 | #### certificate 要信任的证书行数组,PEM 格式。 #### certificate_path !!! note "" 文件修改时将自动重新加载。 要信任的证书路径,PEM 格式。 #### certificate_directory_path !!! note "" 文件修改时将自动重新加载。 搜索要信任的证书的目录路径,PEM 格式。 ================================================ FILE: docs/configuration/dns/fakeip.md ================================================ --- icon: material/note-remove --- !!! failure "Removed in sing-box 1.14.0" 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 ```json { "enabled": true, "inet4_range": "198.18.0.0/15", "inet6_range": "fc00::/18" } ``` ### Fields #### enabled Enable FakeIP service. #### inet4_range IPv4 address range for FakeIP. #### inet6_range IPv6 address range for FakeIP. ================================================ FILE: docs/configuration/dns/fakeip.zh.md ================================================ --- icon: material/note-remove --- !!! failure "已在 sing-box 1.14.0 移除" 旧的 fake-ip 配置已在 sing-box 1.12.0 废弃且已在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。 ### 结构 ```json { "enabled": true, "inet4_range": "198.18.0.0/15", "inet6_range": "fc00::/18" } ``` ### 字段 #### enabled 启用 FakeIP 服务。 #### inet4_range 用于 FakeIP 的 IPv4 地址范围。 #### inet6_range 用于 FakeIP 的 IPv6 地址范围。 ================================================ FILE: docs/configuration/dns/index.md ================================================ --- 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: [timeout](#timeout) !!! quote "Changes in sing-box 1.12.0" :material-decagram: [servers](#servers) !!! quote "Changes in sing-box 1.11.0" :material-plus: [cache_capacity](#cache_capacity) # DNS ### Structure ```json { "dns": { "servers": [], "rules": [], "final": "", "strategy": "", "disable_cache": false, "disable_expire": false, "independent_cache": false, "cache_capacity": 0, "optimistic": false, // or {} "timeout": "", "reverse_mapping": false, "client_subnet": "", "fakeip": {} } } ``` ### Fields | Key | Format | |----------|---------------------------------| | `server` | List of [DNS Server](./server/) | | `rules` | List of [DNS Rule](./rule/) | | `fakeip` | :material-note-remove: [FakeIP](./fakeip/) | #### final Default dns server tag. The first server will be used if empty. #### strategy Default domain strategy for resolving the domain names. One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`. #### disable_cache 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 !!! question "Since sing-box 1.11.0" 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. #### 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. Since this process relies on the act of resolving domain names by an application before making a request, it can be problematic in environments such as macOS, where DNS is proxied and cached by the system. #### client_subnet !!! question "Since sing-box 1.9.0" 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. Can be overridden by `servers.[].client_subnet` or `rules.[].client_subnet`. ================================================ FILE: docs/configuration/dns/index.zh.md ================================================ --- icon: material/alert-decagram --- !!! quote "sing-box 1.14.0 中的更改" :material-delete-clock: [independent_cache](#independent_cache) :material-plus: [optimistic](#optimistic) :material-plus: [timeout](#timeout) !!! quote "sing-box 1.12.0 中的更改" :material-decagram: [servers](#servers) !!! quote "sing-box 1.11.0 中的更改" :material-plus: [cache_capacity](#cache_capacity) # DNS ### 结构 ```json { "dns": { "servers": [], "rules": [], "final": "", "strategy": "", "disable_cache": false, "disable_expire": false, "independent_cache": false, "cache_capacity": 0, "optimistic": false, // or {} "timeout": "", "reverse_mapping": false, "client_subnet": "", "fakeip": {} } } ``` ### 字段 | 键 | 格式 | |----------|-------------------------| | `server` | 一组 [DNS 服务器](./server/) | | `rules` | 一组 [DNS 规则](./rule/) | #### final 默认 DNS 服务器的标签。 默认使用第一个服务器。 #### strategy 默认解析域名策略。 可选值: `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。 #### disable_cache 禁用 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 !!! question "自 sing-box 1.11.0 起" 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`。 #### timeout !!! question "自 sing-box 1.14.0 起" 每次 DNS 查询的默认超时时间。 默认使用 `10s`。 可被 `rules.[].timeout`(DNS 规则动作)或 `domain_resolver.timeout` 覆盖。 #### reverse_mapping 在响应 DNS 查询后存储 IP 地址的反向映射以为路由目的提供域名。 由于此过程依赖于应用程序在发出请求之前解析域名的行为,因此在 macOS 等 DNS 由系统代理和缓存的环境中可能会出现问题。 #### client_subnet !!! question "自 sing-box 1.9.0 起" 默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。 如果值是 IP 地址而不是前缀,则会自动附加 `/32` 或 `/128`。 可以被 `servers.[].client_subnet` 或 `rules.[].client_subnet` 覆盖。 #### fakeip :material-note-remove: [FakeIP](./fakeip/) 设置。 ================================================ FILE: docs/configuration/dns/rule.md ================================================ --- 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: [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) :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-alert: [ip_version](#ip_version) :material-alert: [query_type](#query_type) !!! quote "Changes in sing-box 1.13.0" :material-plus: [interface_address](#interface_address) :material-plus: [network_interface_address](#network_interface_address) :material-plus: [default_interface_address](#default_interface_address) !!! quote "Changes in sing-box 1.12.0" :material-plus: [ip_accept_any](#ip_accept_any) :material-delete-clock: [outbound](#outbound) !!! quote "Changes in sing-box 1.11.0" :material-plus: [action](#action) :material-alert: [server](#server) :material-alert: [disable_cache](#disable_cache) :material-alert: [rewrite_ttl](#rewrite_ttl) :material-alert: [client_subnet](#client_subnet) :material-plus: [network_type](#network_type) :material-plus: [network_is_expensive](#network_is_expensive) :material-plus: [network_is_constrained](#network_is_constrained) !!! quote "Changes in sing-box 1.10.0" :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) :material-plus: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty) :material-plus: [process_path_regex](#process_path_regex) !!! quote "Changes in sing-box 1.9.0" :material-plus: [geoip](#geoip) :material-plus: [ip_cidr](#ip_cidr) :material-plus: [ip_is_private](#ip_is_private) :material-plus: [client_subnet](#client_subnet) :material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) !!! quote "Changes in sing-box 1.8.0" :material-plus: [rule_set](#rule_set) :material-plus: [source_ip_is_private](#source_ip_is_private) :material-delete-clock: [geoip](#geoip) :material-delete-clock: [geosite](#geosite) ### Structure ```json { "dns": { "rules": [ { "inbound": [ "mixed-in" ], "ip_version": 6, "query_type": [ "A", "HTTPS", 32768 ], "network": "tcp", "auth_user": [ "usera", "userb" ], "protocol": [ "tls", "http", "quic" ], "domain": [ "test.com" ], "domain_suffix": [ ".cn" ], "domain_keyword": [ "test" ], "domain_regex": [ "^stun\\..+" ], "source_ip_cidr": [ "10.0.0.0/24", "192.168.0.1" ], "source_ip_is_private": false, "source_port": [ 12345 ], "source_port_range": [ "1000:2000", ":3000", "4000:" ], "port": [ 80, 443 ], "port_range": [ "1000:2000", ":3000", "4000:" ], "process_name": [ "curl" ], "process_path": [ "/usr/bin/curl" ], "process_path_regex": [ "^/usr/bin/.+" ], "package_name": [ "com.termux" ], "package_name_regex": [ "^com\\.termux.*" ], "user": [ "sekai" ], "user_id": [ 1000 ], "clash_mode": "direct", "network_type": [ "wifi" ], "network_is_expensive": false, "network_is_constrained": false, "interface_address": { "en0": [ "2000::/3" ] }, "network_interface_address": { "wifi": [ "2000::/3" ] }, "default_interface_address": [ "2000::/3" ], "source_mac_address": [ "00:11:22:33:44:55" ], "source_hostname": [ "my-device" ], "preferred_by": [ "local", "ts-dns" ], "wifi_ssid": [ "My WIFI" ], "wifi_bssid": [ "00:00:00:00:00:00" ], "rule_set": [ "geoip-cn", "geosite-cn" ], "rule_set_ip_cidr_match_source": false, "match_response": false, "ip_cidr": [ "10.0.0.0/24", "192.168.0.1" ], "ip_is_private": false, "ip_accept_any": false, "response_rcode": "", "response_answer": [], "response_ns": [], "response_extra": [], "invert": false, "outbound": [ "direct" ], "action": "route", "server": "local", // Deprecated "rule_set_ip_cidr_accept_empty": false, "rule_set_ipcidr_match_source": false, "geosite": [ "cn" ], "source_geoip": [ "private" ], "geoip": [ "cn" ] }, { "type": "logical", "mode": "and", "rules": [], "action": "route", "server": "local" } ] } } ``` !!! note "" You can ignore the JSON Array [] tag when the content is only one item ### Default Fields !!! note "" The default rule uses the following matching logic: (`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite`) && (`port` || `port_range`) && (`source_geoip` || `source_ip_cidr` || `source_ip_is_private`) && (`source_port` || `source_port_range`) && `other fields` Additionally, each branch inside an included rule-set can be considered merged into the outer rule, while different branches keep OR semantics. #### inbound 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 `tcp` or `udp`. #### auth_user Username, see each inbound for details. #### protocol Sniffed protocol, see [Sniff](/configuration/route/sniff/) for details. #### domain Match full domain. #### domain_suffix Match domain suffix. #### domain_keyword Match domain using keyword. #### domain_regex Match domain using regular expression. #### geosite !!! failure "Deprecated in sing-box 1.8.0" Geosite is deprecated and will be removed in sing-box 1.12.0, check [Migration](/migration/#migrate-geosite-to-rule-sets). Match geosite. #### source_geoip !!! failure "Deprecated in sing-box 1.8.0" GeoIP is deprecated and will be removed in sing-box 1.12.0, check [Migration](/migration/#migrate-geoip-to-rule-sets). Match source geoip. #### source_ip_cidr Match source IP CIDR. #### source_ip_is_private !!! question "Since sing-box 1.8.0" Match non-public source IP. #### source_port Match source port. #### source_port_range Match source port range. #### port Match port. #### port_range Match port range. #### process_name !!! quote "" Only supported on Linux, Windows, and macOS. Match process name. #### process_path !!! quote "" Only supported on Linux, Windows, and macOS. Match process path. #### process_path_regex !!! question "Since sing-box 1.10.0" !!! quote "" Only supported on Linux, Windows, and macOS. Match process path using regular expression. #### package_name Match android package name. #### package_name_regex !!! question "Since sing-box 1.14.0" Match android package name using regular expression. #### user !!! quote "" Only supported on Linux. Match user name. #### user_id !!! quote "" Only supported on Linux. Match user id. #### clash_mode Match Clash mode. #### network_type !!! question "Since sing-box 1.11.0" !!! quote "" Only supported in graphical clients on Android and Apple platforms. Match network type. Available values: `wifi`, `cellular`, `ethernet` and `other`. #### network_is_expensive !!! question "Since sing-box 1.11.0" !!! quote "" Only supported in graphical clients on Android and Apple platforms. Match if network is considered Metered (on Android) or considered expensive, such as Cellular or a Personal Hotspot (on Apple platforms). #### network_is_constrained !!! question "Since sing-box 1.11.0" !!! quote "" Only supported in graphical clients on Apple platforms. Match if network is in Low Data Mode. #### interface_address !!! question "Since sing-box 1.13.0" !!! quote "" Only supported on Linux, Windows, and macOS. Match interface address. #### network_interface_address !!! question "Since sing-box 1.13.0" !!! quote "" Only supported in graphical clients on Android and Apple platforms. Matches network interface (same values as `network_type`) address. #### default_interface_address !!! question "Since sing-box 1.13.0" !!! quote "" Only supported on Linux, Windows, and macOS. Match default interface address. #### source_mac_address !!! question "Since sing-box 1.14.0" !!! quote "" 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. #### source_hostname !!! question "Since sing-box 1.14.0" !!! quote "" 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. #### 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, 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 !!! quote "" Only supported in graphical clients on Android and Apple platforms, or on Linux. Match WiFi SSID. #### wifi_bssid !!! quote "" Only supported in graphical clients on Android and Apple platforms, or on Linux. Match WiFi BSSID. #### rule_set !!! question "Since sing-box 1.8.0" Match [rule-set](/configuration/route/#rule_set). #### rule_set_ipcidr_match_source !!! question "Since sing-box 1.9.0" !!! failure "Deprecated in sing-box 1.10.0" `rule_set_ipcidr_match_source` is renamed to `rule_set_ip_cidr_match_source` and will be remove in sing-box 1.11.0. Make `ip_cidr` rule items in rule-sets match the source IP. #### rule_set_ip_cidr_match_source !!! question "Since sing-box 1.10.0" 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`, `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 Invert match result. #### outbound !!! failure "Deprecated in sing-box 1.12.0" `outbound` rule items are deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-outbound-dns-rule-items-to-domain-resolver). Match outbound. `any` can be used as a value to match any outbound. #### action ==Required== See [DNS Rule Actions](../rule_action/) for details. #### server !!! failure "Deprecated in sing-box 1.11.0" Moved to [DNS Rule Action](../rule_action#route). #### disable_cache !!! failure "Deprecated in sing-box 1.11.0" Moved to [DNS Rule Action](../rule_action#route). #### rewrite_ttl !!! failure "Deprecated in sing-box 1.11.0" Moved to [DNS Rule Action](../rule_action#route). #### client_subnet !!! failure "Deprecated in sing-box 1.11.0" Moved to [DNS Rule Action](../rule_action#route). ### 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. !!! info "" `ip_cidr` items in included rule-sets also takes effect as an address filtering field. !!! note "" Enable `experimental.cache_file.store_rdrc` to cache results. #### geoip !!! failure "Removed in sing-box 1.12.0" GeoIP is deprecated in sing-box 1.8.0 and removed in sing-box 1.12.0, check [Migration](/migration/#migrate-geoip-to-rule-sets). Match GeoIP with query response. #### ip_cidr !!! question "Since sing-box 1.9.0" 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. ### 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 `logical` #### mode `and` or `or` #### rules Included rules. ================================================ FILE: docs/configuration/dns/rule.zh.md ================================================ --- 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: [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) :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-alert: [ip_version](#ip_version) :material-alert: [query_type](#query_type) !!! quote "sing-box 1.13.0 中的更改" :material-plus: [interface_address](#interface_address) :material-plus: [network_interface_address](#network_interface_address) :material-plus: [default_interface_address](#default_interface_address) !!! quote "sing-box 1.12.0 中的更改" :material-plus: [ip_accept_any](#ip_accept_any) :material-delete-clock: [outbound](#outbound) !!! quote "sing-box 1.11.0 中的更改" :material-plus: [action](#action) :material-alert: [server](#server) :material-alert: [disable_cache](#disable_cache) :material-alert: [rewrite_ttl](#rewrite_ttl) :material-alert: [client_subnet](#client_subnet) :material-plus: [network_type](#network_type) :material-plus: [network_is_expensive](#network_is_expensive) :material-plus: [network_is_constrained](#network_is_constrained) !!! quote "sing-box 1.10.0 中的更改" :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) :material-plus: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty) :material-plus: [process_path_regex](#process_path_regex) !!! quote "sing-box 1.9.0 中的更改" :material-plus: [geoip](#geoip) :material-plus: [ip_cidr](#ip_cidr) :material-plus: [ip_is_private](#ip_is_private) :material-plus: [client_subnet](#client_subnet) :material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) !!! quote "sing-box 1.8.0 中的更改" :material-plus: [rule_set](#rule_set) :material-plus: [source_ip_is_private](#source_ip_is_private) :material-delete-clock: [geoip](#geoip) :material-delete-clock: [geosite](#geosite) ### 结构 ```json { "dns": { "rules": [ { "inbound": [ "mixed-in" ], "ip_version": 6, "query_type": [ "A", "HTTPS", 32768 ], "network": "tcp", "auth_user": [ "usera", "userb" ], "protocol": [ "tls", "http", "quic" ], "domain": [ "test.com" ], "domain_suffix": [ ".cn" ], "domain_keyword": [ "test" ], "domain_regex": [ "^stun\\..+" ], "source_ip_cidr": [ "10.0.0.0/24", "192.168.0.1" ], "source_ip_is_private": false, "source_port": [ 12345 ], "source_port_range": [ "1000:2000", ":3000", "4000:" ], "port": [ 80, 443 ], "port_range": [ "1000:2000", ":3000", "4000:" ], "process_name": [ "curl" ], "process_path": [ "/usr/bin/curl" ], "process_path_regex": [ "^/usr/bin/.+" ], "package_name": [ "com.termux" ], "package_name_regex": [ "^com\\.termux.*" ], "user": [ "sekai" ], "user_id": [ 1000 ], "clash_mode": "direct", "network_type": [ "wifi" ], "network_is_expensive": false, "network_is_constrained": false, "interface_address": { "en0": [ "2000::/3" ] }, "network_interface_address": { "wifi": [ "2000::/3" ] }, "default_interface_address": [ "2000::/3" ], "source_mac_address": [ "00:11:22:33:44:55" ], "source_hostname": [ "my-device" ], "preferred_by": [ "local", "ts-dns" ], "wifi_ssid": [ "My WIFI" ], "wifi_bssid": [ "00:00:00:00:00:00" ], "rule_set": [ "geoip-cn", "geosite-cn" ], "rule_set_ip_cidr_match_source": false, "match_response": false, "ip_cidr": [ "10.0.0.0/24", "192.168.0.1" ], "ip_is_private": false, "ip_accept_any": false, "response_rcode": "", "response_answer": [], "response_ns": [], "response_extra": [], "invert": false, "outbound": [ "direct" ], "action": "route", "server": "local", // 已弃用 "rule_set_ip_cidr_accept_empty": false, "rule_set_ipcidr_match_source": false, "geosite": [ "cn" ], "source_geoip": [ "private" ], "geoip": [ "cn" ] }, { "type": "logical", "mode": "and", "rules": [], "action": "route", "server": "local" } ] } } ``` !!! note "" 当内容只有一项时,可以忽略 JSON 数组 [] 标签 ### 默认字段 !!! note "" 默认规则使用以下匹配逻辑: (`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite`) && (`port` || `port_range`) && (`source_geoip` || `source_ip_cidr` || `source_ip_is_private`) && (`source_port` || `source_port_range`) && `other fields` 另外,引用规则集中的每个分支都可视为与外层规则合并,不同分支之间仍保持 OR 语义。 #### inbound [入站](/zh/configuration/inbound/) 标签. #### 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 `tcp` 或 `udp`。 #### auth_user 认证用户名,参阅入站设置。 #### protocol 探测到的协议, 参阅 [协议探测](/zh/configuration/route/sniff/)。 #### domain 匹配完整域名。 #### domain_suffix 匹配域名后缀。 #### domain_keyword 匹配域名关键字。 #### domain_regex 匹配域名正则表达式。 #### geosite !!! failure "已在 sing-box 1.12.0 中被移除" GeoSite 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geosite-到规则集)。 匹配 Geosite。 #### source_geoip !!! failure "已在 sing-box 1.12.0 中被移除" GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。 匹配源 GeoIP。 #### source_ip_cidr 匹配源 IP CIDR。 #### source_ip_is_private !!! question "自 sing-box 1.8.0 起" 匹配非公开源 IP。 #### source_port 匹配源端口。 #### source_port_range 匹配源端口范围。 #### port 匹配端口。 #### port_range 匹配端口范围。 #### process_name !!! quote "" 仅支持 Linux、Windows 和 macOS. 匹配进程名称。 #### process_path !!! quote "" 仅支持 Linux、Windows 和 macOS. 匹配进程路径。 #### process_path_regex !!! question "自 sing-box 1.10.0 起" !!! quote "" 仅支持 Linux、Windows 和 macOS. 使用正则表达式匹配进程路径。 #### package_name 匹配 Android 应用包名。 #### package_name_regex !!! question "自 sing-box 1.14.0 起" 使用正则表达式匹配 Android 应用包名。 #### user !!! quote "" 仅支持 Linux。 匹配用户名。 #### user_id !!! quote "" 仅支持 Linux。 匹配用户 ID。 #### clash_mode 匹配 Clash 模式。 #### network_type !!! question "自 sing-box 1.11.0 起" !!! quote "" 仅在 Android 与 Apple 平台图形客户端中支持。 匹配网络类型。 Available values: `wifi`, `cellular`, `ethernet` and `other`. #### network_is_expensive !!! question "自 sing-box 1.11.0 起" !!! quote "" 仅在 Android 与 Apple 平台图形客户端中支持。 匹配如果网络被视为计费 (在 Android) 或被视为昂贵, 像蜂窝网络或个人热点 (在 Apple 平台)。 #### network_is_constrained !!! question "自 sing-box 1.11.0 起" !!! quote "" 仅在 Apple 平台图形客户端中支持。 匹配如果网络在低数据模式下。 #### interface_address !!! question "自 sing-box 1.13.0 起" !!! quote "" 仅支持 Linux、Windows 和 macOS. 匹配接口地址。 #### network_interface_address !!! question "自 sing-box 1.13.0 起" !!! quote "" 仅在 Android 与 Apple 平台图形客户端中支持。 匹配网络接口(可用值同 `network_type`)地址。 #### default_interface_address !!! question "自 sing-box 1.13.0 起" !!! quote "" 仅支持 Linux、Windows 和 macOS. 匹配默认接口地址。 #### source_mac_address !!! question "自 sing-box 1.14.0 起" !!! quote "" 仅支持 Linux、macOS,或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。 匹配源设备 MAC 地址。 #### source_hostname !!! question "自 sing-box 1.14.0 起" !!! quote "" 仅支持 Linux、macOS,或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。 匹配源设备从 DHCP 租约获取的主机名。 #### preferred_by !!! question "自 sing-box 1.14.0 起" 匹配指定 DNS 服务器的首选域名。 | 类型 | 匹配 | |-------------|-------------------------------------------------------------| | `hosts` | 匹配预定义条目和 hosts 文件中的条目 | | `local` | 匹配 hosts 中的条目、邻居解析得到的主机名以及 mDNS 本地域名 | | `mdns` | 匹配 mDNS 本地域名(`*.local.` 以及 IPv4/IPv6 链路本地反向区域) | | `tailscale` | 匹配 MagicDNS 主机和 DNS 路由后缀 | | `resolved` | 匹配 systemd-resolved 链路中的分流域名和搜索域 | #### wifi_ssid !!! quote "" 仅在 Android 与 Apple 平台图形客户端和 Linux 中支持。 匹配 WiFi SSID。 #### wifi_bssid !!! quote "" 仅在 Android 与 Apple 平台图形客户端和 Linux 中支持。 匹配 WiFi BSSID。 #### rule_set !!! question "自 sing-box 1.8.0 起" 匹配[规则集](/zh/configuration/route/#rule_set)。 #### rule_set_ipcidr_match_source !!! question "自 sing-box 1.9.0 起" !!! failure "已在 sing-box 1.10.0 废弃" `rule_set_ipcidr_match_source` 已重命名为 `rule_set_ip_cidr_match_source` 且将在 sing-box 1.11.0 中被移除。 使规则集中的 `ip_cidr` 规则匹配源 IP。 #### rule_set_ip_cidr_match_source !!! question "自 sing-box 1.10.0 起" 使规则集中的 `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` 和 `ip_accept_any` 也需要此选项。 #### ip_accept_any !!! question "自 sing-box 1.12.0 起" 当 DNS 查询响应包含至少一个地址时匹配。 #### invert 反选匹配结果。 #### outbound !!! failure "已在 sing-box 1.12.0 废弃" `outbound` 规则项已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-outbound-dns-规则项到域解析选项)。 匹配出站。 `any` 可作为值用于匹配任意出站。 #### action ==必填== 参阅 [规则动作](../rule_action/)。 #### server !!! failure "已在 sing-box 1.11.0 废弃" 已移动到 [DNS 规则动作](../rule_action#route). #### disable_cache !!! failure "已在 sing-box 1.11.0 废弃" 已移动到 [DNS 规则动作](../rule_action#route). #### rewrite_ttl !!! failure "已在 sing-box 1.11.0 废弃" 已移动到 [DNS 规则动作](../rule_action#route). #### client_subnet !!! failure "已在 sing-box 1.11.0 废弃" 已移动到 [DNS 规则动作](../rule_action#route). ### 旧版地址筛选字段 !!! failure "已在 sing-box 1.14.0 废弃" 旧版地址筛选字段已废弃,且将在 sing-box 1.16.0 中被移除, 参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。 仅对地址请求 (A/AAAA/HTTPS) 生效。 当查询结果与地址筛选规则项不匹配时,将跳过当前规则。 !!! info "" 引用的规则集中的 `ip_cidr` 项也作为地址筛选字段生效。 !!! note "" 启用 `experimental.cache_file.store_rdrc` 以缓存结果。 #### geoip !!! failure "已在 sing-box 1.12.0 中被移除" GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。 与查询响应匹配 GeoIP。 #### ip_cidr !!! question "自 sing-box 1.9.0 起" 与查询响应匹配 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` 规则接受空查询响应。 ### 响应匹配字段 !!! question "自 sing-box 1.14.0 起" 已评估的响应的匹配字段。需要将 `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) 中相同。 ### 逻辑字段 #### type `logical` #### mode ==必填== `and` 或 `or` #### rules ==必填== 包括的规则。 ================================================ FILE: docs/configuration/dns/rule_action.md ================================================ --- 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) :material-plus: [disable_optimistic_cache](#disable_optimistic_cache) :material-plus: [timeout](#timeout) !!! quote "Changes in sing-box 1.12.0" :material-plus: [strategy](#strategy) :material-plus: [predefined](#predefined) !!! question "Since sing-box 1.11.0" ### route ```json { "action": "route", // default "server": "", "strategy": "", "disable_cache": false, "disable_optimistic_cache": false, "rewrite_ttl": null, "timeout": "", "client_subnet": null } ``` `route` inherits the classic rule behavior of routing DNS requests to the specified server. #### server ==Required== Tag of target server. #### strategy !!! question "Since sing-box 1.12.0" !!! 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. One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`. #### disable_cache 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. #### 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. If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically. Will override `dns.client_subnet`. ### evaluate !!! question "Since sing-box 1.14.0" ```json { "action": "evaluate", "server": "", "disable_cache": false, "disable_optimistic_cache": false, "rewrite_ttl": null, "timeout": "", "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. #### disable_optimistic_cache !!! question "Since sing-box 1.14.0" Disable optimistic DNS caching in this query. #### rewrite_ttl 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. 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 ```json { "action": "route-options", "disable_cache": false, "disable_optimistic_cache": false, "rewrite_ttl": null, "timeout": "", "client_subnet": null } ``` `route-options` set options for routing. ### reject ```json { "action": "reject", "method": "", "no_drop": false } ``` `reject` reject DNS requests. #### method - `default`: Reply with REFUSED. - `drop`: Drop the request. `default` will be used by default. #### no_drop If not enabled, `method` will be temporarily overwritten to `drop` after 50 triggers in 30s. Not available when `method` is set to drop. ### predefined !!! question "Since sing-box 1.12.0" ```json { "action": "predefined", "rcode": "", "answer": [], "ns": [], "extra": [] } ``` `predefined` responds with predefined DNS records. #### rcode The response code. | Value | Value in the legacy rcode server | Description | |------------|----------------------------------|-----------------| | `NOERROR` | `success` | Ok | | `FORMERR` | `format_error` | Bad request | | `SERVFAIL` | `server_failure` | Server failure | | `NXDOMAIN` | `name_error` | Not found | | `NOTIMP` | `not_implemented` | Not implemented | | `REFUSED` | `refused` | Refused | `NOERROR` will be used by default. #### answer List of text DNS record to respond as answers. Examples: | Record Type | Example | |-------------|-------------------------------| | `A` | `localhost. IN A 127.0.0.1` | | `AAAA` | `localhost. IN AAAA ::1` | | `TXT` | `localhost. IN TXT \"Hello\"` | #### ns List of text DNS record to respond as name servers. #### extra List of text DNS record to respond as extra records. ================================================ FILE: docs/configuration/dns/rule_action.zh.md ================================================ --- icon: material/new-box --- !!! quote "sing-box 1.14.0 中的更改" :material-delete-clock: [strategy](#strategy) :material-plus: [evaluate](#evaluate) :material-plus: [respond](#respond) :material-plus: [disable_optimistic_cache](#disable_optimistic_cache) :material-plus: [timeout](#timeout) !!! quote "sing-box 1.12.0 中的更改" :material-plus: [strategy](#strategy) :material-plus: [predefined](#predefined) !!! question "自 sing-box 1.11.0 起" ### route ```json { "action": "route", // 默认 "server": "", "strategy": "", "disable_cache": false, "disable_optimistic_cache": false, "rewrite_ttl": null, "timeout": "", "client_subnet": null } ``` `route` 继承了将 DNS 请求 路由到指定服务器的经典规则动作。 #### server ==必填== 目标 DNS 服务器的标签。 #### strategy !!! question "自 sing-box 1.12.0 起" !!! failure "已在 sing-box 1.14.0 废弃" `strategy` 已在 sing-box 1.14.0 废弃,且将在 sing-box 1.16.0 中被移除。 为此查询设置域名策略。 可选项:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。 #### disable_cache 在此查询中禁用缓存。 #### disable_optimistic_cache !!! question "自 sing-box 1.14.0 起" 在此查询中禁用乐观 DNS 缓存。 #### rewrite_ttl 重写 DNS 回应中的 TTL。 #### timeout !!! question "自 sing-box 1.14.0 起" 覆盖匹配查询的 DNS 查询超时时间。 将覆盖 `dns.timeout`。 #### client_subnet 默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。 如果值是 IP 地址而不是前缀,则会自动附加 `/32` 或 `/128`。 将覆盖 `dns.client_subnet`. ### evaluate !!! question "自 sing-box 1.14.0 起" ```json { "action": "evaluate", "server": "", "disable_cache": false, "disable_optimistic_cache": false, "rewrite_ttl": null, "timeout": "", "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 在此查询中禁用缓存。 #### disable_optimistic_cache !!! question "自 sing-box 1.14.0 起" 在此查询中禁用乐观 DNS 缓存。 #### rewrite_ttl 重写 DNS 回应中的 TTL。 #### timeout !!! question "自 sing-box 1.14.0 起" 覆盖匹配查询的 DNS 查询超时时间。 将覆盖 `dns.timeout`。 #### 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 { "action": "route-options", "disable_cache": false, "disable_optimistic_cache": false, "rewrite_ttl": null, "timeout": "", "client_subnet": null } ``` `route-options` 为路由设置选项。 ### reject ```json { "action": "reject", "method": "", "no_drop": false } ``` `reject` 拒绝 DNS 请求。 #### method - `default`: 返回 REFUSED。 - `drop`: 丢弃请求。 默认使用 `default`。 #### no_drop 如果未启用,则 30 秒内触发 50 次后,`method` 将被暂时覆盖为 `drop`。 当 `method` 设为 `drop` 时不可用。 ### predefined !!! question "自 sing-box 1.12.0 起" ```json { "action": "predefined", "rcode": "", "answer": [], "ns": [], "extra": [] } ``` `predefined` 以预定义的 DNS 记录响应。 #### rcode 响应码。 | 值 | 旧 rcode DNS 服务器中的值 | 描述 | |------------|--------------------|-----------------| | `NOERROR` | `success` | Ok | | `FORMERR` | `format_error` | Bad request | | `SERVFAIL` | `server_failure` | Server failure | | `NXDOMAIN` | `name_error` | Not found | | `NOTIMP` | `not_implemented` | Not implemented | | `REFUSED` | `refused` | Refused | 默认使用 `NOERROR`。 #### answer 用于作为回答响应的文本 DNS 记录列表。 例子: | 记录类型 | 例子 | |--------|-------------------------------| | `A` | `localhost. IN A 127.0.0.1` | | `AAAA` | `localhost. IN AAAA ::1` | | `TXT` | `localhost. IN TXT \"Hello\"` | #### ns 用于作为名称服务器响应的文本 DNS 记录列表。 #### extra 用于作为额外记录响应的文本 DNS 记录列表。 ================================================ FILE: docs/configuration/dns/server/dhcp.md ================================================ --- icon: material/new-box --- !!! question "Since sing-box 1.12.0" # DHCP ### Structure ```json { "dns": { "servers": [ { "type": "dhcp", "tag": "", "interface": "", // Dial Fields } ] } } ``` ### Fields #### interface Interface name to listen on. Tge default interface will be used by default. ### Dial Fields See [Dial Fields](/configuration/shared/dial/) for details. ================================================ FILE: docs/configuration/dns/server/dhcp.zh.md ================================================ --- icon: material/new-box --- !!! question "自 sing-box 1.12.0 起" # DHCP ### 结构 ```json { "dns": { "servers": [ { "type": "dhcp", "tag": "", "interface": "", // 拨号字段 } ] } } ``` ### 字段 #### interface 要监听的网络接口名称。 默认使用默认接口。 ### 拨号字段 参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。 ================================================ FILE: docs/configuration/dns/server/fakeip.md ================================================ --- icon: material/new-box --- !!! question "Since sing-box 1.12.0" # Fake IP ### Structure ```json { "dns": { "servers": [ { "type": "fakeip", "tag": "", "inet4_range": "198.18.0.0/15", "inet6_range": "fc00::/18" } ] } } ``` ### Fields #### inet4_range IPv4 address range for FakeIP. #### inet6_address IPv6 address range for FakeIP. ================================================ FILE: docs/configuration/dns/server/fakeip.zh.md ================================================ --- icon: material/new-box --- !!! question "自 sing-box 1.12.0 起" # Fake IP ### 结构 ```json { "dns": { "servers": [ { "type": "fakeip", "tag": "", "inet4_range": "198.18.0.0/15", "inet6_range": "fc00::/18" } ] } } ``` ### 字段 #### inet4_range FakeIP 的 IPv4 地址范围。 #### inet6_range FakeIP 的 IPv6 地址范围。 ================================================ FILE: docs/configuration/dns/server/hosts.md ================================================ --- icon: material/new-box --- !!! question "Since sing-box 1.12.0" # Hosts ### Structure ```json { "dns": { "servers": [ { "type": "hosts", "tag": "", "path": [], "predefined": {} } ] } } ``` !!! note "" You can ignore the JSON Array [] tag when the content is only one item ### Fields #### path List of paths to hosts files. `/etc/hosts` is used by default. `C:\Windows\System32\Drivers\etc\hosts` is used by default on Windows. Example: ```json { // "path": "/etc/hosts" "path": [ "/etc/hosts", "$HOME/.hosts" ] } ``` #### predefined Predefined hosts. Example: ```json { "predefined": { "www.google.com": "127.0.0.1", "localhost": [ "127.0.0.1", "::1" ] } } ``` ### Examples === "Use hosts if available" === ":material-card-multiple: sing-box 1.14.0" ```json { "dns": { "servers": [ { ... }, { "type": "hosts", "tag": "hosts" } ], "rules": [ { "preferred_by": "hosts", "action": "route", "server": "hosts" } ] } } ``` === ":material-card-remove: sing-box < 1.14.0" ```json { "dns": { "servers": [ { ... }, { "type": "hosts", "tag": "hosts" } ], "rules": [ { "ip_accept_any": true, "server": "hosts" } ] } } ``` ================================================ FILE: docs/configuration/dns/server/hosts.zh.md ================================================ --- icon: material/new-box --- !!! question "自 sing-box 1.12.0 起" # Hosts ### 结构 ```json { "dns": { "servers": [ { "type": "hosts", "tag": "", "path": [], "predefined": {} } ] } } ``` !!! note "" 当内容只有一项时,可以忽略 JSON 数组 [] 标签 ### 字段 #### path hosts 文件路径列表。 默认使用 `/etc/hosts`。 在 Windows 上默认使用 `C:\Windows\System32\Drivers\etc\hosts`。 示例: ```json { // "path": "/etc/hosts" "path": [ "/etc/hosts", "$HOME/.hosts" ] } ``` #### predefined 预定义的 hosts。 示例: ```json { "predefined": { "www.google.com": "127.0.0.1", "localhost": [ "127.0.0.1", "::1" ] } } ``` ### 示例 === "如果可用则使用 hosts" === ":material-card-multiple: sing-box 1.14.0" ```json { "dns": { "servers": [ { ... }, { "type": "hosts", "tag": "hosts" } ], "rules": [ { "preferred_by": "hosts", "action": "route", "server": "hosts" } ] } } ``` === ":material-card-remove: sing-box < 1.14.0" ```json { "dns": { "servers": [ { ... }, { "type": "hosts", "tag": "hosts" } ], "rules": [ { "ip_accept_any": true, "server": "hosts" } ] } } ``` ================================================ FILE: docs/configuration/dns/server/http3.md ================================================ --- icon: material/new-box --- !!! question "Since sing-box 1.12.0" # DNS over HTTP3 (DoH3) ### Structure ```json { "dns": { "servers": [ { "type": "h3", "tag": "", "server": "", "server_port": 443, "path": "", "headers": {}, "tls": {}, // Dial Fields } ] } } ``` !!! info "Difference from legacy H3 server" * The old server uses default outbound by default unless detour is specified; the new one uses dialer just like outbound, which is equivalent to using an empty direct outbound by default. * The old server uses `address_resolver` and `address_strategy` to resolve the domain name in the server; the new one uses `domain_resolver` and `domain_strategy` in [Dial Fields](/configuration/shared/dial/) instead. ### Fields #### server ==Required== The address of the DNS server. If domain name is used, `domain_resolver` must also be set to resolve IP address. #### server_port The port of the DNS server. `443` will be used by default. #### path The path of the DNS server. `/dns-query` will be used by default. #### headers Additional headers to be sent to the DNS server. #### tls TLS configuration, see [TLS](/configuration/shared/tls/#outbound). ### Dial Fields See [Dial Fields](/configuration/shared/dial/) for details. ================================================ FILE: docs/configuration/dns/server/http3.zh.md ================================================ --- icon: material/new-box --- !!! question "自 sing-box 1.12.0 起" # DNS over HTTP3 (DoH3) ### 结构 ```json { "dns": { "servers": [ { "type": "h3", "tag": "", "server": "", "server_port": 443, "path": "", "headers": {}, "tls": {}, // 拨号字段 } ] } } ``` !!! info "与旧版 H3 服务器的区别" * 旧服务器默认使用默认出站,除非指定了绕行;新服务器像出站一样使用拨号器,相当于默认使用空的直连出站。 * 旧服务器使用 `address_resolver` 和 `address_strategy` 来解析服务器中的域名;新服务器改用 [拨号字段](/zh/configuration/shared/dial/) 中的 `domain_resolver` 和 `domain_strategy`。 ### 字段 #### server ==必填== DNS 服务器的地址。 如果使用域名,还必须设置 `domain_resolver` 来解析 IP 地址。 #### server_port DNS 服务器的端口。 默认使用 `443`。 #### path DNS 服务器的路径。 默认使用 `/dns-query`。 #### headers 发送到 DNS 服务器的额外标头。 #### tls TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。 ### 拨号字段 参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。 ================================================ FILE: docs/configuration/dns/server/https.md ================================================ --- icon: material/new-box --- !!! question "Since sing-box 1.12.0" # DNS over HTTPS (DoH) ### Structure ```json { "dns": { "servers": [ { "type": "https", "tag": "", "server": "", "server_port": 443, "path": "", "headers": {}, "tls": {}, // Dial Fields } ] } } ``` !!! info "Difference from legacy HTTPS server" * The old server uses default outbound by default unless detour is specified; the new one uses dialer just like outbound, which is equivalent to using an empty direct outbound by default. * The old server uses `address_resolver` and `address_strategy` to resolve the domain name in the server; the new one uses `domain_resolver` and `domain_strategy` in [Dial Fields](/configuration/shared/dial/) instead. ### Fields #### server ==Required== The address of the DNS server. If domain name is used, `domain_resolver` must also be set to resolve IP address. #### server_port The port of the DNS server. `443` will be used by default. #### path The path of the DNS server. `/dns-query` will be used by default. #### headers Additional headers to be sent to the DNS server. #### tls TLS configuration, see [TLS](/configuration/shared/tls/#outbound). ### Dial Fields See [Dial Fields](/configuration/shared/dial/) for details. ================================================ FILE: docs/configuration/dns/server/https.zh.md ================================================ --- icon: material/new-box --- !!! question "自 sing-box 1.12.0 起" # DNS over HTTPS (DoH) ### 结构 ```json { "dns": { "servers": [ { "type": "https", "tag": "", "server": "", "server_port": 443, "path": "", "headers": {}, "tls": {}, // 拨号字段 } ] } } ``` !!! info "与旧版 HTTPS 服务器的区别" * 旧服务器默认使用默认出站,除非指定了绕行;新服务器像出站一样使用拨号器,相当于默认使用空的直连出站。 * 旧服务器使用 `address_resolver` 和 `address_strategy` 来解析服务器中的域名;新服务器改用 [拨号字段](/zh/configuration/shared/dial/) 中的 `domain_resolver` 和 `domain_strategy`。 ### 字段 #### server ==必填== DNS 服务器的地址。 如果使用域名,还必须设置 `domain_resolver` 来解析 IP 地址。 #### server_port DNS 服务器的端口。 默认使用 `443`。 #### path DNS 服务器的路径。 默认使用 `/dns-query`。 #### headers 发送到 DNS 服务器的额外标头。 #### tls TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。 ### 拨号字段 参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。 ================================================ FILE: docs/configuration/dns/server/index.md ================================================ --- 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) # DNS Server ### Structure ```json { "dns": { "servers": [ { "type": "", "tag": "" } ] } } ``` #### type The type of the DNS server. | Type | Format | |-----------------|---------------------------| | empty (default) | :material-note-remove: [Legacy](./legacy/) | | `local` | [Local](./local/) | | `hosts` | [Hosts](./hosts/) | | `tcp` | [TCP](./tcp/) | | `udp` | [UDP](./udp/) | | `tls` | [TLS](./tls/) | | `quic` | [QUIC](./quic/) | | `https` | [HTTPS](./https/) | | `h3` | [HTTP/3](./http3/) | | `dhcp` | [DHCP](./dhcp/) | | `mdns` | [mDNS](./mdns/) | | `fakeip` | [Fake IP](./fakeip/) | | `tailscale` | [Tailscale](./tailscale/) | | `resolved` | [Resolved](./resolved/) | #### tag The tag of the DNS server. ================================================ FILE: docs/configuration/dns/server/index.zh.md ================================================ --- icon: material/alert-decagram --- !!! quote "sing-box 1.14.0 中的更改" :material-plus: [mdns](./mdns/) !!! quote "sing-box 1.12.0 中的更改" :material-plus: [type](#type) # DNS Server ### 结构 ```json { "dns": { "servers": [ { "type": "", "tag": "" } ] } } ``` #### type DNS 服务器的类型。 | 类型 | 格式 | |-----------------|---------------------------| | empty (default) | :material-note-remove: [Legacy](./legacy/) | | `local` | [Local](./local/) | | `hosts` | [Hosts](./hosts/) | | `tcp` | [TCP](./tcp/) | | `udp` | [UDP](./udp/) | | `tls` | [TLS](./tls/) | | `quic` | [QUIC](./quic/) | | `https` | [HTTPS](./https/) | | `h3` | [HTTP/3](./http3/) | | `dhcp` | [DHCP](./dhcp/) | | `mdns` | [mDNS](./mdns/) | | `fakeip` | [Fake IP](./fakeip/) | | `tailscale` | [Tailscale](./tailscale/) | | `resolved` | [Resolved](./resolved/) | #### tag DNS 服务器的标签。 ================================================ FILE: docs/configuration/dns/server/legacy.md ================================================ --- icon: material/note-remove --- !!! failure "Removed in sing-box 1.14.0" 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" :material-plus: [client_subnet](#client_subnet) ### Structure ```json { "dns": { "servers": [ { "tag": "", "address": "", "address_resolver": "", "address_strategy": "", "strategy": "", "detour": "", "client_subnet": "" } ] } } ``` ### Fields #### tag The tag of the dns server. #### address ==Required== The address of the dns server. | Protocol | Format | |--------------------------------------|-------------------------------| | `System` | `local` | | `TCP` | `tcp://1.0.0.1` | | `UDP` | `8.8.8.8` `udp://8.8.4.4` | | `TLS` | `tls://dns.google` | | `HTTPS` | `https://1.1.1.1/dns-query` | | `QUIC` | `quic://dns.adguard.com` | | `HTTP3` | `h3://8.8.8.8/dns-query` | | `RCode` | `rcode://refused` | | `DHCP` | `dhcp://auto` or `dhcp://en0` | | [FakeIP](/configuration/dns/fakeip/) | `fakeip` | !!! warning "" To ensure that Android system DNS is in effect, rather than Go's built-in default resolver, enable CGO at compile time. !!! info "" the RCode transport is often used to block queries. Use with rules and the `disable_cache` rule option. | RCode | Description | |-------------------|-----------------------| | `success` | `No error` | | `format_error` | `Format error` | | `server_failure` | `Server failure` | | `name_error` | `Non-existent domain` | | `not_implemented` | `Not implemented` | | `refused` | `Query refused` | #### address_resolver ==Required if address contains domain== Tag of a another server to resolve the domain name in the address. #### address_strategy The domain strategy for resolving the domain name in the address. One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`. `dns.strategy` will be used if empty. #### strategy Default domain strategy for resolving the domain names. One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`. Take no effect if overridden by other settings. #### detour Tag of an outbound for connecting to the dns server. Default outbound will be used if empty. #### client_subnet !!! question "Since sing-box 1.9.0" 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. Can be overridden by `rules.[].client_subnet`. Will override `dns.client_subnet`. ================================================ FILE: docs/configuration/dns/server/legacy.zh.md ================================================ --- icon: material/note-remove --- !!! failure "已在 sing-box 1.14.0 移除" 旧的 DNS 服务器配置已在 sing-box 1.12.0 废弃且已在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。 !!! quote "sing-box 1.9.0 中的更改" :material-plus: [client_subnet](#client_subnet) ### 结构 ```json { "dns": { "servers": [ { "tag": "", "address": "", "address_resolver": "", "address_strategy": "", "strategy": "", "detour": "", "client_subnet": "" } ] } } ``` ### 字段 #### tag DNS 服务器的标签。 #### address ==必填== DNS 服务器的地址。 | 协议 | 格式 | |--------------------------------------|------------------------------| | `System` | `local` | | `TCP` | `tcp://1.0.0.1` | | `UDP` | `8.8.8.8` `udp://8.8.4.4` | | `TLS` | `tls://dns.google` | | `HTTPS` | `https://1.1.1.1/dns-query` | | `QUIC` | `quic://dns.adguard.com` | | `HTTP3` | `h3://8.8.8.8/dns-query` | | `RCode` | `rcode://refused` | | `DHCP` | `dhcp://auto` 或 `dhcp://en0` | | [FakeIP](/zh/configuration/dns/fakeip/) | `fakeip` | !!! warning "" 为了确保 Android 系统 DNS 生效,而不是 Go 的内置默认解析器,请在编译时启用 CGO。 !!! info "" RCode 传输层传输层常用于屏蔽请求. 与 DNS 规则和 `disable_cache` 规则选项一起使用。 | RCode | 描述 | |-------------------|----------| | `success` | `无错误` | | `format_error` | `请求格式错误` | | `server_failure` | `服务器出错` | | `name_error` | `域名不存在` | | `not_implemented` | `功能未实现` | | `refused` | `请求被拒绝` | #### address_resolver ==如果服务器地址包括域名则必须== 用于解析本 DNS 服务器的域名的另一个 DNS 服务器的标签。 #### address_strategy 用于解析本 DNS 服务器的域名的策略。 可选项:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。 默认使用 `dns.strategy`。 #### strategy 默认解析策略。 可选项:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。 如果被其他设置覆盖则不生效。 #### detour 用于连接到 DNS 服务器的出站的标签。 如果为空,将使用默认出站。 #### client_subnet !!! question "自 sing-box 1.9.0 起" 默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。 如果值是 IP 地址而不是前缀,则会自动附加 `/32` 或 `/128`。 可以被 `rules.[].client_subnet` 覆盖。 将覆盖 `dns.client_subnet`。 ================================================ FILE: docs/configuration/dns/server/local.md ================================================ --- 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) !!! question "Since sing-box 1.12.0" # Local ### Structure ```json { "dns": { "servers": [ { "type": "local", "tag": "", "prefer_go": false, "neighbor_domain": [] // Dial Fields } ] } } ``` !!! info "Difference from legacy local server" * The old legacy local server only handles IP requests; the new one handles all types of requests and supports concurrent for IP requests. * The old local server uses default outbound by default unless detour is specified; the new one uses dialer just like outbound, which is equivalent to using an empty direct outbound by default. ### Fields #### prefer_go !!! question "Since sing-box 1.13.0" When enabled, `local` DNS server will resolve DNS by dialing itself whenever possible. Specifically, it disables following behaviors which was added as features in sing-box 1.13.0: 1. On Apple platforms: Attempt to resolve A/AAAA requests using `getaddrinfo` in NetworkExtension. 2. On Linux: Resolve through `systemd-resolvd`'s DBus interface when available. As a sole exception, it cannot disable the following behavior: 1. In the Android graphical client, `local` will always resolve DNS through the platform interface, as there is no other way to obtain upstream DNS servers; On devices running Android versions lower than 10, this interface can only resolve A/AAAA requests. 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. ================================================ FILE: docs/configuration/dns/server/local.zh.md ================================================ --- 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) !!! question "自 sing-box 1.12.0 起" # Local ### 结构 ```json { "dns": { "servers": [ { "type": "local", "tag": "", "prefer_go": false, "neighbor_domain": [], // 拨号字段 } ] } } ``` !!! info "与旧版本地服务器的区别" * 旧的传统本地服务器只处理 IP 请求;新的服务器处理所有类型的请求,并支持 IP 请求的并发处理。 * 旧的本地服务器默认使用默认出站,除非指定了绕行;新服务器像出站一样使用拨号器,相当于默认使用空的直连出站。 ### 字段 #### prefer_go !!! question "自 sing-box 1.13.0 起" 启用后,`local` DNS 服务器将尽可能通过拨号自身来解析 DNS。 具体来说,它禁用了在 sing-box 1.13.0 中作为功能添加的以下行为: 1. 在 Apple 平台上:尝试在 NetworkExtension 中使用 `getaddrinfo` 解析 A/AAAA 请求。 2. 在 Linux 上:当可用时通过 `systemd-resolvd` 的 DBus 接口进行解析。 作为唯一的例外,它无法禁用以下行为: 1. 在 Android 图形客户端中, `local` 将始终通过平台接口解析 DNS, 因为没有其他方法来获取上游 DNS 服务器; 在运行 Android 10 以下版本的设备上,此接口只能解析 A/AAAA 请求。 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/) 了解详情。 ================================================ FILE: docs/configuration/dns/server/mdns.md ================================================ --- 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. ================================================ FILE: docs/configuration/dns/server/mdns.zh.md ================================================ --- 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/) 了解详情。 ================================================ FILE: docs/configuration/dns/server/quic.md ================================================ --- icon: material/new-box --- !!! question "Since sing-box 1.12.0" # DNS over QUIC (DoQ) ### Structure ```json { "dns": { "servers": [ { "type": "quic", "tag": "", "server": "", "server_port": 853, "tls": {}, // Dial Fields } ] } } ``` !!! info "Difference from legacy QUIC server" * The old server uses default outbound by default unless detour is specified; the new one uses dialer just like outbound, which is equivalent to using an empty direct outbound by default. * The old server uses `address_resolver` and `address_strategy` to resolve the domain name in the server; the new one uses `domain_resolver` and `domain_strategy` in [Dial Fields](/configuration/shared/dial/) instead. ### Fields #### server ==Required== The address of the DNS server. If domain name is used, `domain_resolver` must also be set to resolve IP address. #### server_port The port of the DNS server. `853` will be used by default. #### tls TLS configuration, see [TLS](/configuration/shared/tls/#outbound). ### Dial Fields See [Dial Fields](/configuration/shared/dial/) for details. ================================================ FILE: docs/configuration/dns/server/quic.zh.md ================================================ --- icon: material/new-box --- !!! question "自 sing-box 1.12.0 起" # DNS over QUIC (DoQ) ### 结构 ```json { "dns": { "servers": [ { "type": "quic", "tag": "", "server": "", "server_port": 853, "tls": {}, // 拨号字段 } ] } } ``` !!! info "与旧版 QUIC 服务器的区别" * 旧服务器默认使用默认出站,除非指定了绕行;新服务器像出站一样使用拨号器,相当于默认使用空的直连出站。 * 旧服务器使用 `address_resolver` 和 `address_strategy` 来解析服务器中的域名;新服务器改用 [拨号字段](/zh/configuration/shared/dial/) 中的 `domain_resolver` 和 `domain_strategy`。 ### 字段 #### server ==必填== DNS 服务器的地址。 如果使用域名,还必须设置 `domain_resolver` 来解析 IP 地址。 #### server_port DNS 服务器的端口。 默认使用 `853`。 #### tls TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。 ### 拨号字段 参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。 ================================================ FILE: docs/configuration/dns/server/resolved.md ================================================ --- icon: material/new-box --- !!! question "Since sing-box 1.12.0" # Resolved ```json { "dns": { "servers": [ { "type": "resolved", "tag": "", "service": "resolved", "accept_default_resolvers": false } ] } } ``` ### Fields #### service ==Required== The tag of the [Resolved Service](/configuration/service/resolved). #### accept_default_resolvers Indicates whether the default DNS resolvers should be accepted for fallback queries in addition to matching domains. Specifically, default DNS resolvers are DNS servers that have `SetLinkDefaultRoute` or `SetLinkDomains ~.` set. If not enabled, `NXDOMAIN` will be returned for requests that do not match search or match domains. ### Examples === "Split DNS only" === ":material-card-multiple: sing-box 1.14.0" ```json { "dns": { "servers": [ { "type": "local", "tag": "local" }, { "type": "resolved", "tag": "resolved", "service": "resolved" } ], "rules": [ { "preferred_by": "resolved", "action": "route", "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" ```json { "dns": { "servers": [ { "type": "resolved", "service": "resolved", "accept_default_resolvers": true } ] } } ``` ================================================ FILE: docs/configuration/dns/server/resolved.zh.md ================================================ --- icon: material/new-box --- !!! question "自 sing-box 1.12.0 起" # Resolved ```json { "dns": { "servers": [ { "type": "resolved", "tag": "", "service": "resolved", "accept_default_resolvers": false } ] } } ``` ### 字段 #### service ==必填== [Resolved 服务](/zh/configuration/service/resolved) 的标签。 #### accept_default_resolvers 指示是否除了匹配域名外,还应接受默认 DNS 解析器以进行回退查询。 具体来说,默认 DNS 解析器是设置了 `SetLinkDefaultRoute` 或 `SetLinkDomains ~.` 的 DNS 服务器。 如果未启用,对于不匹配搜索域或匹配域的请求,将返回 `NXDOMAIN`。 ### 示例 === "仅分割 DNS" === ":material-card-multiple: sing-box 1.14.0" ```json { "dns": { "servers": [ { "type": "local", "tag": "local" }, { "type": "resolved", "tag": "resolved", "service": "resolved" } ], "rules": [ { "preferred_by": "resolved", "action": "route", "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" ```json { "dns": { "servers": [ { "type": "resolved", "service": "resolved", "accept_default_resolvers": true } ] } } ``` ================================================ FILE: docs/configuration/dns/server/tailscale.md ================================================ --- 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 ### Structure ```json { "dns": { "servers": [ { "type": "tailscale", "tag": "", "endpoint": "ts-ep", "accept_default_resolvers": false, "accept_search_domain": false } ] } } ``` ### Fields #### endpoint ==Required== The tag of the [Tailscale Endpoint](/configuration/endpoint/tailscale). #### accept_default_resolvers Indicates whether default DNS resolvers should be accepted for fallback queries in addition to MagicDNS。 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" === ":material-card-multiple: sing-box 1.14.0" ```json { "dns": { "servers": [ { "type": "local", "tag": "local" }, { "type": "tailscale", "tag": "ts", "endpoint": "ts-ep" } ], "rules": [ { "preferred_by": "ts", "action": "route", "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" ```json { "dns": { "servers": [ { "type": "tailscale", "endpoint": "ts-ep", "accept_default_resolvers": true } ] } } ``` ================================================ FILE: docs/configuration/dns/server/tailscale.zh.md ================================================ --- 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 ### 结构 ```json { "dns": { "servers": [ { "type": "tailscale", "tag": "", "endpoint": "ts-ep", "accept_default_resolvers": false, "accept_search_domain": false } ] } } ``` ### 字段 #### endpoint ==必填== [Tailscale 端点](/zh/configuration/endpoint/tailscale) 的标签。 #### accept_default_resolvers 指示是否除了 MagicDNS 外,还应接受默认 DNS 解析器以进行回退查询。 如果未启用,对于非 Tailscale 域名查询将返回 `NXDOMAIN`。 #### accept_search_domain !!! question "自 sing-box 1.14.0 起" 启用后,单标签查询(例如 `my-device`)将依次附加 Tailscale 搜索域进行重试,直到其中一个解析成功。 对于单标签查询,无论 `accept_default_resolvers` 是否启用,都不会使用默认 DNS 解析器。 ### 示例 === "仅 MagicDNS" === ":material-card-multiple: sing-box 1.14.0" ```json { "dns": { "servers": [ { "type": "local", "tag": "local" }, { "type": "tailscale", "tag": "ts", "endpoint": "ts-ep" } ], "rules": [ { "preferred_by": "ts", "action": "route", "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" ```json { "dns": { "servers": [ { "type": "tailscale", "endpoint": "ts-ep", "accept_default_resolvers": true } ] } } ``` ================================================ FILE: docs/configuration/dns/server/tcp.md ================================================ --- icon: material/new-box --- !!! question "Since sing-box 1.12.0" # TCP ### Structure ```json { "dns": { "servers": [ { "type": "tcp", "tag": "", "server": "", "server_port": 53, // Dial Fields } ] } } ``` !!! info "Difference from legacy TCP server" * The old server uses default outbound by default unless detour is specified; the new one uses dialer just like outbound, which is equivalent to using an empty direct outbound by default. * The old server uses `address_resolver` and `address_strategy` to resolve the domain name in the server; the new one uses `domain_resolver` and `domain_strategy` in [Dial Fields](/configuration/shared/dial/) instead. ### Fields #### server ==Required== The address of the DNS server. If domain name is used, `domain_resolver` must also be set to resolve IP address. #### server_port The port of the DNS server. `53` will be used by default. ### Dial Fields See [Dial Fields](/configuration/shared/dial/) for details. ================================================ FILE: docs/configuration/dns/server/tcp.zh.md ================================================ --- icon: material/new-box --- !!! question "自 sing-box 1.12.0 起" # TCP ### 结构 ```json { "dns": { "servers": [ { "type": "tcp", "tag": "", "server": "", "server_port": 53, // 拨号字段 } ] } } ``` !!! info "与旧版 TCP 服务器的区别" * 旧服务器默认使用默认出站,除非指定了绕行;新服务器像出站一样使用拨号器,相当于默认使用空的直连出站。 * 旧服务器使用 `address_resolver` 和 `address_strategy` 来解析服务器中的域名;新服务器改用 [拨号字段](/zh/configuration/shared/dial/) 中的 `domain_resolver` 和 `domain_strategy`。 ### 字段 #### server ==必填== DNS 服务器的地址。 如果使用域名,还必须设置 `domain_resolver` 来解析 IP 地址。 #### server_port DNS 服务器的端口。 默认使用 `53`。 ### 拨号字段 参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。 ================================================ FILE: docs/configuration/dns/server/tls.md ================================================ --- icon: material/new-box --- !!! question "Since sing-box 1.12.0" # DNS over TLS (DoT) ### Structure ```json { "dns": { "servers": [ { "type": "tls", "tag": "", "server": "", "server_port": 853, "tls": {}, // Dial Fields } ] } } ``` !!! info "Difference from legacy TLS server" * The old server uses default outbound by default unless detour is specified; the new one uses dialer just like outbound, which is equivalent to using an empty direct outbound by default. * The old server uses `address_resolver` and `address_strategy` to resolve the domain name in the server; the new one uses `domain_resolver` and `domain_strategy` in [Dial Fields](/configuration/shared/dial/) instead. ### Fields #### server ==Required== The address of the DNS server. If domain name is used, `domain_resolver` must also be set to resolve IP address. #### server_port The port of the DNS server. `853` will be used by default. #### tls TLS configuration, see [TLS](/configuration/shared/tls/#outbound). ### Dial Fields See [Dial Fields](/configuration/shared/dial/) for details. ================================================ FILE: docs/configuration/dns/server/tls.zh.md ================================================ --- icon: material/new-box --- !!! question "自 sing-box 1.12.0 起" # DNS over TLS (DoT) ### 结构 ```json { "dns": { "servers": [ { "type": "tls", "tag": "", "server": "", "server_port": 853, "tls": {}, // 拨号字段 } ] } } ``` !!! info "与旧版 TLS 服务器的区别" * 旧服务器默认使用默认出站,除非指定了绕行;新服务器像出站一样使用拨号器,相当于默认使用空的直连出站。 * 旧服务器使用 `address_resolver` 和 `address_strategy` 来解析服务器中的域名;新服务器改用 [拨号字段](/zh/configuration/shared/dial/) 中的 `domain_resolver` 和 `domain_strategy`。 ### 字段 #### server ==必填== DNS 服务器的地址。 如果使用域名,还必须设置 `domain_resolver` 来解析 IP 地址。 #### server_port DNS 服务器的端口。 默认使用 `853`。 #### tls TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。 ### 拨号字段 参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。 ================================================ FILE: docs/configuration/dns/server/udp.md ================================================ --- icon: material/new-box --- !!! question "Since sing-box 1.12.0" # UDP ### Structure ```json { "dns": { "servers": [ { "type": "udp", "tag": "", "server": "", "server_port": 53, // Dial Fields } ] } } ``` !!! info "Difference from legacy UDP server" * The old server uses default outbound by default unless detour is specified; the new one uses dialer just like outbound, which is equivalent to using an empty direct outbound by default. * The old server uses `address_resolver` and `address_strategy` to resolve the domain name in the server; the new one uses `domain_resolver` and `domain_strategy` in [Dial Fields](/configuration/shared/dial/) instead. ### Fields #### server ==Required== The address of the DNS server. If domain name is used, `domain_resolver` must also be set to resolve IP address. #### server_port The port of the DNS server. `53` will be used by default. ### Dial Fields See [Dial Fields](/configuration/shared/dial/) for details. ================================================ FILE: docs/configuration/dns/server/udp.zh.md ================================================ --- icon: material/new-box --- !!! question "自 sing-box 1.12.0 起" # UDP ### 结构 ```json { "dns": { "servers": [ { "type": "udp", "tag": "", "server": "", "server_port": 53, // 拨号字段 } ] } } ``` !!! info "与旧版 UDP 服务器的区别" * 旧服务器默认使用默认出站,除非指定了绕行;新服务器像出站一样使用拨号器,相当于默认使用空的直连出站。 * 旧服务器使用 `address_resolver` 和 `address_strategy` 来解析服务器中的域名;新服务器改用 [拨号字段](/zh/configuration/shared/dial/) 中的 `domain_resolver` 和 `domain_strategy`。 ### 字段 #### server ==必填== DNS 服务器的地址。 如果使用域名,还必须设置 `domain_resolver` 来解析 IP 地址。 #### server_port DNS 服务器的端口。 默认使用 `53`。 ### 拨号字段 参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。 ================================================ FILE: docs/configuration/endpoint/index.md ================================================ !!! question "Since sing-box 1.11.0" # Endpoint An endpoint is a protocol with inbound and outbound behavior. ### Structure ```json { "endpoints": [ { "type": "", "tag": "" } ] } ``` ### Fields | Type | Format | |-------------|---------------------------| | `wireguard` | [WireGuard](./wireguard/) | | `tailscale` | [Tailscale](./tailscale/) | #### tag The tag of the endpoint. ================================================ FILE: docs/configuration/endpoint/index.zh.md ================================================ !!! question "自 sing-box 1.11.0 起" # 端点 端点是具有入站和出站行为的协议。 ### 结构 ```json { "endpoints": [ { "type": "", "tag": "" } ] } ``` ### 字段 | 类型 | 格式 | |-------------|---------------------------| | `wireguard` | [WireGuard](./wireguard/) | | `tailscale` | [Tailscale](./tailscale/) | #### tag 端点的标签。 ================================================ FILE: docs/configuration/endpoint/tailscale.md ================================================ --- 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) :material-plus: [relay_server_static_endpoints](#relay_server_static_endpoints) :material-plus: [system_interface](#system_interface) :material-plus: [system_interface_name](#system_interface_name) :material-plus: [system_interface_mtu](#system_interface_mtu) :material-plus: [advertise_tags](#advertise_tags) !!! question "Since sing-box 1.12.0" ### Structure ```json { "type": "tailscale", "tag": "ts-ep", "state_directory": "", "auth_key": "", "control_url": "", "control_http_client": {}, // or "" "ephemeral": false, "hostname": "", "accept_routes": false, "exit_node": "", "exit_node_allow_lan_access": false, "advertise_routes": [], "advertise_exit_node": false, "advertise_tags": [], "relay_server_port": 0, "relay_server_static_endpoints": [], "system_interface": false, "system_interface_name": "", "system_interface_mtu": 0, "udp_timeout": "5m", ... // Dial Fields } ``` ### Fields #### state_directory The directory where the Tailscale state is stored. `tailscale` is used by default. Example: `$HOME/.tailscale` #### auth_key !!! note Auth key is not required. By default, sing-box will log the login URL (or popup a notification on graphical clients). The auth key to create the node. If the node is already created (from state previously stored), then this field is not used. #### control_url The coordination server URL. `https://controlplane.tailscale.com` is used by default. #### ephemeral Indicates whether the instance should register as an Ephemeral node (https://tailscale.com/s/ephemeral-nodes). #### hostname The hostname of the node. System hostname is used by default. Example: `localhost` #### accept_routes Indicates whether the node should accept routes advertised by other nodes. #### exit_node The exit node name or IP address to use. #### exit_node_allow_lan_access !!! note When the exit node does not have a corresponding advertised route, private traffics cannot be routed to the exit node even if `exit_node_allow_lan_access is` set. Indicates whether locally accessible subnets should be routed directly or via the exit node. #### advertise_routes CIDR prefixes to advertise into the Tailscale network as reachable through the current node. Example: `["192.168.1.1/24"]` #### advertise_exit_node Indicates whether the node should advertise itself as an exit node. #### advertise_tags !!! question "Since sing-box 1.13.0" Tags to advertise for this node, for ACL enforcement purposes. Example: `["tag:server"]` #### relay_server_port !!! question "Since sing-box 1.13.0" The port to listen on for incoming relay connections from other Tailscale nodes. #### relay_server_static_endpoints !!! question "Since sing-box 1.13.0" Static endpoints to advertise for the relay server. #### system_interface !!! question "Since sing-box 1.13.0" Create a system TUN interface for Tailscale. #### system_interface_name !!! question "Since sing-box 1.13.0" Custom TUN interface name. By default, `tailscale` (or `utun` on macOS) will be used. #### system_interface_mtu !!! question "Since sing-box 1.13.0" Override the TUN MTU. By default, Tailscale's own MTU is used. #### udp_timeout 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 !!! failure "Deprecated in sing-box 1.14.0" 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. ================================================ FILE: docs/configuration/endpoint/tailscale.zh.md ================================================ --- 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) :material-plus: [relay_server_static_endpoints](#relay_server_static_endpoints) :material-plus: [system_interface](#system_interface) :material-plus: [system_interface_name](#system_interface_name) :material-plus: [system_interface_mtu](#system_interface_mtu) :material-plus: [advertise_tags](#advertise_tags) !!! question "自 sing-box 1.12.0 起" ### 结构 ```json { "type": "tailscale", "tag": "ts-ep", "state_directory": "", "auth_key": "", "control_url": "", "control_http_client": {}, // 或 "" "ephemeral": false, "hostname": "", "accept_routes": false, "exit_node": "", "exit_node_allow_lan_access": false, "advertise_routes": [], "advertise_exit_node": false, "advertise_tags": [], "relay_server_port": 0, "relay_server_static_endpoints": [], "system_interface": false, "system_interface_name": "", "system_interface_mtu": 0, "udp_timeout": "5m", ... // 拨号字段 } ``` ### 字段 #### state_directory 存储 Tailscale 状态的目录。 默认使用 `tailscale`。 示例:`$HOME/.tailscale` #### auth_key !!! note 认证密钥不是必需的。默认情况下,sing-box 将记录登录 URL(或在图形客户端上弹出通知)。 用于创建节点的认证密钥。如果节点已经创建(从之前存储的状态),则不使用此字段。 #### control_url 协调服务器 URL。 默认使用 `https://controlplane.tailscale.com`。 #### ephemeral 指示实例是否应注册为临时节点 (https://tailscale.com/s/ephemeral-nodes)。 #### hostname 节点的主机名。 默认使用系统主机名。 示例:`localhost` #### accept_routes 指示节点是否应接受其他节点通告的路由。 #### exit_node 要使用的出口节点名称或 IP 地址。 #### exit_node_allow_lan_access !!! note 当出口节点没有相应的通告路由时,即使设置了 `exit_node_allow_lan_access`,私有流量也无法路由到出口节点。 指示本地可访问的子网应该直接路由还是通过出口节点路由。 #### advertise_routes 通告到 Tailscale 网络的 CIDR 前缀,作为可通过当前节点访问的路由。 示例:`["192.168.1.1/24"]` #### advertise_exit_node 指示节点是否应将自己通告为出口节点。 #### advertise_tags !!! question "自 sing-box 1.13.0 起" 为此节点通告的标签,用于 ACL 执行。 示例:`["tag:server"]` #### relay_server_port !!! question "自 sing-box 1.13.0 起" 监听来自其他 Tailscale 节点的中继连接的端口。 #### relay_server_static_endpoints !!! question "自 sing-box 1.13.0 起" 为中继服务器通告的静态端点。 #### system_interface !!! question "自 sing-box 1.13.0 起" 为 Tailscale 创建系统 TUN 接口。 #### system_interface_name !!! question "自 sing-box 1.13.0 起" 自定义 TUN 接口名。默认使用 `tailscale`(macOS 上为 `utun`)。 #### system_interface_mtu !!! question "自 sing-box 1.13.0 起" 覆盖 TUN 的 MTU。默认使用 Tailscale 自己的 MTU。 #### udp_timeout UDP NAT 过期时间。 默认使用 `5m`。 #### control_http_client !!! question "自 sing-box 1.14.0 起" 用于连接 Tailscale 控制平面的 HTTP 客户端。 参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。 ### 拨号字段 !!! failure "已在 sing-box 1.14.0 废弃" Tailscale 端点中的拨号字段已在 sing-box 1.14.0 废弃且将在 sing-box 1.16.0 中被移除,请使用 `control_http_client` 代替。 参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。 ================================================ FILE: docs/configuration/endpoint/wireguard.md ================================================ !!! question "Since sing-box 1.11.0" ### Structure ```json { "type": "wireguard", "tag": "wg-ep", "system": false, "name": "", "mtu": 1408, "address": [], "private_key": "", "listen_port": 10000, "peers": [ { "address": "127.0.0.1", "port": 10001, "public_key": "", "pre_shared_key": "", "allowed_ips": [], "persistent_keepalive_interval": 0, "reserved": [0, 0, 0] } ], "udp_timeout": "", "workers": 0, ... // Dial Fields } ``` !!! note "" You can ignore the JSON Array [] tag when the content is only one item ### Fields #### system Use system interface. Requires privilege and cannot conflict with exists system interfaces. #### name Custom interface name for system interface. #### mtu WireGuard MTU. `1408` will be used by default. #### address ==Required== List of IP (v4 or v6) address prefixes to be assigned to the interface. #### private_key ==Required== WireGuard requires base64-encoded public and private keys. These can be generated using the wg(8) utility: ```shell wg genkey echo "private key" || wg pubkey ``` or `sing-box generate wg-keypair`. #### peers ==Required== List of WireGuard peers. #### peers.address WireGuard peer address. #### peers.port WireGuard peer port. #### peers.public_key ==Required== WireGuard peer public key. #### peers.pre_shared_key WireGuard peer pre-shared key. #### peers.allowed_ips ==Required== WireGuard allowed IPs. #### peers.persistent_keepalive_interval WireGuard persistent keepalive interval, in seconds. Disabled by default. #### peers.reserved WireGuard reserved field bytes. #### udp_timeout UDP NAT expiration time. `5m` will be used by default. #### workers WireGuard worker count. CPU count is used by default. ### Dial Fields See [Dial Fields](/configuration/shared/dial/) for details. ================================================ FILE: docs/configuration/endpoint/wireguard.zh.md ================================================ !!! question "自 sing-box 1.11.0 起" ### 结构 ```json { "type": "wireguard", "tag": "wg-ep", "system": false, "name": "", "mtu": 1408, "address": [], "private_key": "", "listen_port": 10000, "peers": [ { "address": "127.0.0.1", "port": 10001, "public_key": "", "pre_shared_key": "", "allowed_ips": [], "persistent_keepalive_interval": 0, "reserved": [0, 0, 0] } ], "udp_timeout": "", "workers": 0, ... // 拨号字段 } ``` !!! note "" 当内容只有一项时,可以忽略 JSON 数组 [] 标签 ### 字段 #### system 使用系统设备。 需要特权且不能与已有系统接口冲突。 #### name 为系统接口自定义设备名称。 #### mtu WireGuard MTU。 默认使用 1408。 #### address ==必填== 接口的 IPv4/IPv6 地址或地址段的列表。 要分配给接口的 IP(v4 或 v6)地址段列表。 #### private_key ==必填== WireGuard 需要 base64 编码的公钥和私钥。 这些可以使用 wg(8) 实用程序生成: ```shell wg genkey echo "private key" || wg pubkey ``` 或 `sing-box generate wg-keypair`. #### peers ==必填== WireGuard 对等方的列表。 #### peers.address 对等方的 IP 地址。 #### peers.port 对等方的 WireGuard 端口。 #### peers.public_key ==必填== 对等方的 WireGuard 公钥。 #### peers.pre_shared_key 对等方的预共享密钥。 #### peers.allowed_ips ==必填== 对等方的允许 IP 地址。 #### peers.persistent_keepalive_interval 对等方的持久性保持活动间隔,以秒为单位。 默认禁用。 #### peers.reserved 对等方的保留字段字节。 #### udp_timeout UDP NAT 过期时间。 默认使用 `5m`。 #### workers WireGuard worker 数量。 默认使用 CPU 数量。 ### 拨号字段 参阅 [拨号字段](/zh/configuration/shared/dial/)。 ================================================ FILE: docs/configuration/experimental/cache-file.md ================================================ !!! 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) :material-plus: [rdrc_timeout](#rdrc_timeout) ### Structure ```json { "enabled": true, "path": "", "cache_id": "", "store_fakeip": false, "store_rdrc": false, "rdrc_timeout": "", "store_dns": false } ``` ### Fields #### enabled Enable cache file. #### path Path to the cache file. `cache.db` will be used if empty. #### cache_id Identifier in the cache file If not empty, configuration specified data will use a separate store keyed by it. #### store_fakeip 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) will be cached until expiration. #### rdrc_timeout 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. ================================================ FILE: docs/configuration/experimental/cache-file.zh.md ================================================ !!! 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) :material-plus: [rdrc_timeout](#rdrc_timeout) ### 结构 ```json { "enabled": true, "path": "", "cache_id": "", "store_fakeip": false, "store_rdrc": false, "rdrc_timeout": "", "store_dns": false } ``` ### 字段 #### enabled 启用缓存文件。 #### path 缓存文件路径,默认使用`cache.db`。 #### cache_id 缓存文件中的标识符。 如果不为空,配置特定的数据将使用由其键控的单独存储。 #### store_fakeip 将 fakeip 存储在缓存文件中。 #### 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/#旧版地址筛选字段) 的检查结果将被缓存至过期。 #### rdrc_timeout 拒绝的 DNS 响应缓存超时。 默认使用 `7d`。 #### store_dns !!! question "自 sing-box 1.14.0 起" 将 DNS 缓存存储在缓存文件中。 ================================================ FILE: docs/configuration/experimental/clash-api.md ================================================ !!! quote "Changes in sing-box 1.10.0" :material-plus: [access_control_allow_origin](#access_control_allow_origin) :material-plus: [access_control_allow_private_network](#access_control_allow_private_network) !!! quote "Changes in sing-box 1.8.0" :material-delete-alert: [store_mode](#store_mode) :material-delete-alert: [store_selected](#store_selected) :material-delete-alert: [store_fakeip](#store_fakeip) :material-delete-alert: [cache_file](#cache_file) :material-delete-alert: [cache_id](#cache_id) ### Structure === "Structure" ```json { "external_controller": "127.0.0.1:9090", "external_ui": "", "external_ui_download_url": "", "external_ui_download_detour": "", "secret": "", "default_mode": "", "access_control_allow_origin": [], "access_control_allow_private_network": false, // Deprecated "store_mode": false, "store_selected": false, "store_fakeip": false, "cache_file": "", "cache_id": "" } ``` === "Example (online)" !!! question "Since sing-box 1.10.0" ```json { "external_controller": "127.0.0.1:9090", "access_control_allow_origin": [ "http://127.0.0.1", "http://yacd.haishan.me" ], "access_control_allow_private_network": true } ``` === "Example (download)" !!! question "Since sing-box 1.10.0" ```json { "external_controller": "0.0.0.0:9090", "external_ui": "dashboard" // "external_ui_download_detour": "direct" } ``` !!! note "" You can ignore the JSON Array [] tag when the content is only one item ### Fields #### external_controller RESTful web API listening address. Clash API will be disabled if empty. #### external_ui A relative path to the configuration directory or an absolute path to a directory in which you put some static web resource. sing-box will then serve it at `http://{{external-controller}}/ui`. #### external_ui_download_url ZIP download URL for the external UI, will be used if the specified `external_ui` directory is empty. `https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip` will be used if empty. #### external_ui_download_detour The tag of the outbound to download the external UI. Default outbound will be used if empty. #### secret Secret for the RESTful API (optional) Authenticate by spedifying HTTP header `Authorization: Bearer ${secret}` ALWAYS set a secret if RESTful API is listening on 0.0.0.0 #### default_mode Default mode in clash, `Rule` will be used if empty. This setting has no direct effect, but can be used in routing and DNS rules via the `clash_mode` rule item. #### access_control_allow_origin !!! question "Since sing-box 1.10.0" CORS allowed origins, `*` will be used if empty. To access the Clash API on a private network from a public website, you must explicitly specify it in `access_control_allow_origin` instead of using `*`. #### access_control_allow_private_network !!! question "Since sing-box 1.10.0" Allow access from private network. To access the Clash API on a private network from a public website, `access_control_allow_private_network` must be enabled. #### store_mode !!! failure "Deprecated in sing-box 1.8.0" `store_mode` is deprecated in Clash API and enabled by default if `cache_file.enabled`. Store Clash mode in cache file. #### store_selected !!! failure "Deprecated in sing-box 1.8.0" `store_selected` is deprecated in Clash API and enabled by default if `cache_file.enabled`. !!! note "" The tag must be set for target outbounds. Store selected outbound for the `Selector` outbound in cache file. #### store_fakeip !!! failure "Deprecated in sing-box 1.8.0" `store_selected` is deprecated in Clash API and migrated to `cache_file.store_fakeip`. Store fakeip in cache file. #### cache_file !!! failure "Deprecated in sing-box 1.8.0" `cache_file` is deprecated in Clash API and migrated to `cache_file.enabled` and `cache_file.path`. Cache file path, `cache.db` will be used if empty. #### cache_id !!! failure "Deprecated in sing-box 1.8.0" `cache_id` is deprecated in Clash API and migrated to `cache_file.cache_id`. Identifier in cache file. If not empty, configuration specified data will use a separate store keyed by it. ================================================ FILE: docs/configuration/experimental/clash-api.zh.md ================================================ !!! quote "sing-box 1.10.0 中的更改" :material-plus: [access_control_allow_origin](#access_control_allow_origin) :material-plus: [access_control_allow_private_network](#access_control_allow_private_network) !!! quote "sing-box 1.8.0 中的更改" :material-delete-alert: [store_mode](#store_mode) :material-delete-alert: [store_selected](#store_selected) :material-delete-alert: [store_fakeip](#store_fakeip) :material-delete-alert: [cache_file](#cache_file) :material-delete-alert: [cache_id](#cache_id) ### 结构 === "结构" ```json { "external_controller": "127.0.0.1:9090", "external_ui": "", "external_ui_download_url": "", "external_ui_download_detour": "", "secret": "", "default_mode": "", "access_control_allow_origin": [], "access_control_allow_private_network": false, // Deprecated "store_mode": false, "store_selected": false, "store_fakeip": false, "cache_file": "", "cache_id": "" } ``` === "示例 (在线)" !!! question "自 sing-box 1.10.0 起" ```json { "external_controller": "127.0.0.1:9090", "access_control_allow_origin": [ "http://127.0.0.1", "http://yacd.haishan.me" ], "access_control_allow_private_network": true } ``` === "示例 (下载)" !!! question "自 sing-box 1.10.0 起" ```json { "external_controller": "0.0.0.0:9090", "external_ui": "dashboard" // "external_ui_download_detour": "direct" } ``` !!! note "" 当内容只有一项时,可以忽略 JSON 数组 [] 标签 ### Fields #### external_controller RESTful web API 监听地址。如果为空,则禁用 Clash API。 #### external_ui 到静态网页资源目录的相对路径或绝对路径。sing-box 会在 `http://{{external-controller}}/ui` 下提供它。 #### external_ui_download_url 静态网页资源的 ZIP 下载 URL,如果指定的 `external_ui` 目录为空,将使用。 默认使用 `https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip`。 #### external_ui_download_detour 用于下载静态网页资源的出站的标签。 如果为空,将使用默认出站。 #### secret RESTful API 的密钥(可选) 通过指定 HTTP 标头 `Authorization: Bearer ${secret}` 进行身份验证 如果 RESTful API 正在监听 0.0.0.0,请始终设置一个密钥。 #### default_mode Clash 中的默认模式,默认使用 `Rule`。 此设置没有直接影响,但可以通过 `clash_mode` 规则项在路由和 DNS 规则中使用。 #### access_control_allow_origin !!! question "自 sing-box 1.10.0 起" 允许的 CORS 来源,默认使用 `*`。 要从公共网站访问私有网络上的 Clash API,必须在 `access_control_allow_origin` 中明确指定它而不是使用 `*`。 #### access_control_allow_private_network !!! question "自 sing-box 1.10.0 起" 允许从私有网络访问。 要从公共网站访问私有网络上的 Clash API,必须启用 `access_control_allow_private_network`。 #### store_mode !!! failure "已在 sing-box 1.8.0 废弃" `store_mode` 已在 Clash API 中废弃,且默认启用当 `cache_file.enabled`。 将 Clash 模式存储在缓存文件中。 #### store_selected !!! failure "已在 sing-box 1.8.0 废弃" `store_selected` 已在 Clash API 中废弃,且默认启用当 `cache_file.enabled`。 !!! note "" 必须为目标出站设置标签。 将 `Selector` 中出站的选定的目标出站存储在缓存文件中。 #### store_fakeip !!! failure "已在 sing-box 1.8.0 废弃" `store_selected` 已在 Clash API 中废弃,且已迁移到 `cache_file.store_fakeip`。 将 fakeip 存储在缓存文件中。 #### cache_file !!! failure "已在 sing-box 1.8.0 废弃" `cache_file` 已在 Clash API 中废弃,且已迁移到 `cache_file.enabled` 和 `cache_file.path`。 缓存文件路径,默认使用`cache.db`。 #### cache_id !!! failure "已在 sing-box 1.8.0 废弃" `cache_id` 已在 Clash API 中废弃,且已迁移到 `cache_file.cache_id`。 缓存 ID。 如果不为空,配置特定的数据将使用由其键控的单独存储。 ================================================ FILE: docs/configuration/experimental/index.md ================================================ # Experimental !!! quote "Changes in sing-box 1.8.0" :material-plus: [cache_file](#cache_file) :material-alert-decagram: [clash_api](#clash_api) ### Structure ```json { "experimental": { "cache_file": {}, "clash_api": {}, "v2ray_api": {} } } ``` ### Fields | Key | Format | |--------------|----------------------------| | `cache_file` | [Cache File](./cache-file/) | | `clash_api` | [Clash API](./clash-api/) | | `v2ray_api` | [V2Ray API](./v2ray-api/) | ================================================ FILE: docs/configuration/experimental/index.zh.md ================================================ # 实验性 !!! quote "sing-box 1.8.0 中的更改" :material-plus: [cache_file](#cache_file) :material-alert-decagram: [clash_api](#clash_api) ### 结构 ```json { "experimental": { "cache_file": {}, "clash_api": {}, "v2ray_api": {} } } ``` ### 字段 | 键 | 格式 | |--------------|--------------------------| | `cache_file` | [缓存文件](./cache-file/) | | `clash_api` | [Clash API](./clash-api/) | | `v2ray_api` | [V2Ray API](./v2ray-api/) | ================================================ FILE: docs/configuration/experimental/v2ray-api.md ================================================ !!! quote "" V2Ray API is not included by default, see [Installation](/installation/build-from-source/#build-tags). ### Structure ```json { "listen": "127.0.0.1:8080", "stats": { "enabled": true, "inbounds": [ "socks-in" ], "outbounds": [ "proxy", "direct" ], "users": [ "sekai" ] } } ``` ### Fields #### listen gRPC API listening address. V2Ray API will be disabled if empty. #### stats Traffic statistics service settings. #### stats.enabled Enable statistics service. #### stats.inbounds Inbound list to count traffic. #### stats.outbounds Outbound list to count traffic. #### stats.users User list to count traffic. ================================================ FILE: docs/configuration/experimental/v2ray-api.zh.md ================================================ !!! quote "" 默认安装不包含 V2Ray API,参阅 [安装](/zh/installation/build-from-source/#构建标记)。 ### 结构 ```json { "listen": "127.0.0.1:8080", "stats": { "enabled": true, "inbounds": [ "socks-in" ], "outbounds": [ "proxy", "direct" ], "users": [ "sekai" ] } } ``` ### 字段 #### listen gRPC API 监听地址。如果为空,则禁用 V2Ray API。 #### stats 流量统计服务设置。 #### stats.enabled 启用统计服务。 #### stats.inbounds 统计流量的入站列表。 #### stats.outbounds 统计流量的出站列表。 #### stats.users 统计流量的用户列表。 ================================================ FILE: docs/configuration/inbound/anytls.md ================================================ --- icon: material/new-box --- !!! question "Since sing-box 1.12.0" ### Structure ```json { "type": "anytls", "tag": "anytls-in", ... // Listen Fields "users": [ { "name": "sekai", "password": "8JCsPssfgS8tiRwiMlhARg==" } ], "padding_scheme": [], "tls": {} } ``` ### Listen Fields See [Listen Fields](/configuration/shared/listen/) for details. ### Fields #### users ==Required== AnyTLS users. #### padding_scheme AnyTLS padding scheme line array. Default padding scheme: ```json [ "stop=8", "0=30-30", "1=100-400", "2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000", "3=9-9,500-1000", "4=500-1000", "5=500-1000", "6=500-1000", "7=500-1000" ] ``` #### tls TLS configuration, see [TLS](/configuration/shared/tls/#inbound). ================================================ FILE: docs/configuration/inbound/anytls.zh.md ================================================ --- icon: material/new-box --- !!! question "自 sing-box 1.12.0 起" ### 结构 ```json { "type": "anytls", "tag": "anytls-in", ... // 监听字段 "users": [ { "name": "sekai", "password": "8JCsPssfgS8tiRwiMlhARg==" } ], "padding_scheme": [], "tls": {} } ``` ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/)。 ### 字段 #### users ==必填== AnyTLS 用户。 #### padding_scheme AnyTLS 填充方案行数组。 默认填充方案: ```json [ "stop=8", "0=30-30", "1=100-400", "2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000", "3=9-9,500-1000", "4=500-1000", "5=500-1000", "6=500-1000", "7=500-1000" ] ``` #### tls TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 ================================================ FILE: docs/configuration/inbound/cloudflared.md ================================================ --- 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. ================================================ FILE: docs/configuration/inbound/cloudflared.zh.md ================================================ --- 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/)。 ================================================ FILE: docs/configuration/inbound/direct.md ================================================ `direct` inbound is a tunnel server. ### Structure ```json { "type": "direct", "tag": "direct-in", ... // Listen Fields "network": "udp", "override_address": "1.0.0.1", "override_port": 53 } ``` ### Listen Fields See [Listen Fields](/configuration/shared/listen/) for details. ### Fields #### network Listen network, one of `tcp` `udp`. Both if empty. #### override_address Override the connection destination address. #### override_port Override the connection destination port. ================================================ FILE: docs/configuration/inbound/direct.zh.md ================================================ `direct` 入站是一个隧道服务器。 ### 结构 ```json { "type": "direct", "tag": "direct-in", ... // 监听字段 "network": "udp", "override_address": "1.0.0.1", "override_port": 53 } ``` ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/)。 ### 字段 #### network 监听的网络协议,`tcp` `udp` 之一。 默认所有。 #### override_address 覆盖连接目标地址。 #### override_port 覆盖连接目标端口。 ================================================ FILE: docs/configuration/inbound/http.md ================================================ ### Structure ```json { "type": "http", "tag": "http-in", ... // Listen Fields "users": [ { "username": "admin", "password": "admin" } ], "tls": {}, "set_system_proxy": false } ``` ### Listen Fields See [Listen Fields](/configuration/shared/listen/) for details. ### Fields #### tls TLS configuration, see [TLS](/configuration/shared/tls/#inbound). #### users HTTP users. No authentication required if empty. #### set_system_proxy !!! quote "" Only supported on Linux, Android, Windows, and macOS. !!! warning "" To work on Android and Apple platforms without privileges, use tun.platform.http_proxy instead. Automatically set system proxy configuration when start and clean up when stop. ================================================ FILE: docs/configuration/inbound/http.zh.md ================================================ ### 结构 ```json { "type": "http", "tag": "http-in", ... // 监听字段 "users": [ { "username": "admin", "password": "admin" } ], "tls": {}, "set_system_proxy": false } ``` ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/)。 ### 字段 #### tls TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 #### users HTTP 用户 如果为空则不需要验证。 #### set_system_proxy !!! quote "" 仅支持 Linux、Android、Windows 和 macOS。 !!! warning "" 要在无特权的 Android 和 iOS 上工作,请改用 tun.platform.http_proxy。 启动时自动设置系统代理,停止时自动清理。 ================================================ FILE: docs/configuration/inbound/hysteria.md ================================================ ### Structure ```json { "type": "hysteria", "tag": "hysteria-in", ... // Listen Fields "up": "100 Mbps", "up_mbps": 100, "down": "100 Mbps", "down_mbps": 100, "obfs": "fuck me till the daylight", "users": [ { "name": "sekai", "auth": "", "auth_str": "password" } ], "tls": {}, ... // QUIC Fields // Deprecated "recv_window_conn": 0, "recv_window_client": 0, "max_conn_client": 0, "disable_mtu_discovery": false } ``` ### Listen Fields See [Listen Fields](/configuration/shared/listen/) for details. ### Fields #### up, down ==Required== Format: `[Integer] [Unit]` e.g. `100 Mbps, 640 KBps, 2 Gbps` Supported units (case sensitive, b = bits, B = bytes, 8b=1B): bps (bits per second) Bps (bytes per second) Kbps (kilobits per second) KBps (kilobytes per second) Mbps (megabits per second) MBps (megabytes per second) Gbps (gigabits per second) GBps (gigabytes per second) Tbps (terabits per second) TBps (terabytes per second) #### up_mbps, down_mbps ==Required== `up, down` in Mbps. #### obfs Obfuscated password. #### users Hysteria users #### users.auth Authentication password, in base64. #### users.auth_str 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 !!! failure "Deprecated in sing-box 1.14.0" Use QUIC fields `stream_receive_window` instead. #### recv_window_client !!! failure "Deprecated in sing-box 1.14.0" Use QUIC fields `connection_receive_window` instead. #### max_conn_client !!! failure "Deprecated in sing-box 1.14.0" Use QUIC fields `max_concurrent_streams` instead. #### disable_mtu_discovery !!! failure "Deprecated in sing-box 1.14.0" Use QUIC fields `disable_path_mtu_discovery` instead. ================================================ FILE: docs/configuration/inbound/hysteria.zh.md ================================================ ### 结构 ```json { "type": "hysteria", "tag": "hysteria-in", ... // 监听字段 "up": "100 Mbps", "up_mbps": 100, "down": "100 Mbps", "down_mbps": 100, "obfs": "fuck me till the daylight", "users": [ { "name": "sekai", "auth": "", "auth_str": "password" } ], "tls": {}, ... // QUIC 字段 // 废弃的 "recv_window_conn": 0, "recv_window_client": 0, "max_conn_client": 0, "disable_mtu_discovery": false } ``` ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/)。 ### 字段 #### up, down ==必填== 格式: `[Integer] [Unit]` 例如: `100 Mbps, 640 KBps, 2 Gbps` 支持的单位 (大小写敏感, b = bits, B = bytes, 8b=1B): bps (bits per second) Bps (bytes per second) Kbps (kilobits per second) KBps (kilobytes per second) Mbps (megabits per second) MBps (megabytes per second) Gbps (gigabits per second) GBps (gigabytes per second) Tbps (terabits per second) TBps (terabytes per second) #### up_mbps, down_mbps ==必填== 以 Mbps 为单位的 `up, down`。 #### obfs 混淆密码。 #### users Hysteria 用户 #### users.auth base64 编码的认证密码。 #### users.auth_str 认证密码。 #### tls ==必填== TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 ### QUIC 字段 参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。 ### 废弃字段 #### recv_window_conn !!! failure "已在 sing-box 1.14.0 废弃" 请使用 QUIC 字段 `stream_receive_window` 代替。 #### recv_window_client !!! failure "已在 sing-box 1.14.0 废弃" 请使用 QUIC 字段 `connection_receive_window` 代替。 #### max_conn_client !!! failure "已在 sing-box 1.14.0 废弃" 请使用 QUIC 字段 `max_concurrent_streams` 代替。 #### disable_mtu_discovery !!! failure "已在 sing-box 1.14.0 废弃" 请使用 QUIC 字段 `disable_path_mtu_discovery` 代替。 ================================================ FILE: docs/configuration/inbound/hysteria2.md ================================================ --- icon: material/alert-decagram --- !!! quote "Changes in sing-box 1.14.0" :material-plus: [bbr_profile](#bbr_profile) :material-plus: [realm](#realm) !!! quote "Changes in sing-box 1.11.0" :material-alert: [masquerade](#masquerade) :material-alert: [ignore_client_bandwidth](#ignore_client_bandwidth) ### Structure ```json { "type": "hysteria2", "tag": "hy2-in", ... // Listen Fields "up_mbps": 100, "down_mbps": 100, "obfs": { "type": "salamander", "password": "cry_me_a_r1ver" }, "users": [ { "name": "tobyxdd", "password": "goofy_ahh_password" } ], "ignore_client_bandwidth": false, "tls": {}, ... // QUIC Fields "masquerade": "", // or {} "bbr_profile": "", "brutal_debug": false, "realm": { "server_url": "https://realm.example.com", "token": "", "realm_id": "", "stun_servers": [], "stun_domain_resolver": "", // or {} "http_client": {} } } ``` !!! warning "Difference from official Hysteria2" The official program supports an authentication method called **userpass**, which essentially uses a combination of `:` as the actual password, while sing-box does not provide this alias. To use sing-box with the official program, you need to fill in that combination as the actual password. ### Listen Fields See [Listen Fields](/configuration/shared/listen/) for details. ### Fields #### up_mbps, down_mbps Max bandwidth, in Mbps. Not limited if empty. Conflict with `ignore_client_bandwidth`. #### obfs.type QUIC traffic obfuscator type, only available with `salamander`. Disabled if empty. #### obfs.password QUIC traffic obfuscator password. #### users Hysteria2 users #### users.password Authentication password #### ignore_client_bandwidth *When `up_mbps` and `down_mbps` are not set*: Commands clients to use the BBR CC instead of Hysteria CC. *When `up_mbps` and `down_mbps` are set*: Deny clients to use the BBR CC. #### tls ==Required== 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. | Scheme | Example | Description | |--------------|-------------------------|--------------------| | `file` | `file:///var/www` | As a file server | | `http/https` | `http://127.0.0.1:8080` | As a reverse proxy | Conflict with `masquerade.type`. A 404 page will be returned if masquerade is not configured. #### masquerade.type HTTP3 server behavior (Object configuration) when authentication fails. | Type | Description | Fields | |----------|-----------------------------|-------------------------------------| | `file` | As a file server | `directory` | | `proxy` | As a reverse proxy | `url`, `rewrite_host` | | `string` | Reply with a fixed response | `status_code`, `headers`, `content` | Conflict with `masquerade`. A 404 page will be returned if masquerade is not configured. #### masquerade.directory File server root directory. #### masquerade.url Reverse proxy target URL. #### masquerade.rewrite_host Rewrite the `Host` header to the target URL. #### masquerade.status_code Fixed response status code. #### masquerade.headers Fixed response headers. #### masquerade.content 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. #### 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. #### 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 HTTP client used to talk to the realm. See [HTTP Client](/configuration/shared/http-client/) for details. ================================================ FILE: docs/configuration/inbound/hysteria2.zh.md ================================================ --- icon: material/alert-decagram --- !!! quote "sing-box 1.14.0 中的更改" :material-plus: [bbr_profile](#bbr_profile) :material-plus: [realm](#realm) !!! quote "sing-box 1.11.0 中的更改" :material-alert: [masquerade](#masquerade) :material-alert: [ignore_client_bandwidth](#ignore_client_bandwidth) ### 结构 ```json { "type": "hysteria2", "tag": "hy2-in", ... // 监听字段 "up_mbps": 100, "down_mbps": 100, "obfs": { "type": "salamander", "password": "cry_me_a_r1ver" }, "users": [ { "name": "tobyxdd", "password": "goofy_ahh_password" } ], "ignore_client_bandwidth": false, "tls": {}, ... // QUIC 字段 "masquerade": "", // 或 {} "bbr_profile": "", "brutal_debug": false, "realm": { "server_url": "https://realm.example.com", "token": "", "realm_id": "", "stun_servers": [], "stun_domain_resolver": "", // 或 {} "http_client": {} } } ``` !!! warning "与官方 Hysteria2 的区别" 官方程序支持一种名为 **userpass** 的验证方式, 本质上是将用户名与密码的组合 `:` 作为实际上的密码,而 sing-box 不提供此别名。 要将 sing-box 与官方程序一起使用, 您需要填写该组合作为实际密码。 ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/)。 ### 字段 #### up_mbps, down_mbps 支持的速率,默认不限制。 与 `ignore_client_bandwidth` 冲突。 #### obfs.type QUIC 流量混淆器类型,仅可设为 `salamander`。 如果为空则禁用。 #### obfs.password QUIC 流量混淆器密码. #### users Hysteria 用户 #### users.password 认证密码。 #### ignore_client_bandwidth *当 `up_mbps` 和 `down_mbps` 未设定时*: 命令客户端使用 BBR 拥塞控制算法而不是 Hysteria CC。 *当 `up_mbps` 和 `down_mbps` 已设定时*: 禁止客户端使用 BBR 拥塞控制算法。 #### tls ==必填== TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 ### QUIC 字段 参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。 #### masquerade HTTP3 服务器认证失败时的行为 (URL 字符串配置)。 | Scheme | 示例 | 描述 | |--------------|-------------------------|---------| | `file` | `file:///var/www` | 作为文件服务器 | | `http/https` | `http://127.0.0.1:8080` | 作为反向代理 | 如果 masquerade 未配置,则返回 404 页。 与 `masquerade.type` 冲突。 #### masquerade.type HTTP3 服务器认证失败时的行为 (对象配置)。 | Type | 描述 | 字段 | |----------|---------|-------------------------------------| | `file` | 作为文件服务器 | `directory` | | `proxy` | 作为反向代理 | `url`, `rewrite_host` | | `string` | 返回固定响应 | `status_code`, `headers`, `content` | 如果 masquerade 未配置,则返回 404 页。 与 `masquerade` 冲突。 #### masquerade.directory 文件服务器根目录。 #### masquerade.url 反向代理目标 URL。 #### masquerade.rewrite_host 重写请求头中的 Host 字段到目标 URL。 #### masquerade.status_code 固定响应状态码。 #### masquerade.headers 固定响应头。 #### masquerade.content 固定响应内容。 #### bbr_profile !!! question "自 sing-box 1.14.0 起" BBR 拥塞控制算法配置,可选 `conservative` `standard` `aggressive`。 默认使用 `standard`。 #### 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`)。 #### realm.stun_domain_resolver 用于解析 STUN 服务器域名的域名解析器。 此选项的格式与 [路由 DNS 规则动作](/zh/configuration/dns/rule_action/#route) 相同,但不包含 `action` 字段。 若直接将此选项设置为字符串,则等同于设置该选项的 `server` 字段。 如果为空,则使用默认域名解析器。 #### realm.http_client 与 realm 通信使用的 HTTP 客户端。 参阅 [HTTP 客户端](/zh/configuration/shared/http-client/) 了解详情。 ================================================ FILE: docs/configuration/inbound/index.md ================================================ # Inbound ### Structure ```json { "inbounds": [ { "type": "", "tag": "" } ] } ``` ### Fields | Type | Format | Injectable | |---------------|-------------------------------|------------------| | `direct` | [Direct](./direct/) | :material-close: | | `mixed` | [Mixed](./mixed/) | TCP | | `socks` | [SOCKS](./socks/) | TCP | | `http` | [HTTP](./http/) | TCP | | `shadowsocks` | [Shadowsocks](./shadowsocks/) | TCP | | `vmess` | [VMess](./vmess/) | TCP | | `trojan` | [Trojan](./trojan/) | TCP | | `naive` | [Naive](./naive/) | :material-close: | | `hysteria` | [Hysteria](./hysteria/) | :material-close: | | `shadowtls` | [ShadowTLS](./shadowtls/) | TCP | | `tuic` | [TUIC](./tuic/) | :material-close: | | `hysteria2` | [Hysteria2](./hysteria2/) | :material-close: | | `vless` | [VLESS](./vless/) | TCP | | `anytls` | [AnyTLS](./anytls/) | TCP | | `tun` | [Tun](./tun/) | :material-close: | | `redirect` | [Redirect](./redirect/) | :material-close: | | `tproxy` | [TProxy](./tproxy/) | :material-close: | | `cloudflared` | [Cloudflared](./cloudflared/) | :material-close: | #### tag The tag of the inbound. ================================================ FILE: docs/configuration/inbound/index.zh.md ================================================ # 入站 ### 结构 ```json { "inbounds": [ { "type": "", "tag": "" } ] } ``` ### 字段 | 类型 | 格式 | 注入支持 | |---------------|-------------------------------|------------------| | `direct` | [Direct](./direct/) | :material-close: | | `mixed` | [Mixed](./mixed/) | TCP | | `socks` | [SOCKS](./socks/) | TCP | | `http` | [HTTP](./http/) | TCP | | `shadowsocks` | [Shadowsocks](./shadowsocks/) | TCP | | `vmess` | [VMess](./vmess/) | TCP | | `trojan` | [Trojan](./trojan/) | TCP | | `naive` | [Naive](./naive/) | :material-close: | | `hysteria` | [Hysteria](./hysteria/) | :material-close: | | `shadowtls` | [ShadowTLS](./shadowtls/) | TCP | | `tuic` | [TUIC](./tuic/) | :material-close: | | `hysteria2` | [Hysteria2](./hysteria2/) | :material-close: | | `vless` | [VLESS](./vless/) | TCP | | `anytls` | [AnyTLS](./anytls/) | TCP | | `tun` | [Tun](./tun/) | :material-close: | | `redirect` | [Redirect](./redirect/) | :material-close: | | `tproxy` | [TProxy](./tproxy/) | :material-close: | | `cloudflared` | [Cloudflared](./cloudflared/) | :material-close: | #### tag 入站的标签。 ================================================ FILE: docs/configuration/inbound/mixed.md ================================================ `mixed` inbound is a socks4, socks4a, socks5 and http server. ### Structure ```json { "type": "mixed", "tag": "mixed-in", ... // Listen Fields "users": [ { "username": "admin", "password": "admin" } ], "set_system_proxy": false } ``` ### Listen Fields See [Listen Fields](/configuration/shared/listen/) for details. ### Fields #### users SOCKS and HTTP users. No authentication required if empty. #### set_system_proxy !!! quote "" Only supported on Linux, Android, Windows, and macOS. !!! warning "" To work on Android and Apple platforms without privileges, use tun.platform.http_proxy instead. Automatically set system proxy configuration when start and clean up when stop. ================================================ FILE: docs/configuration/inbound/mixed.zh.md ================================================ `mixed` 入站是一个 socks4, socks4a, socks5 和 http 服务器. ### 结构 ```json { "type": "mixed", "tag": "mixed-in", ... // 监听字段 "users": [ { "username": "admin", "password": "admin" } ], "set_system_proxy": false } ``` ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/)。 ### 字段 #### users SOCKS 和 HTTP 用户 如果为空则不需要验证。 #### set_system_proxy !!! quote "" 仅支持 Linux、Android、Windows 和 macOS。 !!! warning "" 要在无特权的 Android 和 iOS 上工作,请改用 tun.platform.http_proxy。 启动时自动设置系统代理,停止时自动清理。 ================================================ FILE: docs/configuration/inbound/naive.md ================================================ !!! quote "Changes in sing-box 1.13.0" :material-plus: [quic_congestion_control](#quic_congestion_control) ### Structure ```json { "type": "naive", "tag": "naive-in", "network": "udp", ... // Listen Fields "users": [ { "username": "sekai", "password": "password" } ], "quic_congestion_control": "", "tls": {} } ``` ### Listen Fields See [Listen Fields](/configuration/shared/listen/) for details. ### Fields #### network Listen network, one of `tcp` `udp`. Both if empty. #### users ==Required== Naive users. #### quic_congestion_control !!! question "Since sing-box 1.13.0" QUIC congestion control algorithm. | Algorithm | Description | |----------------|---------------------------------| | `bbr` | BBR | | `bbr_standard` | BBR (Standard version) | | `bbr2` | BBRv2 | | `bbr2_variant` | BBRv2 (An experimental variant) | | `cubic` | CUBIC | | `reno` | New Reno | `bbr` is used by default (the default of QUICHE, used by Chromium which NaiveProxy is based on). #### tls TLS configuration, see [TLS](/configuration/shared/tls/#inbound). ================================================ FILE: docs/configuration/inbound/naive.zh.md ================================================ !!! quote "sing-box 1.13.0 中的更改" :material-plus: [quic_congestion_control](#quic_congestion_control) ### 结构 ```json { "type": "naive", "tag": "naive-in", "network": "udp", ... // 监听字段 "users": [ { "username": "sekai", "password": "password" } ], "quic_congestion_control": "", "tls": {} } ``` ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/)。 ### 字段 #### network 监听的网络协议,`tcp` `udp` 之一。 默认所有。 #### users ==必填== Naive 用户。 #### quic_congestion_control !!! question "Since sing-box 1.13.0" QUIC 拥塞控制算法。 | 算法 | 描述 | |----------------|--------------------| | `bbr` | BBR | | `bbr_standard` | BBR (标准版) | | `bbr2` | BBRv2 | | `bbr2_variant` | BBRv2 (一种试验变体) | | `cubic` | CUBIC | | `reno` | New Reno | 默认使用 `bbr`(NaiveProxy 基于的 Chromium 使用的 QUICHE 的默认值)。 #### tls TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 ================================================ FILE: docs/configuration/inbound/redirect.md ================================================ !!! quote "" Only supported on Linux and macOS. ### Structure ```json { "type": "redirect", "tag": "redirect-in", ... // Listen Fields } ``` ### Listen Fields See [Listen Fields](/configuration/shared/listen/) for details. ================================================ FILE: docs/configuration/inbound/redirect.zh.md ================================================ !!! quote "" 仅支持 Linux 和 macOS。 ### 结构 ```json { "type": "redirect", "tag": "redirect-in", ... // 监听字段 } ``` ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/)。 ================================================ FILE: docs/configuration/inbound/shadowsocks.md ================================================ ### Structure ```json { "type": "shadowsocks", "tag": "ss-in", ... // Listen Fields "method": "2022-blake3-aes-128-gcm", "password": "8JCsPssfgS8tiRwiMlhARg==", "managed": false, "multiplex": {} } ``` ### Multi-User Structure ```json { "method": "2022-blake3-aes-128-gcm", "password": "8JCsPssfgS8tiRwiMlhARg==", "users": [ { "name": "sekai", "password": "PCD2Z4o12bKUoFa3cC97Hw==" } ], "multiplex": {} } ``` ### Relay Structure ```json { "type": "shadowsocks", "method": "2022-blake3-aes-128-gcm", "password": "8JCsPssfgS8tiRwiMlhARg==", "destinations": [ { "name": "test", "server": "example.com", "server_port": 8080, "password": "PCD2Z4o12bKUoFa3cC97Hw==" } ], "multiplex": {} } ``` ### Listen Fields See [Listen Fields](/configuration/shared/listen/) for details. ### Fields #### network Listen network, one of `tcp` `udp`. Both if empty. #### method ==Required== | Method | Key Length | |-------------------------------|------------| | 2022-blake3-aes-128-gcm | 16 | | 2022-blake3-aes-256-gcm | 32 | | 2022-blake3-chacha20-poly1305 | 32 | | none | / | | aes-128-gcm | / | | aes-192-gcm | / | | aes-256-gcm | / | | chacha20-ietf-poly1305 | / | | xchacha20-ietf-poly1305 | / | #### password ==Required== | Method | Password Format | |---------------|------------------------------------------------| | none | / | | 2022 methods | `sing-box generate rand --base64 ` | | other methods | any string | #### managed Defaults to `false`. Enable this when the inbound is managed by the [SSM API](/configuration/service/ssm-api) for dynamic user. #### multiplex See [Multiplex](/configuration/shared/multiplex#inbound) for details. ================================================ FILE: docs/configuration/inbound/shadowsocks.zh.md ================================================ ### 结构 ```json { "type": "shadowsocks", "tag": "ss-in", ... // 监听字段 "method": "2022-blake3-aes-128-gcm", "password": "8JCsPssfgS8tiRwiMlhARg==", "managed": false, "multiplex": {} } ``` ### 多用户结构 ```json { "method": "2022-blake3-aes-128-gcm", "password": "8JCsPssfgS8tiRwiMlhARg==", "users": [ { "name": "sekai", "password": "PCD2Z4o12bKUoFa3cC97Hw==" } ], "multiplex": {} } ``` ### 中转结构 ```json { "type": "shadowsocks", "method": "2022-blake3-aes-128-gcm", "password": "8JCsPssfgS8tiRwiMlhARg==", "destinations": [ { "name": "test", "server": "example.com", "server_port": 8080, "password": "PCD2Z4o12bKUoFa3cC97Hw==" } ], "multiplex": {} } ``` ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/)。 ### 字段 #### network 监听的网络协议,`tcp` `udp` 之一。 默认所有。 #### method ==必填== | 方法 | 密钥长度 | |-------------------------------|------| | 2022-blake3-aes-128-gcm | 16 | | 2022-blake3-aes-256-gcm | 32 | | 2022-blake3-chacha20-poly1305 | 32 | | none | / | | aes-128-gcm | / | | aes-192-gcm | / | | aes-256-gcm | / | | chacha20-ietf-poly1305 | / | | xchacha20-ietf-poly1305 | / | #### password ==必填== | 方法 | 密码格式 | |---------------|------------------------------------------| | none | / | | 2022 methods | `sing-box generate rand --base64 <密钥长度>` | | other methods | 任意字符串 | #### managed 默认为 `false`。当该入站需要由 [SSM API](/zh/configuration/service/ssm-api) 管理用户时必须启用此字段。 #### multiplex 参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。 ================================================ FILE: docs/configuration/inbound/shadowtls.md ================================================ --- icon: material/new-box --- !!! quote "Changes in sing-box 1.12.0" :material-plus: [wildcard_sni](#wildcard_sni) ### Structure ```json { "type": "shadowtls", "tag": "st-in", ... // Listen Fields "version": 3, "password": "fuck me till the daylight", "users": [ { "name": "sekai", "password": "8JCsPssfgS8tiRwiMlhARg==" } ], "handshake": { "server": "google.com", "server_port": 443, ... // Dial Fields }, "handshake_for_server_name": { "example.com": { "server": "example.com", "server_port": 443, ... // Dial Fields } }, "strict_mode": false, "wildcard_sni": "" } ``` ### Listen Fields See [Listen Fields](/configuration/shared/listen/) for details. ### Fields #### version ShadowTLS protocol version. | Value | Protocol Version | |---------------|-----------------------------------------------------------------------------------------| | `1` (default) | [ShadowTLS v1](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-en.md#v1) | | `2` | [ShadowTLS v2](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-en.md#v2) | | `3` | [ShadowTLS v3](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-v3-en.md) | #### password ShadowTLS password. Only available in the ShadowTLS protocol 2. #### users ShadowTLS users. Only available in the ShadowTLS protocol 3. #### handshake ==Required== When `wildcard_sni` is configured to `all`, the server address is optional. Handshake server address and [Dial Fields](/configuration/shared/dial/). #### handshake_for_server_name Handshake server address and [Dial Fields](/configuration/shared/dial/) for specific server name. Only available in the ShadowTLS protocol 2/3. #### strict_mode ShadowTLS strict mode. Only available in the ShadowTLS protocol 3. #### wildcard_sni !!! question "Since sing-box 1.12.0" ShadowTLS wildcard SNI mode. Available values are: * `off`: (default) Disabled. * `authed`: Authenticated connections will have their destination overwritten to `(servername):443` * `all`: All connections will have their destination overwritten to `(servername):443` Additionally, connections matching `handshake_for_server_name` are not affected. Only available in the ShadowTLS protocol 3. ================================================ FILE: docs/configuration/inbound/shadowtls.zh.md ================================================ --- icon: material/new-box --- !!! quote "sing-box 1.12.0 中的更改" :material-plus: [wildcard_sni](#wildcard_sni) ### 结构 ```json { "type": "shadowtls", "tag": "st-in", ... // 监听字段 "version": 3, "password": "fuck me till the daylight", "users": [ { "name": "sekai", "password": "8JCsPssfgS8tiRwiMlhARg==" } ], "handshake": { "server": "google.com", "server_port": 443, ... // 拨号字段 }, "handshake_for_server_name": { "example.com": { "server": "example.com", "server_port": 443, ... // 拨号字段 } }, "strict_mode": false, "wildcard_sni": "" } ``` ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/)。 ### 字段 #### version ShadowTLS 协议版本。 | 值 | 协议版本 | |---------------|-----------------------------------------------------------------------------------------| | `1` (default) | [ShadowTLS v1](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-en.md#v1) | | `2` | [ShadowTLS v2](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-en.md#v2) | | `3` | [ShadowTLS v3](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-v3-en.md) | #### password ShadowTLS 密码。 仅在 ShadowTLS 协议版本 2 中可用。 #### users ShadowTLS 用户。 仅在 ShadowTLS 协议版本 3 中可用。 #### handshake ==必填== 握手服务器地址和 [拨号参数](/zh/configuration/shared/dial/)。 #### handshake_for_server_name ==必填== 对于特定服务器名称的握手服务器地址和 [拨号参数](/zh/configuration/shared/dial/)。 仅在 ShadowTLS 协议版本 2/3 中可用。 #### strict_mode ShadowTLS 严格模式。 仅在 ShadowTLS 协议版本 3 中可用。 #### wildcard_sni !!! question "自 sing-box 1.12.0 起" ShadowTLS 通配符 SNI 模式。 可用值: * `off`:(默认)禁用。 * `authed`:已认证的连接的目标将被重写为 `(servername):443`。 * `all`:所有连接的目标将被重写为 `(servername):443`。 此外,匹配 `handshake_for_server_name` 的连接不受影响。 仅在 ShadowTLS 协议 3 中可用。 ================================================ FILE: docs/configuration/inbound/socks.md ================================================ `socks` inbound is a socks4, socks4a, socks5 server. ### Structure ```json { "type": "socks", "tag": "socks-in", ... // Listen Fields "users": [ { "username": "admin", "password": "admin" } ] } ``` ### Listen Fields See [Listen Fields](/configuration/shared/listen/) for details. ### Fields #### users SOCKS users. No authentication required if empty. ================================================ FILE: docs/configuration/inbound/socks.zh.md ================================================ `socks` 入站是一个 socks4, socks4a 和 socks5 服务器. ### 结构 ```json { "type": "socks", "tag": "socks-in", ... // 监听字段 "users": [ { "username": "admin", "password": "admin" } ] } ``` ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/)。 ### 字段 #### users SOCKS 用户 如果为空则不需要验证。 ================================================ FILE: docs/configuration/inbound/tproxy.md ================================================ !!! quote "" Only supported on Linux. ### Structure ```json { "type": "tproxy", "tag": "tproxy-in", ... // Listen Fields "network": "udp" } ``` ### Listen Fields See [Listen Fields](/configuration/shared/listen/) for details. ### Fields #### network Listen network, one of `tcp` `udp`. Both if empty. ================================================ FILE: docs/configuration/inbound/tproxy.zh.md ================================================ !!! quote "" 仅支持 Linux。 ### 结构 ```json { "type": "tproxy", "tag": "tproxy-in", ... // 监听字段 "network": "udp" } ``` ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/)。 ### 字段 #### network 监听的网络协议,`tcp` `udp` 之一。 默认所有。 ================================================ FILE: docs/configuration/inbound/trojan.md ================================================ ### Structure ```json { "type": "trojan", "tag": "trojan-in", ... // Listen Fields "users": [ { "name": "sekai", "password": "8JCsPssfgS8tiRwiMlhARg==" } ], "tls": {}, "fallback": { "server": "127.0.0.1", "server_port": 8080 }, "fallback_for_alpn": { "http/1.1": { "server": "127.0.0.1", "server_port": 8081 } }, "multiplex": {}, "transport": {} } ``` ### Listen Fields See [Listen Fields](/configuration/shared/listen/) for details. ### Fields #### users ==Required== Trojan users. #### tls TLS configuration, see [TLS](/configuration/shared/tls/#inbound). #### fallback !!! failure "" There is no evidence that GFW detects and blocks Trojan servers based on HTTP responses, and opening the standard http/s port on the server is a much bigger signature. Fallback server configuration. Disabled if `fallback` and `fallback_for_alpn` are empty. #### fallback_for_alpn Fallback server configuration for specified ALPN. If not empty, TLS fallback requests with ALPN not in this table will be rejected. #### multiplex See [Multiplex](/configuration/shared/multiplex#inbound) for details. #### transport V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport/). ================================================ FILE: docs/configuration/inbound/trojan.zh.md ================================================ ### 结构 ```json { "type": "trojan", "tag": "trojan-in", ... // 监听字段 "users": [ { "name": "sekai", "password": "8JCsPssfgS8tiRwiMlhARg==" } ], "tls": {}, "fallback": { "server": "127.0.0.1", "server_port": 8080 }, "fallback_for_alpn": { "http/1.1": { "server": "127.0.0.1", "server_port": 8081 } }, "multiplex": {}, "transport": {} } ``` ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/)。 ### 字段 #### users ==必填== Trojan 用户。 #### tls TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。 #### fallback !!! failure "" 没有证据表明 GFW 基于 HTTP 响应检测并阻止 Trojan 服务器,并且在服务器上打开标准 http/s 端口是一个更大的特征。 回退服务器配置。如果 `fallback` 和 `fallback_for_alpn` 为空,则禁用回退。 #### fallback_for_alpn 为 ALPN 指定回退服务器配置。 如果不为空,ALPN 不在此列表中的 TLS 回退请求将被拒绝。 #### multiplex 参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。 #### transport V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport/)。 ================================================ FILE: docs/configuration/inbound/tuic.md ================================================ ### Structure ```json { "type": "tuic", "tag": "tuic-in", ... // Listen Fields "users": [ { "name": "sekai", "uuid": "059032A9-7D40-4A96-9BB1-36823D848068", "password": "hello" } ], "congestion_control": "cubic", "auth_timeout": "3s", "zero_rtt_handshake": false, "heartbeat": "10s", "tls": {}, ... // QUIC Fields } ``` ### Listen Fields See [Listen Fields](/configuration/shared/listen/) for details. ### Fields #### users TUIC users #### users.uuid ==Required== TUIC user uuid #### users.password TUIC user password #### congestion_control QUIC congestion control algorithm One of: `cubic`, `new_reno`, `bbr` `cubic` is used by default. #### auth_timeout How long the server should wait for the client to send the authentication command `3s` is used by default. #### zero_rtt_handshake Enable 0-RTT QUIC connection handshake on the client side This is not impacting much on the performance, as the protocol is fully multiplexed !!! warning "" Disabling this is highly recommended, as it is vulnerable to replay attacks. See [Attack of the clones](https://blog.cloudflare.com/even-faster-connection-establishment-with-quic-0-rtt-resumption/#attack-of-the-clones) #### heartbeat Interval for sending heartbeat packets for keeping the connection alive `10s` is used by default. #### tls ==Required== TLS configuration, see [TLS](/configuration/shared/tls/#inbound). ### QUIC Fields See [QUIC Fields](/configuration/shared/quic/) for details. ================================================ FILE: docs/configuration/inbound/tuic.zh.md ================================================ ### 结构 ```json { "type": "tuic", "tag": "tuic-in", ... // 监听字段 "users": [ { "name": "sekai", "uuid": "059032A9-7D40-4A96-9BB1-36823D848068", "password": "hello" } ], "congestion_control": "cubic", "auth_timeout": "3s", "zero_rtt_handshake": false, "heartbeat": "10s", "tls": {}, ... // QUIC 字段 } ``` ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/)。 ### 字段 #### users TUIC 用户 #### users.uuid ==必填== TUIC 用户 UUID #### users.password TUIC 用户密码 #### congestion_control QUIC 拥塞控制算法 可选值: `cubic`, `new_reno`, `bbr` 默认使用 `cubic`。 #### auth_timeout 服务器等待客户端发送认证命令的时间 默认使用 `3s`。 #### zero_rtt_handshake 在客户端启用 0-RTT QUIC 连接握手 这对性能影响不大,因为协议是完全复用的 !!! warning "" 强烈建议禁用此功能,因为它容易受到重放攻击。 请参阅 [Attack of the clones](https://blog.cloudflare.com/even-faster-connection-establishment-with-quic-0-rtt-resumption/#attack-of-the-clones) #### heartbeat 发送心跳包以保持连接存活的时间间隔 默认使用 `10s`。 #### tls ==必填== TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 ### QUIC 字段 参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。 ================================================ FILE: docs/configuration/inbound/tun.md ================================================ --- 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: [dns_mode](#dns_mode) :material-plus: [dns_address](#dns_address) !!! quote "Changes in sing-box 1.13.3" :material-alert: [strict_route](#strict_route) !!! quote "Changes in sing-box 1.13.0" :material-plus: [auto_redirect_reset_mark](#auto_redirect_reset_mark) :material-plus: [auto_redirect_nfqueue](#auto_redirect_nfqueue) :material-plus: [exclude_mptcp](#exclude_mptcp) :material-plus: [auto_redirect_iproute2_fallback_rule_index](#auto_redirect_iproute2_fallback_rule_index) !!! quote "Changes in sing-box 1.12.0" :material-plus: [loopback_address](#loopback_address) !!! quote "Changes in sing-box 1.11.0" :material-delete-alert: [gso](#gso) :material-alert-decagram: [route_address_set](#stack) :material-alert-decagram: [route_exclude_address_set](#stack) !!! quote "Changes in sing-box 1.10.0" :material-plus: [address](#address) :material-delete-clock: [inet4_address](#inet4_address) :material-delete-clock: [inet6_address](#inet6_address) :material-plus: [route_address](#route_address) :material-delete-clock: [inet4_route_address](#inet4_route_address) :material-delete-clock: [inet6_route_address](#inet6_route_address) :material-plus: [route_exclude_address](#route_address) :material-delete-clock: [inet4_route_exclude_address](#inet4_route_exclude_address) :material-delete-clock: [inet6_route_exclude_address](#inet6_route_exclude_address) :material-plus: [iproute2_table_index](#iproute2_table_index) :material-plus: [iproute2_rule_index](#iproute2_table_index) :material-plus: [auto_redirect](#auto_redirect) :material-plus: [auto_redirect_input_mark](#auto_redirect_input_mark) :material-plus: [auto_redirect_output_mark](#auto_redirect_output_mark) :material-plus: [route_address_set](#route_address_set) :material-plus: [route_exclude_address_set](#route_address_set) !!! quote "Changes in sing-box 1.9.0" :material-plus: [platform.http_proxy.bypass_domain](#platformhttp_proxybypass_domain) :material-plus: [platform.http_proxy.match_domain](#platformhttp_proxymatch_domain) !!! quote "Changes in sing-box 1.8.0" :material-plus: [gso](#gso) :material-alert-decagram: [stack](#stack) !!! quote "" Only supported on Linux, Windows and macOS. ### Structure ```json { "type": "tun", "tag": "tun-in", "interface_name": "tun0", "address": [ "172.18.0.1/30", "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, "auto_redirect": true, "auto_redirect_input_mark": "0x2023", "auto_redirect_output_mark": "0x2024", "auto_redirect_reset_mark": "0x2025", "auto_redirect_nfqueue": 100, "auto_redirect_iproute2_fallback_rule_index": 32768, "exclude_mptcp": false, "loopback_address": [ "10.7.0.1" ], "strict_route": true, "route_address": [ "0.0.0.0/1", "128.0.0.0/1", "::/1", "8000::/1" ], "route_exclude_address": [ "192.168.0.0/16", "fc00::/7" ], "route_address_set": [ "geoip-cloudflare" ], "route_exclude_address_set": [ "geoip-cn" ], "endpoint_independent_nat": false, "udp_timeout": "5m", "stack": "system", "include_interface": [ "lan0" ], "exclude_interface": [ "lan1" ], "include_uid": [ 0 ], "include_uid_range": [ "1000:99999" ], "exclude_uid": [ 1000 ], "exclude_uid_range": [ "1000:99999" ], "include_android_user": [ 0, 10 ], "include_package": [ "com.android.chrome" ], "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, "server": "127.0.0.1", "server_port": 8080, "bypass_domain": [], "match_domain": [] } }, // Deprecated "gso": false, "inet4_address": [ "172.19.0.1/30" ], "inet6_address": [ "fdfe:dcba:9876::1/126" ], "inet4_route_address": [ "0.0.0.0/1", "128.0.0.0/1" ], "inet6_route_address": [ "::/1", "8000::/1" ], "inet4_route_exclude_address": [ "192.168.0.0/16" ], "inet6_route_exclude_address": [ "fc00::/7" ], ... // Listen Fields } ``` !!! note "" You can ignore the JSON Array [] tag when the content is only one item !!! warning "" If tun is running in non-privileged mode, addresses and MTU will not be configured automatically, please make sure the settings are accurate. ### Fields #### interface_name Virtual device name, automatically selected if empty. #### address !!! question "Since sing-box 1.10.0" IPv4 and IPv6 prefix for the tun interface. #### inet4_address !!! failure "Deprecated in sing-box 1.10.0" `inet4_address` is merged to `address` and will be removed in sing-box 1.12.0. IPv4 prefix for the tun interface. #### inet6_address !!! failure "Deprecated in sing-box 1.10.0" `inet6_address` is merged to `address` and will be removed in sing-box 1.12.0. IPv6 prefix for the tun interface. #### mtu 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" GSO has no advantages for transparent proxy scenarios, is deprecated and no longer works, and will be removed in sing-box 1.12.0. !!! question "Since sing-box 1.8.0" !!! quote "" Only supported on Linux with `auto_route` enabled. Enable generic segmentation offload. #### auto_route Set the default route to the Tun. !!! quote "" To avoid traffic loopback, set `route.auto_detect_interface` or `route.default_interface` or `outbound.bind_interface` !!! note "Use with Android VPN" By default, VPN takes precedence over tun. To make tun go through VPN, enable `route.override_android_vpn`. !!! note "Also enable `auto_redirect`" `auto_redirect` is always recommended on Linux, it provides better routing, higher performance (better than tproxy), and avoids conflicts between TUN and Docker bridge networks. #### iproute2_table_index !!! question "Since sing-box 1.10.0" Linux iproute2 table index generated by `auto_route`. `2022` is used by default. #### iproute2_rule_index !!! question "Since sing-box 1.10.0" Linux iproute2 rule start index generated by `auto_route`. `9000` is used by default. #### auto_redirect !!! question "Since sing-box 1.10.0" !!! quote "" Only supported on Linux with `auto_route` enabled. Improve TUN routing and performance using nftables. `auto_redirect` is always recommended on Linux, it provides better routing, higher performance (better than tproxy), and avoids conflicts between TUN and Docker bridge networks. Note that `auto_redirect` also works on Android, but due to the lack of `nftables` and `ip6tables`, only simple IPv4 TCP forwarding is performed. To share your VPN connection over hotspot or repeater on Android, use [VPNHotspot](https://github.com/Mygod/VPNHotspot). `auto_redirect` also automatically inserts compatibility rules into the OpenWrt fw4 table, i.e. it will work on routers without any extra configuration. Conflict with `route.default_mark` and `[dialOptions].routing_mark`. #### auto_redirect_input_mark !!! question "Since sing-box 1.10.0" Connection input mark used by `auto_redirect`. `0x2023` is used by default. #### auto_redirect_output_mark !!! question "Since sing-box 1.10.0" Connection output mark used by `auto_redirect`. `0x2024` is used by default. #### auto_redirect_reset_mark !!! question "Since sing-box 1.13.0" Connection reset mark used by `auto_redirect` pre-matching. `0x2025` is used by default. #### auto_redirect_nfqueue !!! question "Since sing-box 1.13.0" NFQueue number used by `auto_redirect` pre-matching. `100` is used by default. #### auto_redirect_iproute2_fallback_rule_index !!! question "Since sing-box 1.12.18" Linux iproute2 fallback rule index generated by `auto_redirect`. This rule is checked after system default rules (32766: main, 32767: default), routing traffic to the sing-box table only when no route is found in system tables. `32768` is used by default. #### exclude_mptcp !!! question "Since sing-box 1.13.0" !!! quote "" Only supported on Linux with nftables and requires `auto_route` and `auto_redirect` enabled. MPTCP cannot be transparently proxied due to protocol limitations. Such traffic is usually created by Apple systems. When enabled, MPTCP connections will bypass sing-box and connect directly, otherwise, will be rejected to avoid errors by default. #### loopback_address !!! question "Since sing-box 1.12.0" Loopback addresses make TCP connections to the specified address connect to the source address. Setting option value to `10.7.0.1` achieves the same behavior as SideStore/StosVPN. When `auto_redirect` is enabled, the same behavior can be achieved for LAN devices (not just local) as a gateway. #### strict_route Enforce strict routing rules when `auto_route` is enabled: *In Linux*: * Let unsupported network unreachable * For legacy reasons, when neither `strict_route` nor `auto_redirect` are enabled, all ICMP traffic will not go through TUN. * When `auto_redirect` is enabled, `strict_route` also affects `SO_BINDTODEVICE` traffic: * Enabled: `SO_BINDTODEVICE` traffic is redirected through sing-box. * Disabled: `SO_BINDTODEVICE` traffic bypasses sing-box. *In Windows*: * Let unsupported network unreachable * prevent DNS leak caused by Windows' [ordinary multihomed DNS resolution behavior](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd197552%28v%3Dws.10%29) It may prevent some Windows applications (such as VirtualBox) from working properly in certain situations. #### route_address !!! question "Since sing-box 1.10.0" Use custom routes instead of default when `auto_route` is enabled. #### inet4_route_address !!! failure "Deprecated in sing-box 1.10.0" `inet4_route_address` is deprecated and will be removed in sing-box 1.12.0, please use [route_address](#route_address) instead. Use custom routes instead of default when `auto_route` is enabled. #### inet6_route_address !!! failure "Deprecated in sing-box 1.10.0" `inet6_route_address` is deprecated and will be removed in sing-box 1.12.0, please use [route_address](#route_address) instead. Use custom routes instead of default when `auto_route` is enabled. #### route_exclude_address !!! question "Since sing-box 1.10.0" Exclude custom routes when `auto_route` is enabled. #### inet4_route_exclude_address !!! failure "Deprecated in sing-box 1.10.0" `inet4_route_exclude_address` is deprecated and will be removed in sing-box 1.12.0, please use [route_exclude_address](#route_exclude_address) instead. Exclude custom routes when `auto_route` is enabled. #### inet6_route_exclude_address !!! failure "Deprecated in sing-box 1.10.0" `inet6_route_exclude_address` is deprecated and will be removed in sing-box 1.12.0, please use [route_exclude_address](#route_exclude_address) instead. Exclude custom routes when `auto_route` is enabled. #### route_address_set === "With `auto_redirect` enabled" !!! question "Since sing-box 1.10.0" !!! quote "" Only supported on Linux with nftables and requires `auto_route` and `auto_redirect` enabled. Add the destination IP CIDR rules in the specified rule-sets to the firewall. Unmatched traffic will bypass the sing-box routes. Conflict with `route.default_mark` and `[dialOptions].routing_mark`. === "Without `auto_redirect` enabled" !!! question "Since sing-box 1.11.0" Add the destination IP CIDR rules in the specified rule-sets to routes, equivalent to adding to `route_address`. Unmatched traffic will bypass the sing-box routes. Note that it **doesn't work on the Android graphical client** due to the Android VpnService not being able to handle a large number of routes (DeadSystemException), but otherwise it works fine on all command line clients and Apple platforms. #### route_exclude_address_set === "With `auto_redirect` enabled" !!! question "Since sing-box 1.10.0" !!! quote "" Only supported on Linux with nftables and requires `auto_route` and `auto_redirect` enabled. Add the destination IP CIDR rules in the specified rule-sets to the firewall. Matched traffic will bypass the sing-box routes. === "Without `auto_redirect` enabled" !!! question "Since sing-box 1.11.0" Add the destination IP CIDR rules in the specified rule-sets to routes, equivalent to adding to `route_exclude_address`. Matched traffic will bypass the sing-box routes. Note that it **doesn't work on the Android graphical client** due to the Android VpnService not being able to handle a large number of routes (DeadSystemException), but otherwise it works fine on all command line clients and Apple platforms. #### endpoint_independent_nat !!! info "" This item is only available on the gvisor stack, other stacks are endpoint-independent NAT by default. Enable endpoint-independent NAT. Performance may degrade slightly, so it is not recommended to enable on when it is not needed. #### udp_timeout UDP NAT expiration time. `5m` will be used by default. #### stack !!! quote "Changes in sing-box 1.8.0" :material-delete-alert: The legacy LWIP stack has been deprecated and removed. TCP/IP stack. | Stack | Description | |----------|-------------------------------------------------------------------------------------------------------| | `system` | Perform L3 to L4 translation using the system network stack | | `gvisor` | Perform L3 to L4 translation using [gVisor](https://github.com/google/gvisor)'s virtual network stack | | `mixed` | Mixed `system` TCP stack and `gvisor` UDP stack | Defaults to the `mixed` stack if the gVisor build tag is enabled, otherwise defaults to the `system` stack. #### include_interface !!! quote "" Interface rules are only supported on Linux and require auto_route. Limit interfaces in route. Not limited by default. Conflict with `exclude_interface`. #### exclude_interface !!! warning "" When `strict_route` enabled, return traffic to excluded interfaces will not be automatically excluded, so add them as well (example: `br-lan` and `pppoe-wan`). Exclude interfaces in route. Conflict with `include_interface`. #### include_uid !!! quote "" UID rules are only supported on Linux and require auto_route. Limit users in route. Not limited by default. #### include_uid_range Limit users in route, but in range. #### exclude_uid Exclude users in route. #### exclude_uid_range Exclude users in route, but in range. #### include_android_user !!! quote "" Android user and package rules are only supported on Android and require auto_route. Limit android users in route. | Common user | ID | |--------------|----| | Main | 0 | | Work Profile | 10 | #### include_package Limit android packages in route. #### exclude_package 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. #### platform.http_proxy System HTTP proxy settings. #### platform.http_proxy.enabled Enable system HTTP proxy. #### platform.http_proxy.server ==Required== HTTP proxy server address. #### platform.http_proxy.server_port ==Required== HTTP proxy server port. #### platform.http_proxy.bypass_domain !!! note "" On Apple platforms, `bypass_domain` items matches hostname **suffixes**. Hostnames that bypass the HTTP proxy. #### platform.http_proxy.match_domain !!! quote "" Only supported in graphical clients on Apple platforms. Hostnames that use the HTTP proxy. ### Listen Fields See [Listen Fields](/configuration/shared/listen/) for details. ================================================ FILE: docs/configuration/inbound/tun.zh.md ================================================ --- 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: [dns_mode](#dns_mode) :material-plus: [dns_address](#dns_address) !!! quote "sing-box 1.13.3 中的更改" :material-alert: [strict_route](#strict_route) !!! quote "sing-box 1.13.0 中的更改" :material-plus: [auto_redirect_reset_mark](#auto_redirect_reset_mark) :material-plus: [auto_redirect_nfqueue](#auto_redirect_nfqueue) :material-plus: [exclude_mptcp](#exclude_mptcp) :material-plus: [auto_redirect_iproute2_fallback_rule_index](#auto_redirect_iproute2_fallback_rule_index) !!! quote "sing-box 1.12.0 中的更改" :material-plus: [loopback_address](#loopback_address) !!! quote "sing-box 1.11.0 中的更改" :material-delete-alert: [gso](#gso) :material-alert-decagram: [route_address_set](#stack) :material-alert-decagram: [route_exclude_address_set](#stack) !!! quote "sing-box 1.10.0 中的更改" :material-plus: [address](#address) :material-delete-clock: [inet4_address](#inet4_address) :material-delete-clock: [inet6_address](#inet6_address) :material-plus: [route_address](#route_address) :material-delete-clock: [inet4_route_address](#inet4_route_address) :material-delete-clock: [inet6_route_address](#inet6_route_address) :material-plus: [route_exclude_address](#route_address) :material-delete-clock: [inet4_route_exclude_address](#inet4_route_exclude_address) :material-delete-clock: [inet6_route_exclude_address](#inet6_route_exclude_address) :material-plus: [iproute2_table_index](#iproute2_table_index) :material-plus: [iproute2_rule_index](#iproute2_table_index) :material-plus: [auto_redirect](#auto_redirect) :material-plus: [auto_redirect_input_mark](#auto_redirect_input_mark) :material-plus: [auto_redirect_output_mark](#auto_redirect_output_mark) :material-plus: [route_address_set](#route_address_set) :material-plus: [route_exclude_address_set](#route_address_set) !!! quote "sing-box 1.9.0 中的更改" :material-plus: [platform.http_proxy.bypass_domain](#platformhttp_proxybypass_domain) :material-plus: [platform.http_proxy.match_domain](#platformhttp_proxymatch_domain) !!! quote "sing-box 1.8.0 中的更改" :material-plus: [gso](#gso) :material-alert-decagram: [stack](#stack) !!! quote "" 仅支持 Linux、Windows 和 macOS。 ### 结构 ```json { "type": "tun", "tag": "tun-in", "interface_name": "tun0", "address": [ "172.18.0.1/30", "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, "auto_redirect": true, "auto_redirect_input_mark": "0x2023", "auto_redirect_output_mark": "0x2024", "auto_redirect_reset_mark": "0x2025", "auto_redirect_nfqueue": 100, "auto_redirect_iproute2_fallback_rule_index": 32768, "exclude_mptcp": false, "loopback_address": [ "10.7.0.1" ], "strict_route": true, "route_address": [ "0.0.0.0/1", "128.0.0.0/1", "::/1", "8000::/1" ], "route_exclude_address": [ "192.168.0.0/16", "fc00::/7" ], "route_address_set": [ "geoip-cloudflare" ], "route_exclude_address_set": [ "geoip-cn" ], "endpoint_independent_nat": false, "udp_timeout": "5m", "stack": "system", "include_interface": [ "lan0" ], "exclude_interface": [ "lan1" ], "include_uid": [ 0 ], "include_uid_range": [ "1000:99999" ], "exclude_uid": [ 1000 ], "exclude_uid_range": [ "1000:99999" ], "include_android_user": [ 0, 10 ], "include_package": [ "com.android.chrome" ], "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, "server": "127.0.0.1", "server_port": 8080, "bypass_domain": [], "match_domain": [] } }, // 已弃用 "gso": false, "inet4_address": [ "172.19.0.1/30" ], "inet6_address": [ "fdfe:dcba:9876::1/126" ], "inet4_route_address": [ "0.0.0.0/1", "128.0.0.0/1" ], "inet6_route_address": [ "::/1", "8000::/1" ], "inet4_route_exclude_address": [ "192.168.0.0/16" ], "inet6_route_exclude_address": [ "fc00::/7" ], ... // 监听字段 } ``` !!! note "" 当内容只有一项时,可以忽略 JSON 数组 [] 标签。 !!! warning "" 如果 tun 在非特权模式下运行,地址和 MTU 将不会自动配置,请确保设置正确。 ### Tun 字段 #### interface_name 虚拟设备名称,默认自动选择。 #### address !!! question "自 sing-box 1.10.0 起" ==必填== tun 接口的 IPv4 和 IPv6 前缀。 #### inet4_address !!! failure "已在 sing-box 1.10.0 废弃" `inet4_address` 已合并到 `address` 且将在 sing-box 1.12.0 中被移除。 ==必填== tun 接口的 IPv4 前缀。 #### inet6_address !!! failure "已在 sing-box 1.10.0 废弃" `inet6_address` 已合并到 `address` 且将在 sing-box 1.12.0 中被移除。 tun 接口的 IPv6 前缀。 #### mtu 最大传输单元。 #### 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 废弃" GSO 对于透明代理场景没有优势,已废弃和不再生效,且将在 sing-box 1.12.0 中被移除。 !!! question "自 sing-box 1.8.0 起" !!! quote "" 仅支持 Linux。 启用通用分段卸载。 #### auto_route 设置到 Tun 的默认路由。 !!! quote "" 为避免流量环回,请设置 `route.auto_detect_interface` 或 `route.default_interface` 或 `outbound.bind_interface`。 !!! note "与 Android VPN 一起使用" VPN 默认优先于 tun。要使 tun 经过 VPN,启用 `route.override_android_vpn`。 !!! note "也启用 `auto_redirect`" 在 Linux 上始终推荐使用 `auto_redirect`,它提供更好的路由, 更高的性能(优于 tproxy), 并避免 TUN 与 Docker 桥接网络冲突。 #### iproute2_table_index !!! question "自 sing-box 1.10.0 起" `auto_route` 生成的 iproute2 路由表索引。 默认使用 `2022`。 #### iproute2_rule_index !!! question "自 sing-box 1.10.0 起" `auto_route` 生成的 iproute2 规则起始索引。 默认使用 `9000`。 #### auto_redirect !!! question "自 sing-box 1.10.0 起" !!! quote "" 仅支持 Linux,且需要 `auto_route` 已启用。 通过使用 nftables 改善 TUN 路由和性能。 在 Linux 上始终推荐使用 `auto_redirect`,它提供更好的路由、更高的性能(优于 tproxy),并避免了 TUN 和 Docker 桥接网络之间的冲突。 请注意,`auto_redirect` 也适用于 Android,但由于缺少 `nftables` 和 `ip6tables`,仅执行简单的 IPv4 TCP 转发。 若要在 Android 上通过热点或中继器共享 VPN 连接,请使用 [VPNHotspot](https://github.com/Mygod/VPNHotspot)。 `auto_redirect` 还会自动将兼容性规则插入 OpenWrt 的 fw4 表中,即无需额外配置即可在路由器上工作。 与 `route.default_mark` 和 `[dialOptions].routing_mark` 冲突。 #### auto_redirect_input_mark !!! question "自 sing-box 1.10.0 起" `auto_redirect` 使用的连接输入标记。 默认使用 `0x2023`。 #### auto_redirect_output_mark !!! question "自 sing-box 1.10.0 起" `auto_redirect` 使用的连接输出标记。 默认使用 `0x2024`。 #### auto_redirect_reset_mark !!! question "自 sing-box 1.13.0 起" `auto_redirect` 预匹配使用的连接重置标记。 默认使用 `0x2025`。 #### auto_redirect_nfqueue !!! question "自 sing-box 1.13.0 起" `auto_redirect` 预匹配使用的 NFQueue 编号。 默认使用 `100`。 #### auto_redirect_iproute2_fallback_rule_index !!! question "自 sing-box 1.12.18 起" `auto_redirect` 生成的 iproute2 回退规则索引。 此规则在系统默认规则(32766: main,32767: default)之后检查, 仅当系统路由表中未找到路由时才将流量路由到 sing-box 路由表。 默认使用 `32768`。 #### exclude_mptcp !!! question "自 sing-box 1.13.0 起" !!! quote "" 仅支持 Linux,且需要 nftables,`auto_route` 和 `auto_redirect` 已启用。 由于协议限制,MPTCP 无法被透明代理。 此类流量通常由 Apple 系统创建。 启用时,MPTCP 连接将绕过 sing-box 直接连接,否则,将被拒绝以避免错误。 #### loopback_address !!! question "自 sing-box 1.12.0 起" 环回地址是用于使指向指定地址的 TCP 连接连接到来源地址的。 将选项值设置为 `10.7.0.1` 可实现与 SideStore/StosVPN 相同的行为。 当启用 `auto_redirect` 时,可以作为网关为局域网设备(而不仅仅是本地)实现相同的行为。 #### strict_route 当启用 `auto_route` 时,强制执行严格的路由规则: *在 Linux 中*: * 使不支持的网络不可达。 * 出于历史遗留原因,当未启用 `strict_route` 或 `auto_redirect` 时,所有 ICMP 流量将不会通过 TUN。 * 当启用 `auto_redirect` 时,`strict_route` 也影响 `SO_BINDTODEVICE` 流量: * 启用:`SO_BINDTODEVICE` 流量被重定向通过 sing-box。 * 禁用:`SO_BINDTODEVICE` 流量绕过 sing-box。 *在 Windows 中*: * 使不支持的网络不可达。 * 阻止 Windows 的 [普通多宿主 DNS 解析行为](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd197552%28v%3Dws.10%29) 造成的 DNS 泄露 它可能会使某些 Windows 应用程序(如 VirtualBox)在某些情况下无法正常工作。 #### route_address !!! question "自 sing-box 1.10.0 起" 设置到 Tun 的自定义路由。 #### inet4_route_address !!! failure "已在 sing-box 1.10.0 废弃" `inet4_route_address` 已合并到 `route_address` 且将在 sing-box 1.12.0 中被移除。 启用 `auto_route` 时使用自定义路由而不是默认路由。 #### inet6_route_address !!! failure "已在 sing-box 1.10.0 废弃" `inet6_route_address` 已合并到 `route_address` 且将在 sing-box 1.12.0 中被移除。 启用 `auto_route` 时使用自定义路由而不是默认路由。 #### route_exclude_address !!! question "自 sing-box 1.10.0 起" 设置到 Tun 的排除自定义路由。 #### inet4_route_exclude_address !!! failure "已在 sing-box 1.10.0 废弃" `inet4_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.12.0 中被移除。 启用 `auto_route` 时排除自定义路由。 #### inet6_route_exclude_address !!! failure "已在 sing-box 1.10.0 废弃" `inet6_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.12.0 中被移除。 启用 `auto_route` 时排除自定义路由。 #### route_address_set === "`auto_redirect` 已启用" !!! question "自 sing-box 1.10.0 起" !!! quote "" 仅支持 Linux,且需要 nftables,`auto_route` 和 `auto_redirect` 已启用。 将指定规则集中的目标 IP CIDR 规则添加到防火墙。 不匹配的流量将绕过 sing-box 路由。 === "`auto_redirect` 未启用" !!! question "自 sing-box 1.11.0 起" 将指定规则集中的目标 IP CIDR 规则添加到路由,相当于添加到 `route_address`。 不匹配的流量将绕过 sing-box 路由。 请注意,由于 Android VpnService 无法处理大量路由(DeadSystemException), 因此它**在 Android 图形客户端上不起作用**,但除此之外,它在所有命令行客户端和 Apple 平台上都可以正常工作。 #### route_exclude_address_set === "`auto_redirect` 已启用" !!! question "自 sing-box 1.10.0 起" !!! quote "" 仅支持 Linux,且需要 nftables,`auto_route` 和 `auto_redirect` 已启用。 将指定规则集中的目标 IP CIDR 规则添加到防火墙。 匹配的流量将绕过 sing-box 路由。 与 `route.default_mark` 和 `[dialOptions].routing_mark` 冲突。 === "`auto_redirect` 未启用" !!! question "自 sing-box 1.11.0 起" 将指定规则集中的目标 IP CIDR 规则添加到路由,相当于添加到 `route_exclude_address`。 匹配的流量将绕过 sing-box 路由。 请注意,由于 Android VpnService 无法处理大量路由(DeadSystemException), 因此它**在 Android 图形客户端上不起作用**,但除此之外,它在所有命令行客户端和 Apple 平台上都可以正常工作。 #### endpoint_independent_nat 启用独立于端点的 NAT。 性能可能会略有下降,所以不建议在不需要的时候开启。 #### udp_timeout UDP NAT 过期时间。 默认使用 `5m`。 #### stack !!! quote "sing-box 1.8.0 中的更改" :material-delete-alert: 旧的 LWIP 栈已被弃用并移除。 TCP/IP 栈。 | 栈 | 描述 | |----------|-------------------------------------------------------------------------------------------------------| | `system` | 基于系统网络栈执行 L3 到 L4 转换 | | `gvisor` | 基于 [gVisor](https://github.com/google/gvisor) 虚拟网络栈执行 L3 到 L4 转换 | | `mixed` | 混合 `system` TCP 栈与 `gvisor` UDP 栈 | 默认使用 `mixed` 栈如果 gVisor 构建标记已启用,否则默认使用 `system` 栈。 #### include_interface !!! quote "" 接口规则仅在 Linux 下被支持,并且需要 `auto_route`。 限制被路由的接口。默认不限制。 与 `exclude_interface` 冲突。 #### exclude_interface !!! warning "" 当 `strict_route` 启用,到被排除接口的回程流量将不会被自动排除,因此也要添加它们(例:`br-lan` 与 `pppoe-wan`)。 排除路由的接口。 与 `include_interface` 冲突。 #### include_uid !!! quote "" UID 规则仅在 Linux 下被支持,并且需要 `auto_route`。 限制被路由的用户。默认不限制。 #### include_uid_range 限制被路由的用户范围。 #### exclude_uid 排除路由的用户。 #### exclude_uid_range 排除路由的用户范围。 #### include_android_user !!! quote "" Android 用户和应用规则仅在 Android 下被支持,并且需要 `auto_route`。 限制被路由的 Android 用户。 | 常用用户 | ID | |------|----| | 您 | 0 | | 工作资料 | 10 | #### include_package 限制被路由的 Android 应用包名。 #### exclude_package 排除路由的 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 平台特定的设置,由客户端应用提供。 #### platform.http_proxy 系统 HTTP 代理设置。 ##### platform.http_proxy.enabled 启用系统 HTTP 代理。 ##### platform.http_proxy.server ==必填== 系统 HTTP 代理服务器地址。 ##### platform.http_proxy.server_port ==必填== 系统 HTTP 代理服务器端口。 ##### platform.http_proxy.bypass_domain !!! note "" 在 Apple 平台,`bypass_domain` 项匹配主机名 **后缀**. 绕过代理的主机名列表。 ##### platform.http_proxy.match_domain !!! quote "" 仅在 Apple 平台图形客户端中支持。 代理的主机名列表。 ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/)。 ================================================ FILE: docs/configuration/inbound/vless.md ================================================ ### Structure ```json { "type": "vless", "tag": "vless-in", ... // Listen Fields "users": [ { "name": "sekai", "uuid": "bf000d23-0752-40b4-affe-68f7707a9661", "flow": "" } ], "tls": {}, "multiplex": {}, "transport": {} } ``` ### Listen Fields See [Listen Fields](/configuration/shared/listen/) for details. ### Fields #### users ==Required== VLESS users. #### users.uuid ==Required== VLESS user id. #### users.flow VLESS Sub-protocol. Available values: * `xtls-rprx-vision` #### tls TLS configuration, see [TLS](/configuration/shared/tls/#inbound). #### multiplex See [Multiplex](/configuration/shared/multiplex#inbound) for details. #### transport V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport/). ================================================ FILE: docs/configuration/inbound/vless.zh.md ================================================ ### 结构 ```json { "type": "vless", "tag": "vless-in", ... // 监听字段 "users": [ { "name": "sekai", "uuid": "bf000d23-0752-40b4-affe-68f7707a9661", "flow": "" } ], "tls": {}, "multiplex": {}, "transport": {} } ``` ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/)。 ### 字段 #### users ==必填== VLESS 用户。 #### users.uuid ==必填== VLESS 用户 ID。 #### users.flow VLESS 子协议。 可用值: * `xtls-rprx-vision` #### tls TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 #### multiplex 参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。 #### transport V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport/)。 ================================================ FILE: docs/configuration/inbound/vmess.md ================================================ ### Structure ```json { "type": "vmess", "tag": "vmess-in", ... // Listen Fields "users": [ { "name": "sekai", "uuid": "bf000d23-0752-40b4-affe-68f7707a9661", "alterId": 0 } ], "tls": {}, "multiplex": {}, "transport": {} } ``` ### Listen Fields See [Listen Fields](/configuration/shared/listen/) for details. ### Fields #### users ==Required== VMess users. | Alter ID | Description | |----------|-------------------------| | 0 | Disable legacy protocol | | > 0 | Enable legacy protocol | !!! warning "" Legacy protocol support (VMess MD5 Authentication) is provided for compatibility purposes only, use of alterId > 1 is not recommended. #### tls TLS configuration, see [TLS](/configuration/shared/tls/#inbound). #### multiplex See [Multiplex](/configuration/shared/multiplex#inbound) for details. #### transport V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport/). ================================================ FILE: docs/configuration/inbound/vmess.zh.md ================================================ ### 结构 ```json { "type": "vmess", "tag": "vmess-in", ... // 监听字段 "users": [ { "name": "sekai", "uuid": "bf000d23-0752-40b4-affe-68f7707a9661", "alterId": 0 } ], "tls": {}, "multiplex": {}, "transport": {} } ``` ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/)。 ### 字段 #### users ==必填== VMess 用户。 | Alter ID | 描述 | |----------|-------| | 0 | 禁用旧协议 | | > 0 | 启用旧协议 | !!! warning "" 提供旧协议支持(VMess MD5 身份验证)仅出于兼容性目的,不建议使用 alterId > 1。 #### tls TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 #### multiplex 参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。 #### transport V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport/)。 ================================================ FILE: docs/configuration/index.md ================================================ # Introduction sing-box uses JSON for configuration files. ### Structure ```json { "log": {}, "dns": {}, "ntp": {}, "certificate": {}, "certificate_providers": [], "http_clients": [], "endpoints": [], "inbounds": [], "outbounds": [], "route": {}, "services": [], "experimental": {} } ``` ### Fields | Key | Format | |----------------|---------------------------------| | `log` | [Log](./log/) | | `dns` | [DNS](./dns/) | | `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/) | | `route` | [Route](./route/) | | `services` | [Service](./service/) | | `experimental` | [Experimental](./experimental/) | ### Check ```bash sing-box check ``` ### Format ```bash sing-box format -w -c config.json -D config_directory ``` ### Merge ```bash sing-box merge output.json -c config.json -D config_directory ``` ================================================ FILE: docs/configuration/index.zh.md ================================================ # 引言 sing-box 使用 JSON 作为配置文件格式。 ### 结构 ```json { "log": {}, "dns": {}, "ntp": {}, "certificate": {}, "certificate_providers": [], "http_clients": [], "endpoints": [], "inbounds": [], "outbounds": [], "route": {}, "services": [], "experimental": {} } ``` ### 字段 | Key | Format | |----------------|------------------------| | `log` | [日志](./log/) | | `dns` | [DNS](./dns/) | | `ntp` | [NTP](./ntp/) | | `certificate` | [证书](./certificate/) | | `certificate_providers` | [证书提供者](./shared/certificate-provider/) | | `http_clients` | [HTTP 客户端](./shared/http-client/) | | `endpoints` | [端点](./endpoint/) | | `inbounds` | [入站](./inbound/) | | `outbounds` | [出站](./outbound/) | | `route` | [路由](./route/) | | `services` | [服务](./service/) | | `experimental` | [实验性](./experimental/) | ### 检查 ```bash sing-box check ``` ### 格式化 ```bash sing-box format -w -c config.json -D config_directory ``` ### 合并 ```bash sing-box merge output.json -c config.json -D config_directory ``` ================================================ FILE: docs/configuration/log/index.md ================================================ # Log ### Structure ```json { "log": { "disabled": false, "level": "info", "output": "box.log", "timestamp": true } } ``` ### Fields #### disabled Disable logging, no output after start. #### level Log level. One of: `trace` `debug` `info` `warn` `error` `fatal` `panic`. #### output Output file path. Will not write log to console after enable. #### timestamp Add time to each line. ================================================ FILE: docs/configuration/log/index.zh.md ================================================ # 日志 ### 结构 ```json { "log": { "disabled": false, "level": "info", "output": "box.log", "timestamp": true } } ``` ### 字段 #### disabled 禁用日志,启动后不输出日志。 #### level 日志等级,可选值:`trace` `debug` `info` `warn` `error` `fatal` `panic`。 #### output 输出文件路径,启动后将不输出到控制台。 #### timestamp 添加时间到每行。 ================================================ FILE: docs/configuration/ntp/index.md ================================================ # NTP Built-in NTP client service. If enabled, it will provide time for protocols like TLS/Shadowsocks/VMess, which is useful for environments where time synchronization is not possible. ### Structure ```json { "ntp": { "enabled": false, "server": "time.apple.com", "server_port": 123, "interval": "30m", ... // Dial Fields } } ``` ### Fields #### enabled Enable NTP service. #### server ==Required== NTP server address. #### server_port NTP server port. 123 is used by default. #### interval Time synchronization interval. 30 minutes is used by default. ### Dial Fields See [Dial Fields](/configuration/shared/dial/) for details. ================================================ FILE: docs/configuration/ntp/index.zh.md ================================================ # NTP 内建的 NTP 客户端服务。 如果启用,它将为像 TLS/Shadowsocks/VMess 这样的协议提供时间,这对于无法进行时间同步的环境很有用。 ### 结构 ```json { "ntp": { "enabled": false, "server": "time.apple.com", "server_port": 123, "interval": "30m", ... // 拨号字段 } } ``` ### 字段 #### enabled 启用 NTP 服务。 #### server ==必填== NTP 服务器地址。 #### server_port NTP 服务器端口。 默认使用 123。 #### interval 时间同步间隔。 默认使用 30 分钟。 ### 拨号字段 参阅 [拨号字段](/zh/configuration/shared/dial/)。 ================================================ FILE: docs/configuration/outbound/anytls.md ================================================ --- icon: material/new-box --- !!! question "Since sing-box 1.12.0" ### Structure ```json { "type": "anytls", "tag": "anytls-out", "server": "127.0.0.1", "server_port": 1080, "password": "8JCsPssfgS8tiRwiMlhARg==", "idle_session_check_interval": "30s", "idle_session_timeout": "30s", "min_idle_session": 5, "tls": {}, ... // Dial Fields } ``` ### Fields #### server ==Required== The server address. #### server_port ==Required== The server port. #### password ==Required== The AnyTLS password. #### idle_session_check_interval Interval checking for idle sessions. Default: 30s. #### idle_session_timeout In the check, close sessions that have been idle for longer than this. Default: 30s. #### min_idle_session In the check, at least the first `n` idle sessions are kept open. Default value: `n`=0 #### tls ==Required== TLS configuration, see [TLS](/configuration/shared/tls/#outbound). ### Dial Fields See [Dial Fields](/configuration/shared/dial/) for details. ================================================ FILE: docs/configuration/outbound/anytls.zh.md ================================================ --- icon: material/new-box --- !!! question "自 sing-box 1.12.0 起" ### 结构 ```json { "type": "anytls", "tag": "anytls-out", "server": "127.0.0.1", "server_port": 1080, "password": "8JCsPssfgS8tiRwiMlhARg==", "idle_session_check_interval": "30s", "idle_session_timeout": "30s", "min_idle_session": 5, "tls": {}, ... // 拨号字段 } ``` ### 字段 #### server ==必填== 服务器地址。 #### server_port ==必填== 服务器端口。 #### password ==必填== AnyTLS 密码。 #### idle_session_check_interval 检查空闲会话的时间间隔。默认值:30秒。 #### idle_session_timeout 在检查中,关闭闲置时间超过此值的会话。默认值:30秒。 #### min_idle_session 在检查中,至少前 `n` 个空闲会话保持打开状态。默认值:`n`=0 #### tls ==必填== TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 ### 拨号字段 参阅 [拨号字段](/zh/configuration/shared/dial/)。 ================================================ FILE: docs/configuration/outbound/block.md ================================================ --- icon: material/delete-clock --- ### Structure ```json { "type": "block", "tag": "block" } ``` ### Fields No fields. ================================================ FILE: docs/configuration/outbound/block.zh.md ================================================ --- icon: material/delete-clock --- `block` 出站关闭所有传入请求。 ### 结构 ```json { "type": "block", "tag": "block" } ``` ### 字段 无字段。 ================================================ FILE: docs/configuration/outbound/direct.md ================================================ --- icon: material/alert-decagram --- !!! quote "Changes in sing-box 1.11.0" :material-delete-clock: [override_address](#override_address) :material-delete-clock: [override_port](#override_port) `direct` outbound send requests directly. ### Structure ```json { "type": "direct", "tag": "direct-out", "override_address": "1.0.0.1", "override_port": 53, ... // Dial Fields } ``` ### Fields #### override_address !!! failure "Deprecated in sing-box 1.11.0" Destination override fields are deprecated in sing-box 1.11.0 and will be removed in sing-box 1.13.0, see [Migration](/migration/#migrate-destination-override-fields-to-route-options). Override the connection destination address. #### override_port !!! failure "Deprecated in sing-box 1.11.0" Destination override fields are deprecated in sing-box 1.11.0 and will be removed in sing-box 1.13.0, see [Migration](/migration/#migrate-destination-override-fields-to-route-options). Override the connection destination port. Protocol value can be `1` or `2`. ### Dial Fields See [Dial Fields](/configuration/shared/dial/) for details. ================================================ FILE: docs/configuration/outbound/direct.zh.md ================================================ --- icon: material/alert-decagram --- !!! quote "sing-box 1.11.0 中的更改" :material-delete-clock: [override_address](#override_address) :material-delete-clock: [override_port](#override_port) `direct` 出站直接发送请求。 ### 结构 ```json { "type": "direct", "tag": "direct-out", "override_address": "1.0.0.1", "override_port": 53, ... // 拨号字段 } ``` ### 字段 #### override_address !!! failure "已在 sing-box 1.11.0 废弃" 目标覆盖字段在 sing-box 1.11.0 中已废弃,并将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-direct-出站中的目标地址覆盖字段到路由字段)。 覆盖连接目标地址。 #### override_port !!! failure "已在 sing-box 1.11.0 废弃" 目标覆盖字段在 sing-box 1.11.0 中已废弃,并将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-direct-出站中的目标地址覆盖字段到路由字段)。 覆盖连接目标端口。 ### 拨号字段 参阅 [拨号字段](/zh/configuration/shared/dial/)。 ================================================ FILE: docs/configuration/outbound/dns.md ================================================ --- icon: material/delete-clock --- !!! failure "Deprecated in sing-box 1.11.0" Legacy special outbounds are deprecated and will be removed in sing-box 1.13.0, check [Migration](/migration/#migrate-legacy-special-outbounds-to-rule-actions). `dns` outbound is a internal DNS server. ### Structure ```json { "type": "dns", "tag": "dns-out" } ``` !!! note "" There are no outbound connections by the DNS outbound, all requests are handled internally. ### Fields No fields. ================================================ FILE: docs/configuration/outbound/dns.zh.md ================================================ --- icon: material/delete-clock --- !!! failure "已在 sing-box 1.11.0 废弃" 旧的特殊出站已被弃用,且将在 sing-box 1.13.0 中被移除, 参阅 [迁移指南](/zh/migration/#迁移旧的特殊出站到规则动作). `dns` 出站是一个内部 DNS 服务器。 ### 结构 ```json { "type": "dns", "tag": "dns-out" } ``` !!! note "" DNS 出站没有出站连接,所有请求均在内部处理。 ### 字段 无字段。 ================================================ FILE: docs/configuration/outbound/http.md ================================================ `http` outbound is a HTTP CONNECT proxy client. ### Structure ```json { "type": "http", "tag": "http-out", "server": "127.0.0.1", "server_port": 1080, "username": "sekai", "password": "admin", "path": "", "headers": {}, "tls": {}, ... // Dial Fields } ``` ### Fields #### server ==Required== The server address. #### server_port ==Required== The server port. #### username Basic authorization username. #### password Basic authorization password. #### path Path of HTTP request. #### headers Extra headers of HTTP request. #### tls TLS configuration, see [TLS](/configuration/shared/tls/#outbound). ### Dial Fields See [Dial Fields](/configuration/shared/dial/) for details. ================================================ FILE: docs/configuration/outbound/http.zh.md ================================================ `http` 出站是一个 HTTP CONNECT 代理客户端 ### 结构 ```json { "type": "http", "tag": "http-out", "server": "127.0.0.1", "server_port": 1080, "username": "sekai", "password": "admin", "path": "", "headers": {}, "tls": {}, ... // 拨号字段 } ``` ### 字段 #### server ==必填== 服务器地址。 #### server_port ==必填== 服务器端口。 #### username Basic 认证用户名。 #### password Basic 认证密码。 #### path HTTP 请求路径。 #### headers HTTP 请求的额外标头。 #### tls TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 ### 拨号字段 参阅 [拨号字段](/zh/configuration/shared/dial/)。 ================================================ FILE: docs/configuration/outbound/hysteria.md ================================================ --- icon: material/new-box --- !!! quote "Changes in sing-box 1.12.0" :material-plus: [server_ports](#server_ports) :material-plus: [hop_interval](#hop_interval) ### Structure ```json { "type": "hysteria", "tag": "hysteria-out", "server": "127.0.0.1", "server_port": 1080, "server_ports": [ "2080:3000" ], "hop_interval": "", "up": "100 Mbps", "up_mbps": 100, "down": "100 Mbps", "down_mbps": 100, "obfs": "fuck me till the daylight", "auth": "", "auth_str": "password", "network": "", "tls": {}, ... // QUIC Fields ... // Dial Fields // Deprecated "recv_window_conn": 0, "recv_window": 0, "disable_mtu_discovery": false } ``` ### Fields #### server ==Required== The server address. #### server_port ==Required== The server port. #### server_ports !!! question "Since sing-box 1.12.0" Server port range list. Conflicts with `server_port`. #### hop_interval !!! question "Since sing-box 1.12.0" Port hopping interval. `30s` is used by default. #### up, down ==Required== Format: `[Integer] [Unit]` e.g. `100 Mbps, 640 KBps, 2 Gbps` Supported units (case sensitive, b = bits, B = bytes, 8b=1B): bps (bits per second) Bps (bytes per second) Kbps (kilobits per second) KBps (kilobytes per second) Mbps (megabits per second) MBps (megabytes per second) Gbps (gigabits per second) GBps (gigabytes per second) Tbps (terabits per second) TBps (terabytes per second) #### up_mbps, down_mbps ==Required== `up, down` in Mbps. #### obfs Obfuscated password. #### auth Authentication password, in base64. #### auth_str Authentication password. #### network Enabled network One of `tcp` `udp`. Both is enabled by default. #### tls ==Required== 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. ================================================ FILE: docs/configuration/outbound/hysteria.zh.md ================================================ --- icon: material/new-box --- !!! quote "sing-box 1.12.0 中的更改" :material-plus: [server_ports](#server_ports) :material-plus: [hop_interval](#hop_interval) ### 结构 ```json { "type": "hysteria", "tag": "hysteria-out", "server": "127.0.0.1", "server_port": 1080, "server_ports": [ "2080:3000" ], "hop_interval": "", "up": "100 Mbps", "up_mbps": 100, "down": "100 Mbps", "down_mbps": 100, "obfs": "fuck me till the daylight", "auth": "", "auth_str": "password", "network": "", "tls": {}, ... // QUIC 字段 ... // 拨号字段 // 废弃的 "recv_window_conn": 0, "recv_window": 0, "disable_mtu_discovery": false } ``` ### 字段 #### server ==必填== 服务器地址。 #### server_port ==必填== 服务器端口。 #### server_ports !!! question "自 sing-box 1.12.0 起" 服务器端口范围列表。 与 `server_port` 冲突。 #### hop_interval !!! question "自 sing-box 1.12.0 起" 端口跳跃间隔。 默认使用 `30s`。 #### up, down ==必填== 格式: `[Integer] [Unit]` 例如: `100 Mbps, 640 KBps, 2 Gbps` 支持的单位 (大小写敏感, b = bits, B = bytes, 8b=1B): bps (bits per second) Bps (bytes per second) Kbps (kilobits per second) KBps (kilobytes per second) Mbps (megabits per second) MBps (megabytes per second) Gbps (gigabits per second) GBps (gigabytes per second) Tbps (terabits per second) TBps (terabytes per second) #### up_mbps, down_mbps ==必填== 以 Mbps 为单位的 `up, down`。 #### obfs 混淆密码。 #### auth base64 编码的认证密码。 #### auth_str 认证密码。 #### network 启用的网络协议。 `tcp` 或 `udp`。 默认所有。 #### tls ==必填== 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` 代替。 ================================================ FILE: docs/configuration/outbound/hysteria2.md ================================================ !!! quote "Changes in sing-box 1.14.0" :material-plus: [hop_interval_max](#hop_interval_max) :material-plus: [bbr_profile](#bbr_profile) :material-plus: [realm](#realm) !!! quote "Changes in sing-box 1.11.0" :material-plus: [server_ports](#server_ports) :material-plus: [hop_interval](#hop_interval) ### Structure ```json { "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": { "type": "salamander", "password": "cry_me_a_r1ver" }, "password": "goofy_ahh_password", "network": "tcp", "tls": {}, ... // QUIC Fields "bbr_profile": "", "brutal_debug": false, "realm": { "server_url": "https://realm.example.com", "token": "", "realm_id": "", "stun_servers": [], "http_client": {} }, ... // Dial Fields } ``` !!! note "" You can ignore the JSON Array [] tag when the content is only one item !!! warning "Difference from official Hysteria2" The official Hysteria2 supports an authentication method called **userpass**, which essentially uses a combination of `:` as the actual password, while sing-box does not provide this alias. If you are planning to use sing-box with the official program, please note that you will need to fill the combination as the password. ### Fields #### server ==Required== The server address. Conflicts with `realm`. #### server_port ==Required== 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` and `realm`. #### hop_interval !!! question "Since sing-box 1.11.0" 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. If empty, the BBR congestion control algorithm will be used instead of Hysteria CC. #### obfs.type QUIC traffic obfuscator type, only available with `salamander`. Disabled if empty. #### obfs.password QUIC traffic obfuscator password. #### password Authentication password. #### network Enabled network One of `tcp` `udp`. Both is enabled by default. #### tls ==Required== 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" 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. #### 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. Domain names are resolved using [`domain_resolver`](/configuration/shared/dial/#domain_resolver) from Dial Fields. #### 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. ================================================ FILE: docs/configuration/outbound/hysteria2.zh.md ================================================ !!! quote "sing-box 1.14.0 中的更改" :material-plus: [hop_interval_max](#hop_interval_max) :material-plus: [bbr_profile](#bbr_profile) :material-plus: [realm](#realm) !!! quote "sing-box 1.11.0 中的更改" :material-plus: [server_ports](#server_ports) :material-plus: [hop_interval](#hop_interval) ### 结构 ```json { "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": { "type": "salamander", "password": "cry_me_a_r1ver" }, "password": "goofy_ahh_password", "network": "tcp", "tls": {}, ... // QUIC 字段 "bbr_profile": "", "brutal_debug": false, "realm": { "server_url": "https://realm.example.com", "token": "", "realm_id": "", "stun_servers": [], "http_client": {} }, ... // 拨号字段 } ``` !!! note "" 当内容只有一项时,可以忽略 JSON 数组 [] 标签 !!! warning "与官方 Hysteria2 的区别" 官方程序支持一种名为 **userpass** 的验证方式, 本质上是将用户名与密码的组合 `:` 作为实际上的密码,而 sing-box 不提供此别名。 要将 sing-box 与官方程序一起使用, 您需要填写该组合作为实际密码。 ### 字段 #### server ==必填== 服务器地址。 与 `realm` 冲突。 #### server_port ==必填== 服务器端口。 如果设置了 `server_ports`,则忽略此项。 与 `realm` 冲突。 #### server_ports !!! question "自 sing-box 1.11.0 起" 服务器端口范围列表。 与 `server_port` 和 `realm` 冲突。 #### hop_interval !!! question "自 sing-box 1.11.0 起" 端口跳跃间隔。 默认使用 `30s`。 #### hop_interval_max !!! question "自 sing-box 1.14.0 起" 最大端口跳跃间隔,用于随机化。 如果设置,实际跳跃间隔将在 `hop_interval` 和 `hop_interval_max` 之间随机选择。 #### up_mbps, down_mbps 最大带宽。 如果为空,将使用 BBR 拥塞控制算法而不是 Hysteria CC。 #### obfs.type QUIC 流量混淆器类型,仅可设为 `salamander`。 如果为空则禁用。 #### obfs.password QUIC 流量混淆器密码. #### password 认证密码。 #### network 启用的网络协议。 `tcp` 或 `udp`。 默认所有。 #### tls ==必填== TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 ### QUIC 字段 参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。 #### bbr_profile !!! question "自 sing-box 1.14.0 起" BBR 拥塞控制算法配置,可选 `conservative` `standard` `aggressive`。 默认使用 `standard`。 #### brutal_debug 启用 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`)。 域名通过 [拨号字段](/zh/configuration/shared/dial/) 中的 [`domain_resolver`](/zh/configuration/shared/dial/#domain_resolver) 解析。 #### realm.http_client 与 realm 通信使用的 HTTP 客户端。 参阅 [HTTP 客户端](/zh/configuration/shared/http-client/) 了解详情。 ### 拨号字段 参阅 [拨号字段](/zh/configuration/shared/dial/)。 ================================================ FILE: docs/configuration/outbound/index.md ================================================ # Outbound ### Structure ```json { "outbounds": [ { "type": "", "tag": "" } ] } ``` ### Fields | Type | Format | |----------------|--------------------------------| | `direct` | [Direct](./direct/) | | `block` | [Block](./block/) | | `socks` | [SOCKS](./socks/) | | `http` | [HTTP](./http/) | | `shadowsocks` | [Shadowsocks](./shadowsocks/) | | `vmess` | [VMess](./vmess/) | | `trojan` | [Trojan](./trojan/) | | `wireguard` | [Wireguard](./wireguard/) | | `hysteria` | [Hysteria](./hysteria/) | | `vless` | [VLESS](./vless/) | | `shadowtls` | [ShadowTLS](./shadowtls/) | | `tuic` | [TUIC](./tuic/) | | `hysteria2` | [Hysteria2](./hysteria2/) | | `anytls` | [AnyTLS](./anytls/) | | `tor` | [Tor](./tor/) | | `ssh` | [SSH](./ssh/) | | `dns` | [DNS](./dns/) | | `selector` | [Selector](./selector/) | | `urltest` | [URLTest](./urltest/) | | `naive` | [NaiveProxy](./naive/) | #### tag The tag of the outbound. ### Features #### Outbounds that support IP connection * `WireGuard` ================================================ FILE: docs/configuration/outbound/index.zh.md ================================================ # 出站 ### 结构 ```json { "outbounds": [ { "type": "", "tag": "" } ] } ``` ### 字段 | 类型 | 格式 | |----------------|--------------------------------| | `direct` | [Direct](./direct/) | | `block` | [Block](./block/) | | `socks` | [SOCKS](./socks/) | | `http` | [HTTP](./http/) | | `shadowsocks` | [Shadowsocks](./shadowsocks/) | | `vmess` | [VMess](./vmess/) | | `trojan` | [Trojan](./trojan/) | | `wireguard` | [Wireguard](./wireguard/) | | `hysteria` | [Hysteria](./hysteria/) | | `vless` | [VLESS](./vless/) | | `shadowtls` | [ShadowTLS](./shadowtls/) | | `tuic` | [TUIC](./tuic/) | | `hysteria2` | [Hysteria2](./hysteria2/) | | `anytls` | [AnyTLS](./anytls/) | | `tor` | [Tor](./tor/) | | `ssh` | [SSH](./ssh/) | | `dns` | [DNS](./dns/) | | `selector` | [Selector](./selector/) | | `urltest` | [URLTest](./urltest/) | | `naive` | [NaiveProxy](./naive/) | #### tag 出站的标签。 ### 特性 #### 支持 IP 连接的出站 * `WireGuard` ================================================ FILE: docs/configuration/outbound/naive.md ================================================ --- icon: material/new-box --- !!! question "Since sing-box 1.13.0" ### Structure ```json { "type": "naive", "tag": "naive-out", "server": "127.0.0.1", "server_port": 443, "username": "sekai", "password": "password", "insecure_concurrency": 0, "extra_headers": {}, "udp_over_tcp": false | {}, "quic": false, "quic_congestion_control": "", "tls": {}, ... // Dial Fields } ``` !!! warning "Platform Support" NaiveProxy outbound is only available on Apple platforms, Android, Windows and certain Linux builds. **Official Release Build Variants:** | Build Variant | Platforms | Description | |---------------|-----------|-------------| | (no suffix) | Linux amd64/arm64 | purego build, `libcronet.so` included | | `-glibc` | Linux 386/amd64/arm/arm64/mipsle/mips64le/riscv64/loong64 | CGO build, dynamically linked with glibc, requires glibc >= 2.31 (loong64: >= 2.36) | | `-musl` | Linux 386/amd64/arm/arm64/mipsle/riscv64/loong64 | CGO build, statically linked with musl | | (no suffix) | Windows amd64/arm64 | purego build, `libcronet.dll` included | For Linux, choose the glibc or musl variant based on your distribution's libc type. **Runtime Requirements:** - **Linux purego**: `libcronet.so` must be in the same directory as the sing-box binary or in system library path - **Windows**: `libcronet.dll` must be in the same directory as `sing-box.exe` or in a directory listed in `PATH` For self-built binaries, see [Build from source](/installation/build-from-source/#with_naive_outbound). ### Fields #### server ==Required== The server address. #### server_port ==Required== The server port. #### username Authentication username. #### password Authentication password. #### insecure_concurrency Number of concurrent tunnel connections. Multiple connections make the tunneling easier to detect through traffic analysis, which defeats the purpose of NaiveProxy's design to resist traffic analysis. #### extra_headers Extra headers to send in HTTP requests. #### udp_over_tcp UDP over TCP protocol settings. See [UDP Over TCP](/configuration/shared/udp-over-tcp/) for details. #### quic Use QUIC instead of HTTP/2. #### quic_congestion_control QUIC congestion control algorithm. | Algorithm | Description | |-----------|-------------| | `bbr` | BBR | | `bbr2` | BBRv2 | | `cubic` | CUBIC | | `reno` | New Reno | `bbr` is used by default (the default of QUICHE, used by Chromium which NaiveProxy is based on). #### tls ==Required== TLS configuration, see [TLS](/configuration/shared/tls/#outbound). Only `server_name`, `certificate`, `certificate_path` and `ech` are supported. Self-signed certificates change traffic behavior significantly, which defeats the purpose of NaiveProxy's design to resist traffic analysis, and should not be used in production. ### Dial Fields See [Dial Fields](/configuration/shared/dial/) for details. ================================================ FILE: docs/configuration/outbound/naive.zh.md ================================================ --- icon: material/new-box --- !!! question "自 sing-box 1.13.0 起" ### 结构 ```json { "type": "naive", "tag": "naive-out", "server": "127.0.0.1", "server_port": 443, "username": "sekai", "password": "password", "insecure_concurrency": 0, "extra_headers": {}, "udp_over_tcp": false | {}, "quic": false, "quic_congestion_control": "", "tls": {}, ... // 拨号字段 } ``` !!! warning "平台支持" NaiveProxy 出站仅在 Apple 平台、Android、Windows 和特定 Linux 构建上可用。 **官方发布版本区别:** | 构建变体 | 平台 | 说明 | |---|---|---| | (无后缀) | Linux amd64/arm64 | purego 构建,包含 `libcronet.so` | | `-glibc` | Linux 386/amd64/arm/arm64/mipsle/mips64le/riscv64/loong64 | CGO 构建,动态链接 glibc,要求 glibc >= 2.31(loong64: >= 2.36) | | `-musl` | Linux 386/amd64/arm/arm64/mipsle/riscv64/loong64 | CGO 构建,静态链接 musl | | (无后缀) | Windows amd64/arm64 | purego 构建,包含 `libcronet.dll` | 对于 Linux,请根据发行版的 libc 类型选择 glibc 或 musl 变体。 **运行时要求:** - **Linux purego**:`libcronet.so` 必须位于 sing-box 二进制文件相同目录或系统库路径中 - **Windows**:`libcronet.dll` 必须位于 `sing-box.exe` 相同目录或 `PATH` 中的任意目录 自行构建请参阅 [从源代码构建](/zh/installation/build-from-source/#with_naive_outbound)。 ### 字段 #### server ==必填== 服务器地址。 #### server_port ==必填== 服务器端口。 #### username 认证用户名。 #### password 认证密码。 #### insecure_concurrency 并发隧道连接数。多连接使隧道更容易被流量分析检测,违背 NaiveProxy 抵抗流量分析的设计目的。 #### extra_headers HTTP 请求中发送的额外头部。 #### udp_over_tcp UDP over TCP 配置。 参阅 [UDP Over TCP](/zh/configuration/shared/udp-over-tcp/)。 #### quic 使用 QUIC 代替 HTTP/2。 #### quic_congestion_control QUIC 拥塞控制算法。 | 算法 | 描述 | |------|------| | `bbr` | BBR | | `bbr2` | BBRv2 | | `cubic` | CUBIC | | `reno` | New Reno | 默认使用 `bbr`(NaiveProxy 基于的 Chromium 使用的 QUICHE 的默认值)。 #### tls ==必填== TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 只有 `server_name`、`certificate`、`certificate_path` 和 `ech` 是被支持的。 自签名证书会显著改变流量行为,违背了 NaiveProxy 旨在抵抗流量分析的设计初衷,不应该在生产环境中使用。 ### 拨号字段 参阅 [拨号字段](/zh/configuration/shared/dial/)。 ================================================ FILE: docs/configuration/outbound/selector.md ================================================ ### Structure ```json { "type": "selector", "tag": "select", "outbounds": [ "proxy-a", "proxy-b", "proxy-c" ], "default": "proxy-c", "interrupt_exist_connections": false } ``` !!! quote "" The selector can only be controlled through the [Clash API](/configuration/experimental#clash-api-fields) currently. ### Fields #### outbounds ==Required== List of outbound tags to select. #### default The default outbound tag. The first outbound will be used if empty. #### interrupt_exist_connections Interrupt existing connections when the selected outbound has changed. Only inbound connections are affected by this setting, internal connections will always be interrupted. ================================================ FILE: docs/configuration/outbound/selector.zh.md ================================================ ### 结构 ```json { "type": "selector", "tag": "select", "outbounds": [ "proxy-a", "proxy-b", "proxy-c" ], "default": "proxy-c", "interrupt_exist_connections": false } ``` !!! quote "" 选择器目前只能通过 [Clash API](/zh/configuration/experimental/clash-api/) 来控制。 ### 字段 #### outbounds ==必填== 用于选择的出站标签列表。 #### default 默认的出站标签。默认使用第一个出站。 #### interrupt_exist_connections 当选定的出站发生更改时,中断现有连接。 仅入站连接受此设置影响,内部连接将始终被中断。 ================================================ FILE: docs/configuration/outbound/shadowsocks.md ================================================ ### Structure ```json { "type": "shadowsocks", "tag": "ss-out", "server": "127.0.0.1", "server_port": 1080, "method": "2022-blake3-aes-128-gcm", "password": "8JCsPssfgS8tiRwiMlhARg==", "plugin": "", "plugin_opts": "", "network": "udp", "udp_over_tcp": false | {}, "multiplex": {}, ... // Dial Fields } ``` ### Fields #### server ==Required== The server address. #### server_port ==Required== The server port. #### method ==Required== Encryption methods: * `2022-blake3-aes-128-gcm` * `2022-blake3-aes-256-gcm` * `2022-blake3-chacha20-poly1305` * `none` * `aes-128-gcm` * `aes-192-gcm` * `aes-256-gcm` * `chacha20-ietf-poly1305` * `xchacha20-ietf-poly1305` Legacy encryption methods: * `aes-128-ctr` * `aes-192-ctr` * `aes-256-ctr` * `aes-128-cfb` * `aes-192-cfb` * `aes-256-cfb` * `rc4-md5` * `chacha20-ietf` * `xchacha20` #### password ==Required== The shadowsocks password. #### plugin Shadowsocks SIP003 plugin, implemented in internal. Only `obfs-local` and `v2ray-plugin` are supported. #### plugin_opts Shadowsocks SIP003 plugin options. #### network Enabled network One of `tcp` `udp`. Both is enabled by default. #### udp_over_tcp UDP over TCP configuration. See [UDP Over TCP](/configuration/shared/udp-over-tcp/) for details. Conflict with `multiplex`. #### multiplex See [Multiplex](/configuration/shared/multiplex#outbound) for details. ### Dial Fields See [Dial Fields](/configuration/shared/dial/) for details. ================================================ FILE: docs/configuration/outbound/shadowsocks.zh.md ================================================ ### 结构 ```json { "type": "shadowsocks", "tag": "ss-out", "server": "127.0.0.1", "server_port": 1080, "method": "2022-blake3-aes-128-gcm", "password": "8JCsPssfgS8tiRwiMlhARg==", "plugin": "", "plugin_opts": "", "network": "udp", "udp_over_tcp": false | {}, "multiplex": {}, ... // 拨号字段 } ``` ### 字段 #### server ==必填== 服务器地址。 #### server_port ==必填== 服务器端口。 #### method ==必填== 加密方法: * `2022-blake3-aes-128-gcm` * `2022-blake3-aes-256-gcm` * `2022-blake3-chacha20-poly1305` * `none` * `aes-128-gcm` * `aes-192-gcm` * `aes-256-gcm` * `chacha20-ietf-poly1305` * `xchacha20-ietf-poly1305` 旧加密方法: * `aes-128-ctr` * `aes-192-ctr` * `aes-256-ctr` * `aes-128-cfb` * `aes-192-cfb` * `aes-256-cfb` * `rc4-md5` * `chacha20-ietf` * `xchacha20` #### password ==必填== Shadowsocks 密码。 #### plugin Shadowsocks SIP003 插件,由内部实现。 仅支持 `obfs-local` 和 `v2ray-plugin`。 #### plugin_opts Shadowsocks SIP003 插件参数。 #### network 启用的网络协议 `tcp` 或 `udp`。 默认所有。 #### udp_over_tcp UDP over TCP 配置。 参阅 [UDP Over TCP](/zh/configuration/shared/udp-over-tcp/)。 与 `multiplex` 冲突。 #### multiplex 参阅 [多路复用](/zh/configuration/shared/multiplex#出站)。 ### 拨号字段 参阅 [拨号字段](/zh/configuration/shared/dial/)。 ================================================ FILE: docs/configuration/outbound/shadowtls.md ================================================ ### Structure ```json { "type": "shadowtls", "tag": "st-out", "server": "127.0.0.1", "server_port": 1080, "version": 3, "password": "fuck me till the daylight", "tls": {}, ... // Dial Fields } ``` ### Fields #### server ==Required== The server address. #### server_port ==Required== The server port. #### version ShadowTLS protocol version. | Value | Protocol Version | |---------------|-----------------------------------------------------------------------------------------| | `1` (default) | [ShadowTLS v1](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-en.md#v1) | | `2` | [ShadowTLS v2](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-en.md#v2) | | `3` | [ShadowTLS v3](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-v3-en.md) | #### password Set password. Only available in the ShadowTLS v2/v3 protocol. #### tls ==Required== TLS configuration, see [TLS](/configuration/shared/tls/#outbound). ### Dial Fields See [Dial Fields](/configuration/shared/dial/) for details. ================================================ FILE: docs/configuration/outbound/shadowtls.zh.md ================================================ ### 结构 ```json { "type": "shadowtls", "tag": "st-out", "server": "127.0.0.1", "server_port": 1080, "version": 3, "password": "fuck me till the daylight", "tls": {}, ... // 拨号字段 } ``` ### 字段 #### server ==必填== 服务器地址。 #### server_port ==必填== 服务器端口。 #### version ShadowTLS 协议版本。 | 值 | 协议版本 | |---------------|-----------------------------------------------------------------------------------------| | `1` (default) | [ShadowTLS v1](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-en.md#v1) | | `2` | [ShadowTLS v2](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-en.md#v2) | | `3` | [ShadowTLS v3](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-v3-en.md) | #### password 设置密码。 仅在 ShadowTLS v2/v3 协议中可用。 #### tls ==必填== TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 ### 拨号字段 参阅 [拨号字段](/zh/configuration/shared/dial/)。 ================================================ FILE: docs/configuration/outbound/socks.md ================================================ `socks` outbound is a socks4/socks4a/socks5 client. ### Structure ```json { "type": "socks", "tag": "socks-out", "server": "127.0.0.1", "server_port": 1080, "version": "5", "username": "sekai", "password": "admin", "network": "udp", "udp_over_tcp": false | {}, ... // Dial Fields } ``` ### Fields #### server ==Required== The server address. #### server_port ==Required== The server port. #### version The SOCKS version, one of `4` `4a` `5`. SOCKS5 used by default. #### username SOCKS username. #### password SOCKS5 password. #### network Enabled network One of `tcp` `udp`. Both is enabled by default. #### udp_over_tcp UDP over TCP protocol settings. See [UDP Over TCP](/configuration/shared/udp-over-tcp/) for details. ### Dial Fields See [Dial Fields](/configuration/shared/dial/) for details. ================================================ FILE: docs/configuration/outbound/socks.zh.md ================================================ `socks` 出站是 socks4/socks4a/socks5 客户端 ### 结构 ```json { "type": "socks", "tag": "socks-out", "server": "127.0.0.1", "server_port": 1080, "version": "5", "username": "sekai", "password": "admin", "network": "udp", "udp_over_tcp": false | {}, ... // 拨号字段 } ``` ### 字段 #### server ==必填== 服务器地址。 #### server_port ==必填== 服务器端口。 #### version SOCKS 版本, 可为 `4` `4a` `5`. 默认使用 SOCKS5。 #### username SOCKS 用户名。 #### password SOCKS5 密码。 #### network 启用的网络协议 `tcp` 或 `udp`。 默认所有。 #### udp_over_tcp UDP over TCP 配置。 参阅 [UDP Over TCP](/zh/configuration/shared/udp-over-tcp/)。 ### 拨号字段 参阅 [拨号字段](/zh/configuration/shared/dial/)。 ================================================ FILE: docs/configuration/outbound/ssh.md ================================================ !!! quote "Changes in sing-box 1.14.0" :material-plus: [cipher](#cipher) :material-plus: [mac](#mac) :material-plus: [kex_algorithm](#kex_algorithm) ### Structure ```json { "type": "ssh", "tag": "ssh-out", "server": "127.0.0.1", "server_port": 22, "user": "root", "password": "admin", "private_key": "", "private_key_path": "$HOME/.ssh/id_rsa", "private_key_passphrase": "", "host_key": [ "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdH..." ], "host_key_algorithms": [], "client_version": "SSH-2.0-OpenSSH_7.4p1", "cipher": [], "mac": [], "kex_algorithm": [], ... // Dial Fields } ``` ### Fields #### server ==Required== Server address. #### server_port Server port. 22 will be used if empty. #### user SSH user, root will be used if empty. #### password Password. #### private_key Private key. #### private_key_path Private key path. #### private_key_passphrase Private key passphrase. #### host_key Host key. Accept any if empty. #### host_key_algorithms Host key algorithms. #### client_version 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. ================================================ FILE: docs/configuration/outbound/ssh.zh.md ================================================ !!! quote "sing-box 1.14.0 中的更改" :material-plus: [cipher](#cipher) :material-plus: [mac](#mac) :material-plus: [kex_algorithm](#kex_algorithm) ### 结构 ```json { "type": "ssh", "tag": "ssh-out", "server": "127.0.0.1", "server_port": 22, "user": "root", "password": "admin", "private_key": "", "private_key_path": "$HOME/.ssh/id_rsa", "private_key_passphrase": "", "host_key": [ "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdH..." ], "host_key_algorithms": [], "client_version": "SSH-2.0-OpenSSH_7.4p1", "cipher": [], "mac": [], "kex_algorithm": [], ... // 拨号字段 } ``` ### 字段 #### server ==必填== 服务器地址。 #### server_port 服务器端口,默认使用 22。 #### user SSH 用户, 默认使用 root。 #### password 密码。 #### private_key 密钥。 #### private_key_path 密钥路径。 #### private_key_passphrase 密钥密码。 #### host_key 主机密钥,留空接受所有。 #### host_key_algorithms 主机密钥算法。 #### client_version 客户端版本,默认使用随机值。 #### 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/)。 ================================================ FILE: docs/configuration/outbound/tor.md ================================================ ### Structure ```json { "type": "tor", "tag": "tor-out", "executable_path": "/usr/bin/tor", "extra_args": [], "data_directory": "$HOME/.cache/tor", "torrc": { "ClientOnly": 1 }, ... // Dial Fields } ``` !!! info "" Embedded Tor is not included by default, see [Installation](/installation/build-from-source/#build-tags). ### Fields #### executable_path The path to the Tor executable. Embedded Tor will be ignored if set. #### extra_args List of extra arguments passed to the Tor instance when started. #### data_directory ==Recommended== The data directory of Tor. Each start will be very slow if not specified. #### torrc Map of torrc options. See [tor(1)](https://linux.die.net/man/1/tor) for details. ### Dial Fields See [Dial Fields](/configuration/shared/dial/) for details. ================================================ FILE: docs/configuration/outbound/tor.zh.md ================================================ ### 结构 ```json { "type": "tor", "tag": "tor-out", "executable_path": "/usr/bin/tor", "extra_args": [], "data_directory": "$HOME/.cache/tor", "torrc": { "ClientOnly": 1 }, ... // 拨号字段 } ``` !!! info "" 默认安装不包含嵌入式 Tor, 参阅 [安装](/zh/installation/build-from-source/#构建标记)。 ### 字段 #### executable_path Tor 可执行文件路径 如果设置,将覆盖嵌入式 Tor。 #### extra_args 启动 Tor 时传递的附加参数列表。 #### data_directory ==推荐== Tor 的数据目录。 如未设置,每次启动都需要长时间。 #### torrc torrc 参数表。 参阅 [tor(1)](https://linux.die.net/man/1/tor)。 ### 拨号字段 参阅 [拨号字段](/zh/configuration/shared/dial/)。 ================================================ FILE: docs/configuration/outbound/trojan.md ================================================ ### Structure ```json { "type": "trojan", "tag": "trojan-out", "server": "127.0.0.1", "server_port": 1080, "password": "8JCsPssfgS8tiRwiMlhARg==", "network": "tcp", "tls": {}, "multiplex": {}, "transport": {}, ... // Dial Fields } ``` ### Fields #### server ==Required== The server address. #### server_port ==Required== The server port. #### password ==Required== The Trojan password. #### network Enabled network One of `tcp` `udp`. Both is enabled by default. #### tls TLS configuration, see [TLS](/configuration/shared/tls/#outbound). #### multiplex See [Multiplex](/configuration/shared/multiplex#outbound) for details. #### transport V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport/). ### Dial Fields See [Dial Fields](/configuration/shared/dial/) for details. ================================================ FILE: docs/configuration/outbound/trojan.zh.md ================================================ ### 结构 ```json { "type": "trojan", "tag": "trojan-out", "server": "127.0.0.1", "server_port": 1080, "password": "8JCsPssfgS8tiRwiMlhARg==", "network": "tcp", "tls": {}, "multiplex": {}, "transport": {}, ... // 拨号字段 } ``` ### 字段 #### server ==必填== 服务器地址。 #### server_port ==必填== 服务器端口。 #### password ==必填== Trojan 密码。 #### network 启用的网络协议。 `tcp` 或 `udp`。 默认所有。 #### tls TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 #### multiplex 参阅 [多路复用](/zh/configuration/shared/multiplex#出站)。 #### transport V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport/)。 ### 拨号字段 参阅 [拨号字段](/zh/configuration/shared/dial/)。 ================================================ FILE: docs/configuration/outbound/tuic.md ================================================ ### Structure ```json { "type": "tuic", "tag": "tuic-out", "server": "127.0.0.1", "server_port": 1080, "uuid": "2DD61D93-75D8-4DA4-AC0E-6AECE7EAC365", "password": "hello", "congestion_control": "cubic", "udp_relay_mode": "native", "udp_over_stream": false, "zero_rtt_handshake": false, "heartbeat": "10s", "network": "tcp", "tls": {}, ... // QUIC Fields ... // Dial Fields } ``` ### Fields #### server ==Required== The server address. #### server_port ==Required== The server port. #### uuid ==Required== TUIC user uuid #### password TUIC user password #### congestion_control QUIC congestion control algorithm One of: `cubic`, `new_reno`, `bbr` `cubic` is used by default. #### udp_relay_mode UDP packet relay mode | Mode | Description | |:-------|:-------------------------------------------------------------------------| | native | native UDP characteristics | | quic | lossless UDP relay using QUIC streams, additional overhead is introduced | `native` is used by default. Conflict with `udp_over_stream`. #### udp_over_stream This is the TUIC port of the [UDP over TCP protocol](/configuration/shared/udp-over-tcp/), designed to provide a QUIC stream based UDP relay mode that TUIC does not provide. Since it is an add-on protocol, you will need to use sing-box or another program compatible with the protocol as a server. This mode has no positive effect in a proper UDP proxy scenario and should only be applied to relay streaming UDP traffic (basically QUIC streams). Conflict with `udp_relay_mode`. #### network Enabled network One of `tcp` `udp`. Both is enabled by default. #### tls ==Required== 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. ================================================ FILE: docs/configuration/outbound/tuic.zh.md ================================================ ### 结构 ```json { "type": "tuic", "tag": "tuic-out", "server": "127.0.0.1", "server_port": 1080, "uuid": "2DD61D93-75D8-4DA4-AC0E-6AECE7EAC365", "password": "hello", "congestion_control": "cubic", "udp_relay_mode": "native", "udp_over_stream": false, "zero_rtt_handshake": false, "heartbeat": "10s", "network": "tcp", "tls": {}, ... // QUIC 字段 ... // 拨号字段 } ``` ### 字段 #### server ==必填== 服务器地址。 #### server_port ==必填== 服务器端口。 #### uuid ==必填== TUIC 用户 UUID #### password TUIC 用户密码 #### congestion_control QUIC 拥塞控制算法 可选值: `cubic`, `new_reno`, `bbr` 默认使用 `cubic`。 #### udp_relay_mode UDP 包中继模式 | 模式 | 描述 | |--------|------------------------------| | native | 原生 UDP | | quic | 使用 QUIC 流的无损 UDP 中继,引入了额外的开销 | 与 `udp_over_stream` 冲突。 #### udp_over_stream 这是 TUIC 的 [UDP over TCP 协议](/zh/configuration/shared/udp-over-tcp/) 移植, 旨在提供 TUIC 不提供的 基于 QUIC 流的 UDP 中继模式。 由于它是一个附加协议,因此您需要使用 sing-box 或其他兼容的程序作为服务器。 此模式在正确的 UDP 代理场景中没有任何积极作用,仅适用于中继流式 UDP 流量(基本上是 QUIC 流)。 与 `udp_relay_mode` 冲突。 #### zero_rtt_handshake 在客户端启用 0-RTT QUIC 连接握手 这对性能影响不大,因为协议是完全复用的 !!! warning "" 强烈建议禁用此功能,因为它容易受到重放攻击。 请参阅 [Attack of the clones](https://blog.cloudflare.com/even-faster-connection-establishment-with-quic-0-rtt-resumption/#attack-of-the-clones) #### heartbeat 发送心跳包以保持连接存活的时间间隔 #### network 启用的网络协议。 `tcp` 或 `udp`。 默认所有。 #### tls ==必填== TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 ### QUIC 字段 参阅 [QUIC 字段](/zh/configuration/shared/quic/) 了解详情。 ### 拨号字段 参阅 [拨号字段](/zh/configuration/shared/dial/)。 ================================================ FILE: docs/configuration/outbound/urltest.md ================================================ ### Structure ```json { "type": "urltest", "tag": "auto", "outbounds": [ "proxy-a", "proxy-b", "proxy-c" ], "url": "", "interval": "", "tolerance": 0, "idle_timeout": "", "interrupt_exist_connections": false } ``` ### Fields #### outbounds ==Required== List of outbound tags to test. #### url The URL to test. `https://www.gstatic.com/generate_204` will be used if empty. #### interval The test interval. `3m` will be used if empty. #### tolerance The test tolerance in milliseconds. `50` will be used if empty. #### idle_timeout The idle timeout. `30m` will be used if empty. #### interrupt_exist_connections Interrupt existing connections when the selected outbound has changed. Only inbound connections are affected by this setting, internal connections will always be interrupted. ================================================ FILE: docs/configuration/outbound/urltest.zh.md ================================================ ### 结构 ```json { "type": "urltest", "tag": "auto", "outbounds": [ "proxy-a", "proxy-b", "proxy-c" ], "url": "", "interval": "", "tolerance": 50, "idle_timeout": "", "interrupt_exist_connections": false } ``` ### 字段 #### outbounds ==必填== 用于测试的出站标签列表。 #### url 用于测试的链接。默认使用 `https://www.gstatic.com/generate_204`。 #### interval 测试间隔。 默认使用 `3m`。 #### tolerance 以毫秒为单位的测试容差。 默认使用 `50`。 #### idle_timeout 空闲超时。默认使用 `30m`。 #### interrupt_exist_connections 当选定的出站发生更改时,中断现有连接。 仅入站连接受此设置影响,内部连接将始终被中断。 ================================================ FILE: docs/configuration/outbound/vless.md ================================================ ### Structure ```json { "type": "vless", "tag": "vless-out", "server": "127.0.0.1", "server_port": 1080, "uuid": "bf000d23-0752-40b4-affe-68f7707a9661", "flow": "xtls-rprx-vision", "network": "tcp", "tls": {}, "packet_encoding": "", "multiplex": {}, "transport": {}, ... // Dial Fields } ``` ### Fields #### server ==Required== The server address. #### server_port ==Required== The server port. #### uuid ==Required== VLESS user id. #### flow VLESS Sub-protocol. Available values: * `xtls-rprx-vision` #### network Enabled network One of `tcp` `udp`. Both is enabled by default. #### tls TLS configuration, see [TLS](/configuration/shared/tls/#outbound). #### packet_encoding UDP packet encoding, xudp is used by default. | Encoding | Description | |------------|-----------------------| | (none) | Disabled | | packetaddr | Supported by v2ray 5+ | | xudp | Supported by xray | #### multiplex See [Multiplex](/configuration/shared/multiplex#outbound) for details. #### transport V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport/). ### Dial Fields See [Dial Fields](/configuration/shared/dial/) for details. ================================================ FILE: docs/configuration/outbound/vless.zh.md ================================================ ### 结构 ```json { "type": "vless", "tag": "vless-out", "server": "127.0.0.1", "server_port": 1080, "uuid": "bf000d23-0752-40b4-affe-68f7707a9661", "flow": "xtls-rprx-vision", "network": "tcp", "tls": {}, "packet_encoding": "", "multiplex": {}, "transport": {}, ... // 拨号字段 } ``` ### 字段 #### server ==必填== 服务器地址。 #### server_port ==必填== 服务器端口。 #### uuid ==必填== VLESS 用户 ID。 #### flow VLESS 子协议。 可用值: * `xtls-rprx-vision` #### network 启用的网络协议。 `tcp` 或 `udp`。 默认所有。 #### tls TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 #### packet_encoding UDP 包编码,默认使用 xudp。 | 编码 | 描述 | |------------|---------------| | (空) | 禁用 | | packetaddr | 由 v2ray 5+ 支持 | | xudp | 由 xray 支持 | #### multiplex 参阅 [多路复用](/zh/configuration/shared/multiplex#出站)。 #### transport V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport/)。 ### 拨号字段 参阅 [拨号字段](/zh/configuration/shared/dial/)。 ================================================ FILE: docs/configuration/outbound/vmess.md ================================================ ### Structure ```json { "type": "vmess", "tag": "vmess-out", "server": "127.0.0.1", "server_port": 1080, "uuid": "bf000d23-0752-40b4-affe-68f7707a9661", "security": "auto", "alter_id": 0, "global_padding": false, "authenticated_length": true, "network": "tcp", "tls": {}, "packet_encoding": "", "transport": {}, "multiplex": {}, ... // Dial Fields } ``` ### Fields #### server ==Required== The server address. #### server_port ==Required== The server port. #### uuid ==Required== The VMess user id. #### security Encryption methods: * `auto` * `none` * `zero` * `aes-128-gcm` * `chacha20-poly1305` Legacy encryption methods: * `aes-128-ctr` #### alter_id | Alter ID | Description | |----------|---------------------| | 0 | Use AEAD protocol | | 1 | Use legacy protocol | | > 1 | Unused, same as 1 | #### global_padding Protocol parameter. Will waste traffic randomly if enabled (enabled by default in v2ray and cannot be disabled). #### authenticated_length Protocol parameter. Enable length block encryption. #### network Enabled network One of `tcp` `udp`. Both is enabled by default. #### tls TLS configuration, see [TLS](/configuration/shared/tls/#outbound). #### packet_encoding UDP packet encoding. | Encoding | Description | |------------|-----------------------| | (none) | Disabled | | packetaddr | Supported by v2ray 5+ | | xudp | Supported by xray | #### multiplex See [Multiplex](/configuration/shared/multiplex#outbound) for details. #### transport V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport/). ### Dial Fields See [Dial Fields](/configuration/shared/dial/) for details. ================================================ FILE: docs/configuration/outbound/vmess.zh.md ================================================ ### 结构 ```json { "type": "vmess", "tag": "vmess-out", "server": "127.0.0.1", "server_port": 1080, "uuid": "bf000d23-0752-40b4-affe-68f7707a9661", "security": "auto", "alter_id": 0, "global_padding": false, "authenticated_length": true, "network": "tcp", "tls": {}, "packet_encoding": "", "multiplex": {}, "transport": {}, ... // 拨号字段 } ``` ### 字段 #### server ==必填== 服务器地址。 #### server_port ==必填== 服务器端口。 #### uuid ==必填== VMess 用户 ID。 #### security 加密方法: * `auto` * `none` * `zero` * `aes-128-gcm` * `chacha20-poly1305` 旧加密方法: * `aes-128-ctr` #### alter_id | Alter ID | 描述 | |----------|------------| | 0 | 禁用旧协议 | | 1 | 启用旧协议 | | > 1 | 未使用, 行为同 1 | #### global_padding 协议参数。 如果启用会随机浪费流量(在 v2ray 中默认启用并且无法禁用)。 #### authenticated_length 协议参数。启用长度块加密。 #### network 启用的网络协议。 `tcp` 或 `udp`。 默认所有。 #### tls TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 #### packet_encoding UDP 包编码。 | 编码 | 描述 | |------------|---------------| | (空) | 禁用 | | packetaddr | 由 v2ray 5+ 支持 | | xudp | 由 xray 支持 | #### multiplex 参阅 [多路复用](/zh/configuration/shared/multiplex#出站)。 #### transport V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport/)。 ### 拨号字段 参阅 [拨号字段](/zh/configuration/shared/dial/)。 ================================================ FILE: docs/configuration/outbound/wireguard.md ================================================ --- icon: material/delete-clock --- !!! failure "Deprecated in sing-box 1.11.0" WireGuard outbound is deprecated and will be removed in sing-box 1.13.0, check [Migration](/migration/#migrate-wireguard-outbound-to-endpoint). !!! quote "Changes in sing-box 1.11.0" :material-delete-alert: [gso](#gso) !!! quote "Changes in sing-box 1.8.0" :material-plus: [gso](#gso) ### Structure ```json { "type": "wireguard", "tag": "wireguard-out", "server": "127.0.0.1", "server_port": 1080, "system_interface": false, "interface_name": "wg0", "local_address": [ "10.0.0.1/32" ], "private_key": "YNXtAzepDqRv9H52osJVDQnznT5AM11eCK3ESpwSt04=", "peers": [ { "server": "127.0.0.1", "server_port": 1080, "public_key": "Z1XXLsKYkYxuiYjJIkRvtIKFepCYHTgON+GwPq7SOV4=", "pre_shared_key": "31aIhAPwktDGpH4JDhA8GNvjFXEf/a6+UaQRyOAiyfM=", "allowed_ips": [ "0.0.0.0/0" ], "reserved": [0, 0, 0] } ], "peer_public_key": "Z1XXLsKYkYxuiYjJIkRvtIKFepCYHTgON+GwPq7SOV4=", "pre_shared_key": "31aIhAPwktDGpH4JDhA8GNvjFXEf/a6+UaQRyOAiyfM=", "reserved": [0, 0, 0], "workers": 4, "mtu": 1408, "network": "tcp", // Deprecated "gso": false, ... // Dial Fields } ``` ### Fields #### server ==Required if multi-peer disabled== The server address. #### server_port ==Required if multi-peer disabled== The server port. #### system_interface Use system interface. Requires privilege and cannot conflict with exists system interfaces. Forced if gVisor not included in the build. #### interface_name Custom interface name for system interface. #### gso !!! failure "Deprecated in sing-box 1.11.0" GSO will be automatically enabled when available since sing-box 1.11.0. !!! question "Since sing-box 1.8.0" !!! quote "" Only supported on Linux. Try to enable generic segmentation offload. #### local_address ==Required== List of IP (v4 or v6) address prefixes to be assigned to the interface. #### private_key ==Required== WireGuard requires base64-encoded public and private keys. These can be generated using the wg(8) utility: ```shell wg genkey echo "private key" || wg pubkey ``` #### peers Multi-peer support. If enabled, `server, server_port, peer_public_key, pre_shared_key` will be ignored. #### peers.allowed_ips WireGuard allowed IPs. #### peers.reserved WireGuard reserved field bytes. `$outbound.reserved` will be used if empty. #### peer_public_key ==Required if multi-peer disabled== WireGuard peer public key. #### pre_shared_key WireGuard pre-shared key. #### reserved WireGuard reserved field bytes. #### workers WireGuard worker count. CPU count is used by default. #### mtu WireGuard MTU. 1408 will be used if empty. #### network Enabled network One of `tcp` `udp`. Both is enabled by default. ### Dial Fields See [Dial Fields](/configuration/shared/dial/) for details. ================================================ FILE: docs/configuration/outbound/wireguard.zh.md ================================================ --- icon: material/delete-clock --- !!! failure "已在 sing-box 1.11.0 废弃" WireGuard 出站已被弃用,且将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-wireguard-出站到端点)。 !!! quote "sing-box 1.11.0 中的更改" :material-delete-alert: [gso](#gso) !!! quote "sing-box 1.8.0 中的更改" :material-plus: [gso](#gso) ### 结构 ```json { "type": "wireguard", "tag": "wireguard-out", "server": "127.0.0.1", "server_port": 1080, "system_interface": false, "interface_name": "wg0", "local_address": [ "10.0.0.1/32" ], "private_key": "YNXtAzepDqRv9H52osJVDQnznT5AM11eCK3ESpwSt04=", "peer_public_key": "Z1XXLsKYkYxuiYjJIkRvtIKFepCYHTgON+GwPq7SOV4=", "pre_shared_key": "31aIhAPwktDGpH4JDhA8GNvjFXEf/a6+UaQRyOAiyfM=", "reserved": [0, 0, 0], "workers": 4, "mtu": 1408, "network": "tcp", // 废弃的 "gso": false, ... // 拨号字段 } ``` ### 字段 #### server ==必填== 服务器地址。 #### server_port ==必填== 服务器端口。 #### system_interface 使用系统设备。 需要特权且不能与已有系统接口冲突。 如果 gVisor 未包含在构建中,则强制执行。 #### interface_name 为系统接口自定义设备名称。 #### gso !!! failure "已在 sing-box 1.11.0 废弃" 自 sing-box 1.11.0 起,GSO 将可用时自动启用。 !!! question "自 sing-box 1.8.0 起" !!! quote "" 仅支持 Linux。 尝试启用通用分段卸载。 #### local_address ==必填== 接口的 IPv4/IPv6 地址或地址段的列表。 要分配给接口的 IP(v4 或 v6)地址段列表。 #### private_key ==必填== WireGuard 需要 base64 编码的公钥和私钥。 这些可以使用 wg(8) 实用程序生成: ```shell wg genkey echo "private key" || wg pubkey ``` #### peer_public_key ==必填== WireGuard 对等公钥。 #### pre_shared_key WireGuard 预共享密钥。 #### reserved WireGuard 保留字段字节。 #### workers WireGuard worker 数量。 默认使用 CPU 数量。 #### mtu WireGuard MTU。 默认使用 1408。 #### network 启用的网络协议 `tcp` 或 `udp`。 默认所有。 ### 拨号字段 参阅 [拨号字段](/zh/configuration/shared/dial/)。 ================================================ FILE: docs/configuration/route/geoip.md ================================================ --- icon: material/note-remove --- !!! failure "Removed in sing-box 1.12.0" GeoIP is deprecated in sing-box 1.8.0 and removed in sing-box 1.12.0, check [Migration](/migration/#migrate-geoip-to-rule-sets). ### Structure ```json { "route": { "geoip": { "path": "", "download_url": "", "download_detour": "" } } } ``` ### Fields #### path The path to the sing-geoip database. `geoip.db` will be used if empty. #### download_url The download URL of the sing-geoip database. Default is `https://github.com/SagerNet/sing-geoip/releases/latest/download/geoip.db`. #### download_detour The tag of the outbound to download the database. Default outbound will be used if empty. ================================================ FILE: docs/configuration/route/geoip.zh.md ================================================ --- icon: material/note-remove --- !!! failure "已在 sing-box 1.12.0 中被移除" GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。 ### 结构 ```json { "route": { "geoip": { "path": "", "download_url": "", "download_detour": "" } } } ``` ### 字段 #### path 指定 GeoIP 资源的路径。 默认 `geoip.db`。 #### download_url 指定 GeoIP 资源的下载链接。 默认为 `https://github.com/SagerNet/sing-geoip/releases/latest/download/geoip.db`。 #### download_detour 用于下载 GeoIP 资源的出站的标签。 如果为空,将使用默认出站。 ================================================ FILE: docs/configuration/route/geosite.md ================================================ --- icon: material/note-remove --- !!! failure "Removed in sing-box 1.12.0" Geosite is deprecated in sing-box 1.8.0 and removed in sing-box 1.12.0, check [Migration](/migration/#migrate-geosite-to-rule-sets). ### Structure ```json { "route": { "geosite": { "path": "", "download_url": "", "download_detour": "" } } } ``` ### Fields #### path The path to the sing-geosite database. `geosite.db` will be used if empty. #### download_url The download URL of the sing-geoip database. Default is `https://github.com/SagerNet/sing-geosite/releases/latest/download/geosite.db`. #### download_detour The tag of the outbound to download the database. Default outbound will be used if empty. ================================================ FILE: docs/configuration/route/geosite.zh.md ================================================ --- icon: material/note-remove --- !!! failure "已在 sing-box 1.12.0 中被移除" Geosite 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geosite-到规则集)。 ### 结构 ```json { "route": { "geosite": { "path": "", "download_url": "", "download_detour": "" } } } ``` ### 字段 #### path 指定 GeoSite 资源的路径。 默认 `geosite.db`。 #### download_url 指定 GeoSite 资源的下载链接。 默认为 `https://github.com/SagerNet/sing-geosite/releases/latest/download/geosite.db`。 #### download_detour 用于下载 GeoSite 资源的出站的标签。 如果为空,将使用默认出站。 ================================================ FILE: docs/configuration/route/index.md ================================================ --- icon: material/alert-decagram --- # Route !!! 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) !!! quote "Changes in sing-box 1.12.0" :material-plus: [default_domain_resolver](#default_domain_resolver) :material-note-remove: [geoip](#geoip) :material-note-remove: [geosite](#geosite) !!! quote "Changes in sing-box 1.11.0" :material-plus: [default_network_strategy](#default_network_strategy) :material-plus: [default_network_type](#default_network_type) :material-plus: [default_fallback_network_type](#default_fallback_network_type) :material-plus: [default_fallback_delay](#default_fallback_delay) !!! quote "Changes in sing-box 1.8.0" :material-plus: [rule_set](#rule_set) :material-delete-clock: [geoip](#geoip) :material-delete-clock: [geosite](#geosite) ### Structure ```json { "route": { "rules": [], "rule_set": [], "final": "", "auto_detect_interface": false, "override_android_vpn": false, "default_interface": "", "default_mark": 0, "find_process": false, "find_neighbor": false, "dhcp_lease_files": [], "default_http_client": "", "default_domain_resolver": "", // or {} "default_network_strategy": "", "default_network_type": [], "default_fallback_network_type": [], "default_fallback_delay": "", // Removed "geoip": {}, "geosite": {} } } ``` !!! note "" You can ignore the JSON Array [] tag when the content is only one item ### Fields #### rules List of [Route Rule](./rule/) #### rule_set !!! question "Since sing-box 1.8.0" List of [rule-set](/configuration/rule-set/) #### final Default outbound tag. the first outbound will be used if empty. #### auto_detect_interface !!! quote "" Only supported on Linux, Windows and macOS. Bind outbound connections to the default NIC by default to prevent routing loops under tun. Takes no effect if `outbound.bind_interface` is set. #### override_android_vpn !!! quote "" Only supported on Android. Accept Android VPN as upstream NIC when `auto_detect_interface` enabled. #### default_interface !!! quote "" Only supported on Linux, Windows and macOS. Bind outbound connections to the specified NIC by default to prevent routing loops under tun. Takes no effect if `auto_detect_interface` is set. #### default_mark !!! quote "" Only supported on Linux. 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 and macOS. Enable neighbor resolution for logging when no `source_mac_address` or `source_hostname` rules exist. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup. #### dhcp_lease_files !!! question "Since sing-box 1.14.0" !!! quote "" Only supported on Linux and macOS. 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" See [Dial Fields](/configuration/shared/dial/#domain_resolver) for details. Can be overridden by `outbound.domain_resolver`. #### default_network_strategy !!! question "Since sing-box 1.11.0" 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 overridden by `outbound.network_strategy`. Conflicts with `default_interface`. #### default_network_type !!! question "Since sing-box 1.11.0" See [Dial Fields](/configuration/shared/dial/#network_type) for details. #### default_fallback_network_type !!! question "Since sing-box 1.11.0" See [Dial Fields](/configuration/shared/dial/#fallback_network_type) for details. #### default_fallback_delay !!! question "Since sing-box 1.11.0" See [Dial Fields](/configuration/shared/dial/#fallback_delay) for details. ================================================ FILE: docs/configuration/route/index.zh.md ================================================ --- 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) !!! quote "sing-box 1.12.0 中的更改" :material-plus: [default_domain_resolver](#default_domain_resolver) :material-note-remove: [geoip](#geoip) :material-note-remove: [geosite](#geosite) !!! quote "sing-box 1.11.0 中的更改" :material-plus: [default_network_strategy](#default_network_strategy) :material-plus: [default_network_type](#default_network_type) :material-plus: [default_fallback_network_type](#default_fallback_network_type) :material-plus: [default_fallback_delay](#default_fallback_delay) !!! quote "sing-box 1.8.0 中的更改" :material-plus: [rule_set](#rule_set) :material-delete-clock: [geoip](#geoip) :material-delete-clock: [geosite](#geosite) ### 结构 ```json { "route": { "geoip": {}, "geosite": {}, "rules": [], "rule_set": [], "final": "", "auto_detect_interface": false, "override_android_vpn": false, "default_interface": "", "default_mark": 0, "find_process": false, "find_neighbor": false, "dhcp_lease_files": [], "default_http_client": "", "default_network_strategy": "", "default_fallback_delay": "" } } ``` !!! note "" 当内容只有一项时,可以忽略 JSON 数组 [] 标签 ### 字段 | 键 | 格式 | |-----------|-----------------------| | `geoip` | [GeoIP](./geoip/) | | `geosite` | [Geosite](./geosite/) | #### rule 一组 [路由规则](./rule/) 。 #### rule_set !!! question "自 sing-box 1.8.0 起" 一组 [规则集](/zh/configuration/rule-set/)。 #### final 默认出站标签。如果为空,将使用第一个可用于对应协议的出站。 #### auto_detect_interface !!! quote "" 仅支持 Linux、Windows 和 macOS。 默认将出站连接绑定到默认网卡,以防止在 tun 下出现路由环路。 如果设置了 `outbound.bind_interface` 设置,则不生效。 #### override_android_vpn !!! quote "" 仅支持 Android。 启用 `auto_detect_interface` 时接受 Android VPN 作为上游网卡。 #### default_interface !!! quote "" 仅支持 Linux、Windows 和 macOS。 默认将出站连接绑定到指定网卡,以防止在 tun 下出现路由环路。 如果设置了 `auto_detect_interface` 设置,则不生效。 #### default_mark !!! quote "" 仅支持 Linux。 默认为出站连接设置路由标记。 如果设置了 `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 和 macOS。 在没有 `source_mac_address` 或 `source_hostname` 规则时启用邻居解析以输出日志。 参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。 #### dhcp_lease_files !!! question "自 sing-box 1.14.0 起" !!! quote "" 仅支持 Linux 和 macOS。 用于主机名和 MAC 地址解析的自定义 DHCP 租约文件路径。 为空时自动从常见 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 起" 详情参阅 [拨号字段](/zh/configuration/shared/dial/#domain_resolver)。 可以被 `outbound.domain_resolver` 覆盖。 #### network_strategy !!! question "自 sing-box 1.11.0 起" 详情参阅 [拨号字段](/zh/configuration/shared/dial/#network_strategy)。 当 `outbound.bind_interface`, `outbound.inet4_bind_address` 或 `outbound.inet6_bind_address` 已设置时不生效。 可以被 `outbound.network_strategy` 覆盖。 与 `default_interface` 冲突。 #### default_network_type !!! question "自 sing-box 1.11.0 起" 详情参阅 [拨号字段](/zh/configuration/shared/dial/#default_network_type)。 #### default_fallback_network_type !!! question "自 sing-box 1.11.0 起" 详情参阅 [拨号字段](/zh/configuration/shared/dial/#default_fallback_network_type)。 #### default_fallback_delay !!! question "自 sing-box 1.11.0 起" 详情参阅 [拨号字段](/zh/configuration/shared/dial/#fallback_delay)。 ================================================ FILE: docs/configuration/route/rule.md ================================================ --- 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: [package_name_regex](#package_name_regex) !!! quote "Changes in sing-box 1.13.0" :material-plus: [interface_address](#interface_address) :material-plus: [network_interface_address](#network_interface_address) :material-plus: [default_interface_address](#default_interface_address) :material-plus: [preferred_by](#preferred_by) :material-alert: [network](#network) !!! quote "Changes in sing-box 1.11.0" :material-plus: [action](#action) :material-alert: [outbound](#outbound) :material-plus: [network_type](#network_type) :material-plus: [network_is_expensive](#network_is_expensive) :material-plus: [network_is_constrained](#network_is_constrained) !!! quote "Changes in sing-box 1.10.0" :material-plus: [client](#client) :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) :material-plus: [process_path_regex](#process_path_regex) !!! quote "Changes in sing-box 1.8.0" :material-plus: [rule_set](#rule_set) :material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) :material-plus: [source_ip_is_private](#source_ip_is_private) :material-plus: [ip_is_private](#ip_is_private) :material-delete-clock: [source_geoip](#source_geoip) :material-delete-clock: [geoip](#geoip) :material-delete-clock: [geosite](#geosite) ### Structure ```json { "route": { "rules": [ { "inbound": [ "mixed-in" ], "ip_version": 6, "network": [ "tcp" ], "auth_user": [ "usera", "userb" ], "protocol": [ "tls", "http", "quic" ], "client": [ "chromium", "safari", "firefox", "quic-go" ], "domain": [ "test.com" ], "domain_suffix": [ ".cn" ], "domain_keyword": [ "test" ], "domain_regex": [ "^stun\\..+" ], "geosite": [ "cn" ], "source_geoip": [ "private" ], "geoip": [ "cn" ], "source_ip_cidr": [ "10.0.0.0/24", "192.168.0.1" ], "source_ip_is_private": false, "ip_cidr": [ "10.0.0.0/24", "192.168.0.1" ], "ip_is_private": false, "source_port": [ 12345 ], "source_port_range": [ "1000:2000", ":3000", "4000:" ], "port": [ 80, 443 ], "port_range": [ "1000:2000", ":3000", "4000:" ], "process_name": [ "curl" ], "process_path": [ "/usr/bin/curl" ], "process_path_regex": [ "^/usr/bin/.+" ], "package_name": [ "com.termux" ], "package_name_regex": [ "^com\\.termux.*" ], "user": [ "sekai" ], "user_id": [ 1000 ], "clash_mode": "direct", "network_type": [ "wifi" ], "network_is_expensive": false, "network_is_constrained": false, "interface_address": { "en0": [ "2000::/3" ] }, "network_interface_address": { "wifi": [ "2000::/3" ] }, "default_interface_address": [ "2000::/3" ], "wifi_ssid": [ "My WIFI" ], "wifi_bssid": [ "00:00:00:00:00:00" ], "preferred_by": [ "tailscale", "wireguard" ], "source_mac_address": [ "00:11:22:33:44:55" ], "source_hostname": [ "my-device" ], "rule_set": [ "geoip-cn", "geosite-cn" ], // deprecated "rule_set_ipcidr_match_source": false, "rule_set_ip_cidr_match_source": false, "invert": false, "action": "route", "outbound": "direct" }, { "type": "logical", "mode": "and", "rules": [], "invert": false, "action": "route", "outbound": "direct" } ] } } ``` !!! note "" You can ignore the JSON Array [] tag when the content is only one item ### Default Fields !!! note "" The default rule uses the following matching logic: (`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite` || `geoip` || `ip_cidr` || `ip_is_private`) && (`port` || `port_range`) && (`source_geoip` || `source_ip_cidr` || `source_ip_is_private`) && (`source_port` || `source_port_range`) && `other fields` Additionally, each branch inside an included rule-set can be considered merged into the outer rule, while different branches keep OR semantics. #### inbound Tags of [Inbound](/configuration/inbound/). #### ip_version 4 or 6. Not limited if empty. #### auth_user Username, see each inbound for details. #### protocol Sniffed protocol, see [Protocol Sniff](/configuration/route/sniff/) for details. #### client !!! question "Since sing-box 1.10.0" Sniffed client type, see [Protocol Sniff](/configuration/route/sniff/) for details. #### network !!! quote "Changes in sing-box 1.13.0" Since sing-box 1.13.0, you can match ICMP echo (ping) requests via the new `icmp` network. Such traffic originates from `TUN`, `WireGuard`, and `Tailscale` inbounds and can be routed to `Direct`, `WireGuard`, and `Tailscale` outbounds. Match network type. `tcp`, `udp` or `icmp`. #### domain Match full domain. #### domain_suffix Match domain suffix. #### domain_keyword Match domain using keyword. #### domain_regex Match domain using regular expression. #### geosite !!! failure "Deprecated in sing-box 1.8.0" Geosite is deprecated and will be removed in sing-box 1.12.0, check [Migration](/migration/#migrate-geosite-to-rule-sets). Match geosite. #### source_geoip !!! failure "Deprecated in sing-box 1.8.0" GeoIP is deprecated and will be removed in sing-box 1.12.0, check [Migration](/migration/#migrate-geoip-to-rule-sets). Match source geoip. #### geoip !!! failure "Deprecated in sing-box 1.8.0" GeoIP is deprecated and will be removed in sing-box 1.12.0, check [Migration](/migration/#migrate-geoip-to-rule-sets). Match geoip. #### source_ip_cidr Match source IP CIDR. #### ip_is_private !!! question "Since sing-box 1.8.0" Match non-public IP. #### ip_cidr Match IP CIDR. #### source_ip_is_private !!! question "Since sing-box 1.8.0" Match non-public source IP. #### source_port Match source port. #### source_port_range Match source port range. #### port Match port. #### port_range Match port range. #### process_name !!! quote "" Only supported on Linux, Windows, and macOS. Match process name. #### process_path !!! quote "" Only supported on Linux, Windows, and macOS. Match process path. #### process_path_regex !!! question "Since sing-box 1.10.0" !!! quote "" Only supported on Linux, Windows, and macOS. Match process path using regular expression. #### package_name Match android package name. #### package_name_regex !!! question "Since sing-box 1.14.0" Match android package name using regular expression. #### user !!! quote "" Only supported on Linux. Match user name. #### user_id !!! quote "" Only supported on Linux. Match user id. #### clash_mode Match Clash mode. #### network_type !!! question "Since sing-box 1.11.0" !!! quote "" Only supported in graphical clients on Android and Apple platforms. Match network type. Available values: `wifi`, `cellular`, `ethernet` and `other`. #### network_is_expensive !!! question "Since sing-box 1.11.0" !!! quote "" Only supported in graphical clients on Android and Apple platforms. Match if network is considered Metered (on Android) or considered expensive, such as Cellular or a Personal Hotspot (on Apple platforms). #### network_is_constrained !!! question "Since sing-box 1.11.0" !!! quote "" Only supported in graphical clients on Apple platforms. Match if network is in Low Data Mode. #### interface_address !!! question "Since sing-box 1.13.0" !!! quote "" Only supported on Linux, Windows, and macOS. Match interface address. #### network_interface_address !!! question "Since sing-box 1.13.0" !!! quote "" Only supported in graphical clients on Android and Apple platforms. Matches network interface (same values as `network_type`) address. #### default_interface_address !!! question "Since sing-box 1.13.0" !!! quote "" Only supported on Linux, Windows, and macOS. Match default interface address. #### wifi_ssid Match WiFi SSID. See [Wi-Fi State](/configuration/shared/wifi-state/) for details. #### wifi_bssid Match WiFi BSSID. See [Wi-Fi State](/configuration/shared/wifi-state/) for details. #### preferred_by !!! question "Since sing-box 1.13.0" Match specified outbounds' preferred routes. | Type | Match | |-------------|-----------------------------------------------| | `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, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup. Match source device MAC address. #### source_hostname !!! question "Since sing-box 1.14.0" !!! quote "" 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. #### rule_set !!! question "Since sing-box 1.8.0" Match [rule-set](/configuration/route/#rule_set). #### rule_set_ipcidr_match_source !!! question "Since sing-box 1.8.0" !!! failure "Deprecated in sing-box 1.10.0" `rule_set_ipcidr_match_source` is renamed to `rule_set_ip_cidr_match_source` and will be remove in sing-box 1.11.0. Make `ip_cidr` in rule-sets match the source IP. #### rule_set_ip_cidr_match_source !!! question "Since sing-box 1.10.0" Make `ip_cidr` in rule-sets match the source IP. #### invert Invert match result. #### action ==Required== See [Rule Actions](../rule_action/) for details. #### outbound !!! failure "Deprecated in sing-box 1.11.0" Moved to [Rule Action](../rule_action#route). ### Logical Fields #### type `logical` #### mode ==Required== `and` or `or` #### rules ==Required== Included rules. ================================================ FILE: docs/configuration/route/rule.zh.md ================================================ --- 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: [package_name_regex](#package_name_regex) !!! quote "sing-box 1.13.0 中的更改" :material-plus: [interface_address](#interface_address) :material-plus: [network_interface_address](#network_interface_address) :material-plus: [default_interface_address](#default_interface_address) :material-plus: [preferred_by](#preferred_by) :material-alert: [network](#network) !!! quote "sing-box 1.11.0 中的更改" :material-plus: [action](#action) :material-alert: [outbound](#outbound) :material-plus: [network_type](#network_type) :material-plus: [network_is_expensive](#network_is_expensive) :material-plus: [network_is_constrained](#network_is_constrained) !!! quote "sing-box 1.10.0 中的更改" :material-plus: [client](#client) :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) :material-plus: [process_path_regex](#process_path_regex) !!! quote "sing-box 1.8.0 中的更改" :material-plus: [rule_set](#rule_set) :material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) :material-plus: [source_ip_is_private](#source_ip_is_private) :material-plus: [ip_is_private](#ip_is_private) :material-delete-clock: [source_geoip](#source_geoip) :material-delete-clock: [geoip](#geoip) :material-delete-clock: [geosite](#geosite) ### 结构 ```json { "route": { "rules": [ { "inbound": [ "mixed-in" ], "ip_version": 6, "network": [ "tcp" ], "auth_user": [ "usera", "userb" ], "protocol": [ "tls", "http", "quic" ], "client": [ "chromium", "safari", "firefox", "quic-go" ], "domain": [ "test.com" ], "domain_suffix": [ ".cn" ], "domain_keyword": [ "test" ], "domain_regex": [ "^stun\\..+" ], "geosite": [ "cn" ], "source_geoip": [ "private" ], "geoip": [ "cn" ], "source_ip_cidr": [ "10.0.0.0/24" ], "source_ip_is_private": false, "ip_cidr": [ "10.0.0.0/24" ], "ip_is_private": false, "source_port": [ 12345 ], "source_port_range": [ "1000:2000", ":3000", "4000:" ], "port": [ 80, 443 ], "port_range": [ "1000:2000", ":3000", "4000:" ], "process_name": [ "curl" ], "process_path": [ "/usr/bin/curl" ], "process_path_regex": [ "^/usr/bin/.+" ], "package_name": [ "com.termux" ], "package_name_regex": [ "^com\\.termux.*" ], "user": [ "sekai" ], "user_id": [ 1000 ], "clash_mode": "direct", "network_type": [ "wifi" ], "network_is_expensive": false, "network_is_constrained": false, "interface_address": { "en0": [ "2000::/3" ] }, "network_interface_address": { "wifi": [ "2000::/3" ] }, "default_interface_address": [ "2000::/3" ], "wifi_ssid": [ "My WIFI" ], "wifi_bssid": [ "00:00:00:00:00:00" ], "preferred_by": [ "tailscale", "wireguard" ], "source_mac_address": [ "00:11:22:33:44:55" ], "source_hostname": [ "my-device" ], "rule_set": [ "geoip-cn", "geosite-cn" ], // 已弃用 "rule_set_ipcidr_match_source": false, "rule_set_ip_cidr_match_source": false, "invert": false, "action": "route", "outbound": "direct" }, { "type": "logical", "mode": "and", "rules": [], "invert": false, "action": "route", "outbound": "direct" } ] } } ``` !!! note "" 当内容只有一项时,可以忽略 JSON 数组 [] 标签。 ### 默认字段 !!! note "" 默认规则使用以下匹配逻辑: (`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite` || `geoip` || `ip_cidr` || `ip_is_private`) && (`port` || `port_range`) && (`source_geoip` || `source_ip_cidr` || `source_ip_is_private`) && (`source_port` || `source_port_range`) && `other fields` 另外,引用规则集中的每个分支都可视为与外层规则合并,不同分支之间仍保持 OR 语义。 #### inbound [入站](/zh/configuration/inbound/) 标签。 #### ip_version 4 或 6。 默认不限制。 #### auth_user 认证用户名,参阅入站设置。 #### protocol 探测到的协议, 参阅 [协议探测](/zh/configuration/route/sniff/)。 #### client !!! question "自 sing-box 1.10.0 起" 探测到的客户端类型, 参阅 [协议探测](/zh/configuration/route/sniff/)。 #### network !!! quote "sing-box 1.13.0 中的更改" 自 sing-box 1.13.0 起,您可以通过新的 `icmp` 网络匹配 ICMP 回显(ping)请求。 此类流量源自 `TUN`、`WireGuard` 和 `Tailscale` 入站,并可路由至 `Direct`、`WireGuard` 和 `Tailscale` 出站。 匹配网络类型。 `tcp`、`udp` 或 `icmp`。 #### domain 匹配完整域名。 #### domain_suffix 匹配域名后缀。 #### domain_keyword 匹配域名关键字。 #### domain_regex 匹配域名正则表达式。 #### geosite !!! failure "已在 sing-box 1.8.0 废弃" Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#迁移-geosite-到规则集)。 匹配 Geosite。 #### source_geoip !!! failure "已在 sing-box 1.8.0 废弃" GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。 匹配源 GeoIP。 #### geoip !!! failure "已在 sing-box 1.8.0 废弃" GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。 匹配 GeoIP。 #### source_ip_cidr 匹配源 IP CIDR。 #### source_ip_is_private !!! question "自 sing-box 1.8.0 起" 匹配非公开源 IP。 #### ip_cidr 匹配 IP CIDR。 #### ip_is_private !!! question "自 sing-box 1.8.0 起" 匹配非公开 IP。 #### source_port 匹配源端口。 #### source_port_range 匹配源端口范围。 #### port 匹配端口。 #### port_range 匹配端口范围。 #### process_name !!! quote "" 仅支持 Linux、Windows 和 macOS。 匹配进程名称。 #### process_path !!! quote "" 仅支持 Linux、Windows 和 macOS. 匹配进程路径。 #### process_path_regex !!! question "自 sing-box 1.10.0 起" !!! quote "" 仅支持 Linux、Windows 和 macOS. 使用正则表达式匹配进程路径。 #### package_name 匹配 Android 应用包名。 #### package_name_regex !!! question "自 sing-box 1.14.0 起" 使用正则表达式匹配 Android 应用包名。 #### user !!! quote "" 仅支持 Linux. 匹配用户名。 #### user_id !!! quote "" 仅支持 Linux. 匹配用户 ID。 #### clash_mode 匹配 Clash 模式。 #### network_type !!! question "自 sing-box 1.11.0 起" !!! quote "" 仅在 Android 与 Apple 平台图形客户端中支持。 匹配网络类型。 可用值: `wifi`, `cellular`, `ethernet` and `other`. #### network_is_expensive !!! question "自 sing-box 1.11.0 起" !!! quote "" 仅在 Android 与 Apple 平台图形客户端中支持。 匹配如果网络被视为计费 (在 Android) 或被视为昂贵, 像蜂窝网络或个人热点 (在 Apple 平台)。 #### network_is_constrained !!! question "自 sing-box 1.11.0 起" !!! quote "" 仅在 Apple 平台图形客户端中支持。 匹配如果网络在低数据模式下。 #### interface_address !!! question "自 sing-box 1.13.0 起" !!! quote "" 仅支持 Linux、Windows 和 macOS. 匹配接口地址。 #### network_interface_address !!! question "自 sing-box 1.13.0 起" !!! quote "" 仅在 Android 与 Apple 平台图形客户端中支持。 匹配网络接口(可用值同 `network_type`)地址。 #### default_interface_address !!! question "自 sing-box 1.13.0 起" !!! quote "" 仅支持 Linux、Windows 和 macOS. 匹配默认接口地址。 #### wifi_ssid 匹配 WiFi SSID。 参阅 [Wi-Fi 状态](/zh/configuration/shared/wifi-state/)。 #### wifi_bssid 匹配 WiFi BSSID。 参阅 [Wi-Fi 状态](/zh/configuration/shared/wifi-state/)。 #### preferred_by !!! question "自 sing-box 1.13.0 起" 匹配制定出站的首选路由。 | 类型 | 匹配 | |-------------|--------------------------------| | `tailscale` | 匹配 MagicDNS 域名和对端的 allowed IPs | | `wireguard` | 匹配对端的 allowed IPs | #### source_mac_address !!! question "自 sing-box 1.14.0 起" !!! quote "" 仅支持 Linux、macOS,或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。 匹配源设备 MAC 地址。 #### source_hostname !!! question "自 sing-box 1.14.0 起" !!! quote "" 仅支持 Linux、macOS,或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。 匹配源设备从 DHCP 租约获取的主机名。 #### rule_set !!! question "自 sing-box 1.8.0 起" 匹配[规则集](/zh/configuration/route/#rule_set)。 #### rule_set_ipcidr_match_source !!! question "自 sing-box 1.8.0 起" !!! failure "已在 sing-box 1.10.0 废弃" `rule_set_ipcidr_match_source` 已重命名为 `rule_set_ip_cidr_match_source` 且将在 sing-box 1.11.0 中被移除。 使规则集中的 `ip_cidr` 规则匹配源 IP。 #### rule_set_ip_cidr_match_source !!! question "自 sing-box 1.10.0 起" 使规则集中的 `ip_cidr` 规则匹配源 IP。 #### invert 反选匹配结果。 #### action ==必填== 参阅 [规则动作](../rule_action/)。 #### outbound !!! failure "已在 sing-box 1.11.0 废弃" 已移动到 [规则动作](../rule_action#route). ### 逻辑字段 #### type `logical` #### mode ==必填== `and` 或 `or` #### rules ==必填== 包括的规则。 ================================================ FILE: docs/configuration/route/rule_action.md ================================================ --- icon: material/new-box --- !!! quote "Changes in sing-box 1.13.0" :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) :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" :material-plus: [tls_fragment](#tls_fragment) :material-plus: [tls_fragment_fallback_delay](#tls_fragment_fallback_delay) :material-plus: [tls_record_fragment](#tls_record_fragment) :material-plus: [resolve.disable_cache](#disable_cache) :material-plus: [resolve.rewrite_ttl](#rewrite_ttl) :material-plus: [resolve.client_subnet](#client_subnet) ## Final actions ### route ```json { "action": "route", // default "outbound": "", ... // route-options Fields } ``` !!! note "" You can ignore the JSON Array [] tag when the content is only one item `route` inherits the classic rule behavior of routing connection to the specified outbound. #### outbound ==Required== Tag of target outbound. #### route-options Fields See `route-options` fields below. ### bypass !!! question "Since sing-box 1.13.0" !!! quote "" Only supported on Linux with `auto_redirect` enabled. ```json { "action": "bypass", "outbound": "", ... // route-options Fields } ``` `bypass` bypasses sing-box at the kernel level for auto redirect connections in pre-match. For non-auto-redirect connections and already established connections, if `outbound` is specified, the behavior is the same as `route`; otherwise, the rule will be skipped. #### outbound Tag of target outbound. If not specified, the rule only matches in [pre-match](/configuration/shared/pre-match/) from auto redirect, and will be skipped in other contexts. #### route-options Fields See `route-options` fields below. ### reject !!! quote "Changes in sing-box 1.13.0" Since sing-box 1.13.0, you can reject (or directly reply to) ICMP echo (ping) requests using `reject` action. ```json { "action": "reject", "method": "default", // default "no_drop": false } ``` `reject` reject connections The specified method is used for reject tun connections if `sniff` action has not been performed yet. For non-tun connections and already established connections, will just be closed. #### method For TCP and UDP connections: - `default`: Reply with TCP RST for TCP connections, and ICMP port unreachable for UDP packets. - `drop`: Drop packets. For ICMP echo requests: - `default`: Reply with ICMP host unreachable. - `drop`: Drop packets. - `reply`: Reply with ICMP echo reply. #### no_drop If not enabled, `method` will be temporarily overwritten to `drop` after 50 triggers in 30s. Not available when `method` is set to drop. ### hijack-dns ```json { "action": "hijack-dns" } ``` `hijack-dns` hijack DNS requests to the sing-box DNS module. ## Non-final actions ### route-options ```json { "action": "route-options", "override_address": "", "override_port": 0, "network_strategy": "", "fallback_delay": "", "udp_disable_domain_unmapping": false, "udp_connect": false, "udp_timeout": "", "tls_fragment": false, "tls_fragment_fallback_delay": "", "tls_record_fragment": "", "tls_spoof": "", "tls_spoof_method": "" } ``` `route-options` set options for routing. #### override_address Override the connection destination address. #### override_port Override the connection destination port. #### network_strategy See [Dial Fields](/configuration/shared/dial/#network_strategy) for details. Only take effect if outbound is direct without `outbound.bind_interface`, `outbound.inet4_bind_address` and `outbound.inet6_bind_address` set. #### network_type See [Dial Fields](/configuration/shared/dial/#network_type) for details. #### fallback_network_type See [Dial Fields](/configuration/shared/dial/#fallback_network_type) for details. #### fallback_delay See [Dial Fields](/configuration/shared/dial/#fallback_delay) for details. #### udp_disable_domain_unmapping If enabled, for UDP proxy requests addressed to a domain, the original packet address will be sent in the response instead of the mapped domain. This option is used for compatibility with clients that do not support receiving UDP packets with domain addresses, such as Surge. #### udp_connect If enabled, attempts to connect UDP connection to the destination instead of listen. #### udp_timeout Timeout for UDP connections. Setting a larger value than the UDP timeout in inbounds will have no effect. Default value for protocol sniffed connections: | Timeout | Protocol | |---------|----------------------| | `10s` | `dns`, `ntp`, `stun` | | `30s` | `quic`, `dtls` | If no protocol is sniffed, the following ports will be recognized as protocols by default: | Port | Protocol | |------|----------| | 53 | `dns` | | 123 | `ntp` | | 443 | `quic` | | 3478 | `stun` | #### tls_fragment !!! question "Since sing-box 1.12.0" Fragment TLS handshakes to bypass firewalls. This feature is intended to circumvent simple firewalls based on **plaintext packet matching**, and should not be used to circumvent real censorship. Due to poor performance, try `tls_record_fragment` first, and only apply to server names known to be blocked. On Linux, Apple platforms, (administrator privileges required) Windows, the wait time can be automatically detected. Otherwise, it will fall back to waiting for a fixed time specified by `tls_fragment_fallback_delay`. In addition, if the actual wait time is less than 20ms, it will also fall back to waiting for a fixed time, because the target is considered to be local or behind a transparent proxy. #### tls_fragment_fallback_delay !!! question "Since sing-box 1.12.0" The fallback value used when TLS segmentation cannot automatically determine the wait time. `500ms` is used by default. #### tls_record_fragment !!! question "Since sing-box 1.12.0" 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 { "action": "sniff", "sniffer": [], "timeout": "" } ``` `sniff` performs protocol sniffing on connections. For deprecated `inbound.sniff` options, it is considered to `sniff()` performed before routing. #### sniffer Enabled sniffers. All sniffers enabled by default. Available protocol values an be found on in [Protocol Sniff](../sniff/) #### timeout Timeout for sniffing. `300ms` is used by default. ### resolve ```json { "action": "resolve", "server": "", "strategy": "", "disable_cache": false, "disable_optimistic_cache": false, "rewrite_ttl": null, "timeout": "", "client_subnet": null } ``` `resolve` resolve request destination from domain to IP addresses. #### server Specifies DNS server tag to use instead of selecting through DNS routing. #### strategy DNS resolution strategy, available values are: `prefer_ipv4`, `prefer_ipv6`, `ipv4_only`, `ipv6_only`. `dns.strategy` will be used by default. #### disable_cache !!! question "Since sing-box 1.12.0" 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" 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" 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`. ================================================ FILE: docs/configuration/route/rule_action.zh.md ================================================ --- icon: material/new-box --- !!! quote "sing-box 1.13.0 中的更改" :material-plus: [bypass](#bypass) :material-alert: [reject](#reject) !!! quote "sing-box 1.14.0 中的更改" :material-plus: [resolve.disable_optimistic_cache](#disable_optimistic_cache) :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 中的更改" :material-plus: [tls_fragment](#tls_fragment) :material-plus: [tls_fragment_fallback_delay](#tls_fragment_fallback_delay) :material-plus: [tls_record_fragment](#tls_record_fragment) :material-plus: [resolve.disable_cache](#disable_cache) :material-plus: [resolve.rewrite_ttl](#rewrite_ttl) :material-plus: [resolve.client_subnet](#client_subnet) ## 最终动作 ### route ```json { "action": "route", // 默认 "outbound": "", ... // route-options 字段 } ``` `route` 继承了将连接路由到指定出站的经典规则动作。 #### outbound ==必填== 目标出站的标签。 #### route-options 字段 参阅下方的 `route-options` 字段。 ### bypass !!! question "自 sing-box 1.13.0 起" !!! quote "" 仅支持 Linux,且需要启用 `auto_redirect`。 ```json { "action": "bypass", "outbound": "", ... // route-options 字段 } ``` `bypass` 在预匹配中为 auto redirect 连接在内核层面绕过 sing-box。 对于非 auto redirect 连接和已建立的连接,如果指定了 `outbound`,行为与 `route` 相同;否则规则将被跳过。 #### outbound 目标出站的标签。 如果未指定,规则仅在来自 auto redirect 的[预匹配](/zh/configuration/shared/pre-match/)中匹配,在其他场景中将被跳过。 #### route-options 字段 参阅下方的 `route-options` 字段。 ### reject !!! quote "sing-box 1.13.0 中的更改" 自 sing-box 1.13.0 起,您可以通过 `reject` 动作拒绝(或直接回复)ICMP 回显(ping)请求。 ```json { "action": "reject", "method": "default", // 默认 "no_drop": false } ``` `reject` 拒绝连接。 如果尚未执行 `sniff` 操作,则将使用指定方法拒绝 tun 连接。 对于非 tun 连接和已建立的连接,将直接关闭。 #### method 对于 TCP 和 UDP 连接: - `default`: 对于 TCP 连接回复 RST,对于 UDP 包回复 ICMP 端口不可达。 - `drop`: 丢弃数据包。 对于 ICMP 回显请求: - `default`: 回复 ICMP 主机不可达。 - `drop`: 丢弃数据包。 - `reply`: 回复以 ICMP 回显应答。 #### no_drop 如果未启用,则 30 秒内触发 50 次后,`method` 将被暂时覆盖为 `drop`。 当 `method` 设为 `drop` 时不可用。 ### hijack-dns ```json { "action": "hijack-dns" } ``` `hijack-dns` 劫持 DNS 请求至 sing-box DNS 模块。 ## 非最终动作 ### route-options ```json { "action": "route-options", "override_address": "", "override_port": 0, "network_strategy": "", "fallback_delay": "", "udp_disable_domain_unmapping": false, "udp_connect": false, "udp_timeout": "", "tls_fragment": false, "tls_fragment_fallback_delay": "", "tls_record_fragment": false, "tls_spoof": "", "tls_spoof_method": "" } ``` !!! note "" 当内容只有一项时,可以忽略 JSON 数组 [] 标签 `route-options` 为路由设置选项。 #### override_address 覆盖目标地址。 #### override_port 覆盖目标端口。 #### network_strategy 详情参阅 [拨号字段](/zh/configuration/shared/dial/#network_strategy)。 仅当出站为 `direct` 且 `outbound.bind_interface`, `outbound.inet4_bind_address` 且 `outbound.inet6_bind_address` 未设置时生效。 #### network_type 详情参阅 [拨号字段](/zh/configuration/shared/dial/#network_type)。 #### fallback_network_type 详情参阅 [拨号字段](/zh/configuration/shared/dial/#fallback_network_type)。 #### fallback_delay 详情参阅 [拨号字段](/zh/configuration/shared/dial/#fallback_delay)。 #### udp_disable_domain_unmapping 如果启用,对于地址为域的 UDP 代理请求,将在响应中发送原始包地址而不是映射的域。 此选项用于兼容不支持接收带有域地址的 UDP 包的客户端,如 Surge。 #### udp_connect 如果启用,将尝试将 UDP 连接 connect 到目标而不是 listen。 #### udp_timeout UDP 连接超时时间。 设置比入站 UDP 超时更大的值将无效。 已探测协议连接的默认值: | 超时 | 协议 | |-------|----------------------| | `10s` | `dns`, `ntp`, `stun` | | `30s` | `quic`, `dtls` | 如果没有探测到协议,以下端口将默认识别为协议: | 端口 | 协议 | |------|--------| | 53 | `dns` | | 123 | `ntp` | | 443 | `quic` | | 3478 | `stun` | #### tls_fragment !!! question "自 sing-box 1.12.0 起" 通过分段 TLS 握手数据包来绕过防火墙检测。 此功能旨在规避基于**明文数据包匹配**的简单防火墙,不应该用于规避真的审查。 由于性能不佳,请首先尝试 `tls_record_fragment`,且仅应用于已知被阻止的服务器名称。 在 Linux、Apple 平台和需要管理员权限的 Windows 系统上,可自动检测等待时间。 若无法自动检测,将回退使用 `tls_fragment_fallback_delay` 指定的固定等待时间。 此外,若实际等待时间小于 20 毫秒,同样会回退至固定等待时间模式,因为此时判定目标处于本地或透明代理之后。 #### tls_fragment_fallback_delay !!! question "自 sing-box 1.12.0 起" 当 TLS 分片功能无法自动判定等待时间时使用的回退值。 默认使用 `500ms`。 #### tls_record_fragment !!! question "自 sing-box 1.12.0 起" 通过分段 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 { "action": "sniff", "sniffer": [], "timeout": "" } ``` `sniff` 对连接执行协议嗅探。 对于已弃用的 `inbound.sniff` 选项,被视为在路由之前执行的 `sniff`。 #### sniffer 启用的探测器。 默认启用所有探测器。 可用的协议值可以在 [协议嗅探](../sniff/) 中找到。 #### timeout 探测超时时间。 默认使用 300ms。 ### resolve ```json { "action": "resolve", "server": "", "strategy": "", "disable_cache": false, "disable_optimistic_cache": false, "rewrite_ttl": null, "timeout": "", "client_subnet": null } ``` `resolve` 将请求的目标从域名解析为 IP 地址。 #### server 指定要使用的 DNS 服务器的标签,而不是通过 DNS 路由进行选择。 #### strategy DNS 解析策略,可用值有:`prefer_ipv4`、`prefer_ipv6`、`ipv4_only`、`ipv6_only`。 默认使用 `dns.strategy`。 #### disable_cache !!! question "自 sing-box 1.12.0 起" 在此查询中禁用缓存。 #### disable_optimistic_cache !!! question "自 sing-box 1.14.0 起" 在此查询中禁用乐观 DNS 缓存。 #### rewrite_ttl !!! question "自 sing-box 1.12.0 起" 重写 DNS 回应中的 TTL。 #### timeout !!! question "自 sing-box 1.14.0 起" 覆盖此查询的 DNS 查询超时时间。 将覆盖 `dns.timeout`。 #### client_subnet !!! question "自 sing-box 1.12.0 起" 默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。 如果值是 IP 地址而不是前缀,则会自动附加 `/32` 或 `/128`。 将覆盖 `dns.client_subnet`. ================================================ FILE: docs/configuration/route/sniff.md ================================================ !!! quote "Changes in sing-box 1.10.0" :material-plus: QUIC client type detect support for QUIC :material-plus: Chromium support for QUIC :material-plus: BitTorrent support :material-plus: DTLS support :material-plus: SSH support :material-plus: RDP support If enabled in the inbound, the protocol and domain name (if present) of by the connection can be sniffed. #### Supported Protocols | Network | Protocol | Domain Name | Client | |:-------:|:------------:|:-----------:|:----------------:| | TCP | `http` | Host | / | | TCP | `tls` | Server Name | / | | UDP | `quic` | Server Name | QUIC Client Type | | UDP | `stun` | / | / | | TCP/UDP | `dns` | / | / | | TCP/UDP | `bittorrent` | / | / | | UDP | `dtls` | / | / | | TCP | `ssh` | / | SSH Client Name | | TCP | `rdp` | / | / | | UDP | `ntp` | / | / | | QUIC Client | Type | |:------------------------:|:----------:| | Chromium/Cronet | `chromium` | | Safari/Apple Network API | `safari` | | Firefox / uquic firefox | `firefox` | | quic-go / uquic chrome | `quic-go` | ================================================ FILE: docs/configuration/route/sniff.zh.md ================================================ !!! quote "sing-box 1.10.0 中的更改" :material-plus: QUIC 的 客户端类型探测支持 :material-plus: QUIC 的 Chromium 支持 :material-plus: BitTorrent 支持 :material-plus: DTLS 支持 :material-plus: SSH 支持 :material-plus: RDP 支持 如果在入站中启用,则可以嗅探连接的协议和域名(如果存在)。 #### 支持的协议 | 网络 | 协议 | 域名 | 客户端 | |:-------:|:------------:|:-----------:|:----------:| | TCP | `http` | Host | / | | TCP | `tls` | Server Name | / | | UDP | `quic` | Server Name | QUIC 客户端类型 | | UDP | `stun` | / | / | | TCP/UDP | `dns` | / | / | | TCP/UDP | `bittorrent` | / | / | | UDP | `dtls` | / | / | | TCP | `ssh` | / | SSH 客户端名称 | | TCP | `rdp` | / | / | | UDP | `ntp` | / | / | | QUIC 客户端 | 类型 | |:------------------------:|:----------:| | Chromium/Cronet | `chromium` | | Safari/Apple Network API | `safari` | | Firefox / uquic firefox | `firefox` | | quic-go / uquic chrome | `quic-go` | ================================================ FILE: docs/configuration/rule-set/adguard.md ================================================ !!! question "Since sing-box 1.10.0" sing-box supports some rule-set formats from other projects which cannot be fully translated to sing-box, currently only AdGuard DNS Filter. These formats are not directly supported as source formats, instead you need to convert them to binary rule-set. ## Convert Use `sing-box rule-set convert --type adguard [--output .srs] .txt` to convert to binary rule-set. ## Performance AdGuard keeps all rules in memory and matches them sequentially, while sing-box chooses high performance and smaller memory usage. As a trade-off, you cannot know which rule item is matched. ## Compatibility Almost all rules in [AdGuardSDNSFilter](https://github.com/AdguardTeam/AdGuardSDNSFilter) and rules in rule-sets listed in [adguard-filter-list](https://github.com/ppfeufer/adguard-filter-list) are supported. ## Supported formats ### AdGuard Filter #### Basic rule syntax | Syntax | Supported | |--------|------------------| | `@@` | :material-check: | | `\|\|` | :material-check: | | `\|` | :material-check: | | `^` | :material-check: | | `*` | :material-check: | #### Host syntax | Syntax | Example | Supported | |-------------|--------------------------|--------------------------| | Scheme | `https://` | :material-alert: Ignored | | Domain Host | `example.org` | :material-check: | | IP Host | `1.1.1.1`, `10.0.0.` | :material-close: | | Regexp | `/regexp/` | :material-check: | | Port | `example.org:80` | :material-close: | | Path | `example.org/path/ad.js` | :material-close: | #### Modifier syntax | Modifier | Supported | |-----------------------|--------------------------| | `$important` | :material-check: | | `$dnsrewrite=0.0.0.0` | :material-alert: Ignored | | Any other modifiers | :material-close: | ### Hosts Only items with `0.0.0.0` IP addresses will be accepted. ### Simple When all rule lines are valid domains, they are treated as simple line-by-line domain rules which, like hosts, only match the exact same domain. ================================================ FILE: docs/configuration/rule-set/adguard.zh.md ================================================ !!! question "自 sing-box 1.10.0 起" sing-box 支持其他项目的一些规则集格式,这些格式无法完全转换为 sing-box, 目前只有 AdGuard DNS Filter。 这些格式不直接作为源格式支持, 而是需要将它们转换为二进制规则集。 ## 转换 使用 `sing-box rule-set convert --type adguard [--output .srs] .txt` 以转换为二进制规则集。 ## 性能 AdGuard 将所有规则保存在内存中并按顺序匹配, 而 sing-box 选择高性能和较小的内存使用量。 作为权衡,您无法知道匹配了哪个规则项。 ## 兼容性 [AdGuardSDNSFilter](https://github.com/AdguardTeam/AdGuardSDNSFilter) 中的几乎所有规则以及 [adguard-filter-list](https://github.com/ppfeufer/adguard-filter-list) 中列出的规则集中的规则均受支持。 ## 支持的格式 ### AdGuard Filter #### 基本规则语法 | 语法 | 支持 | |--------|------------------| | `@@` | :material-check: | | `\|\|` | :material-check: | | `\|` | :material-check: | | `^` | :material-check: | | `*` | :material-check: | #### 主机语法 | 语法 | 示例 | 支持 | |-------------|--------------------------|--------------------------| | Scheme | `https://` | :material-alert: Ignored | | Domain Host | `example.org` | :material-check: | | IP Host | `1.1.1.1`, `10.0.0.` | :material-close: | | Regexp | `/regexp/` | :material-check: | | Port | `example.org:80` | :material-close: | | Path | `example.org/path/ad.js` | :material-close: | #### 描述符语法 | 描述符 | 支持 | |-----------------------|--------------------------| | `$important` | :material-check: | | `$dnsrewrite=0.0.0.0` | :material-alert: Ignored | | 任何其他描述符 | :material-close: | ### Hosts 只有 IP 地址为 `0.0.0.0` 的条目将被接受。 ### 简易 当所有行都是有效域时,它们被视为简单的逐行域规则, 与 hosts 一样,只匹配完全相同的域。 ================================================ FILE: docs/configuration/rule-set/headless-rule.md ================================================ --- icon: material/new-box --- !!! quote "Changes in sing-box 1.14.0" :material-plus: [package_name_regex](#package_name_regex) :material-alert: [query_type](#query_type) !!! quote "Changes in sing-box 1.13.0" :material-plus: [network_interface_address](#network_interface_address) :material-plus: [default_interface_address](#default_interface_address) !!! quote "Changes in sing-box 1.11.0" :material-plus: [network_type](#network_type) :material-plus: [network_is_expensive](#network_is_expensive) :material-plus: [network_is_constrained](#network_is_constrained) ### Structure !!! question "Since sing-box 1.8.0" ```json { "rules": [ { "query_type": [ "A", "HTTPS", 32768 ], "network": [ "tcp" ], "domain": [ "test.com" ], "domain_suffix": [ ".cn" ], "domain_keyword": [ "test" ], "domain_regex": [ "^stun\\..+" ], "source_ip_cidr": [ "10.0.0.0/24", "192.168.0.1" ], "ip_cidr": [ "10.0.0.0/24", "192.168.0.1" ], "source_port": [ 12345 ], "source_port_range": [ "1000:2000", ":3000", "4000:" ], "port": [ 80, 443 ], "port_range": [ "1000:2000", ":3000", "4000:" ], "process_name": [ "curl" ], "process_path": [ "/usr/bin/curl" ], "process_path_regex": [ "^/usr/bin/.+" ], "package_name": [ "com.termux" ], "package_name_regex": [ "^com\\.termux.*" ], "network_type": [ "wifi" ], "network_is_expensive": false, "network_is_constrained": false, "network_interface_address": { "wifi": [ "2000::/3" ] }, "default_interface_address": [ "2000::/3" ], "wifi_ssid": [ "My WIFI" ], "wifi_bssid": [ "00:00:00:00:00:00" ], "invert": false }, { "type": "logical", "mode": "and", "rules": [], "invert": false } ] } ``` !!! note "" You can ignore the JSON Array [] tag when the content is only one item ### Default Fields !!! note "" The default rule uses the following matching logic: (`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `ip_cidr`) && (`port` || `port_range`) && (`source_port` || `source_port_range`) && `other fields` #### 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 `tcp` or `udp`. #### domain Match full domain. #### domain_suffix Match domain suffix. #### domain_keyword Match domain using keyword. #### domain_regex Match domain using regular expression. #### source_ip_cidr Match source IP CIDR. #### ip_cidr !!! info "" `ip_cidr` is an alias for `source_ip_cidr` when `rule_set_ipcidr_match_source` enabled in route/DNS rules. Match IP CIDR. #### source_port Match source port. #### source_port_range Match source port range. #### port Match port. #### port_range Match port range. #### process_name !!! quote "" Only supported on Linux, Windows, and macOS. Match process name. #### process_path !!! quote "" Only supported on Linux, Windows, and macOS. Match process path. #### process_path_regex !!! question "Since sing-box 1.10.0" !!! quote "" Only supported on Linux, Windows, and macOS. Match process path using regular expression. #### package_name 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" !!! quote "" Only supported in graphical clients on Android and Apple platforms. Match network type. Available values: `wifi`, `cellular`, `ethernet` and `other`. #### network_is_expensive !!! question "Since sing-box 1.11.0" !!! quote "" Only supported in graphical clients on Android and Apple platforms. Match if network is considered Metered (on Android) or considered expensive, such as Cellular or a Personal Hotspot (on Apple platforms). #### network_is_constrained !!! question "Since sing-box 1.11.0" !!! quote "" Only supported in graphical clients on Apple platforms. Match if network is in Low Data Mode. #### network_interface_address !!! question "Since sing-box 1.13.0" !!! quote "" Only supported in graphical clients on Android and Apple platforms. Matches network interface (same values as `network_type`) address. #### default_interface_address !!! question "Since sing-box 1.13.0" !!! quote "" Only supported on Linux, Windows, and macOS. Match default interface address. #### wifi_ssid !!! quote "" Only supported in graphical clients on Android and Apple platforms. Match WiFi SSID. #### wifi_bssid !!! quote "" Only supported in graphical clients on Android and Apple platforms. Match WiFi BSSID. #### invert Invert match result. ### Logical Fields #### type `logical` #### mode ==Required== `and` or `or` #### rules ==Required== Included rules. ================================================ FILE: docs/configuration/rule-set/headless-rule.zh.md ================================================ --- icon: material/new-box --- !!! quote "sing-box 1.14.0 中的更改" :material-plus: [package_name_regex](#package_name_regex) :material-alert: [query_type](#query_type) !!! quote "sing-box 1.13.0 中的更改" :material-plus: [network_interface_address](#network_interface_address) :material-plus: [default_interface_address](#default_interface_address) !!! quote "sing-box 1.11.0 中的更改" :material-plus: [network_type](#network_type) :material-plus: [network_is_expensive](#network_is_expensive) :material-plus: [network_is_constrained](#network_is_constrained) ### 结构 !!! question "自 sing-box 1.8.0 起" ```json { "rules": [ { "query_type": [ "A", "HTTPS", 32768 ], "network": [ "tcp" ], "domain": [ "test.com" ], "domain_suffix": [ ".cn" ], "domain_keyword": [ "test" ], "domain_regex": [ "^stun\\..+" ], "source_ip_cidr": [ "10.0.0.0/24", "192.168.0.1" ], "ip_cidr": [ "10.0.0.0/24", "192.168.0.1" ], "source_port": [ 12345 ], "source_port_range": [ "1000:2000", ":3000", "4000:" ], "port": [ 80, 443 ], "port_range": [ "1000:2000", ":3000", "4000:" ], "process_name": [ "curl" ], "process_path": [ "/usr/bin/curl" ], "process_path_regex": [ "^/usr/bin/.+" ], "package_name": [ "com.termux" ], "package_name_regex": [ "^com\\.termux.*" ], "network_type": [ "wifi" ], "network_is_expensive": false, "network_is_constrained": false, "network_interface_address": { "wifi": [ "2000::/3" ] }, "default_interface_address": [ "2000::/3" ], "wifi_ssid": [ "My WIFI" ], "wifi_bssid": [ "00:00:00:00:00:00" ], "invert": false }, { "type": "logical", "mode": "and", "rules": [], "invert": false } ] } ``` !!! note "" 当内容只有一项时,可以忽略 JSON 数组 [] 标签。 ### Default Fields !!! note "" 默认规则使用以下匹配逻辑: (`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `ip_cidr`) && (`port` || `port_range`) && (`source_port` || `source_port_range`) && `other fields` #### 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 `tcp` 或 `udp`。 #### domain 匹配完整域名。 #### domain_suffix 匹配域名后缀。 #### domain_keyword 匹配域名关键字。 #### domain_regex 匹配域名正则表达式。 #### source_ip_cidr 匹配源 IP CIDR。 #### ip_cidr 匹配 IP CIDR。 #### source_port 匹配源端口。 #### source_port_range 匹配源端口范围。 #### port 匹配端口。 #### port_range 匹配端口范围。 #### process_name !!! quote "" 仅支持 Linux、Windows 和 macOS。 匹配进程名称。 #### process_path !!! quote "" 仅支持 Linux、Windows 和 macOS. 匹配进程路径。 #### process_path_regex !!! question "自 sing-box 1.10.0 起" !!! quote "" 仅支持 Linux、Windows 和 macOS. 使用正则表达式匹配进程路径。 #### package_name 匹配 Android 应用包名。 #### package_name_regex !!! question "自 sing-box 1.14.0 起" 使用正则表达式匹配 Android 应用包名。 #### network_type !!! question "自 sing-box 1.11.0 起" !!! quote "" 仅在 Android 与 Apple 平台图形客户端中支持。 匹配网络类型。 Available values: `wifi`, `cellular`, `ethernet` and `other`. #### network_is_expensive !!! question "自 sing-box 1.11.0 起" !!! quote "" 仅在 Android 与 Apple 平台图形客户端中支持。 匹配如果网络被视为计费 (在 Android) 或被视为昂贵, 像蜂窝网络或个人热点 (在 Apple 平台)。 #### network_is_constrained !!! question "自 sing-box 1.11.0 起" !!! quote "" 仅在 Apple 平台图形客户端中支持。 匹配如果网络在低数据模式下。 #### network_interface_address !!! question "自 sing-box 1.13.0 起" !!! quote "" 仅在 Android 与 Apple 平台图形客户端中支持。 匹配网络接口(可用值同 `network_type`)地址。 #### default_interface_address !!! question "自 sing-box 1.13.0 起" !!! quote "" 仅支持 Linux、Windows 和 macOS. 匹配默认接口地址。 #### wifi_ssid !!! quote "" 仅在 Android 与 Apple 平台图形客户端中支持。 匹配 WiFi SSID。 #### wifi_bssid !!! quote "" 仅在 Android 与 Apple 平台图形客户端中支持。 #### invert 反选匹配结果。 ### 逻辑字段 #### type `logical` #### mode ==必填== `and` 或 `or` #### rules ==必填== 包括的规则。 ================================================ FILE: docs/configuration/rule-set/index.md ================================================ !!! 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` # rule-set !!! question "Since sing-box 1.8.0" ### Structure === "Inline" !!! question "Since sing-box 1.10.0" ```json { "type": "inline", // optional "tag": "", "rules": [] } ``` === "Local File" ```json { "type": "local", "tag": "", "format": "source", // or binary "path": "" } ``` === "Remote File" !!! info "" Remote rule-set will be cached if `experimental.cache_file.enabled`. ```json { "type": "remote", "tag": "", "format": "source", // or binary "url": "", "http_client": "", // or {} "update_interval": "", // Deprecated "download_detour": "" } ``` ### Fields #### type ==Required== Type of rule-set, `local` or `remote`. #### tag ==Required== Tag of rule-set. ### Inline Fields !!! question "Since sing-box 1.10.0" #### rules ==Required== List of [Headless Rule](./headless-rule/). ### Local or Remote Fields #### format ==Required== Format of rule-set file, `source` or `binary`. Optional when `path` or `url` uses `json` or `srs` as extension. ### Local Fields #### path ==Required== !!! note "" Will be automatically reloaded if file modified since sing-box 1.10.0. File path of rule-set. ### Remote Fields #### url ==Required== Download URL of rule-set. #### http_client !!! question "Since sing-box 1.14.0" HTTP Client for downloading rule-set. 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. ================================================ FILE: docs/configuration/rule-set/index.zh.md ================================================ !!! 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` # 规则集 !!! question "自 sing-box 1.8.0 起" ### 结构 === "内联" !!! question "自 sing-box 1.10.0 起" ```json { "type": "inline", // 可选 "tag": "", "rules": [] } ``` === "本地文件" ```json { "type": "local", "tag": "", "format": "source", // or binary "path": "" } ``` === "远程文件" !!! info "" 远程规则集将被缓存如果 `experimental.cache_file.enabled` 已启用。 ```json { "type": "remote", "tag": "", "format": "source", // or binary "url": "", "http_client": "", // 或 {} "update_interval": "", // 废弃的 "download_detour": "" } ``` ### 字段 #### type ==必填== 规则集类型, `local` 或 `remote`。 #### tag ==必填== 规则集的标签。 ### 内联字段 !!! question "自 sing-box 1.10.0 起" #### rules ==必填== 一组 [无头规则](./headless-rule/). ### 本地或远程字段 #### format ==必填== 规则集格式, `source` 或 `binary`。 当 `path` 或 `url` 使用 `json` 或 `srs` 作为扩展名时可选。 ### 本地字段 #### path ==必填== !!! note "" 自 sing-box 1.10.0 起,文件更改时将自动重新加载。 规则集的文件路径。 ### 远程字段 #### url ==必填== 规则集的下载 URL。 #### 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` 代替。 用于下载规则集的出站的标签。 ================================================ FILE: docs/configuration/rule-set/source-format.md ================================================ --- 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` !!! quote "Changes in sing-box 1.11.0" :material-plus: version `3` !!! quote "Changes in sing-box 1.10.0" :material-plus: version `2` !!! question "Since sing-box 1.8.0" ### Structure ```json { "version": 3, "rules": [] } ``` ### Compile Use `sing-box rule-set compile [--output .srs] .json` to compile source to binary rule-set. ### Fields #### version ==Required== Version of rule-set. * 1: sing-box 1.8.0: Initial rule-set version. * 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 ==Required== List of [Headless Rule](../headless-rule/). ================================================ FILE: docs/configuration/rule-set/source-format.zh.md ================================================ --- icon: material/new-box --- !!! quote "sing-box 1.14.0 中的更改" :material-plus: version `5` !!! quote "sing-box 1.13.0 中的更改" :material-plus: version `4` !!! quote "sing-box 1.11.0 中的更改" :material-plus: version `3` !!! quote "sing-box 1.10.0 中的更改" :material-plus: version `2` !!! question "自 sing-box 1.8.0 起" ### 结构 ```json { "version": 3, "rules": [] } ``` ### 编译 使用 `sing-box rule-set compile [--output .srs] .json` 以编译源文件为二进制规则集。 ### 字段 #### version ==必填== 规则集版本。 * 1: sing-box 1.8.0: 初始规则集版本。 * 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 ==必填== 一组 [无头规则](../headless-rule/). ================================================ FILE: docs/configuration/service/ccm.md ================================================ --- icon: material/new-box --- !!! question "Since sing-box 1.13.0" # CCM CCM (Claude Code Multiplexer) service is a multiplexing service that allows you to access your local Claude Code subscription remotely through custom tokens. It handles OAuth authentication with Claude's API on your local machine while allowing remote Claude Code to authenticate using Auth Tokens via the `ANTHROPIC_AUTH_TOKEN` environment variable. ### Structure ```json { "type": "ccm", ... // Listen Fields "credential_path": "", "usages_path": "", "users": [], "headers": {}, "detour": "", "tls": {} } ``` ### Listen Fields See [Listen Fields](/configuration/shared/listen/) for details. ### Fields #### credential_path Path to the Claude Code OAuth credentials file. If not specified, defaults to: - `$CLAUDE_CONFIG_DIR/.credentials.json` if `CLAUDE_CONFIG_DIR` environment variable is set - `~/.claude/.credentials.json` otherwise On macOS, credentials are read from the system keychain first, then fall back to the file if unavailable. Refreshed tokens are automatically written back to the same location. #### usages_path Path to the file for storing aggregated API usage statistics. Usage tracking is disabled if not specified. When enabled, the service tracks and saves comprehensive statistics including: - Request counts - Token usage (input, output, cache read, cache creation) - Calculated costs in USD based on Claude API pricing Statistics are organized by model, context window (200k standard vs 1M premium), and optionally by user when authentication is enabled. The statistics file is automatically saved every minute and upon service shutdown. #### users List of authorized users for token authentication. If empty, no authentication is required. Object format: ```json { "name": "", "token": "" } ``` Object fields: - `name`: Username identifier for tracking purposes. - `token`: Bearer token for authentication. Claude Code authenticates by setting the `ANTHROPIC_AUTH_TOKEN` environment variable to their token value. #### headers Custom HTTP headers to send to the Claude API. These headers will override any existing headers with the same name. #### detour Outbound tag for connecting to the Claude API. #### tls TLS configuration, see [TLS](/configuration/shared/tls/#inbound). ### Example #### Server ```json { "services": [ { "type": "ccm", "listen": "0.0.0.0", "listen_port": 8080, "usages_path": "./claude-usages.json", "users": [ { "name": "alice", "token": "ak-ccm-hello-world" }, { "name": "bob", "token": "ak-ccm-hello-bob" } ] } ] } ``` #### Client ```bash export ANTHROPIC_BASE_URL="http://127.0.0.1:8080" export ANTHROPIC_AUTH_TOKEN="ak-ccm-hello-world" claude ``` ================================================ FILE: docs/configuration/service/ccm.zh.md ================================================ --- icon: material/new-box --- !!! question "自 sing-box 1.13.0 起" # CCM CCM(Claude Code 多路复用器)服务是一个多路复用服务,允许您通过自定义令牌远程访问本地的 Claude Code 订阅。 它在本地机器上处理与 Claude API 的 OAuth 身份验证,同时允许远程 Claude Code 通过 `ANTHROPIC_AUTH_TOKEN` 环境变量使用认证令牌进行身份验证。 ### 结构 ```json { "type": "ccm", ... // 监听字段 "credential_path": "", "usages_path": "", "users": [], "headers": {}, "detour": "", "tls": {} } ``` ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/) 了解详情。 ### 字段 #### credential_path Claude Code OAuth 凭据文件的路径。 如果未指定,默认值为: - 如果设置了 `CLAUDE_CONFIG_DIR` 环境变量,则使用 `$CLAUDE_CONFIG_DIR/.credentials.json` - 否则使用 `~/.claude/.credentials.json` 在 macOS 上,首先从系统钥匙串读取凭据,如果不可用则回退到文件。 刷新的令牌会自动写回相同位置。 #### usages_path 用于存储聚合 API 使用统计信息的文件路径。 如果未指定,使用跟踪将被禁用。 启用后,服务会跟踪并保存全面的统计信息,包括: - 请求计数 - 令牌使用量(输入、输出、缓存读取、缓存创建) - 基于 Claude API 定价计算的美元成本 统计信息按模型、上下文窗口(200k 标准版 vs 1M 高级版)以及可选的用户(启用身份验证时)进行组织。 统计文件每分钟自动保存一次,并在服务关闭时保存。 #### users 用于令牌身份验证的授权用户列表。 如果为空,则不需要身份验证。 对象格式: ```json { "name": "", "token": "" } ``` 对象字段: - `name`:用于跟踪的用户名标识符。 - `token`:用于身份验证的 Bearer 令牌。Claude Code 通过设置 `ANTHROPIC_AUTH_TOKEN` 环境变量为其令牌值进行身份验证。 #### headers 发送到 Claude API 的自定义 HTTP 头。 这些头会覆盖同名的现有头。 #### detour 用于连接 Claude API 的出站标签。 #### tls TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。 ### 示例 #### 服务端 ```json { "services": [ { "type": "ccm", "listen": "0.0.0.0", "listen_port": 8080, "usages_path": "./claude-usages.json", "users": [ { "name": "alice", "token": "ak-ccm-hello-world" }, { "name": "bob", "token": "ak-ccm-hello-bob" } ] } ] } ``` #### 客户端 ```bash export ANTHROPIC_BASE_URL="http://127.0.0.1:8080" export ANTHROPIC_AUTH_TOKEN="ak-ccm-hello-world" claude ``` ================================================ FILE: docs/configuration/service/derp.md ================================================ --- icon: material/new-box --- !!! question "Since sing-box 1.12.0" # DERP DERP service is a Tailscale DERP server, similar to [derper](https://pkg.go.dev/tailscale.com/cmd/derper). ### Structure ```json { "type": "derp", ... // Listen Fields "tls": {}, "config_path": "", "verify_client_endpoint": [], "verify_client_url": [], "home": "", "mesh_with": [], "mesh_psk": "", "mesh_psk_file": "", "stun": {} } ``` ### Listen Fields See [Listen Fields](/configuration/shared/listen/) for details. ### Fields #### tls TLS configuration, see [TLS](/configuration/shared/tls/#inbound). #### config_path ==Required== Derper configuration file path. Example: `derper.key` #### verify_client_endpoint Tailscale endpoints tags to verify clients. #### verify_client_url URL to verify clients. Object format: ```json { "url": "", ... // HTTP Client Fields } ``` Setting Array value to a string `__URL__` is equivalent to configuring: ```json { "url": __URL__ } ``` #### home What to serve at the root path. It may be left empty (the default, for a default homepage), `blank` for a blank page, or a URL to redirect to #### mesh_with Mesh with other DERP servers. Object format: ```json { "server": "", "server_port": "", "host": "", "tls": {}, ... // Dial Fields } ``` Object fields: - `server`: **Required** DERP server address. - `server_port`: **Required** DERP server port. - `host`: Custom DERP hostname. - `tls`: [TLS](/configuration/shared/tls/#outbound) - `Dial Fields`: [Dial Fields](/configuration/shared/dial/) #### mesh_psk Pre-shared key for DERP mesh. #### mesh_psk_file Pre-shared key file for DERP mesh. #### stun STUN server listen options. Object format: ```json { "enabled": true, ... // Listen Fields } ``` Object fields: - `enabled`: **Required** Enable STUN server. - `listen`: **Required** STUN server listen address, default to `::`. - `listen_port`: **Required** STUN server listen port, default to `3478`. - `other Listen Fields`: [Listen Fields](/configuration/shared/listen/) Setting `stun` value to a number `__PORT__` is equivalent to configuring: ```json { "enabled": true, "listen_port": __PORT__ } ``` ================================================ FILE: docs/configuration/service/derp.zh.md ================================================ --- icon: material/new-box --- !!! question "自 sing-box 1.12.0 起" # DERP DERP 服务是一个 Tailscale DERP 服务器,类似于 [derper](https://pkg.go.dev/tailscale.com/cmd/derper)。 ### 结构 ```json { "type": "derp", ... // 监听字段 "tls": {}, "config_path": "", "verify_client_endpoint": [], "verify_client_url": [], "home": "", "mesh_with": [], "mesh_psk": "", "mesh_psk_file": "", "stun": {} } ``` ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/) 了解详情。 ### 字段 #### tls TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。 #### config_path ==必填== Derper 配置文件路径。 示例:`derper.key` #### verify_client_endpoint 用于验证客户端的 Tailscale 端点标签。 #### verify_client_url 用于验证客户端的 URL。 对象格式: ```json { "url": "", ... // HTTP 客户端字段 } ``` 将数组值设置为字符串 `__URL__` 等同于配置: ```json { "url": __URL__ } ``` #### home 在根路径提供的内容。可以留空(默认值,显示默认主页)、`blank` 显示空白页面,或一个重定向的 URL。 #### mesh_with 与其他 DERP 服务器组网。 对象格式: ```json { "server": "", "server_port": "", "host": "", "tls": {}, ... // 拨号字段 } ``` 对象字段: - `server`:**必填** DERP 服务器地址。 - `server_port`:**必填** DERP 服务器端口。 - `host`:自定义 DERP 主机名。 - `tls`:[TLS](/zh/configuration/shared/tls/#出站) - `拨号字段`:[拨号字段](/zh/configuration/shared/dial/) #### mesh_psk DERP 组网的预共享密钥。 #### mesh_psk_file DERP 组网的预共享密钥文件。 #### stun STUN 服务器监听选项。 对象格式: ```json { "enabled": true, ... // 监听字段 } ``` 对象字段: - `enabled`:**必填** 启用 STUN 服务器。 - `listen`:**必填** STUN 服务器监听地址,默认为 `::`。 - `listen_port`:**必填** STUN 服务器监听端口,默认为 `3478`。 - `其他监听字段`:[监听字段](/zh/configuration/shared/listen/) 将 `stun` 值设置为数字 `__PORT__` 等同于配置: ```json { "enabled": true, "listen_port": __PORT__ } ``` ================================================ FILE: docs/configuration/service/hysteria-realm.md ================================================ --- 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": {}, ... // HTTP2 Fields "users": [ { "name": "", "token": "", "max_realms": 0 } ] } ``` ### Listen Fields See [Listen Fields](/configuration/shared/listen/) for details. ### HTTP2 Fields See [HTTP2 Fields](/configuration/shared/http2/) 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. ================================================ FILE: docs/configuration/service/hysteria-realm.zh.md ================================================ --- 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": {}, ... // HTTP2 字段 "users": [ { "name": "", "token": "", "max_realms": 0 } ] } ``` ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/) 了解详情。 ### HTTP2 字段 参阅 [HTTP2 字段](/zh/configuration/shared/http2/) 了解详情。 ### 字段 #### 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 槽位数量上限。 ================================================ FILE: docs/configuration/service/index.md ================================================ --- icon: material/new-box --- !!! question "Since sing-box 1.12.0" # Service ### Structure ```json { "services": [ { "type": "", "tag": "" } ] } ``` ### Fields | 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 The tag of the endpoint. ================================================ FILE: docs/configuration/service/index.zh.md ================================================ --- icon: material/new-box --- !!! question "自 sing-box 1.12.0 起" # 服务 ### 结构 ```json { "services": [ { "type": "", "tag": "" } ] } ``` ### 字段 | 类型 | 格式 | |-------------------|---------------------------------------| | `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 端点的标签。 ================================================ FILE: docs/configuration/service/ocm.md ================================================ --- icon: material/new-box --- !!! question "Since sing-box 1.13.0" # OCM OCM (OpenAI Codex Multiplexer) service is a multiplexing service that allows you to access your local OpenAI Codex subscription remotely through custom tokens. It handles OAuth authentication with OpenAI's API on your local machine while allowing remote clients to authenticate using custom tokens. ### Structure ```json { "type": "ocm", ... // Listen Fields "credential_path": "", "usages_path": "", "users": [], "headers": {}, "detour": "", "tls": {} } ``` ### Listen Fields See [Listen Fields](/configuration/shared/listen/) for details. ### Fields #### credential_path Path to the OpenAI OAuth credentials file. If not specified, defaults to: - `$CODEX_HOME/auth.json` if `CODEX_HOME` environment variable is set - `~/.codex/auth.json` otherwise Refreshed tokens are automatically written back to the same location. #### usages_path Path to the file for storing aggregated API usage statistics. Usage tracking is disabled if not specified. When enabled, the service tracks and saves comprehensive statistics including: - Request counts - Token usage (input, output, cached) - Calculated costs in USD based on OpenAI API pricing Statistics are organized by model and optionally by user when authentication is enabled. The statistics file is automatically saved every minute and upon service shutdown. #### users List of authorized users for token authentication. If empty, no authentication is required. Object format: ```json { "name": "", "token": "" } ``` Object fields: - `name`: Username identifier for tracking purposes. - `token`: Bearer token for authentication. Clients authenticate by setting the `Authorization: Bearer ` header. #### headers Custom HTTP headers to send to the OpenAI API. These headers will override any existing headers with the same name. #### detour Outbound tag for connecting to the OpenAI API. #### tls TLS configuration, see [TLS](/configuration/shared/tls/#inbound). ### Example #### Server ```json { "services": [ { "type": "ocm", "listen": "127.0.0.1", "listen_port": 8080 } ] } ``` #### Client Add to `~/.codex/config.toml`: ```toml # profile = "ocm" # set as default profile [model_providers.ocm] name = "OCM Proxy" base_url = "http://127.0.0.1:8080/v1" supports_websockets = true [profiles.ocm] model_provider = "ocm" # model = "gpt-5.4" # if the latest model is not yet publicly released # model_reasoning_effort = "xhigh" ``` Then run: ```bash codex --profile ocm ``` ### Example with Authentication #### Server ```json { "services": [ { "type": "ocm", "listen": "0.0.0.0", "listen_port": 8080, "usages_path": "./codex-usages.json", "users": [ { "name": "alice", "token": "sk-ocm-hello-world" }, { "name": "bob", "token": "sk-ocm-hello-bob" } ] } ] } ``` #### Client Add to `~/.codex/config.toml`: ```toml # profile = "ocm" # set as default profile [model_providers.ocm] name = "OCM Proxy" base_url = "http://127.0.0.1:8080/v1" supports_websockets = true experimental_bearer_token = "sk-ocm-hello-world" [profiles.ocm] model_provider = "ocm" # model = "gpt-5.4" # if the latest model is not yet publicly released # model_reasoning_effort = "xhigh" ``` Then run: ```bash codex --profile ocm ``` ================================================ FILE: docs/configuration/service/ocm.zh.md ================================================ --- icon: material/new-box --- !!! question "自 sing-box 1.13.0 起" # OCM OCM(OpenAI Codex 多路复用器)服务是一个多路复用服务,允许您通过自定义令牌远程访问本地的 OpenAI Codex 订阅。 它在本地机器上处理与 OpenAI API 的 OAuth 身份验证,同时允许远程客户端使用自定义令牌进行身份验证。 ### 结构 ```json { "type": "ocm", ... // 监听字段 "credential_path": "", "usages_path": "", "users": [], "headers": {}, "detour": "", "tls": {} } ``` ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/) 了解详情。 ### 字段 #### credential_path OpenAI OAuth 凭据文件的路径。 如果未指定,默认值为: - 如果设置了 `CODEX_HOME` 环境变量,则使用 `$CODEX_HOME/auth.json` - 否则使用 `~/.codex/auth.json` 刷新的令牌会自动写回相同位置。 #### usages_path 用于存储聚合 API 使用统计信息的文件路径。 如果未指定,使用跟踪将被禁用。 启用后,服务会跟踪并保存全面的统计信息,包括: - 请求计数 - 令牌使用量(输入、输出、缓存) - 基于 OpenAI API 定价计算的美元成本 统计信息按模型以及可选的用户(启用身份验证时)进行组织。 统计文件每分钟自动保存一次,并在服务关闭时保存。 #### users 用于令牌身份验证的授权用户列表。 如果为空,则不需要身份验证。 对象格式: ```json { "name": "", "token": "" } ``` 对象字段: - `name`:用于跟踪的用户名标识符。 - `token`:用于身份验证的 Bearer 令牌。客户端通过设置 `Authorization: Bearer ` 头进行身份验证。 #### headers 发送到 OpenAI API 的自定义 HTTP 头。 这些头会覆盖同名的现有头。 #### detour 用于连接 OpenAI API 的出站标签。 #### tls TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。 ### 示例 #### 服务端 ```json { "services": [ { "type": "ocm", "listen": "127.0.0.1", "listen_port": 8080 } ] } ``` #### 客户端 在 `~/.codex/config.toml` 中添加: ```toml # profile = "ocm" # 设为默认配置 [model_providers.ocm] name = "OCM Proxy" base_url = "http://127.0.0.1:8080/v1" supports_websockets = true [profiles.ocm] model_provider = "ocm" # model = "gpt-5.4" # 如果最新模型尚未公开发布 # model_reasoning_effort = "xhigh" ``` 然后运行: ```bash codex --profile ocm ``` ### 带身份验证的示例 #### 服务端 ```json { "services": [ { "type": "ocm", "listen": "0.0.0.0", "listen_port": 8080, "usages_path": "./codex-usages.json", "users": [ { "name": "alice", "token": "sk-ocm-hello-world" }, { "name": "bob", "token": "sk-ocm-hello-bob" } ] } ] } ``` #### 客户端 在 `~/.codex/config.toml` 中添加: ```toml # profile = "ocm" # 设为默认配置 [model_providers.ocm] name = "OCM Proxy" base_url = "http://127.0.0.1:8080/v1" supports_websockets = true experimental_bearer_token = "sk-ocm-hello-world" [profiles.ocm] model_provider = "ocm" # model = "gpt-5.4" # 如果最新模型尚未公开发布 # model_reasoning_effort = "xhigh" ``` 然后运行: ```bash codex --profile ocm ``` ================================================ FILE: docs/configuration/service/resolved.md ================================================ --- icon: material/new-box --- !!! question "Since sing-box 1.12.0" # Resolved Resolved service is a fake systemd-resolved DBUS service to receive DNS settings from other programs (e.g. NetworkManager) and provide DNS resolution. See also: [Resolved DNS Server](/configuration/dns/server/resolved/) ### Structure ```json { "type": "resolved", ... // Listen Fields } ``` ### Listen Fields See [Listen Fields](/configuration/shared/listen/) for details. ### Fields #### listen ==Required== Listen address. `127.0.0.53` will be used by default. #### listen_port ==Required== Listen port. `53` will be used by default. ================================================ FILE: docs/configuration/service/resolved.zh.md ================================================ --- icon: material/new-box --- !!! question "自 sing-box 1.12.0 起" # Resolved Resolved 服务是一个伪造的 systemd-resolved DBUS 服务,用于从其他程序 (如 NetworkManager)接收 DNS 设置并提供 DNS 解析。 另请参阅:[Resolved DNS 服务器](/zh/configuration/dns/server/resolved/) ### 结构 ```json { "type": "resolved", ... // 监听字段 } ``` ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/) 了解详情。 ### 字段 #### listen ==必填== 监听地址。 默认使用 `127.0.0.53`。 #### listen_port ==必填== 监听端口。 默认使用 `53`。 ================================================ FILE: docs/configuration/service/ssm-api.md ================================================ --- icon: material/new-box --- !!! question "Since sing-box 1.12.0" # SSM API SSM API service is a RESTful API server for managing Shadowsocks servers. See https://github.com/Shadowsocks-NET/shadowsocks-specs/blob/main/2023-1-shadowsocks-server-management-api-v1.md ### Structure ```json { "type": "ssm-api", ... // Listen Fields "servers": {}, "cache_path": "", "tls": {} } ``` ### Listen Fields See [Listen Fields](/configuration/shared/listen/) for details. ### Fields #### servers ==Required== A mapping Object from HTTP endpoints to [Shadowsocks Inbound](/configuration/inbound/shadowsocks) tags. Selected Shadowsocks inbounds must be configured with [managed](/configuration/inbound/shadowsocks#managed) enabled. Example: ```json { "servers": { "/": "ss-in" } } ``` #### cache_path If set, when the server is about to stop, traffic and user state will be saved to the specified JSON file to be restored on the next startup. #### tls TLS configuration, see [TLS](/configuration/shared/tls/#inbound). ================================================ FILE: docs/configuration/service/ssm-api.zh.md ================================================ --- icon: material/new-box --- !!! question "自 sing-box 1.12.0 起" # SSM API SSM API 服务是一个用于管理 Shadowsocks 服务器的 RESTful API 服务器。 参阅 https://github.com/Shadowsocks-NET/shadowsocks-specs/blob/main/2023-1-shadowsocks-server-management-api-v1.md ### 结构 ```json { "type": "ssm-api", ... // 监听字段 "servers": {}, "cache_path": "", "tls": {} } ``` ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/) 了解详情。 ### 字段 #### servers ==必填== 从 HTTP 端点到 [Shadowsocks 入站](/zh/configuration/inbound/shadowsocks) 标签的映射对象。 选定的 Shadowsocks 入站必须配置启用 [managed](/zh/configuration/inbound/shadowsocks#managed)。 示例: ```json { "servers": { "/": "ss-in" } } ``` #### cache_path 如果设置,当服务器即将停止时,流量和用户状态将保存到指定的 JSON 文件中, 以便在下次启动时恢复。 #### tls TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。 ================================================ FILE: docs/configuration/shared/certificate-provider/acme.md ================================================ --- 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: [profile](#profile) :material-plus: [http_client](#http_client) # 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": "", "profile": "", "http_client": "" // or {} } ``` ### 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 | #### 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" HTTP Client for all provider HTTP requests. See [HTTP Client Fields](/configuration/shared/http-client/) for details. ================================================ FILE: docs/configuration/shared/certificate-provider/acme.zh.md ================================================ --- icon: material/new-box --- !!! quote "sing-box 1.14.0 中的更改" :material-plus: [account_key](#account_key) :material-plus: [key_type](#key_type) :material-plus: [profile](#profile) :material-plus: [http_client](#http_client) # 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": "", "profile": "", "http_client": "" // 或 {} } ``` ### 字段 #### 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 | #### profile !!! question "自 sing-box 1.14.0 起" 用于证书签发的 ACME profile。 当为空且 `provider` 为 Let's Encrypt 时,如果任意域名为 IP 地址,将自动使用 `shortlived`。 #### http_client !!! question "自 sing-box 1.14.0 起" 用于所有提供者 HTTP 请求的 HTTP 客户端。 参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。 所有提供者 HTTP 请求将使用此出站。 ================================================ FILE: docs/configuration/shared/certificate-provider/cloudflare-origin-ca.md ================================================ --- 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, "http_client": "" // or {} } ``` ### 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. #### http_client HTTP Client for all provider HTTP requests. See [HTTP Client Fields](/configuration/shared/http-client/) for details. ================================================ FILE: docs/configuration/shared/certificate-provider/cloudflare-origin-ca.zh.md ================================================ --- 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, "http_client": "" // 或 {} } ``` ### 字段 #### 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 年)。 #### http_client 用于所有提供者 HTTP 请求的 HTTP 客户端。 参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。 ================================================ FILE: docs/configuration/shared/certificate-provider/index.md ================================================ --- 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. ================================================ FILE: docs/configuration/shared/certificate-provider/index.zh.md ================================================ --- 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 证书提供者的标签。 ================================================ FILE: docs/configuration/shared/certificate-provider/tailscale.md ================================================ --- 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. ================================================ FILE: docs/configuration/shared/certificate-provider/tailscale.zh.md ================================================ --- 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)。 ================================================ FILE: docs/configuration/shared/dial.md ================================================ --- 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) :material-plus: [tcp_keep_alive](#tcp_keep_alive) :material-plus: [tcp_keep_alive_interval](#tcp_keep_alive_interval) :material-plus: [bind_address_no_port](#bind_address_no_port) !!! quote "Changes in sing-box 1.12.0" :material-plus: [domain_resolver](#domain_resolver) :material-delete-clock: [domain_strategy](#domain_strategy) :material-plus: [netns](#netns) !!! quote "Changes in sing-box 1.11.0" :material-plus: [network_strategy](#network_strategy) :material-alert: [fallback_delay](#fallback_delay) :material-alert: [network_type](#network_type) :material-alert: [fallback_network_type](#fallback_network_type) ### Structure ```json { "detour": "", "bind_interface": "", "inet4_bind_address": "", "inet6_bind_address": "", "bind_address_no_port": false, "routing_mark": 0, "reuse_addr": false, "netns": "", "connect_timeout": "", "tcp_fast_open": false, "tcp_multi_path": false, "disable_tcp_keep_alive": false, "tcp_keep_alive": "", "tcp_keep_alive_interval": "", "udp_fragment": false, "domain_resolver": "", // or {} "network_strategy": "", "network_type": [], "fallback_network_type": [], "fallback_delay": "", // Deprecated "domain_strategy": "" } ``` !!! note "" You can ignore the JSON Array [] tag when the content is only one item ### Fields #### detour The tag of the upstream outbound. If enabled, all other fields will be ignored. #### bind_interface The network interface to bind to. #### inet4_bind_address The IPv4 address to bind to. #### inet6_bind_address The IPv6 address to bind to. #### bind_address_no_port !!! question "Since sing-box 1.13.0" !!! quote "" Only supported on Linux. Do not reserve a port when binding to a source address. This allows reusing the same source port for multiple connections if the full 4-tuple (source IP, source port, destination IP, destination port) remains unique. #### routing_mark !!! quote "" Only supported on Linux. Set netfilter routing mark. Integers (e.g. `1234`) and string hexadecimals (e.g. `"0x1234"`) are supported. #### reuse_addr Reuse listener address. #### netns !!! question "Since sing-box 1.12.0" !!! quote "" Only supported on Linux. Set network namespace, name or path. #### connect_timeout Connect timeout, in golang's Duration format. A duration string is a possibly signed sequence of decimal numbers, each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". #### tcp_fast_open Enable TCP Fast Open. #### tcp_multi_path !!! warning "" Go 1.21 required. Enable TCP Multi Path. #### disable_tcp_keep_alive !!! question "Since sing-box 1.13.0" Disable TCP keep alive. #### tcp_keep_alive !!! question "Since sing-box 1.13.0" Default value changed from `10m` to `5m`. TCP keep alive initial period. `5m` will be used by default. #### tcp_keep_alive_interval !!! question "Since sing-box 1.13.0" TCP keep alive interval. `75s` will be used by default. #### udp_fragment Enable UDP fragmentation. #### domain_resolver !!! warning "" `outbound` DNS rule items are deprecated and will be removed in sing-box 1.14.0, so this item will be required for outbound/endpoints using domain name in server address since sing-box 1.14.0. !!! info "" `domain_resolver` or `route.default_domain_resolver` is optional when only one DNS server is configured. Set domain resolver to use for resolving 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. | Outbound/Endpoints | Effected domains | |--------------------|--------------------------| | `direct` | Domain in request | | others | Domain in server address | #### network_strategy !!! question "Since sing-box 1.11.0" !!! quote "" Only supported in graphical clients on Android and Apple platforms with `auto_detect_interface` enabled. Strategy for selecting network interfaces. Available values: - `default` (default): Connect to default network or networks specified in `network_type` sequentially. - `hybrid`: Connect to all networks or networks specified in `network_type` concurrently. - `fallback`: Connect to default network or preferred networks specified in `network_type` concurrently, and try fallback networks when unavailable or timeout. For fallback, when preferred interfaces fails or times out, it will enter a 15s fast fallback state (Connect to all preferred and fallback networks concurrently), and exit immediately if preferred networks recover. Conflicts with `bind_interface`, `inet4_bind_address` and `inet6_bind_address`. #### network_type !!! question "Since sing-box 1.11.0" !!! quote "" Only supported in graphical clients on Android and Apple platforms with `auto_detect_interface` enabled. Network types to use when using `default` or `hybrid` network strategy or preferred network types to use when using `fallback` network strategy. Available values: `wifi`, `cellular`, `ethernet`, `other`. Device's default network is used by default. #### fallback_network_type !!! question "Since sing-box 1.11.0" !!! quote "" Only supported in graphical clients on Android and Apple platforms with `auto_detect_interface` enabled. Fallback network types when preferred networks are unavailable or timeout when using `fallback` network strategy. All other networks expect preferred are used by default. #### fallback_delay !!! question "Since sing-box 1.11.0" !!! quote "" Only supported in graphical clients on Android and Apple platforms with `auto_detect_interface` enabled. The length of time to wait before spawning a RFC 6555 Fast Fallback connection. For `domain_strategy`, is the amount of time to wait for connection to succeed before assuming that IPv4/IPv6 is misconfigured and falling back to other type of addresses. For `network_strategy`, is the amount of time to wait for connection to succeed before falling back to other interfaces. Only take effect when `domain_strategy` or `network_strategy` is set. `300ms` is used by default. #### domain_strategy !!! failure "Deprecated in sing-box 1.12.0" `domain_strategy` is deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-outbound-domain-strategy-option-to-domain-resolver). Available values: `prefer_ipv4`, `prefer_ipv6`, `ipv4_only`, `ipv6_only`. If set, the requested domain name will be resolved to IP before connect. | Outbound | Effected domains | Fallback Value | |----------|--------------------------|-------------------------------------------| | `direct` | Domain in request | Take `inbound.domain_strategy` if not set | | others | Domain in server address | / | ================================================ FILE: docs/configuration/shared/dial.zh.md ================================================ --- 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) :material-plus: [tcp_keep_alive](#tcp_keep_alive) :material-plus: [tcp_keep_alive_interval](#tcp_keep_alive_interval) :material-plus: [bind_address_no_port](#bind_address_no_port) !!! quote "sing-box 1.12.0 中的更改" :material-plus: [domain_resolver](#domain_resolver) :material-delete-clock: [domain_strategy](#domain_strategy) :material-plus: [netns](#netns) !!! quote "sing-box 1.11.0 中的更改" :material-plus: [network_strategy](#network_strategy) :material-alert: [fallback_delay](#fallback_delay) :material-alert: [network_type](#network_type) :material-alert: [fallback_network_type](#fallback_network_type) ### 结构 ```json { "detour": "", "bind_interface": "", "inet4_bind_address": "", "inet6_bind_address": "", "bind_address_no_port": false, "routing_mark": 0, "reuse_addr": false, "netns": "", "connect_timeout": "", "tcp_fast_open": false, "tcp_multi_path": false, "disable_tcp_keep_alive": false, "tcp_keep_alive": "", "tcp_keep_alive_interval": "", "udp_fragment": false, "domain_resolver": "", // 或 {} "network_strategy": "", "network_type": [], "fallback_network_type": [], "fallback_delay": "", // 废弃的 "domain_strategy": "" } ``` !!! note "" 当内容只有一项时,可以忽略 JSON 数组 [] 标签 ### 字段 #### detour 上游出站的标签。 启用时,其他拨号字段将被忽略。 #### bind_interface 要绑定到的网络接口。 #### inet4_bind_address 要绑定的 IPv4 地址。 #### inet6_bind_address 要绑定的 IPv6 地址。 #### bind_address_no_port !!! question "自 sing-box 1.13.0 起" !!! quote "" 仅支持 Linux。 绑定到源地址时不保留端口。 这允许在完整的四元组(源 IP、源端口、目标 IP、目标端口)保持唯一的情况下,为多个连接复用同一源端口。 #### routing_mark !!! quote "" 仅支持 Linux。 设置 netfilter 路由标记。 支持数字 (如 `1234`) 和十六进制字符串 (如 `"0x1234"`)。 #### reuse_addr 重用监听地址。 #### netns !!! question "自 sing-box 1.12.0 起" !!! quote "" 仅支持 Linux。 设置网络命名空间,名称或路径。 #### connect_timeout 连接超时,采用 golang 的 Duration 格式。 持续时间字符串是一个可能有符号的序列十进制数,每个都有可选的分数和单位后缀, 例如 "300ms"、"-1.5h" 或 "2h45m"。 有效时间单位为 "ns"、"us"(或 "µs")、"ms"、"s"、"m"、"h"。 #### tcp_fast_open 启用 TCP Fast Open。 #### tcp_multi_path !!! warning "" 需要 Go 1.21。 启用 TCP Multi Path。 #### disable_tcp_keep_alive !!! question "自 sing-box 1.13.0 起" 禁用 TCP keep alive。 #### tcp_keep_alive !!! question "自 sing-box 1.13.0 起" 默认值从 `10m` 更改为 `5m`。 TCP keep alive 初始周期。 默认使用 `5m`。 #### tcp_keep_alive_interval !!! question "自 sing-box 1.13.0 起" TCP keep alive 间隔。 默认使用 `75s`。 #### udp_fragment 启用 UDP 分段。 #### domain_resolver !!! warning "" `outbound` DNS 规则项已弃用,且将在 sing-box 1.14.0 中被移除。因此,从 sing-box 1.14.0 版本开始,所有在服务器地址中使用域名的出站/端点均需配置此项。 !!! info "" 当只有一个 DNS 服务器已配置时,`domain_resolver` 或 `route.default_domain_resolver` 是可选的。 用于设置解析域名的域名解析器。 此选项的格式与 [路由 DNS 规则动作](/zh/configuration/dns/rule_action/#route) 相同,但不包含 `action` 字段。 若直接将此选项设置为字符串,则等同于设置该选项的 `server` 字段。 | 出站/端点 | 受影响的域名 | |----------------|---------------------------| | `direct` | 请求中的域名 | | 其他类型 | 服务器地址中的域名 | #### network_strategy !!! question "自 sing-box 1.11.0 起" !!! quote "" 仅在 Android 与 iOS 平台图形客户端中支持,并且需要 `route.auto_detect_interface`。 用于选择网络接口的策略。 可用值: - `default`(默认值):按顺序连接默认网络或 `network_type` 中指定的网络。 - `hybrid`:同时连接所有网络或 `network_type` 中指定的网络。 - `fallback`:同时连接默认网络或 `network_type` 中指定的首选网络,当不可用或超时时尝试回退网络。 对于回退模式,当首选接口失败或超时时, 将进入15秒的快速回退状态(同时连接所有首选和回退网络), 如果首选网络恢复,则立即退出。 与 `bind_interface`, `bind_inet4_address` 和 `bind_inet6_address` 冲突。 #### network_type !!! question "自 sing-box 1.11.0 起" !!! quote "" 仅在 Android 与 iOS 平台图形客户端中支持,并且需要 `route.auto_detect_interface`。 当使用 `default` 或 `hybrid` 网络策略时要使用的网络类型,或当使用 `fallback` 网络策略时要使用的首选网络类型。 可用值:`wifi`, `cellular`, `ethernet`, `other`。 默认使用设备默认网络。 #### fallback_network_type !!! question "自 sing-box 1.11.0 起" !!! quote "" 仅在 Android 与 iOS 平台图形客户端中支持,并且需要 `route.auto_detect_interface`。 当使用 `fallback` 网络策略时,在首选网络不可用或超时的情况下要使用的回退网络类型。 默认使用除首选网络外的所有其他网络。 #### fallback_delay 在生成 RFC 6555 快速回退连接之前等待的时间长度。 对于 `domain_strategy`,是在假设之前等待 IPv6 成功的时间量如果设置了 "prefer_ipv4",则 IPv6 配置错误并回退到 IPv4。 对于 `network_strategy`,对于 `network_strategy`,是在回退到其他接口之前等待连接成功的时间。 仅当 `domain_strategy` 或 `network_strategy` 已设置时生效。 默认使用 `300ms`。 #### domain_strategy !!! failure "已在 sing-box 1.12.0 废弃" `domain_strategy` 已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移出站域名策略选项到域名解析器)。 可选值:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。 如果设置,域名将在请求发出之前解析为 IP。 | 出站 | 受影响的域名 | 默认回退值 | |----------|-----------|---------------------------| | `direct` | 请求中的域名 | `inbound.domain_strategy` | | others | 服务器地址中的域名 | / | ================================================ FILE: docs/configuration/shared/dns01_challenge.md ================================================ --- 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) :material-plus: [cloudflare.zone_token](#zone_token) :material-plus: [acmedns](#acmedns) ### Structure ```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 ```json { "provider": "alidns", "access_key_id": "", "access_key_secret": "", "region_id": "", "security_token": "" } ``` ##### security_token !!! question "Since sing-box 1.13.0" The Security Token for STS temporary credentials. #### Cloudflare ```json { "provider": "cloudflare", "api_token": "", "zone_token": "" } ``` ##### zone_token !!! question "Since sing-box 1.13.0" Optional API token with `Zone:Read` permission. When provided, allows `api_token` to be scoped to a single zone. #### ACME-DNS !!! question "Since sing-box 1.13.0" ```json { "provider": "acmedns", "username": "", "password": "", "subdomain": "", "server_url": "" } ``` See [ACME-DNS](https://github.com/joohoi/acme-dns) for details. ================================================ FILE: docs/configuration/shared/dns01_challenge.zh.md ================================================ --- 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) :material-plus: [cloudflare.zone_token](#zone_token) :material-plus: [acmedns](#acmedns) ### 结构 ```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 ```json { "provider": "alidns", "access_key_id": "", "access_key_secret": "", "region_id": "", "security_token": "" } ``` ##### security_token !!! question "自 sing-box 1.13.0 起" 用于 STS 临时凭证的安全令牌。 #### Cloudflare ```json { "provider": "cloudflare", "api_token": "", "zone_token": "" } ``` ##### zone_token !!! question "自 sing-box 1.13.0 起" 具有 `Zone:Read` 权限的可选 API 令牌。 提供后可将 `api_token` 限定到单个区域。 #### ACME-DNS !!! question "自 sing-box 1.13.0 起" ```json { "provider": "acmedns", "username": "", "password": "", "subdomain": "", "server_url": "" } ``` 参阅 [ACME-DNS](https://github.com/joohoi/acme-dns)。 ================================================ FILE: docs/configuration/shared/http-client.md ================================================ --- 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. ================================================ FILE: docs/configuration/shared/http-client.zh.md ================================================ --- 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/) 了解详情。 ================================================ FILE: docs/configuration/shared/http2.md ================================================ --- 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. ================================================ FILE: docs/configuration/shared/http2.zh.md ================================================ --- 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 每个连接的最大并发流数。 ================================================ FILE: docs/configuration/shared/listen.md ================================================ --- icon: material/new-box --- !!! quote "Changes in sing-box 1.13.0" :material-plus: [disable_tcp_keep_alive](#disable_tcp_keep_alive) :material-alert: [tcp_keep_alive](#tcp_keep_alive) !!! quote "Changes in sing-box 1.12.0" :material-plus: [netns](#netns) :material-plus: [bind_interface](#bind_interface) :material-plus: [routing_mark](#routing_mark) :material-plus: [reuse_addr](#reuse_addr) !!! quote "Changes in sing-box 1.11.0" :material-delete-clock: [sniff](#sniff) :material-delete-clock: [sniff_override_destination](#sniff_override_destination) :material-delete-clock: [sniff_timeout](#sniff_timeout) :material-delete-clock: [domain_strategy](#domain_strategy) :material-delete-clock: [udp_disable_domain_unmapping](#udp_disable_domain_unmapping) ### Structure ```json { "listen": "", "listen_port": 0, "bind_interface": "", "routing_mark": 0, "reuse_addr": false, "netns": "", "tcp_fast_open": false, "tcp_multi_path": false, "disable_tcp_keep_alive": false, "tcp_keep_alive": "", "tcp_keep_alive_interval": "", "udp_fragment": false, "udp_timeout": "", "detour": "", // Deprecated "sniff": false, "sniff_override_destination": false, "sniff_timeout": "", "domain_strategy": "", "udp_disable_domain_unmapping": false } ``` ### Fields #### listen ==Required== Listen address. #### listen_port Listen port. #### bind_interface !!! question "Since sing-box 1.12.0" The network interface to bind to. #### routing_mark !!! question "Since sing-box 1.12.0" !!! quote "" Only supported on Linux. Set netfilter routing mark. Integers (e.g. `1234`) and string hexadecimals (e.g. `"0x1234"`) are supported. #### reuse_addr !!! question "Since sing-box 1.12.0" Reuse listener address. #### netns !!! question "Since sing-box 1.12.0" !!! quote "" Only supported on Linux. Set network namespace, name or path. #### tcp_fast_open Enable TCP Fast Open. #### tcp_multi_path !!! warning "" Go 1.21 required. Enable TCP Multi Path. #### disable_tcp_keep_alive !!! question "Since sing-box 1.13.0" Disable TCP keep alive. #### tcp_keep_alive !!! question "Since sing-box 1.13.0" Default value changed from `10m` to `5m`. TCP keep alive initial period. `5m` will be used by default. #### tcp_keep_alive_interval TCP keep alive interval. `75s` will be used by default. #### udp_fragment Enable UDP fragmentation. #### udp_timeout UDP NAT expiration time. `5m` will be used by default. #### detour If set, connections will be forwarded to the specified inbound. Requires target inbound support, see [Injectable](/configuration/inbound/#fields). #### sniff !!! failure "Deprecated in sing-box 1.11.0" Inbound fields are deprecated and will be removed in sing-box 1.13.0, check [Migration](/migration/#migrate-legacy-inbound-fields-to-rule-actions). Enable sniffing. See [Protocol Sniff](/configuration/route/sniff/) for details. #### sniff_override_destination !!! failure "Deprecated in sing-box 1.11.0" Inbound fields are deprecated and will be removed in sing-box 1.13.0. Override the connection destination address with the sniffed domain. If the domain name is invalid (like tor), this will not work. #### sniff_timeout !!! failure "Deprecated in sing-box 1.11.0" Inbound fields are deprecated and will be removed in sing-box 1.13.0, check [Migration](/migration/#migrate-legacy-inbound-fields-to-rule-actions). Timeout for sniffing. `300ms` is used by default. #### domain_strategy !!! failure "Deprecated in sing-box 1.11.0" Inbound fields are deprecated and will be removed in sing-box 1.13.0, check [Migration](/migration/#migrate-legacy-inbound-fields-to-rule-actions). One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`. If set, the requested domain name will be resolved to IP before routing. If `sniff_override_destination` is in effect, its value will be taken as a fallback. #### udp_disable_domain_unmapping !!! failure "Deprecated in sing-box 1.11.0" Inbound fields are deprecated and will be removed in sing-box 1.13.0, check [Migration](/migration/#migrate-legacy-inbound-fields-to-rule-actions). If enabled, for UDP proxy requests addressed to a domain, the original packet address will be sent in the response instead of the mapped domain. This option is used for compatibility with clients that do not support receiving UDP packets with domain addresses, such as Surge. ================================================ FILE: docs/configuration/shared/listen.zh.md ================================================ --- icon: material/new-box --- !!! quote "sing-box 1.13.0 中的更改" :material-plus: [disable_tcp_keep_alive](#disable_tcp_keep_alive) :material-alert: [tcp_keep_alive](#tcp_keep_alive) !!! quote "sing-box 1.12.0 中的更改" :material-plus: [netns](#netns) :material-plus: [bind_interface](#bind_interface) :material-plus: [routing_mark](#routing_mark) :material-plus: [reuse_addr](#reuse_addr) !!! quote "sing-box 1.11.0 中的更改" :material-delete-clock: [sniff](#sniff) :material-delete-clock: [sniff_override_destination](#sniff_override_destination) :material-delete-clock: [sniff_timeout](#sniff_timeout) :material-delete-clock: [domain_strategy](#domain_strategy) :material-delete-clock: [udp_disable_domain_unmapping](#udp_disable_domain_unmapping) ### 结构 ```json { "listen": "", "listen_port": 0, "bind_interface": "", "routing_mark": 0, "reuse_addr": false, "netns": "", "tcp_fast_open": false, "tcp_multi_path": false, "disable_tcp_keep_alive": false, "tcp_keep_alive": "", "tcp_keep_alive_interval": "", "udp_fragment": false, "udp_timeout": "", "detour": "", // 废弃的 "sniff": false, "sniff_override_destination": false, "sniff_timeout": "", "domain_strategy": "", "udp_disable_domain_unmapping": false } ``` ### 字段 #### listen ==必填== 监听地址。 #### listen_port 监听端口。 #### bind_interface !!! question "自 sing-box 1.12.0 起" 要绑定到的网络接口。 #### routing_mark !!! question "自 sing-box 1.12.0 起" !!! quote "" 仅支持 Linux。 设置 netfilter 路由标记。 支持数字 (如 `1234`) 和十六进制字符串 (如 `"0x1234"`)。 #### reuse_addr !!! question "自 sing-box 1.12.0 起" 重用监听地址。 #### netns !!! question "自 sing-box 1.12.0 起" !!! quote "" 仅支持 Linux。 设置网络命名空间,名称或路径。 #### tcp_fast_open 启用 TCP Fast Open。 #### tcp_multi_path !!! warning "" 需要 Go 1.21。 启用 TCP Multi Path。 #### disable_tcp_keep_alive !!! question "自 sing-box 1.13.0 起" 禁用 TCP keep alive。 #### tcp_keep_alive !!! question "自 sing-box 1.13.0 起" 默认值从 `10m` 更改为 `5m`。 TCP keep alive 初始周期。 默认使用 `5m`。 #### tcp_keep_alive_interval TCP keep alive 间隔。 默认使用 `75s`。 #### udp_fragment 启用 UDP 分段。 #### udp_timeout UDP NAT 过期时间。 默认使用 `5m`。 #### detour 如果设置,连接将被转发到指定的入站。 需要目标入站支持,参阅 [注入支持](/zh/configuration/inbound/#字段)。 #### sniff !!! failure "已在 sing-box 1.11.0 废弃" 入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作). 启用协议探测。 参阅 [协议探测](/zh/configuration/route/sniff/) #### sniff_override_destination !!! failure "已在 sing-box 1.11.0 废弃" 入站字段已废弃且将在 sing-box 1.12.0 中被移除。 用探测出的域名覆盖连接目标地址。 如果域名无效(如 Tor),将不生效。 #### sniff_timeout !!! failure "已在 sing-box 1.11.0 废弃" 入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作). 探测超时时间。 默认使用 300ms。 #### domain_strategy !!! failure "已在 sing-box 1.11.0 废弃" 入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作). 可选值: `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。 如果设置,请求的域名将在路由之前解析为 IP。 如果 `sniff_override_destination` 生效,它的值将作为后备。 #### udp_disable_domain_unmapping !!! failure "已在 sing-box 1.11.0 废弃" 入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作). 如果启用,对于地址为域的 UDP 代理请求,将在响应中发送原始包地址而不是映射的域。 此选项用于兼容不支持接收带有域地址的 UDP 包的客户端,如 Surge。 ================================================ FILE: docs/configuration/shared/multiplex.md ================================================ ### Inbound ```json { "enabled": true, "padding": false, "brutal": {} } ``` ### Outbound ```json { "enabled": true, "protocol": "smux", "max_connections": 4, "min_streams": 4, "max_streams": 0, "padding": false, "brutal": {} } ``` ### Inbound Fields #### enabled Enable multiplex support. #### padding If enabled, non-padded connections will be rejected. #### brutal See [TCP Brutal](/configuration/shared/tcp-brutal/) for details. ### Outbound Fields #### enabled Enable multiplex. #### protocol Multiplex protocol. | Protocol | Description | |----------|------------------------------------| | smux | https://github.com/xtaci/smux | | yamux | https://github.com/hashicorp/yamux | | h2mux | https://golang.org/x/net/http2 | h2mux is used by default. #### max_connections Maximum connections. Conflict with `max_streams`. #### min_streams Minimum multiplexed streams in a connection before opening a new connection. Conflict with `max_streams`. #### max_streams Maximum multiplexed streams in a connection before opening a new connection. Conflict with `max_connections` and `min_streams`. #### padding !!! info Requires sing-box server version 1.3-beta9 or later. Enable padding. #### brutal See [TCP Brutal](/configuration/shared/tcp-brutal/) for details. ================================================ FILE: docs/configuration/shared/multiplex.zh.md ================================================ ### 入站 ```json { "enabled": true, "padding": false, "brutal": {} } ``` ### 出站 ```json { "enabled": true, "protocol": "smux", "max_connections": 4, "min_streams": 4, "max_streams": 0, "padding": false, "brutal": {} } ``` ### 入站字段 #### enabled 启用多路复用支持。 #### padding 如果启用,将拒绝非填充连接。 #### brutal 参阅 [TCP Brutal](/zh/configuration/shared/tcp-brutal/)。 ### 出站字段 #### enabled 启用多路复用。 #### protocol 多路复用协议 | 协议 | 描述 | |-------|------------------------------------| | smux | https://github.com/xtaci/smux | | yamux | https://github.com/hashicorp/yamux | | h2mux | https://golang.org/x/net/http2 | 默认使用 h2mux。 #### max_connections 最大连接数量。 与 `max_streams` 冲突。 #### min_streams 在打开新连接之前,连接中的最小多路复用流数量。 与 `max_streams` 冲突。 #### max_streams 在打开新连接之前,连接中的最大多路复用流数量。 与 `max_connections` 和 `min_streams` 冲突。 #### padding !!! info 需要 sing-box 服务器版本 1.3-beta9 或更高。 启用填充。 #### brutal 参阅 [TCP Brutal](/zh/configuration/shared/tcp-brutal/)。 ================================================ FILE: docs/configuration/shared/neighbor.md ================================================ --- 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 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 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. ================================================ FILE: docs/configuration/shared/neighbor.zh.md ================================================ --- 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 原生支持,无需特殊设置。 主机名解析需要 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) 了解互联网共享设置。 ================================================ FILE: docs/configuration/shared/pre-match.md ================================================ --- icon: material/new-box --- # Pre-match !!! quote "Changes in sing-box 1.13.0" :material-plus: [bypass](#bypass) Pre-match is rule matching that runs before the connection is established. ### How it works When TUN receives a connection request, the connection has not yet been established, so no connection data can be read. In this phase, sing-box runs the routing rules in pre-match mode. Since connection data is unavailable, only actions that do not require connection data can be executed. When a rule matches an action that requires an established connection, pre-match stops at that rule. ### Supported actions #### reject Reject with TCP RST / ICMP unreachable. See [reject](/configuration/route/rule_action/#reject) for details. #### route Route ICMP connections to the specified outbound for direct reply. See [route](/configuration/route/rule_action/#route) for details. #### bypass !!! question "Since sing-box 1.13.0" !!! quote "" Only supported on Linux with `auto_redirect` enabled. Bypass sing-box and connect directly at kernel level. If `outbound` is not specified, the rule only matches in pre-match from auto redirect, and will be skipped in other contexts. For all other contexts, bypass with `outbound` behaves like `route` action. See [bypass](/configuration/route/rule_action/#bypass) for details. ================================================ FILE: docs/configuration/shared/pre-match.zh.md ================================================ --- icon: material/new-box --- # 预匹配 !!! quote "sing-box 1.13.0 中的更改" :material-plus: [bypass](#bypass) 预匹配是在连接建立之前运行的规则匹配。 ### 工作原理 当 TUN 收到连接请求时,连接尚未建立,因此无法读取连接数据。在此阶段,sing-box 在预匹配模式下运行路由规则。 由于连接数据不可用,只有不需要连接数据的动作才能执行。当规则匹配到需要已建立连接的动作时,预匹配将在该规则处停止。 ### 支持的动作 #### reject 以 TCP RST / ICMP 不可达拒绝。 详情参阅 [reject](/zh/configuration/route/rule_action/#reject)。 #### route 将 ICMP 连接路由到指定出站以直接回复。 详情参阅 [route](/zh/configuration/route/rule_action/#route)。 #### bypass !!! question "自 sing-box 1.13.0 起" !!! quote "" 仅支持 Linux,且需要启用 `auto_redirect`。 在内核层面绕过 sing-box 直接连接。 如果未指定 `outbound`,规则仅在来自 auto redirect 的预匹配中匹配,在其他场景中将被跳过。 对于其他所有场景,指定了 `outbound` 的 bypass 行为与 `route` 相同。 详情参阅 [bypass](/zh/configuration/route/rule_action/#bypass)。 ================================================ FILE: docs/configuration/shared/quic.md ================================================ --- 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. ================================================ FILE: docs/configuration/shared/quic.zh.md ================================================ --- 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/) 了解详情。 ================================================ FILE: docs/configuration/shared/tcp-brutal.md ================================================ ### Server Requirements * Linux * `brutal` congestion control algorithm kernel module installed See [tcp-brutal](https://github.com/apernet/tcp-brutal) for details. ### Structure ```json { "enabled": true, "up_mbps": 100, "down_mbps": 100 } ``` ### Fields #### enabled Enable TCP Brutal congestion control algorithm。 #### up_mbps, down_mbps ==Required== Upload and download bandwidth, in Mbps. ================================================ FILE: docs/configuration/shared/tcp-brutal.zh.md ================================================ ### 服务器要求 * Linux * `brutal` 拥塞控制算法内核模块已安装 参阅 [tcp-brutal](https://github.com/apernet/tcp-brutal)。 ### 结构 ```json { "enabled": true, "up_mbps": 100, "down_mbps": 100 } ``` ### 字段 #### enabled 启用 TCP Brutal 拥塞控制算法。 #### up_mbps, down_mbps ==必填== 上传和下载带宽,以 Mbps 为单位。 ================================================ FILE: docs/configuration/shared/tls.md ================================================ --- 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-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" :material-plus: [kernel_tx](#kernel_tx) :material-plus: [kernel_rx](#kernel_rx) :material-plus: [curve_preferences](#curve_preferences) :material-plus: [certificate_public_key_sha256](#certificate_public_key_sha256) :material-plus: [client_certificate](#client_certificate) :material-plus: [client_certificate_path](#client_certificate_path) :material-plus: [client_key](#client_key) :material-plus: [client_key_path](#client_key_path) :material-plus: [client_authentication](#client_authentication) :material-plus: [client_certificate_public_key_sha256](#client_certificate_public_key_sha256) :material-plus: [ech.query_server_name](#query_server_name) !!! quote "Changes in sing-box 1.12.0" :material-plus: [fragment](#fragment) :material-plus: [fragment_fallback_delay](#fragment_fallback_delay) :material-plus: [record_fragment](#record_fragment) :material-delete-clock: [ech.pq_signature_schemes_enabled](#pq_signature_schemes_enabled) :material-delete-clock: [ech.dynamic_record_sizing_disabled](#dynamic_record_sizing_disabled) !!! quote "Changes in sing-box 1.10.0" :material-alert-decagram: [utls](#utls) ### Inbound ```json { "enabled": true, "server_name": "", "alpn": [], "min_version": "", "max_version": "", "cipher_suites": [], "curve_preferences": [], "certificate": [], "certificate_path": "", "client_authentication": "", "client_certificate": [], "client_certificate_path": [], "client_certificate_public_key_sha256": [], "key": [], "key_path": "", "kernel_tx": false, "kernel_rx": false, "handshake_timeout": "", "certificate_provider": "", // Deprecated "acme": { "domain": [], "data_directory": "", "default_server_name": "", "email": "", "provider": "", "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": {} }, "ech": { "enabled": false, "key": [], "key_path": "", // Deprecated "pq_signature_schemes_enabled": false, "dynamic_record_sizing_disabled": false }, "reality": { "enabled": false, "handshake": { "server": "google.com", "server_port": 443, ... // Dial Fields }, "private_key": "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc", "short_id": [ "0123456789abcdef" ], "max_time_difference": "1m" } } ``` ### Outbound ```json { "enabled": true, "engine": "", "disable_sni": false, "server_name": "", "insecure": false, "alpn": [], "min_version": "", "max_version": "", "cipher_suites": [], "curve_preferences": [], "certificate": "", "certificate_path": "", "certificate_public_key_sha256": [], "client_certificate": [], "client_certificate_path": "", "client_key": [], "client_key_path": "", "fragment": false, "fragment_fallback_delay": "", "record_fragment": false, "spoof": "", "spoof_method": "", "kernel_tx": false, "kernel_rx": false, "handshake_timeout": "", "ech": { "enabled": false, "config": [], "config_path": "", "query_server_name": "", // Deprecated "pq_signature_schemes_enabled": false, "dynamic_record_sizing_disabled": false }, "utls": { "enabled": false, "fingerprint": "" }, "reality": { "enabled": false, "public_key": "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0", "short_id": "0123456789abcdef" } } ``` TLS version values: * `1.0` * `1.1` * `1.2` * `1.3` Cipher suite values: * `TLS_RSA_WITH_AES_128_CBC_SHA` * `TLS_RSA_WITH_AES_256_CBC_SHA` * `TLS_RSA_WITH_AES_128_GCM_SHA256` * `TLS_RSA_WITH_AES_256_GCM_SHA384` * `TLS_AES_128_GCM_SHA256` * `TLS_AES_256_GCM_SHA384` * `TLS_CHACHA20_POLY1305_SHA256` * `TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA` * `TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA` * `TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA` * `TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA` * `TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256` * `TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384` * `TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256` * `TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384` * `TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256` * `TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256` !!! note "" You can ignore the JSON Array [] tag when the content is only one item ### Fields #### enabled Enable TLS. #### engine !!! question "Since sing-box 1.14.0" ==Client only== TLS engine to use. Values: * `go` (default) * `apple` * `windows` 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` !!! 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`. The default version range is TLS 1.2 to TLS 1.3, matching the `go` engine. 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== Do not send server name in ClientHello. #### server_name Used to verify the hostname on the returned certificates unless insecure is given. It is also included in the client's handshake to support virtual hosting unless it is an IP address. #### insecure ==Client only== Accepts any server certificate. #### alpn List of supported application level protocols, in order of preference. If both peers support ALPN, the selected protocol will be one from this list, and the connection will fail if there is no mutually supported protocol. See [Application-Layer Protocol Negotiation](https://en.wikipedia.org/wiki/Application-Layer_Protocol_Negotiation). #### min_version The minimum TLS version that is acceptable. By default, TLS 1.2 is currently used as the minimum when acting as a client, and TLS 1.0 when acting as a server. #### max_version The maximum TLS version that is acceptable. By default, the maximum version is currently TLS 1.3. #### cipher_suites List of enabled TLS 1.0–1.2 cipher suites. The order of the list is ignored. Note that TLS 1.3 cipher suites are not configurable. If empty, a safe default list is used. The default cipher suites might change over time. #### curve_preferences !!! question "Since sing-box 1.13.0" Set of supported key exchange mechanisms. The order of the list is ignored, and key exchange mechanisms are chosen from this list using an internal preference order by Golang. Available values, also the default list: * `P256` * `P384` * `P521` * `X25519` * `X25519MLKEM768` #### certificate Server certificates chain line array, in PEM format. #### certificate_path !!! note "" Will be automatically reloaded if file modified. The path to server certificate chain, in PEM format. #### certificate_public_key_sha256 !!! question "Since sing-box 1.13.0" ==Client only== List of SHA-256 hashes of server certificate public keys, in base64 format. To generate the SHA-256 hash for a certificate's public key, use the following commands: ```bash # For a certificate file openssl x509 -in certificate.pem -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 # For a certificate from a remote server echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 ``` #### client_certificate !!! question "Since sing-box 1.13.0" ==Client only== Client certificate chain line array, in PEM format. #### client_certificate_path !!! question "Since sing-box 1.13.0" ==Client only== The path to client certificate chain, in PEM format. #### client_key !!! question "Since sing-box 1.13.0" ==Client only== Client private key line array, in PEM format. #### client_key_path !!! question "Since sing-box 1.13.0" ==Client only== The path to client private key, in PEM format. #### key ==Server only== The server private key line array, in PEM format. #### key_path ==Server only== !!! note "" Will be automatically reloaded if file modified. The path to the server private key, in PEM format. #### client_authentication !!! question "Since sing-box 1.13.0" ==Server only== The type of client authentication to use. Available values: * `no` (default) * `request` * `require-any` * `verify-if-given` * `require-and-verify` One of `client_certificate`, `client_certificate_path`, or `client_certificate_public_key_sha256` is required if this option is set to `verify-if-given`, or `require-and-verify`. #### client_certificate !!! question "Since sing-box 1.13.0" ==Server only== Client certificate chain line array, in PEM format. #### client_certificate_path !!! question "Since sing-box 1.13.0" ==Server only== !!! note "" Will be automatically reloaded if file modified. List of path to client certificate chain, in PEM format. #### client_certificate_public_key_sha256 !!! question "Since sing-box 1.13.0" ==Server only== List of SHA-256 hashes of client certificate public keys, in base64 format. To generate the SHA-256 hash for a certificate's public key, use the following commands: ```bash # For a certificate file openssl x509 -in certificate.pem -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 # For a certificate from a remote server echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 ``` #### kernel_tx !!! question "Since sing-box 1.13.0" !!! quote "" Only supported on Linux 5.1+, use a newer kernel if possible. !!! quote "" Only TLS 1.3 is supported. !!! warning "" kTLS TX may only improve performance when `splice(2)` is available (both ends must be TCP or TLS without additional protocols after handshake); otherwise, it will definitely degrade performance. Enable kernel TLS transmit support. #### kernel_rx !!! question "Since sing-box 1.13.0" !!! quote "" Only supported on Linux 5.1+, use a newer kernel if possible. !!! quote "" Only TLS 1.3 is supported. !!! failure "" kTLS RX will definitely degrade performance even if `splice(2)` is in use, so enabling it is not recommended. 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" ==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" Only ECH is supported in QUIC. #### utls ==Client only== !!! failure "Not Recommended" uTLS has had repeated fingerprinting vulnerabilities discovered by researchers. uTLS is a Go library that attempts to imitate browser TLS fingerprints by copying ClientHello structure. However, browsers use completely different TLS stacks (Chrome uses BoringSSL, Firefox uses NSS) with distinct implementation behaviors that cannot be replicated by simply copying the handshake format, making detection possible. Additionally, the library lacks active maintenance and has poor code quality, making it unsuitable for censorship circumvention. For TLS fingerprint resistance, use [NaiveProxy](/configuration/inbound/naive/) instead. uTLS is a fork of "crypto/tls", which provides ClientHello fingerprinting resistance. Available fingerprint values: !!! warning "Removed since sing-box 1.10.0" Some legacy chrome fingerprints have been removed and will fallback to chrome: :material-close: chrome_psk :material-close: chrome_psk_shuffle :material-close: chrome_padding_psk_shuffle :material-close: chrome_pq :material-close: chrome_pq_psk * chrome * firefox * edge * safari * 360 * qq * ios * android * random * randomized Chrome fingerprint will be used if empty. ### ECH Fields ECH (Encrypted Client Hello) is a TLS extension that allows a client to encrypt the first part of its ClientHello message. The ECH key and configuration can be generated by `sing-box generate ech-keypair`. #### pq_signature_schemes_enabled !!! failure "Deprecated in sing-box 1.12.0" `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. #### dynamic_record_sizing_disabled !!! failure "Deprecated in sing-box 1.12.0" `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. When true, the largest possible TLS record size is always used. When false, the size of TLS records may be adjusted in an attempt to improve latency. #### key ==Server only== ECH key line array, in PEM format. #### key_path ==Server only== !!! note "" Will be automatically reloaded if file modified. The path to ECH key, in PEM format. #### config ==Client only== ECH configuration line array, in PEM format. If empty, load from DNS will be attempted. #### config_path ==Client only== The path to ECH configuration, in PEM format. If empty, load from DNS will be attempted. #### query_server_name !!! question "Since sing-box 1.13.0" ==Client only== Overrides the domain name used for ECH HTTPS record queries. If empty, `server_name` is used for queries. #### fragment !!! question "Since sing-box 1.12.0" ==Client only== Fragment TLS handshakes to bypass firewalls. This feature is intended to circumvent simple firewalls based on **plaintext packet matching**, and should not be used to circumvent real censorship. Due to poor performance, try `record_fragment` first, and only apply to server names known to be blocked. On Linux, Apple platforms, (administrator privileges required) Windows, the wait time can be automatically detected. Otherwise, it will fall back to waiting for a fixed time specified by `fragment_fallback_delay`. In addition, if the actual wait time is less than 20ms, it will also fall back to waiting for a fixed time, because the target is considered to be local or behind a transparent proxy. #### fragment_fallback_delay !!! question "Since sing-box 1.12.0" ==Client only== The fallback value used when TLS segmentation cannot automatically determine the wait time. `500ms` is used by default. #### record_fragment !!! question "Since sing-box 1.12.0" ==Client only== Fragment TLS handshake into multiple TLS records to bypass firewalls. #### spoof !!! question "Since sing-box 1.14.0" ==Client only, Linux/macOS/Windows only, requires elevated privileges== Inject a forged TLS ClientHello carrying a whitelisted SNI before the real one, to fool SNI-filtering middleboxes that permit specific hostnames. The forged segment is a copy of the real ClientHello with only the SNI value replaced by the value of this field, so TLS fingerprinting cannot distinguish it from the real one. The receiving server drops the forged segment (see `spoof_method`) while the middlebox treats it as a legitimate session. Requires raw-socket access (`CAP_NET_RAW` on Linux, root on macOS); on Linux, `CAP_NET_ADMIN` is additionally required because the send sequence number is read via `TCP_REPAIR`. On Windows, Administrator is required to install the embedded WinDivert kernel driver on first use. Windows on ARM64 is not supported. #### spoof_method !!! question "Since sing-box 1.14.0" ==Client only== 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. | | `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 !!! 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. ACME will be disabled if empty. #### 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 | #### 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. ### Reality Fields #### handshake ==Server only== ==Required== Handshake server address and [Dial Fields](/configuration/shared/dial/). #### private_key ==Server only== ==Required== Private key, generated by `sing-box generate reality-keypair`. #### public_key ==Client only== ==Required== Public key, generated by `sing-box generate reality-keypair`. #### short_id ==Required== A hexadecimal string with zero to eight digits. #### max_time_difference ==Server only== The maximum time difference between the server and the client. Check disabled if empty. ================================================ FILE: docs/configuration/shared/tls.zh.md ================================================ --- icon: material/new-box --- !!! quote "sing-box 1.14.0 中的更改" :material-plus: [certificate_provider](#certificate_provider) :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 中的更改" :material-plus: [kernel_tx](#kernel_tx) :material-plus: [kernel_rx](#kernel_rx) :material-plus: [curve_preferences](#curve_preferences) :material-plus: [certificate_public_key_sha256](#certificate_public_key_sha256) :material-plus: [client_certificate](#client_certificate) :material-plus: [client_certificate_path](#client_certificate_path) :material-plus: [client_key](#client_key) :material-plus: [client_key_path](#client_key_path) :material-plus: [client_authentication](#client_authentication) :material-plus: [client_certificate_public_key_sha256](#client_certificate_public_key_sha256) :material-plus: [ech.query_server_name](#query_server_name) !!! quote "sing-box 1.12.0 中的更改" :material-plus: [fragment](#fragment) :material-plus: [fragment_fallback_delay](#fragment_fallback_delay) :material-plus: [record_fragment](#record_fragment) :material-delete-clock: [ech.pq_signature_schemes_enabled](#pq_signature_schemes_enabled) :material-delete-clock: [ech.dynamic_record_sizing_disabled](#dynamic_record_sizing_disabled) !!! quote "sing-box 1.10.0 中的更改" :material-alert-decagram: [utls](#utls) ### 入站 ```json { "enabled": true, "server_name": "", "alpn": [], "min_version": "", "max_version": "", "cipher_suites": [], "curve_preferences": [], "certificate": [], "certificate_path": "", "client_authentication": "", "client_certificate": [], "client_certificate_path": [], "client_certificate_public_key_sha256": [], "key": [], "key_path": "", "kernel_tx": false, "kernel_rx": false, "handshake_timeout": "", "certificate_provider": "", // 废弃的 "acme": { "domain": [], "data_directory": "", "default_server_name": "", "email": "", "provider": "", "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": {} }, "ech": { "enabled": false, "key": [], "key_path": "", // 废弃的 "pq_signature_schemes_enabled": false, "dynamic_record_sizing_disabled": false }, "reality": { "enabled": false, "handshake": { "server": "google.com", "server_port": 443, ... // 拨号字段 }, "private_key": "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc", "short_id": [ "0123456789abcdef" ], "max_time_difference": "1m" } } ``` ### 出站 ```json { "enabled": true, "engine": "", "disable_sni": false, "server_name": "", "insecure": false, "alpn": [], "min_version": "", "max_version": "", "cipher_suites": [], "curve_preferences": [], "certificate": "", "certificate_path": "", "certificate_public_key_sha256": [], "client_certificate": [], "client_certificate_path": "", "client_key": [], "client_key_path": "", "fragment": false, "fragment_fallback_delay": "", "record_fragment": false, "spoof": "", "spoof_method": "", "kernel_tx": false, "kernel_rx": false, "handshake_timeout": "", "ech": { "enabled": false, "config": [], "config_path": "", "query_server_name": "", // 废弃的 "pq_signature_schemes_enabled": false, "dynamic_record_sizing_disabled": false }, "utls": { "enabled": false, "fingerprint": "" }, "reality": { "enabled": false, "public_key": "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0", "short_id": "0123456789abcdef" } } ``` TLS 版本值: * `1.0` * `1.1` * `1.2` * `1.3` 密码套件值: * `TLS_RSA_WITH_AES_128_CBC_SHA` * `TLS_RSA_WITH_AES_256_CBC_SHA` * `TLS_RSA_WITH_AES_128_GCM_SHA256` * `TLS_RSA_WITH_AES_256_GCM_SHA384` * `TLS_AES_128_GCM_SHA256` * `TLS_AES_256_GCM_SHA384` * `TLS_CHACHA20_POLY1305_SHA256` * `TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA` * `TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA` * `TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA` * `TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA` * `TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256` * `TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384` * `TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256` * `TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384` * `TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256` * `TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256` !!! note "" 当内容只有一项时,可以忽略 JSON 数组 [] 标签 ### 字段 #### enabled 启用 TLS #### engine !!! question "自 sing-box 1.14.0 起" ==仅客户端== 要使用的 TLS 引擎。 可用值: * `go`(默认) * `apple` * `windows` 支持的字段: * `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。 默认版本范围为 TLS 1.2 到 TLS 1.3,与 `go` 引擎一致。证书验证在 Go 侧基于 Schannel 返回的证书链执行,默认使用系统证书存储。当设置了 `certificate` 或 `certificate_path` 时,这些根证书会替代系统存储。 支持的字段: * `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 ==仅客户端== 不要在 ClientHello 中发送服务器名称. #### server_name 用于验证返回证书上的主机名,除非设置不安全。 它还包含在 ClientHello 中以支持虚拟主机,除非它是 IP 地址。 #### insecure ==仅客户端== 接受任何服务器证书。 #### alpn 支持的应用层协议协商列表,按优先顺序排列。 如果两个对等点都支持 ALPN,则选择的协议将是此列表中的一个,如果没有相互支持的协议则连接将失败。 参阅 [Application-Layer Protocol Negotiation](https://en.wikipedia.org/wiki/Application-Layer_Protocol_Negotiation)。 #### min_version 可接受的最低 TLS 版本。 默认情况下,当前使用 TLS 1.2 作为客户端的最低要求。作为服务器时使用 TLS 1.0。 #### max_version 可接受的最大 TLS 版本。 默认情况下,当前最高版本为 TLS 1.3。 #### cipher_suites 启用的 TLS 1.0–1.2 密码套件列表。列表的顺序被忽略。请注意,TLS 1.3 的密码套件是不可配置的。 如果为空,则使用安全的默认列表。默认密码套件可能会随着时间的推移而改变。 #### curve_preferences !!! question "自 sing-box 1.13.0 起" 支持的密钥交换机制集合。列表的顺序被忽略,密钥交换机制通过 Golang 的内部偏好顺序从此列表中选择。 可用值,同时也是默认列表: * `P256` * `P384` * `P521` * `X25519` * `X25519MLKEM768` #### certificate 服务器证书链行数组,PEM 格式。 #### certificate_path !!! note "" 文件更改时将自动重新加载。 服务器证书链路径,PEM 格式。 #### certificate_public_key_sha256 !!! question "自 sing-box 1.13.0 起" ==仅客户端== 服务器证书公钥的 SHA-256 哈希列表,base64 格式。 要生成证书公钥的 SHA-256 哈希,请使用以下命令: ```bash # 对于证书文件 openssl x509 -in certificate.pem -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 # 对于远程服务器的证书 echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 ``` #### client_certificate !!! question "自 sing-box 1.13.0 起" ==仅客户端== 客户端证书链行数组,PEM 格式。 #### client_certificate_path !!! question "自 sing-box 1.13.0 起" ==仅客户端== 客户端证书链路径,PEM 格式。 #### client_key !!! question "自 sing-box 1.13.0 起" ==仅客户端== 客户端私钥行数组,PEM 格式。 #### client_key_path !!! question "自 sing-box 1.13.0 起" ==仅客户端== 客户端私钥路径,PEM 格式。 #### key ==仅服务器== !!! note "" 文件更改时将自动重新加载。 服务器 PEM 私钥行数组。 #### key_path ==仅服务器== !!! note "" 文件更改时将自动重新加载。 服务器私钥路径,PEM 格式。 #### client_authentication !!! question "自 sing-box 1.13.0 起" ==仅服务器== 要使用的客户端身份验证类型。 可用值: * `no`(默认) * `request` * `require-any` * `verify-if-given` * `require-and-verify` 如果此选项设置为 `verify-if-given` 或 `require-and-verify`, 则需要 `client_certificate`、`client_certificate_path` 或 `client_certificate_public_key_sha256` 中的一个。 #### client_certificate !!! question "自 sing-box 1.13.0 起" ==仅服务器== 客户端证书链行数组,PEM 格式。 #### client_certificate_path !!! question "自 sing-box 1.13.0 起" ==仅服务器== !!! note "" 文件更改时将自动重新加载。 客户端证书链路径列表,PEM 格式。 #### client_certificate_public_key_sha256 !!! question "自 sing-box 1.13.0 起" ==仅服务器== 客户端证书公钥的 SHA-256 哈希列表,base64 格式。 要生成证书公钥的 SHA-256 哈希,请使用以下命令: ```bash # 对于证书文件 openssl x509 -in certificate.pem -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 # 对于远程服务器的证书 echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 ``` #### kernel_tx !!! question "自 sing-box 1.13.0 起" !!! quote "" 仅支持 Linux 5.1+,如果可能,使用较新的内核。 !!! quote "" 仅支持 TLS 1.3。 !!! warning "" kTLS TX 仅当 `splice(2)` 可用时(两端经过握手后必须为没有附加协议的 TCP 或 TLS)才能提高性能;否则肯定会降低性能。 启用内核 TLS 发送支持。 #### kernel_rx !!! question "自 sing-box 1.13.0 起" !!! quote "" 仅支持 Linux 5.1+,如果可能,使用较新的内核。 !!! quote "" 仅支持 TLS 1.3。 !!! failure "" 即使使用 `splice(2)`,kTLS RX 也肯定会降低性能,因此不建议启用。 启用内核 TLS 接收支持。 #### handshake_timeout !!! question "自 sing-box 1.14.0 起" TLS 握手超时,采用 golang 的 Duration 格式。 默认使用 `15s`。 #### certificate_provider !!! question "自 sing-box 1.14.0 起" ==仅服务器== 字符串或对象。 为字符串时,共享[证书提供者](/zh/configuration/shared/certificate-provider/)的标签。 为对象时,内联的证书提供者。可用类型和字段参阅[证书提供者](/zh/configuration/shared/certificate-provider/)。 ## 自定义 TLS 支持 !!! info "QUIC 支持" 只有 ECH 在 QUIC 中被支持. #### utls ==仅客户端== !!! failure "不推荐" uTLS 已被研究人员多次发现其指纹可被识别的漏洞。 uTLS 是一个试图通过复制 ClientHello 结构来模仿浏览器 TLS 指纹的 Go 库。 然而,浏览器使用完全不同的 TLS 实现(Chrome 使用 BoringSSL,Firefox 使用 NSS), 其实现行为无法通过简单复制握手格式来复现,其行为细节必然存在差异,使得检测成为可能。 此外,此库缺乏积极维护,且代码质量较差,不建议用于反审查场景。 如需 TLS 指纹抵抗,请改用 [NaiveProxy](/zh/configuration/inbound/naive/)。 uTLS 是 "crypto/tls" 的一个分支,它提供了 ClientHello 指纹识别阻力。 可用的指纹值: !!! warning "已在 sing-box 1.10.0 移除" 一些旧 chrome 指纹已被删除,并将会退到 chrome: :material-close: chrome_psk :material-close: chrome_psk_shuffle :material-close: chrome_padding_psk_shuffle :material-close: chrome_pq :material-close: chrome_pq_psk * chrome * firefox * edge * safari * 360 * qq * ios * android * random * randomized 默认使用 chrome 指纹。 ### ECH 字段 ECH (Encrypted Client Hello) 是一个 TLS 扩展,它允许客户端加密其 ClientHello 的第一部分信息。 ECH 密钥和配置可以通过 `sing-box generate ech-keypair` 生成。 #### pq_signature_schemes_enabled !!! failure "已在 sing-box 1.12.0 废弃" `pq_signature_schemes_enabled` 已在 sing-box 1.12.0 废弃且已在 sing-box 1.13.0 中被移除。 启用对后量子对等证书签名方案的支持。 #### dynamic_record_sizing_disabled !!! failure "已在 sing-box 1.12.0 废弃" `dynamic_record_sizing_disabled` 已在 sing-box 1.12.0 废弃且已在 sing-box 1.13.0 中被移除。 禁用 TLS 记录的自适应大小调整。 当为 true 时,总是使用最大可能的 TLS 记录大小。 当为 false 时,可能会调整 TLS 记录的大小以尝试改善延迟。 #### key ==仅服务器== ECH 密钥行数组,PEM 格式。 #### key_path ==仅服务器== !!! note "" 文件更改时将自动重新加载。 ECH 密钥路径,PEM 格式。 #### config ==仅客户端== ECH 配置行数组,PEM 格式。 如果为空,将尝试从 DNS 加载。 #### config_path ==仅客户端== ECH 配置路径,PEM 格式。 如果为空,将尝试从 DNS 加载。 #### query_server_name !!! question "自 sing-box 1.13.0 起" ==仅客户端== 覆盖用于 ECH HTTPS 记录查询的域名。 如果为空,使用 `server_name` 查询。 #### fragment !!! question "自 sing-box 1.12.0 起" ==仅客户端== 通过分段 TLS 握手数据包来绕过防火墙。 此功能旨在规避基于**明文数据包匹配**的简单防火墙,不应该用于规避真正的审查。 由于性能不佳,请首先尝试 `record_fragment`,且仅应用于已知被阻止的服务器名称。 在 Linux、Apple 平台和(需要管理员权限的)Windows 系统上, 可以自动检测等待时间。否则,将回退到 等待 `fragment_fallback_delay` 指定的固定时间。 此外,如果实际等待时间少于 20ms,也会回退到等待固定时间, 因为目标被认为是本地的或在透明代理后面。 #### fragment_fallback_delay !!! question "自 sing-box 1.12.0 起" ==仅客户端== 当 TLS 分段无法自动确定等待时间时使用的回退值。 默认使用 `500ms`。 #### record_fragment !!! question "自 sing-box 1.12.0 起" ==仅客户端== 将 TLS 握手分段为多个 TLS 记录以绕过防火墙。 #### spoof !!! question "自 sing-box 1.14.0 起" ==仅客户端,仅 Linux/macOS/Windows,需要提权== 在真实 ClientHello 之前注入一个伪造的、携带白名单 SNI 的 TLS ClientHello, 以欺骗基于 SNI 过滤的中间盒放行连接。 伪造报文是真实 ClientHello 的副本,仅将 SNI 值替换为本字段的值, 因此 TLS 指纹无法区分伪造与真实报文。真实服务器会丢弃伪造报文(见 `spoof_method`), 而中间盒将该连接视为合法会话。 需要原始套接字权限(Linux 上需 `CAP_NET_RAW`,macOS 上需 root); 在 Linux 上还需 `CAP_NET_ADMIN`,因为需要通过 `TCP_REPAIR` 读取发送序列号。 Windows 上首次使用时需要 Administrator 以安装内嵌的 WinDivert 内核驱动, 不支持 Windows ARM64。 #### spoof_method !!! question "自 sing-box 1.14.0 起" ==仅客户端== 控制伪造报文被真实服务器拒绝的方式。 | 取值 | 行为 | |--------------------------|-------------------------------------------------------------------| | `wrong-sequence`(默认) | 伪造报文的 TCP 序列号位于服务器接收窗口之前。 | | `wrong-checksum` | 伪造报文的 TCP 校验和被故意设为无效。 | | `wrong-ack` | 伪造报文的 TCP 确认号位于服务器发送窗口之前。 | | `wrong-md5` | 伪造报文携带 TCP-MD5 签名选项,未协商 MD5 密钥的服务器将拒绝。 | | `wrong-timestamp` | 伪造报文携带回退的 TCP 时间戳,服务器按 PAWS 规则视为重放并拒绝。仅支持 Linux/Windows,不支持 macOS。 | ### ACME 字段 !!! failure "已在 sing-box 1.14.0 废弃" 内联 ACME 选项已在 sing-box 1.14.0 废弃且将在 sing-box 1.16.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移内联-acme-到证书提供者)。 #### domain 域名列表。 如果为空则禁用 ACME。 #### 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://...` | 自定义 | #### 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/)。 ### Reality 字段 #### handshake ==仅服务器== ==必填== 握手服务器地址和 [拨号参数](/zh/configuration/shared/dial/)。 #### private_key ==仅服务器== ==必填== 私钥,由 `sing-box generate reality-keypair` 生成。 #### public_key ==仅客户端== ==必填== 公钥,由 `sing-box generate reality-keypair` 生成。 #### short_id ==必填== 一个零到八位的十六进制字符串。 #### max_time_difference ==仅服务器== 服务器和客户端之间的最大时间差。 如果为空则禁用检查。 ================================================ FILE: docs/configuration/shared/udp-over-tcp.md ================================================ !!! warning "" It's a proprietary protocol created by SagerNet, not part of shadowsocks. The UDP over TCP protocol is used to transmit UDP packets in TCP. ### Structure ```json { "enabled": true, "version": 2 } ``` !!! info "" The structure can be replaced with a boolean value when the version is not specified. ### Fields #### enabled Enable the UDP over TCP protocol. #### version The protocol version, `1` or `2`. 2 is used by default. ### Application support | Project | UoT v1 | UoT v2 | |--------------|----------------------|----------------------| | sing-box | v0 (2022/08/11) | v1.2-beta9 | | Clash.Meta | v1.12.0 (2022/07/02) | v1.14.3 (2023/03/31) | | Shadowrocket | v2.2.12 (2022/08/13) | / | ### Protocol details #### Protocol version 1 The client requests the magic address to the upper layer proxy protocol to indicate the request: `sp.udp-over-tcp.arpa` #### Stream format | ATYP | address | port | length | data | |------|----------|-------|--------|----------| | u8 | variable | u16be | u16be | variable | **ATYP / address / port**: Uses the SOCKS address format, but with different address types: | ATYP | Address type | |--------|--------------| | `0x00` | IPv4 Address | | `0x01` | IPv6 Address | | `0x02` | Domain Name | #### Protocol version 2 Protocol version 2 uses a new magic address: `sp.v2.udp-over-tcp.arpa` ##### Request format | isConnect | ATYP | address | port | |-----------|------|----------|-------| | u8 | u8 | variable | u16be | **isConnect**: Set to 1 to indicates that the stream uses the connect format, 0 to disable. **ATYP / address / port**: Request destination, uses the SOCKS address format. ##### Connect stream format | length | data | |--------|----------| | u16be | variable | ##### Non-connect stream format As the same as the stream format in protocol version 1. ================================================ FILE: docs/configuration/shared/udp-over-tcp.zh.md ================================================ !!! warning "" 这是 SagerNet 创建的专有协议,不是 shadowsocks 的一部分。 UDP over TCP 协议用于在 TCP 中传输 UDP 数据包。 ### 结构 ```json { "enabled": true, "version": 2 } ``` !!! info "" 当不指定版本时,结构可以用布尔值替换。 ### 字段 #### enabled 启用 UDP over TCP 协议。 #### version 协议版本,`1` 或 `2`。 默认使用 2。 ### 应用程序支持 | 项目 | UoT v1 | UoT v2 | |--------------|----------------------|----------------------| | sing-box | v0 (2022/08/11) | v1.2-beta9 | | Clash.Meta | v1.12.0 (2022/07/02) | v1.14.3 (2023/03/31) | | Shadowrocket | v2.2.12 (2022/08/13) | / | ### 协议详情 #### 协议版本 1 客户端向上层代理协议请求魔法地址以表示请求:`sp.udp-over-tcp.arpa` #### 流格式 | ATYP | 地址 | 端口 | 长度 | 数据 | |------|----------|-------|--------|----------| | u8 | 可变长 | u16be | u16be | 可变长 | **ATYP / 地址 / 端口**:使用 SOCKS 地址格式,但使用不同的地址类型: | ATYP | 地址类型 | |--------|-----------| | `0x00` | IPv4 地址 | | `0x01` | IPv6 地址 | | `0x02` | 域名 | #### 协议版本 2 协议版本 2 使用新的魔法地址:`sp.v2.udp-over-tcp.arpa` ##### 请求格式 | isConnect | ATYP | 地址 | 端口 | |-----------|------|----------|-------| | u8 | u8 | 可变长 | u16be | **isConnect**:设置为 1 表示流使用连接格式,0 表示禁用。 **ATYP / 地址 / 端口**:请求目标,使用 SOCKS 地址格式。 ##### 连接流格式 | 长度 | 数据 | |--------|----------| | u16be | 可变长 | ##### 非连接流格式 与协议版本 1 中的流格式相同。 ================================================ FILE: docs/configuration/shared/v2ray-transport.md ================================================ V2Ray Transport is a set of private protocols invented by v2ray, and has contaminated the names of other protocols, such as `trojan-grpc` in clash. ### Structure ```json { "type": "" } ``` Available transports: * HTTP * WebSocket * QUIC * gRPC * HTTPUpgrade !!! warning "Difference from v2ray-core" * No TCP transport, plain HTTP is merged into the HTTP transport. * No mKCP transport. * No DomainSocket transport. !!! note "" You can ignore the JSON Array [] tag when the content is only one item ### HTTP ```json { "type": "http", "host": [], "path": "", "method": "", "headers": {}, "idle_timeout": "15s", "ping_timeout": "15s" } ``` !!! warning "Difference from v2ray-core" TLS is not enforced. If TLS is not configured, plain HTTP 1.1 is used. #### host List of host domain. The client will choose randomly and the server will verify if not empty. #### path !!! warning V2Ray's documentation says that the path between the server and the client must be consistent, but the actual code allows the client to add any suffix to the path. sing-box uses the same behavior as V2Ray, but note that the behavior does not exist in `WebSocket` and `HTTPUpgrade` transport. Path of HTTP request. The server will verify. #### method Method of HTTP request. The server will verify if not empty. #### headers Extra headers of HTTP request. The server will write in response if not empty. #### idle_timeout In HTTP2 server: Specifies the time until idle clients should be closed with a GOAWAY frame. PING frames are not considered as activity. In HTTP2 client: Specifies the period of time after which a health check will be performed using a ping frame if no frames have been received on the connection.Please note that a ping response is considered a received frame, so if there is no other traffic on the connection, the health check will be executed every interval. If the value is zero, no health check will be performed. Zero is used by default. #### ping_timeout In HTTP2 client: Specifies the timeout duration after sending a PING frame, within which a response must be received. If a response to the PING frame is not received within the specified timeout duration, the connection will be closed. The default timeout duration is 15 seconds. ### WebSocket ```json { "type": "ws", "path": "", "headers": {}, "max_early_data": 0, "early_data_header_name": "" } ``` #### path Path of HTTP request. The server will verify. #### headers Extra headers of HTTP request. The server will write in response if not empty. #### max_early_data Allowed payload size is in the request. Enabled if not zero. #### early_data_header_name Early data is sent in path instead of header by default. To be compatible with Xray-core, set this to `Sec-WebSocket-Protocol`. It needs to be consistent with the server. ### QUIC ```json { "type": "quic" } ``` !!! warning "Difference from v2ray-core" No additional encryption support: It's basically duplicate encryption. And Xray-core is not compatible with v2ray-core in here. ### gRPC !!! note "" standard gRPC has good compatibility but poor performance and is not included by default, see [Installation](/installation/build-from-source/#build-tags). ```json { "type": "grpc", "service_name": "TunService", "idle_timeout": "15s", "ping_timeout": "15s", "permit_without_stream": false } ``` #### service_name Service name of gRPC. #### idle_timeout In standard gRPC server/client: If the transport doesn't see any activity after a duration of this time, it pings the client to check if the connection is still active. In default gRPC server/client: It has the same behavior as the corresponding setting in HTTP transport. #### ping_timeout In standard gRPC server/client: The timeout that after performing a keepalive check, the client will wait for activity. If no activity is detected, the connection will be closed. In default gRPC server/client: It has the same behavior as the corresponding setting in HTTP transport. #### permit_without_stream In standard gRPC client: If enabled, the client transport sends keepalive pings even with no active connections. If disabled, when there are no active connections, `idle_timeout` and `ping_timeout` will be ignored and no keepalive pings will be sent. Disabled by default. ### HTTPUpgrade ```json { "type": "httpupgrade", "host": "", "path": "", "headers": {} } ``` #### host Host domain. The server will verify if not empty. #### path Path of HTTP request. The server will verify. #### headers Extra headers of HTTP request. The server will write in response if not empty. ================================================ FILE: docs/configuration/shared/v2ray-transport.zh.md ================================================ V2Ray Transport 是 v2ray 发明的一组私有协议,并污染了其他协议的名称,如 clash 中的 `trojan-grpc`。 ### 结构 ```json { "type": "" } ``` 可用的传输协议: * HTTP * WebSocket * QUIC * gRPC * HTTPUpgrade !!! warning "与 v2ray-core 的区别" * 没有 TCP 传输层, 纯 HTTP 已合并到 HTTP 传输层。 * 没有 mKCP 传输层。 * 没有 DomainSocket 传输层。 !!! note "" 当内容只有一项时,可以忽略 JSON 数组 [] 标签。 ### HTTP ```json { "type": "http", "host": [], "path": "", "method": "", "headers": {}, "idle_timeout": "15s", "ping_timeout": "15s" } ``` !!! warning "与 v2ray-core 的区别" 不强制执行 TLS。如果未配置 TLS,将使用纯 HTTP 1.1。 #### host 主机域名列表。 如果设置,客户端将随机选择,服务器将验证。 #### path !!! warning V2Ray 文档称服务端和客户端的路径必须一致,但实际代码允许客户端向路径添加任何后缀。 sing-box 使用与 V2Ray 相同的行为,但请注意,该行为在 `WebSocket` 和 `HTTPUpgrade` 传输层中不存在。 HTTP 请求路径 服务器将验证。 #### method HTTP 请求方法 如果设置,服务器将验证。 #### headers HTTP 请求的额外标头 如果设置,服务器将写入响应。 #### idle_timeout 在 HTTP2 服务器中: 指定闲置客户端应在多长时间内使用 GOAWAY 帧关闭。PING 帧不被视为活动。 在 HTTP2 客户端中: 如果连接上没有收到任何帧,指定一段时间后将使用 PING 帧执行健康检查。需要注意的是,PING 响应被视为已接收的帧,因此如果连接上没有其他流量,则健康检查将在每个间隔执行一次。如果值为零,则不会执行健康检查。 默认使用零。 #### ping_timeout 在 HTTP2 客户端中: 指定发送 PING 帧后,在指定的超时时间内必须接收到响应。如果在指定的超时时间内没有收到 PING 帧的响应,则连接将关闭。默认超时持续时间为 15 秒。 ### WebSocket ```json { "type": "ws", "path": "", "headers": {}, "max_early_data": 0, "early_data_header_name": "" } ``` #### path HTTP 请求路径 服务器将验证。 #### headers HTTP 请求的额外标头 如果设置,服务器将写入响应。 #### max_early_data 请求中允许的最大有效负载大小。默认启用。 #### early_data_header_name 默认情况下,早期数据在路径而不是标头中发送。 要与 Xray-core 兼容,请将其设置为 `Sec-WebSocket-Protocol`。 它需要与服务器保持一致。 ### QUIC ```json { "type": "quic" } ``` !!! warning "与 v2ray-core 的区别" 没有额外的加密支持: 它基本上是重复加密。 并且 Xray-core 在这里与 v2ray-core 不兼容。 ### gRPC !!! note "" 默认安装不包含标准 gRPC (兼容性好,但性能较差), 参阅 [安装](/zh/installation/build-from-source/#构建标记)。 ```json { "type": "grpc", "service_name": "TunService", "idle_timeout": "15s", "ping_timeout": "15s", "permit_without_stream": false } ``` #### service_name gRPC 服务名称。 #### idle_timeout 在标准 gRPC 服务器/客户端: 如果传输在此时间段后没有看到任何活动,它会向客户端发送 ping 请求以检查连接是否仍然活动。 在默认 gRPC 服务器/客户端: 它的行为与 HTTP 传输层中的相应设置相同。 #### ping_timeout 在标准 gRPC 服务器/客户端: 经过一段时间之后,客户端将执行 keepalive 检查并等待活动。如果没有检测到任何活动,则会关闭连接。 在默认 gRPC 服务器/客户端: 它的行为与 HTTP 传输层中的相应设置相同。 #### permit_without_stream 在标准 gRPC 客户端: 如果启用,客户端传输即使没有活动连接也会发送 keepalive ping。如果禁用,则在没有活动连接时,将忽略 `idle_timeout` 和 `ping_timeout`,并且不会发送 keepalive ping。 默认禁用。 ### HTTPUpgrade ```json { "type": "httpupgrade", "host": "", "path": "", "headers": {} } ``` #### host 主机域名。 服务器将验证。 #### path HTTP 请求路径 服务器将验证。 #### headers HTTP 请求的额外标头。 如果设置,服务器将写入响应。 ================================================ FILE: docs/configuration/shared/wifi-state.md ================================================ --- icon: material/new-box --- # Wi-Fi State !!! quote "Changes in sing-box 1.13.0" :material-plus: Linux support :material-plus: Windows support sing-box can monitor Wi-Fi state to enable routing rules based on `wifi_ssid` and `wifi_bssid`. ### Platform Support | Platform | Support | Notes | |-----------------|------------------|--------------------------| | Android | :material-check: | In graphical client | | Apple platforms | :material-check: | In graphical clients | | Linux | :material-check: | Requires supported daemon | | Windows | :material-check: | WLAN API | | Others | :material-close: | | ### Linux !!! question "Since sing-box 1.13.0" The following backends are supported and will be auto-detected in order of priority: | Backend | Interface | |------------------|-------------| | NetworkManager | D-Bus | | IWD | D-Bus | | wpa_supplicant | Unix socket | | ConnMan | D-Bus | ### Windows !!! question "Since sing-box 1.13.0" Uses Windows WLAN API. ================================================ FILE: docs/configuration/shared/wifi-state.zh.md ================================================ --- icon: material/new-box --- # Wi-Fi 状态 !!! quote "sing-box 1.13.0 中的更改" :material-plus: Linux 支持 :material-plus: Windows 支持 sing-box 可以监控 Wi-Fi 状态,以启用基于 `wifi_ssid` 和 `wifi_bssid` 的路由规则。 ### 平台支持 | 平台 | 支持 | 备注 | |-----------------|------------------|----------------| | Android | :material-check: | 仅图形客户端 | | Apple 平台 | :material-check: | 仅图形客户端 | | Linux | :material-check: | 需要支持的守护进程 | | Windows | :material-check: | WLAN API | | 其他 | :material-close: | | ### Linux !!! question "自 sing-box 1.13.0 起" 支持以下后端,将按优先级顺序自动探测: | 后端 | 接口 | |------------------|-------------| | NetworkManager | D-Bus | | IWD | D-Bus | | wpa_supplicant | Unix socket | | ConnMan | D-Bus | ### Windows !!! question "自 sing-box 1.13.0 起" 使用 Windows WLAN API。 ================================================ FILE: docs/deprecated.md ================================================ --- icon: material/delete-alert --- # Deprecated Feature List ## 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 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. #### Legacy `strategy` DNS rule action option Legacy `strategy` DNS rule action option is deprecated. 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. #### `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`) 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-server-formats). Old formats were removed in sing-box 1.14.0. #### `outbound` DNS rule item Legacy `outbound` DNS rules are deprecated and can be replaced by dial fields, check [Migration](../migration/#migrate-outbound-dns-rule-items-to-domain-resolver). #### Legacy ECH fields 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. Also, `dynamic_record_sizing_disabled` has nothing to do with ECH, was added by mistake, has been deprecated and no longer works. These fields were removed in sing-box 1.13.0. ## 1.11.0 #### Legacy special outbounds 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 were removed in sing-box 1.13.0. #### Legacy inbound fields 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 were removed in sing-box 1.13.0. #### Destination override fields in direct outbound Destination override fields (`override_address` / `override_port`) in direct outbound are deprecated 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 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 were removed in sing-box 1.13.0. ## 1.10.0 #### TUN address fields are merged `inet4_address` and `inet6_address` are merged into `address`, `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 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 were removed in sing-box 1.11.0. #### Drop support for go1.18 and go1.19 Due to maintenance difficulties, sing-box 1.10.0 requires at least Go 1.20 to compile. ## 1.8.0 #### Cache file and related features in Clash API `cache_file` and related features in Clash API is migrated to independent `cache_file` options, check [Migration](/migration/#migrate-cache-file-from-clash-api-to-independent-options). #### GeoIP 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, and all existing implementations suffer from high memory usage and difficult management. sing-box 1.8.0 introduces [rule-set](/configuration/rule-set/), which can completely replace GeoIP, check [Migration](/migration/#migrate-geoip-to-rule-sets). #### Geosite 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. sing-box 1.8.0 introduces [rule-set](/configuration/rule-set/), which can completely replace Geosite, check [Migration](/migration/#migrate-geosite-to-rule-sets). ## 1.6.0 The following features will be marked deprecated in 1.5.0 and removed entirely in 1.6.0. #### ShadowsocksR ShadowsocksR support has never been enabled by default, since the most commonly used proxy sales panel in the illegal industry stopped using this protocol, it does not make sense to continue to maintain it. #### Proxy Protocol Proxy Protocol is added by Pull Request, has problems, is only used by the backend of HTTP multiplexers such as nginx, is intrusive, and is meaningless for proxy purposes. ================================================ FILE: docs/deprecated.zh.md ================================================ --- 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`)已废弃, 且可以通过 ACME 证书提供者替代, 参阅 [迁移指南](/zh/migration/#迁移内联-acme-到证书提供者)。 旧字段将在 sing-box 1.16.0 中被移除。 #### 旧版 DNS 规则动作 `strategy` 选项 旧版 DNS 规则动作 `strategy` 选项已废弃。 旧字段将在 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 中被移除。 #### `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`)已废弃, 参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。 旧行为将在 sing-box 1.16.0 中被移除。 ## 1.12.0 #### 旧的 DNS 服务器格式 DNS 服务器已重构, 参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式). 旧格式已在 sing-box 1.14.0 中被移除。 #### `outbound` DNS 规则项 旧的 `outbound` DNS 规则已废弃, 且可被拨号字段代替, 参阅 [迁移指南](/zh/migration/#迁移-outbound-dns-规则项到域解析选项). #### 旧的 ECH 字段 ECH 支持已在 sing-box 1.12.0 迁移至使用标准库,但标准库不支持后量子对等证书签名方案, 因此 `pq_signature_schemes_enabled` 已被弃用且不再工作。 另外,`dynamic_record_sizing_disabled` 与 ECH 无关,是错误添加的,现已弃用且不再工作。 相关字段已在 sing-box 1.13.0 中被移除。 ## 1.11.0 #### 旧的特殊出站 旧的特殊出站(`block` / `dns`)已废弃且可以通过规则动作替代, 参阅 [迁移指南](/zh/migration/#迁移旧的特殊出站到规则动作)。 旧字段已在 sing-box 1.13.0 中被移除。 #### 旧的入站字段 旧的入站字段(`inbound.`)已废弃且可以通过规则动作替代, 参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作)。 旧字段已在 sing-box 1.13.0 中被移除。 #### direct 出站中的目标地址覆盖字段 direct 出站中的目标地址覆盖字段(`override_address` / `override_port`)已废弃且可以通过规则动作替代, 参阅 [迁移指南](/zh/migration/#迁移-direct-出站中的目标地址覆盖字段到路由字段)。 旧字段已在 sing-box 1.13.0 中被移除。 #### WireGuard 出站 WireGuard 出站已废弃且可以通过端点替代, 参阅 [迁移指南](/zh/migration/#迁移-wireguard-出站到端点)。 旧出站已在 sing-box 1.13.0 中被移除。 #### TUN 的 GSO 字段 GSO 对透明代理场景没有优势,已废弃且在 TUN 中不再起作用。 旧字段已在 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 中被移除。 #### TUN 地址字段已合并 `inet4_address` 和 `inet6_address` 已合并为 `address`, `inet4_route_address` 和 `inet6_route_address` 已合并为 `route_address`, `inet4_route_exclude_address` 和 `inet6_route_exclude_address` 已合并为 `route_exclude_address`。 旧字段已在 sing-box 1.12.0 中被移除。 #### 移除对 go1.18 和 go1.19 的支持 由于维护困难,sing-box 1.10.0 要求至少 Go 1.20 才能编译。 ## 1.8.0 #### Clash API 中的 Cache file 及相关功能 Clash API 中的 `cache_file` 及相关功能已废弃且已迁移到独立的 `cache_file` 设置, 参阅 [迁移指南](/zh/migration/#将缓存文件从-clash-api-迁移到独立选项)。 #### GeoIP GeoIP 已废弃且已在 sing-box 1.12.0 中被移除。 maxmind GeoIP 国家数据库作为 IP 分类数据库,不完全适合流量绕过, 且现有的实现均存在内存使用大与管理困难的问题。 sing-box 1.8.0 引入了[规则集](/zh/configuration/rule-set/), 可以完全替代 GeoIP, 参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。 #### Geosite Geosite 已废弃且已在 sing-box 1.12.0 中被移除。 Geosite,即由 V2Ray 维护的 domain-list-community 项目,作为早期流量绕过解决方案, 存在着包括缺少维护、规则不准确和管理困难内的大量问题。 sing-box 1.8.0 引入了[规则集](/zh/configuration/rule-set/), 可以完全替代 Geosite,参阅 [迁移指南](/zh/migration/#迁移-geosite-到规则集)。 ## 1.6.0 下列功能已在 1.5.0 中标记为已弃用,并在 1.6.0 中完全删除。 #### ShadowsocksR ShadowsocksR 支持从未默认启用,自从常用的黑产代理销售面板停止使用该协议,继续维护它是没有意义的。 #### Proxy Protocol Proxy Protocol 支持由 Pull Request 添加,存在问题且仅由 HTTP 多路复用器(如 nginx)的后端使用,具有侵入性,对于代理目的毫无意义。 ================================================ FILE: docs/index.md ================================================ --- description: Welcome to the wiki page for the sing-box project. --- # :material-home: Home Welcome to the wiki page for the sing-box project. The universal proxy platform. ## License ``` Copyright (C) 2022 by nekohasekai 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 . In addition, no derivative work may use the name or imply association with this application without prior consent. ``` ================================================ FILE: docs/index.zh.md ================================================ --- description: 欢迎来到该 sing-box 项目的文档页。 --- # :material-home: 开始 欢迎来到该 sing-box 项目的文档页。 通用代理平台。 ## 授权 ``` Copyright (C) 2022 by nekohasekai 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 . In addition, no derivative work may use the name or imply association with this application without prior consent. ``` ================================================ FILE: docs/installation/build-from-source.md ================================================ --- icon: material/file-code --- # Build from source ## :material-graph: Requirements ### sing-box 1.11 * Go 1.23.1 - ~ ### sing-box 1.10 * Go 1.20.0 - ~ ### sing-box 1.9 * Go 1.18.5 - 1.22.x * Go 1.20.0 - 1.22.x with tag `with_quic`, or `with_utls` enabled ## :material-fast-forward: Simple Build ```bash make ``` Or build and install binary to `$GOBIN`: ```bash make install ``` ## :material-cog: Custom Build ```bash TAGS="tag_a tag_b" make ``` or ```bash go build -tags "tag_a tag_b" ./cmd/sing-box ``` ## :material-folder-settings: Build Tags | Build Tag | Enabled by default | Description | |------------------------------------|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `with_quic` | :material-check: | Build with QUIC support, see [QUIC and HTTP3 DNS transports](/configuration/dns/server/), [Naive inbound](/configuration/inbound/naive/), [Hysteria Inbound](/configuration/inbound/hysteria/), [Hysteria Outbound](/configuration/outbound/hysteria/) and [V2Ray Transport#QUIC](/configuration/shared/v2ray-transport#quic). | | `with_grpc` | :material-close:️ | Build with standard gRPC support, see [V2Ray Transport#gRPC](/configuration/shared/v2ray-transport#grpc). | | `with_dhcp` | :material-check: | Build with DHCP support, see [DHCP DNS transport](/configuration/dns/server/). | | `with_wireguard` | :material-check: | Build with WireGuard support, see [WireGuard outbound](/configuration/outbound/wireguard/). | | `with_utls` | :material-check: | Build with [uTLS](https://github.com/refraction-networking/utls) support for TLS outbound, see [TLS](/configuration/shared/tls#utls). | | `with_acme` | :material-check: | Build with ACME TLS certificate issuer support, see [TLS](/configuration/shared/tls/). | | `with_clash_api` | :material-check: | Build with Clash API support, see [Experimental](/configuration/experimental#clash-api-fields). | | `with_v2ray_api` | :material-close:️ | Build with V2Ray API support, see [Experimental](/configuration/experimental#v2ray-api-fields). | | `with_gvisor` | :material-check: | Build with gVisor support, see [Tun inbound](/configuration/inbound/tun#stack) and [WireGuard outbound](/configuration/outbound/wireguard#system_interface). | | `with_embedded_tor` (CGO required) | :material-close:️ | Build with embedded Tor support, see [Tor outbound](/configuration/outbound/tor/). | | `with_tailscale` | :material-check: | Build with Tailscale support, see [Tailscale endpoint](/configuration/endpoint/tailscale). | | `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. | It is not recommended to change the default build tag list unless you really know what you are adding. ## :material-wrench: Linker Flags The following `-ldflags` are used in official builds: | Flag | Description | |-------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `-X 'internal/godebug.defaultGODEBUG=multipathtcp=0'` | Go 1.24 enabled Multipath TCP for listeners by default (`multipathtcp=2`). This may cause errors on low-level sockets, and sing-box has its own MPTCP control (`tcp_multi_path` option). This flag disables the Go default. | | `-checklinkname=0` | Go 1.23+ linker rejects unauthorized `go:linkname` usage. This flag disables the check, required together with the `badlinkname` build tag. | ## :material-package-variant: For Downstream Packagers The default build tag lists and linker flags are available as files in the repository for downstream packagers to reference directly: | File | Description | |------|-------------| | `release/DEFAULT_BUILD_TAGS` | Default for Linux (common architectures), Darwin, and Android. | | `release/DEFAULT_BUILD_TAGS_WINDOWS` | Default for Windows (includes `with_purego`). | | `release/DEFAULT_BUILD_TAGS_OTHERS` | Default for other platforms (no `with_naive_outbound`). | | `release/LDFLAGS` | Required linker flags (see above). | ## :material-layers: with_naive_outbound NaiveProxy outbound requires special build configurations depending on your target platform. ### Supported Platforms | Platform | Architectures | Mode | Requirements | |-----------------|--------------------------------------------------------|--------|-----------------------------------------------------------------| | Linux | amd64, arm64 | purego | None (library included in official releases) | | Linux | 386, amd64, arm, arm64, mipsle, mips64le, riscv64, loong64 | CGO | Chromium toolchain, glibc >= 2.31 (loong64: >= 2.36) at runtime | | Linux (musl) | 386, amd64, arm, arm64, mipsle, riscv64, loong64 | CGO | Chromium toolchain | | Windows | amd64, arm64 | purego | None (library included in official releases) | | Apple platforms | * | CGO | Xcode | | Android | * | CGO | Android NDK | ### Windows Use `with_purego` tag. For official releases, `libcronet.dll` is included in the archive. For self-built binaries, download from [cronet-go releases](https://github.com/sagernet/cronet-go/releases) and place in the same directory as `sing-box.exe` or in a directory listed in `PATH`. ### Linux (purego, amd64/arm64 only) Use `with_purego` tag. For official releases, `libcronet.so` is included in the archive. For self-built binaries, download from [cronet-go releases](https://github.com/sagernet/cronet-go/releases) and place in the same directory as sing-box binary or in system library path. ### Linux (CGO) See [cronet-go](https://github.com/sagernet/cronet-go#linux-build-instructions). - **glibc build**: Requires glibc >= 2.31 at runtime - **musl build**: Use `with_musl` tag, statically linked, no runtime requirements ### Apple platforms / Android See [cronet-go](https://github.com/sagernet/cronet-go). ================================================ FILE: docs/installation/build-from-source.zh.md ================================================ --- icon: material/file-code --- # 从源代码构建 ## :material-graph: 要求 ### sing-box 1.11 * Go 1.23.1 - ~ ### sing-box 1.10 * Go 1.20.0 - ~ * Go 1.21.0 - ~ with tag `with_ech` enabled ### sing-box 1.9 * Go 1.18.5 - 1.22.x * Go 1.20.0 - 1.22.x with tag `with_quic`, or `with_utls` enabled * Go 1.21.0 - 1.22.x with tag `with_ech` enabled 您可以从 https://go.dev/doc/install 下载并安装 Go,推荐使用最新版本。 ## :material-fast-forward: 快速开始 ```bash make ``` 或者构建二进制文件并将其安装到 `$GOBIN`: ```bash make install ``` ## :material-cog: 自定义构建 ```bash TAGS="tag_a tag_b" make ``` or ```bash go build -tags "tag_a tag_b" ./cmd/sing-box ``` ## :material-folder-settings: 构建标记 | 构建标记 | 默认启动 | 说明 | |------------------------------------|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `with_quic` | :material-check: | Build with QUIC support, see [QUIC and HTTP3 DNS transports](/zh/configuration/dns/server/), [Naive inbound](/zh/configuration/inbound/naive/), [Hysteria Inbound](/zh/configuration/inbound/hysteria/), [Hysteria Outbound](/zh/configuration/outbound/hysteria/) and [V2Ray Transport#QUIC](/zh/configuration/shared/v2ray-transport#quic). | | `with_grpc` | :material-close:️ | Build with standard gRPC support, see [V2Ray Transport#gRPC](/zh/configuration/shared/v2ray-transport#grpc). | | `with_dhcp` | :material-check: | Build with DHCP support, see [DHCP DNS transport](/zh/configuration/dns/server/). | | `with_wireguard` | :material-check: | Build with WireGuard support, see [WireGuard outbound](/zh/configuration/outbound/wireguard/). | | `with_utls` | :material-check: | Build with [uTLS](https://github.com/refraction-networking/utls) support for TLS outbound, see [TLS](/zh/configuration/shared/tls#utls). | | `with_acme` | :material-check: | Build with ACME TLS certificate issuer support, see [TLS](/zh/configuration/shared/tls/). | | `with_clash_api` | :material-check: | Build with Clash API support, see [Experimental](/zh/configuration/experimental#clash-api-fields). | | `with_v2ray_api` | :material-close:️ | Build with V2Ray API support, see [Experimental](/zh/configuration/experimental#v2ray-api-fields). | | `with_gvisor` | :material-check: | Build with gVisor support, see [Tun inbound](/zh/configuration/inbound/tun#stack) and [WireGuard outbound](/zh/configuration/outbound/wireguard#system_interface). | | `with_embedded_tor` (CGO required) | :material-close:️ | Build with embedded Tor support, see [Tor outbound](/zh/configuration/outbound/tor/). | | `with_tailscale` | :material-check: | 构建 Tailscale 支持,参阅 [Tailscale 端点](/zh/configuration/endpoint/tailscale)。 | | `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` 以绕过该限制。 | 除非您确实知道您正在启用什么,否则不建议更改默认构建标签列表。 ## :material-wrench: 链接器标志 以下 `-ldflags` 在官方构建中使用: | 标志 | 说明 | |-------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------| | `-X 'internal/godebug.defaultGODEBUG=multipathtcp=0'` | Go 1.24 默认为监听器启用 Multipath TCP(`multipathtcp=2`)。这可能在底层 socket 上导致错误,且 sing-box 有自己的 MPTCP 控制(`tcp_multi_path` 选项)。此标志禁用 Go 的默认行为。 | | `-checklinkname=0` | Go 1.23+ 链接器拒绝未授权的 `go:linkname` 使用。此标志禁用该检查,需要与 `badlinkname` 构建标记一起使用。 | ## :material-package-variant: 下游打包者 默认构建标签列表和链接器标志以文件形式存放在仓库中,供下游打包者直接引用: | 文件 | 说明 | |------|------| | `release/DEFAULT_BUILD_TAGS` | Linux(常见架构)、Darwin 和 Android 的默认标签。 | | `release/DEFAULT_BUILD_TAGS_WINDOWS` | Windows 的默认标签(包含 `with_purego`)。 | | `release/DEFAULT_BUILD_TAGS_OTHERS` | 其他平台的默认标签(不含 `with_naive_outbound`)。 | | `release/LDFLAGS` | 必需的链接器标志(参见上文)。 | ## :material-layers: with_naive_outbound NaiveProxy 出站需要根据目标平台进行特殊的构建配置。 ### 支持的平台 | 平台 | 架构 | 模式 | 要求 | |--------------|----------------------------------------------------------|--------|-----------------------------------------------------| | Linux | amd64, arm64 | purego | 无(官方发布版本已包含库文件) | | Linux | 386, amd64, arm, arm64, mipsle, mips64le, riscv64, loong64 | CGO | Chromium 工具链,运行时需要 glibc >= 2.31(loong64: >= 2.36) | | Linux (musl) | 386, amd64, arm, arm64, mipsle, riscv64, loong64 | CGO | Chromium 工具链 | | Windows | amd64, arm64 | purego | 无(官方发布版本已包含库文件) | | Apple 平台 | * | CGO | Xcode | | Android | * | CGO | Android NDK | ### Windows 使用 `with_purego` 标记。 官方发布版本已包含 `libcronet.dll`。自行构建时,从 [cronet-go releases](https://github.com/sagernet/cronet-go/releases) 下载并放置在 `sing-box.exe` 相同目录或 `PATH` 中的任意目录。 ### Linux (purego, 仅 amd64/arm64) 使用 `with_purego` 标记。 官方发布版本已包含 `libcronet.so`。自行构建时,从 [cronet-go releases](https://github.com/sagernet/cronet-go/releases) 下载并放置在 sing-box 二进制文件相同目录或系统库路径中。 ### Linux (CGO) 参阅 [cronet-go](https://github.com/sagernet/cronet-go#linux-build-instructions)。 - **glibc 构建**:运行时需要 glibc >= 2.31 - **musl 构建**:使用 `with_musl` 标记,静态链接,无运行时要求 ### Apple 平台 / Android 参阅 [cronet-go](https://github.com/sagernet/cronet-go)。 ================================================ FILE: docs/installation/docker.md ================================================ --- icon: material/docker --- # Docker ## :material-console: Command ```bash docker run -d \ -v /etc/sing-box:/etc/sing-box/ \ --name=sing-box \ --restart=always \ ghcr.io/sagernet/sing-box \ -D /var/lib/sing-box \ -C /etc/sing-box/ run ``` ## :material-box-shadow: Compose ```yaml version: "3.8" services: sing-box: image: ghcr.io/sagernet/sing-box container_name: sing-box restart: always volumes: - /etc/sing-box:/etc/sing-box/ command: -D /var/lib/sing-box -C /etc/sing-box/ run ``` ================================================ FILE: docs/installation/docker.zh.md ================================================ --- icon: material/docker --- # Docker ## :material-console: 命令 ```bash docker run -d \ -v /etc/sing-box:/etc/sing-box/ \ --name=sing-box \ --restart=always \ ghcr.io/sagernet/sing-box \ -D /var/lib/sing-box \ -C /etc/sing-box/ run ``` ## :material-box-shadow: Compose ```yaml version: "3.8" services: sing-box: image: ghcr.io/sagernet/sing-box container_name: sing-box restart: always volumes: - /etc/sing-box:/etc/sing-box/ command: -D /var/lib/sing-box -C /etc/sing-box/ run ``` ================================================ FILE: docs/installation/package-manager.md ================================================ --- icon: material/package --- # Package Manager ## :material-tram: Repository Installation === ":material-debian: Debian / APT" ```bash sudo mkdir -p /etc/apt/keyrings && sudo curl -fsSL https://sing-box.app/gpg.key -o /etc/apt/keyrings/sagernet.asc && sudo chmod a+r /etc/apt/keyrings/sagernet.asc && echo ' Types: deb URIs: https://deb.sagernet.org/ Suites: * Components: * Enabled: yes Signed-By: /etc/apt/keyrings/sagernet.asc ' | sudo tee /etc/apt/sources.list.d/sagernet.sources && sudo apt-get update && sudo apt-get install sing-box # or sing-box-beta ``` === ":material-redhat: Redhat / DNF 5" ```bash sudo dnf config-manager addrepo --from-repofile=https://sing-box.app/sing-box.repo && sudo dnf install sing-box # or sing-box-beta ``` === ":material-redhat: Redhat / DNF 4" ```bash sudo dnf config-manager --add-repo https://sing-box.app/sing-box.repo && sudo dnf -y install dnf-plugins-core && sudo dnf install sing-box # or sing-box-beta ``` ## :material-download-box: Manual Installation The script download and install the latest package from GitHub releases for deb or rpm based Linux distributions, ArchLinux and OpenWrt. ```shell curl -fsSL https://sing-box.app/install.sh | sh ``` or latest beta: ```shell curl -fsSL https://sing-box.app/install.sh | sh -s -- --beta ``` or specific version: ```shell curl -fsSL https://sing-box.app/install.sh | sh -s -- --version ``` ## :material-book-lock-open: Managed Installation === ":material-linux: Linux" | Type | Platform | Command | Link | |----------|---------------|------------------------------|---------------------------------------------------------------------------------------------------------------| | AUR | Arch Linux | `? -S sing-box` | [![AUR package](https://repology.org/badge/version-for-repo/aur/sing-box.svg)][aur] | | nixpkgs | NixOS | `nix-env -iA nixos.sing-box` | [![nixpkgs unstable package](https://repology.org/badge/version-for-repo/nix_unstable/sing-box.svg)][nixpkgs] | | Homebrew | macOS / Linux | `brew install sing-box` | [![Homebrew package](https://repology.org/badge/version-for-repo/homebrew/sing-box.svg)][brew] | | APK | Alpine | `apk add sing-box` | [![Alpine Linux Edge package](https://repology.org/badge/version-for-repo/alpine_edge/sing-box.svg)][alpine] | | DEB | AOSC | `apt install sing-box` | [![AOSC package](https://repology.org/badge/version-for-repo/aosc/sing-box.svg)][aosc] | === ":material-apple: macOS" | Type | Platform | Command | Link | |----------|----------|-------------------------|------------------------------------------------------------------------------------------------| | Homebrew | macOS | `brew install sing-box` | [![Homebrew package](https://repology.org/badge/version-for-repo/homebrew/sing-box.svg)][brew] | === ":material-microsoft-windows: Windows" | Type | Platform | Command | Link | |------------|----------|---------------------------|-----------------------------------------------------------------------------------------------------| | Scoop | Windows | `scoop install sing-box` | [![Scoop package](https://repology.org/badge/version-for-repo/scoop/sing-box.svg)][scoop] | | Chocolatey | Windows | `choco install sing-box` | [![Chocolatey package](https://repology.org/badge/version-for-repo/chocolatey/sing-box.svg)][choco] | | winget | Windows | `winget install sing-box` | [![winget package](https://repology.org/badge/version-for-repo/winget/sing-box.svg)][winget] | === ":material-android: Android" | Type | Platform | Command | Link | |--------|----------|--------------------|----------------------------------------------------------------------------------------------| | Termux | Android | `pkg add sing-box` | [![Termux package](https://repology.org/badge/version-for-repo/termux/sing-box.svg)][termux] | === ":material-freebsd: FreeBSD" | Type | Platform | Command | Link | |------------|----------|------------------------|--------------------------------------------------------------------------------------------| | FreshPorts | FreeBSD | `pkg install sing-box` | [![FreeBSD port](https://repology.org/badge/version-for-repo/freebsd/sing-box.svg)][ports] | ## :material-alert: Problematic Sources | Type | Platform | Link | Promblem(s) | |------------|----------|-------------------------------------------------------------------------------------------|-----------------------------------------| | DEB | AOSC | [aosc-os-abbs](https://github.com/AOSC-Dev/aosc-os-abbs/tree/stable/app-network/sing-box) | Problematic build tag list modification | | Homebrew | / | [homebrew-core][brew] | Problematic build tag list modification | | Termux | Android | [termux-packages][termux] | Problematic build tag list modification | | FreshPorts | FreeBSD | [FreeBSD ports][ports] | Old Go (go1.20) | If you are a user of them, please report issues to them: 1. Please do not modify release build tags without full understanding of the related functionality: enabling non-default labels may result in decreased performance; the lack of default labels may cause user confusion. 2. sing-box supports compiling with some older Go versions, but it is not recommended (especially versions that are no longer supported by Go). ## :material-book-multiple: Service Management For Linux systems with [systemd][systemd], usually the installation already includes a sing-box service, you can manage the service using the following command: | Operation | Command | |-----------|-----------------------------------------------| | Enable | `sudo systemctl enable sing-box` | | Disable | `sudo systemctl disable sing-box` | | Start | `sudo systemctl start sing-box` | | Stop | `sudo systemctl stop sing-box` | | Kill | `sudo systemctl kill sing-box` | | Restart | `sudo systemctl restart sing-box` | | Logs | `sudo journalctl -u sing-box --output cat -e` | | New Logs | `sudo journalctl -u sing-box --output cat -f` | [alpine]: https://pkgs.alpinelinux.org/packages?name=sing-box [aur]: https://aur.archlinux.org/packages/sing-box [nixpkgs]: https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/tools/networking/sing-box/default.nix [brew]: https://formulae.brew.sh/formula/sing-box [openwrt]: https://github.com/openwrt/packages/tree/master/net/sing-box [immortalwrt]: https://github.com/immortalwrt/packages/tree/master/net/sing-box [choco]: https://chocolatey.org/packages/sing-box [scoop]: https://github.com/ScoopInstaller/Main/blob/master/bucket/sing-box.json [winget]: https://github.com/microsoft/winget-pkgs/tree/master/manifests/s/SagerNet/sing-box [termux]: https://github.com/termux/termux-packages/tree/master/packages/sing-box [ports]: https://www.freshports.org/net/sing-box [aosc]: https://packages.aosc.io/packages/sing-box [systemd]: https://systemd.io/ ================================================ FILE: docs/installation/package-manager.zh.md ================================================ --- icon: material/package --- # 包管理器 ## :material-tram: 仓库安装 === ":material-debian: Debian / APT" ```bash sudo mkdir -p /etc/apt/keyrings && sudo curl -fsSL https://sing-box.app/gpg.key -o /etc/apt/keyrings/sagernet.asc && sudo chmod a+r /etc/apt/keyrings/sagernet.asc && echo ' Types: deb URIs: https://deb.sagernet.org/ Suites: * Components: * Enabled: yes Signed-By: /etc/apt/keyrings/sagernet.asc ' | sudo tee /etc/apt/sources.list.d/sagernet.sources && sudo apt-get update && sudo apt-get install sing-box # or sing-box-beta ``` === ":material-redhat: Redhat / DNF 5" ```bash sudo dnf config-manager addrepo --from-repofile=https://sing-box.app/sing-box.repo && sudo dnf install sing-box # or sing-box-beta ``` === ":material-redhat: Redhat / DNF 4" ```bash sudo dnf config-manager --add-repo https://sing-box.app/sing-box.repo && sudo dnf -y install dnf-plugins-core && sudo dnf install sing-box # or sing-box-beta ``` ## :material-download-box: 手动安装 该脚本从 GitHub 发布中下载并安装最新的软件包,适用于基于 deb 或 rpm 的 Linux 发行版、ArchLinux 和 OpenWrt。 ```shell curl -fsSL https://sing-box.app/install.sh | sh ``` 或最新测试版: ```shell curl -fsSL https://sing-box.app/install.sh | sh -s -- --beta ``` 或指定版本: ```shell curl -fsSL https://sing-box.app/install.sh | sh -s -- --version ``` ## :material-book-lock-open: 托管安装 === ":material-linux: Linux" | 类型 | 平台 | 命令 | 链接 | |----------|---------------|------------------------------|---------------------------------------------------------------------------------------------------------------| | AUR | Arch Linux | `? -S sing-box` | [![AUR package](https://repology.org/badge/version-for-repo/aur/sing-box.svg)][aur] | | nixpkgs | NixOS | `nix-env -iA nixos.sing-box` | [![nixpkgs unstable package](https://repology.org/badge/version-for-repo/nix_unstable/sing-box.svg)][nixpkgs] | | Homebrew | macOS / Linux | `brew install sing-box` | [![Homebrew package](https://repology.org/badge/version-for-repo/homebrew/sing-box.svg)][brew] | | APK | Alpine | `apk add sing-box` | [![Alpine Linux Edge package](https://repology.org/badge/version-for-repo/alpine_edge/sing-box.svg)][alpine] | | DEB | AOSC | `apt install sing-box` | [![AOSC package](https://repology.org/badge/version-for-repo/aosc/sing-box.svg)][aosc] | === ":material-apple: macOS" | 类型 | 平台 | 命令 | 链接 | |----------|-------|-------------------------|------------------------------------------------------------------------------------------------| | Homebrew | macOS | `brew install sing-box` | [![Homebrew package](https://repology.org/badge/version-for-repo/homebrew/sing-box.svg)][brew] | === ":material-microsoft-windows: Windows" | 类型 | 平台 | 命令 | 链接 | |------------|---------|---------------------------|-----------------------------------------------------------------------------------------------------| | Scoop | Windows | `scoop install sing-box` | [![Scoop package](https://repology.org/badge/version-for-repo/scoop/sing-box.svg)][scoop] | | Chocolatey | Windows | `choco install sing-box` | [![Chocolatey package](https://repology.org/badge/version-for-repo/chocolatey/sing-box.svg)][choco] | | winget | Windows | `winget install sing-box` | [![winget package](https://repology.org/badge/version-for-repo/winget/sing-box.svg)][winget] | === ":material-android: Android" | 类型 | 平台 | 命令 | 链接 | |--------|---------|--------------------|----------------------------------------------------------------------------------------------| | Termux | Android | `pkg add sing-box` | [![Termux package](https://repology.org/badge/version-for-repo/termux/sing-box.svg)][termux] | === ":material-freebsd: FreeBSD" | 类型 | 平台 | 命令 | 链接 | |------------|---------|------------------------|--------------------------------------------------------------------------------------------| | FreshPorts | FreeBSD | `pkg install sing-box` | [![FreeBSD port](https://repology.org/badge/version-for-repo/freebsd/sing-box.svg)][ports] | ## :material-alert: 存在问题的源 | 类型 | 平台 | 链接 | 原因 | |------------|---------|-------------------------------------------------------------------------------------------|-----------------| | DEB | AOSC | [aosc-os-abbs](https://github.com/AOSC-Dev/aosc-os-abbs/tree/stable/app-network/sing-box) | 存在问题的构建标志列表修改 | | Homebrew | / | [homebrew-core][brew] | 存在问题的构建标志列表修改 | | Termux | Android | [termux-packages][termux] | 存在问题的构建标志列表修改 | | FreshPorts | FreeBSD | [FreeBSD ports][ports] | 太旧的 Go (go1.20) | 如果您是其用户,请向他们报告问题: 1. 在未完全了解相关功能的情况下,请勿修改发布版本标签:启用非默认标签可能会导致性能下降;缺少默认标签可能会引起用户混淆。 2. sing-box 支持使用一些较旧的 Go 版本进行编译,但不推荐使用(特别是已不再受 Go 支持的版本)。 ## :material-book-multiple: 服务管理 对于带有 [systemd][systemd] 的 Linux 系统,通常安装已经包含 sing-box 服务, 您可以使用以下命令管理服务: | 行动 | 命令 | |------|-----------------------------------------------| | 启用 | `sudo systemctl enable sing-box` | | 禁用 | `sudo systemctl disable sing-box` | | 启动 | `sudo systemctl start sing-box` | | 停止 | `sudo systemctl stop sing-box` | | 强行停止 | `sudo systemctl kill sing-box` | | 重新启动 | `sudo systemctl restart sing-box` | | 查看日志 | `sudo journalctl -u sing-box --output cat -e` | | 实时日志 | `sudo journalctl -u sing-box --output cat -f` | [alpine]: https://pkgs.alpinelinux.org/packages?name=sing-box [aur]: https://aur.archlinux.org/packages/sing-box [nixpkgs]: https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/tools/networking/sing-box/default.nix [brew]: https://formulae.brew.sh/formula/sing-box [choco]: https://chocolatey.org/packages/sing-box [scoop]: https://github.com/ScoopInstaller/Main/blob/master/bucket/sing-box.json [winget]: https://github.com/microsoft/winget-pkgs/tree/master/manifests/s/SagerNet/sing-box [termux]: https://github.com/termux/termux-packages/tree/master/packages/sing-box [ports]: https://www.freshports.org/net/sing-box [aosc]: https://packages.aosc.io/packages/sing-box [systemd]: https://systemd.io/ ================================================ FILE: docs/installation/tools/arch-install.sh ================================================ #!/bin/bash set -e -o pipefail ARCH_RAW=$(uname -m) case "${ARCH_RAW}" in 'x86_64') ARCH='amd64';; 'x86' | 'i686' | 'i386') ARCH='386';; 'aarch64' | 'arm64') ARCH='arm64';; 'armv7l') ARCH='armv7';; 's390x') ARCH='s390x';; *) echo "Unsupported architecture: ${ARCH_RAW}"; exit 1;; esac VERSION=$(curl -s https://api.github.com/repos/SagerNet/sing-box/releases/latest \ | grep tag_name \ | cut -d ":" -f2 \ | sed 's/\"//g;s/\,//g;s/\ //g;s/v//') curl -Lo sing-box.pkg.tar.zst "https://github.com/SagerNet/sing-box/releases/download/v${VERSION}/sing-box_${VERSION}_linux_${ARCH}.pkg.tar.zst" sudo pacman -U sing-box.pkg.tar.zst rm sing-box.pkg.tar.zst ================================================ FILE: docs/installation/tools/deb-install.sh ================================================ #!/bin/bash set -e -o pipefail ARCH_RAW=$(uname -m) case "${ARCH_RAW}" in 'x86_64') ARCH='amd64';; 'x86' | 'i686' | 'i386') ARCH='386';; 'aarch64' | 'arm64') ARCH='arm64';; 'armv7l') ARCH='armv7';; 's390x') ARCH='s390x';; *) echo "Unsupported architecture: ${ARCH_RAW}"; exit 1;; esac VERSION=$(curl -s https://api.github.com/repos/SagerNet/sing-box/releases/latest \ | grep tag_name \ | cut -d ":" -f2 \ | sed 's/\"//g;s/\,//g;s/\ //g;s/v//') curl -Lo sing-box.deb "https://github.com/SagerNet/sing-box/releases/download/v${VERSION}/sing-box_${VERSION}_linux_${ARCH}.deb" sudo dpkg -i sing-box.deb rm sing-box.deb ================================================ FILE: docs/installation/tools/install.sh ================================================ #!/bin/sh download_beta=false download_version="" while [ $# -gt 0 ]; do case "$1" in --beta) download_beta=true shift ;; --version) shift if [ $# -eq 0 ]; then echo "Missing argument for --version" echo "Usage: $0 [--beta] [--version ]" exit 1 fi download_version="$1" shift ;; *) echo "Unknown argument: $1" echo "Usage: $0 [--beta] [--version ]" exit 1 ;; esac done if command -v pacman >/dev/null 2>&1; then os="linux" arch=$(uname -m) package_suffix=".pkg.tar.zst" package_install="pacman -U --noconfirm" elif command -v dpkg >/dev/null 2>&1; then os="linux" arch=$(dpkg --print-architecture) package_suffix=".deb" package_install="dpkg -i" elif command -v dnf >/dev/null 2>&1; then os="linux" arch=$(uname -m) package_suffix=".rpm" package_install="dnf install -y" elif command -v rpm >/dev/null 2>&1; then os="linux" arch=$(uname -m) package_suffix=".rpm" package_install="rpm -i" elif command -v apk >/dev/null 2>&1 && [ -f /etc/os-release ] && grep -q OPENWRT_ARCH /etc/os-release; then os="openwrt" . /etc/os-release arch="$OPENWRT_ARCH" package_suffix=".apk" package_install="apk add --allow-untrusted" elif command -v apk >/dev/null 2>&1; then os="linux" arch=$(apk --print-arch) package_suffix=".apk" package_install="apk add --allow-untrusted" elif command -v opkg >/dev/null 2>&1; then os="openwrt" . /etc/os-release arch="$OPENWRT_ARCH" package_suffix=".ipk" package_install="opkg update && opkg install" else echo "Missing supported package manager." exit 1 fi if [ -z "$download_version" ]; then if [ "$download_beta" != "true" ]; then if [ -n "$GITHUB_TOKEN" ]; then latest_release=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" https://api.github.com/repos/SagerNet/sing-box/releases/latest) else latest_release=$(curl -s https://api.github.com/repos/SagerNet/sing-box/releases/latest) fi curl_exit_status=$? if [ $curl_exit_status -ne 0 ]; then exit $curl_exit_status fi if [ "$(echo "$latest_release" | grep tag_name | wc -l)" -eq 0 ]; then echo "$latest_release" exit 1 fi download_version=$(echo "$latest_release" | grep tag_name | head -n 1 | awk -F: '{print $2}' | sed 's/[", v]//g') else if [ -n "$GITHUB_TOKEN" ]; then latest_release=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" https://api.github.com/repos/SagerNet/sing-box/releases) else latest_release=$(curl -s https://api.github.com/repos/SagerNet/sing-box/releases) fi curl_exit_status=$? if [ $curl_exit_status -ne 0 ]; then exit $curl_exit_status fi if [ "$(echo "$latest_release" | grep tag_name | wc -l)" -eq 0 ]; then echo "$latest_release" exit 1 fi download_version=$(echo "$latest_release" | grep tag_name | head -n 1 | awk -F: '{print $2}' | sed 's/[", v]//g') fi fi package_name="sing-box_${download_version}_${os}_${arch}${package_suffix}" package_url="https://github.com/SagerNet/sing-box/releases/download/v${download_version}/${package_name}" echo "Downloading $package_url" if [ -n "$GITHUB_TOKEN" ]; then curl --fail -Lo "$package_name" -H "Authorization: token ${GITHUB_TOKEN}" "$package_url" else curl --fail -Lo "$package_name" "$package_url" fi curl_exit_status=$? if [ $curl_exit_status -ne 0 ]; then exit $curl_exit_status fi if command -v sudo >/dev/null 2>&1; then package_install="sudo $package_install" fi echo "$package_install $package_name" sh -c "$package_install \"$package_name\"" rm -f "$package_name" ================================================ FILE: docs/installation/tools/rpm-install.sh ================================================ #!/bin/bash set -e -o pipefail ARCH_RAW=$(uname -m) case "${ARCH_RAW}" in 'x86_64') ARCH='amd64';; 'x86' | 'i686' | 'i386') ARCH='386';; 'aarch64' | 'arm64') ARCH='arm64';; 'armv7l') ARCH='armv7';; 's390x') ARCH='s390x';; *) echo "Unsupported architecture: ${ARCH_RAW}"; exit 1;; esac VERSION=$(curl -s https://api.github.com/repos/SagerNet/sing-box/releases/latest \ | grep tag_name \ | cut -d ":" -f2 \ | sed 's/\"//g;s/\,//g;s/\ //g;s/v//') curl -Lo sing-box.rpm "https://github.com/SagerNet/sing-box/releases/download/v${VERSION}/sing-box_${VERSION}_linux_${ARCH}.rpm" sudo rpm -i sing-box.rpm rm sing-box.rpm ================================================ FILE: docs/installation/tools/sing-box.repo ================================================ [sing-box] name=sing-box baseurl=https://rpm.sagernet.org/ enabled=1 repo_gpgcheck=1 gpgcheck=1 gpgkey=https://sing-box.app/gpg.key ================================================ FILE: docs/manual/misc/tunnelvision.md ================================================ --- icon: material/book-lock-open --- # TunnelVision TunnelVision is an attack that uses DHCP option 121 to set higher priority routes so that traffic does not go through the VPN. Reference: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-3661 ## Status ### Android Android does not handle DHCP option 121 and is not affected. ### Apple platforms Update [sing-box graphical client](/clients/apple/#download) to `1.9.0-rc.16` or newer, then enable `includeAllNetworks` in `Settings` — `Packet Tunnel` and you will be unaffected. Note: when `includeAllNetworks` is enabled, the default TUN stack is changed to `gvisor`, and the `system` and `mixed` stacks are not available. ### Linux Update sing-box to `1.9.0-rc.16` or newer, rules generated by `auto-route` are unaffected. ### Windows No solution yet. ## Workarounds * Don't connect to untrusted networks * Relay untrusted network through another device * Just ignore it ================================================ FILE: docs/manual/proxy/client.md ================================================ --- icon: material/cellphone-link --- # Client ### :material-ray-start: Introduction For a long time, the modern usage and principles of proxy clients for graphical operating systems have not been clearly described. However, we can categorize them into three types: system proxy, firewall redirection, and virtual interface. ### :material-web-refresh: System Proxy Almost all graphical environments support system-level proxies, which are essentially ordinary HTTP proxies that only support TCP. | Operating System / Desktop Environment | System Proxy | Application Support | |:---------------------------------------------|:-------------------------------------|:--------------------| | Windows | :material-check: | :material-check: | | macOS | :material-check: | :material-check: | | GNOME/KDE | :material-check: | :material-check: | | Android | ROOT or adb (permission) is required | :material-check: | | Android/iOS (with sing-box graphical client) | via `tun.platform.http_proxy` | :material-check: | As one of the most well-known proxy methods, it has many shortcomings: many TCP clients that are not based on HTTP do not check and use the system proxy. Moreover, UDP and ICMP traffics bypass the proxy. ```mermaid flowchart LR dns[DNS query] -- Is HTTP request? --> proxy[HTTP proxy] dns --> leak[Leak] tcp[TCP connection] -- Is HTTP request? --> proxy tcp -- Check and use HTTP CONNECT? --> proxy tcp --> leak udp[UDP packet] --> leak ``` ### :material-wall-fire: Firewall Redirection This type of usage typically relies on the firewall or hook interface provided by the operating system, such as Windows’ WFP, Linux’s redirect, TProxy and eBPF, and macOS’s pf. Although it is intrusive and cumbersome to configure, it remains popular within the community of amateur proxy open source projects like V2Ray, due to the low technical requirements it imposes on the software. ### :material-expansion-card: Virtual Interface All L2/L3 proxies (seriously defined VPNs, such as OpenVPN, WireGuard) are based on virtual network interfaces, which is also the only way for all L4 proxies to work as VPNs on mobile platforms like Android, iOS. The sing-box inherits and develops clash-premium’s TUN inbound (L3 to L4 conversion) as the most reasonable method for performing transparent proxying. ```mermaid flowchart TB packet[IP Packet] packet --> windows[Windows / macOS] packet --> linux[Linux] tun[TUN interface] windows -. route .-> tun linux -. iproute2 route/rule .-> tun tun --> gvisor[gVisor TUN stack] tun --> system[system TUN stack] assemble([L3 to L4 assemble]) gvisor --> assemble system --> assemble assemble --> conn[TCP and UDP connections] conn --> router[sing-box Router] router --> direct[Direct outbound] router --> proxy[Proxy outbounds] router -- DNS hijack --> dns_out[DNS outbound] dns_out --> dns_router[DNS router] dns_router --> router direct --> adi([auto detect interface]) proxy --> adi adi --> default[Default network interface in the system] default --> destination[Destination server] default --> proxy_server[Proxy server] proxy_server --> destination ``` ## :material-cellphone-link: Examples ### Basic TUN usage for Chinese users === ":material-numeric-4-box: IPv4 only" ```json { "dns": { "servers": [ { "tag": "google", "type": "tls", "server": "8.8.8.8" }, { "tag": "local", "type": "udp", "server": "223.5.5.5" } ], "strategy": "ipv4_only" }, "inbounds": [ { "type": "tun", "address": ["172.19.0.1/30"], "auto_route": true, // "auto_redirect": true, // On linux "strict_route": true } ], "outbounds": [ // ... { "type": "direct", "tag": "direct" } ], "route": { "rules": [ { "action": "sniff" }, { "protocol": "dns", "action": "hijack-dns" }, { "ip_is_private": true, "outbound": "direct" } ], "default_domain_resolver": "local", "auto_detect_interface": true } } ``` === ":material-numeric-6-box: IPv4 & IPv6" ```json { "dns": { "servers": [ { "tag": "google", "type": "tls", "server": "8.8.8.8" }, { "tag": "local", "type": "udp", "server": "223.5.5.5" } ] }, "inbounds": [ { "type": "tun", "address": ["172.19.0.1/30", "fdfe:dcba:9876::1/126"], "auto_route": true, // "auto_redirect": true, // On linux "strict_route": true } ], "outbounds": [ // ... { "type": "direct", "tag": "direct" } ], "route": { "rules": [ { "action": "sniff" }, { "protocol": "dns", "action": "hijack-dns" }, { "ip_is_private": true, "outbound": "direct" } ], "default_domain_resolver": "local", "auto_detect_interface": true } } ``` === ":material-domain-switch: FakeIP" ```json { "dns": { "servers": [ { "tag": "google", "type": "tls", "server": "8.8.8.8" }, { "tag": "local", "type": "udp", "server": "223.5.5.5" }, { "tag": "remote", "type": "fakeip", "inet4_range": "198.18.0.0/15", "inet6_range": "fc00::/18" } ], "rules": [ { "query_type": [ "A", "AAAA" ], "server": "remote" } ], "independent_cache": true }, "inbounds": [ { "type": "tun", "address": ["172.19.0.1/30","fdfe:dcba:9876::1/126"], "auto_route": true, // "auto_redirect": true, // On linux "strict_route": true } ], "outbounds": [ // ... { "type": "direct", "tag": "direct" } ], "route": { "rules": [ { "action": "sniff" }, { "protocol": "dns", "action": "hijack-dns" }, { "ip_is_private": true, "outbound": "direct" } ], "default_domain_resolver": "local", "auto_detect_interface": true } } ``` ### Traffic bypass usage for Chinese users === ":material-dns: DNS rules" === ":material-shield-off: With DNS leaks" ```json { "dns": { "servers": [ { "tag": "google", "type": "tls", "server": "8.8.8.8" }, { "tag": "local", "type": "https", "server": "223.5.5.5" } ], "rules": [ { "rule_set": "geosite-geolocation-cn", "server": "local" }, { "type": "logical", "mode": "and", "rules": [ { "rule_set": "geosite-geolocation-!cn", "invert": true }, { "rule_set": "geoip-cn" } ], "server": "local" } ] }, "route": { "default_domain_resolver": "local", "rule_set": [ { "type": "remote", "tag": "geosite-geolocation-cn", "format": "binary", "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs" }, { "type": "remote", "tag": "geosite-geolocation-!cn", "format": "binary", "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs" }, { "type": "remote", "tag": "geoip-cn", "format": "binary", "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs" } ] }, "experimental": { "cache_file": { "enabled": true, "store_rdrc": true }, "clash_api": { "default_mode": "Enhanced" } } } ``` === ":material-security: Without DNS leaks, but slower" ```json { "dns": { "servers": [ { "tag": "google", "type": "tls", "server": "8.8.8.8" }, { "tag": "local", "type": "https", "server": "223.5.5.5" } ], "rules": [ { "rule_set": "geosite-geolocation-cn", "server": "local" }, { "type": "logical", "mode": "and", "rules": [ { "rule_set": "geosite-geolocation-!cn", "invert": true }, { "rule_set": "geoip-cn" } ], "server": "google", "client_subnet": "114.114.114.114/24" // Any China client IP address } ] }, "route": { "default_domain_resolver": "local", "rule_set": [ { "type": "remote", "tag": "geosite-geolocation-cn", "format": "binary", "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs" }, { "type": "remote", "tag": "geosite-geolocation-!cn", "format": "binary", "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs" }, { "type": "remote", "tag": "geoip-cn", "format": "binary", "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs" } ] }, "experimental": { "cache_file": { "enabled": true, "store_rdrc": true }, "clash_api": { "default_mode": "Enhanced" } } } ``` === ":material-router-network: Route rules" ```json { "outbounds": [ { "type": "direct", "tag": "direct" } ], "route": { "rules": [ { "action": "sniff" }, { "type": "logical", "mode": "or", "rules": [ { "protocol": "dns" }, { "port": 53 } ], "action": "hijack-dns" }, { "ip_is_private": true, "outbound": "direct" }, { "type": "logical", "mode": "or", "rules": [ { "port": 853 }, { "network": "udp", "port": 443 }, { "protocol": "stun" } ], "action": "reject" }, { "rule_set": "geosite-geolocation-cn", "outbound": "direct" }, { "type": "logical", "mode": "and", "rules": [ { "rule_set": "geoip-cn" }, { "rule_set": "geosite-geolocation-!cn", "invert": true } ], "outbound": "direct" } ], "rule_set": [ { "type": "remote", "tag": "geoip-cn", "format": "binary", "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs" }, { "type": "remote", "tag": "geosite-geolocation-cn", "format": "binary", "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs" } ] } } ``` ================================================ FILE: docs/manual/proxy/server.md ================================================ --- icon: material/server --- # Server To use sing-box as a proxy protocol server, you pretty much only need to configure the inbound for that protocol. The Proxy Protocol menu below contains descriptions and configuration examples of recommended protocols for bypassing GFW. ================================================ FILE: docs/manual/proxy-protocol/hysteria2.md ================================================ --- icon: material/lightning-bolt --- # Hysteria 2 Hysteria 2 is a simple, Chinese-made protocol based on QUIC. The selling point is Brutal, a congestion control algorithm that tries to achieve a user-defined bandwidth despite packet loss. !!! warning Even though GFW rarely blocks UDP-based proxies, such protocols actually have far more obvious characteristics than TCP based proxies. | Specification | Resists passive detection | Resists active probes | |---------------------------------------------------------------------------|---------------------------|-----------------------| | [hysteria.network](https://v2.hysteria.network/docs/developers/Protocol/) | :material-alert: | :material-check: | ## :material-text-box-check: Password Generator | Generate Password | Action | |----------------------------|-----------------------------------------------------------------| | | | ## :material-alert: Difference from official Hysteria The official program supports an authentication method called **userpass**, which essentially uses a combination of `:` as the actual password, while sing-box does not provide this alias. To use sing-box with the official program, you need to fill in that combination as the actual password. ## :material-server: Server Example !!! info "" Replace `up_mbps` and `down_mbps` values with the actual bandwidth of your server. === ":material-harddisk: With local certificate" ```json { "inbounds": [ { "type": "hysteria2", "listen": "::", "listen_port": 8080, "up_mbps": 100, "down_mbps": 100, "users": [ { "name": "sekai", "password": "" } ], "tls": { "enabled": true, "server_name": "example.org", "key_path": "/path/to/key.pem", "certificate_path": "/path/to/certificate.pem" } } ] } ``` === ":material-auto-fix: With ACME" ```json { "inbounds": [ { "type": "hysteria2", "listen": "::", "listen_port": 8080, "up_mbps": 100, "down_mbps": 100, "users": [ { "name": "sekai", "password": "" } ], "tls": { "enabled": true, "server_name": "example.org", "acme": { "domain": "example.org", "email": "admin@example.org" } } } ] } ``` === ":material-cloud: With ACME and Cloudflare API" ```json { "inbounds": [ { "type": "hysteria2", "listen": "::", "listen_port": 8080, "up_mbps": 100, "down_mbps": 100, "users": [ { "name": "sekai", "password": "" } ], "tls": { "enabled": true, "server_name": "example.org", "acme": { "domain": "example.org", "email": "admin@example.org", "dns01_challenge": { "provider": "cloudflare", "api_token": "my_token" } } } } ] } ``` ## :material-cellphone-link: Client Example !!! info "" Replace `up_mbps` and `down_mbps` values with the actual bandwidth of your client. === ":material-web-check: With valid certificate" ```json { "outbounds": [ { "type": "hysteria2", "server": "127.0.0.1", "server_port": 8080, "up_mbps": 100, "down_mbps": 100, "password": "", "tls": { "enabled": true, "server_name": "example.org" } } ] } ``` === ":material-check: With self-sign certificate" !!! info "Tip" Use `sing-box merge` command to merge configuration and certificate into one file. ```json { "outbounds": [ { "type": "hysteria2", "server": "127.0.0.1", "server_port": 8080, "up_mbps": 100, "down_mbps": 100, "password": "", "tls": { "enabled": true, "server_name": "example.org", "certificate_path": "/path/to/certificate.pem" } } ] } ``` === ":material-alert: Ignore certificate verification" ```json { "outbounds": [ { "type": "hysteria2", "server": "127.0.0.1", "server_port": 8080, "up_mbps": 100, "down_mbps": 100, "password": "", "tls": { "enabled": true, "server_name": "example.org", "insecure": true } } ] } ``` ================================================ FILE: docs/manual/proxy-protocol/shadowsocks.md ================================================ --- icon: material/send --- # Shadowsocks Shadowsocks is the most well-known Chinese-made proxy protocol. It exists in multiple versions, but only AEAD 2022 ciphers over TCP with multiplexing is recommended. | Ciphers | Specification | Cryptographically sound | Resists passive detection | Resists active probes | |----------------|------------------------------------------------------------|-------------------------|---------------------------|-----------------------| | Stream Ciphers | [shadowsocks.org](https://shadowsocks.org/doc/stream.html) | :material-alert: | :material-alert: | :material-alert: | | AEAD | [shadowsocks.org](https://shadowsocks.org/doc/aead.html) | :material-check: | :material-alert: | :material-alert: | | AEAD 2022 | [shadowsocks.org](https://shadowsocks.org/doc/sip022.html) | :material-check: | :material-check: | :material-help: | (We strongly recommend using multiplexing to send UDP traffic over TCP, because doing otherwise is vulnerable to passive detection.) ## :material-text-box-check: Password Generator | For `2022-blake3-aes-128-gcm` cipher | For other ciphers | Action | |--------------------------------------|-------------------------------|-----------------------------------------------------------------| | | | | ## :material-server: Server Example === ":material-account: Single-user" ```json { "inbounds": [ { "type": "shadowsocks", "listen": "::", "listen_port": 8080, "network": "tcp", "method": "2022-blake3-aes-128-gcm", "password": "", "multiplex": { "enabled": true } } ] } ``` === ":material-account-multiple: Multi-user" ```json { "inbounds": [ { "type": "shadowsocks", "listen": "::", "listen_port": 8080, "network": "tcp", "method": "2022-blake3-aes-128-gcm", "password": "", "users": [ { "name": "sekai", "password": "" } ], "multiplex": { "enabled": true } } ] } ``` ## :material-cellphone-link: Client Example === ":material-account: Single-user" ```json { "outbounds": [ { "type": "shadowsocks", "server": "127.0.0.1", "server_port": 8080, "method": "2022-blake3-aes-128-gcm", "password": "", "multiplex": { "enabled": true } } ] } ``` === ":material-account-multiple: Multi-user" ```json { "outbounds": [ { "type": "shadowsocks", "server": "127.0.0.1", "server_port": 8080, "method": "2022-blake3-aes-128-gcm", "password": ":", "multiplex": { "enabled": true } } ] } ``` ================================================ FILE: docs/manual/proxy-protocol/trojan.md ================================================ --- icon: material/horse --- # Trojan Trojan is the most commonly used TLS proxy made in China. It can be used in various combinations. | Protocol and implementation combination | Specification | Resists passive detection | Resists active probes | |-----------------------------------------|----------------------------------------------------------------------|---------------------------|-----------------------| | Origin / trojan-gfw | [trojan-gfw.github.io](https://trojan-gfw.github.io/trojan/protocol) | :material-check: | :material-check: | | Basic Go implementation | / | :material-alert: | :material-check: | | with privates transport by V2Ray | No formal definition | :material-alert: | :material-alert: | | with uTLS enabled | No formal definition | :material-help: | :material-check: | ## :material-text-box-check: Password Generator | Generate Password | Action | |----------------------------|-----------------------------------------------------------------| | | | ## :material-server: Server Example === ":material-harddisk: With local certificate" ```json { "inbounds": [ { "type": "trojan", "listen": "::", "listen_port": 8080, "users": [ { "name": "example", "password": "password" } ], "tls": { "enabled": true, "server_name": "example.org", "key_path": "/path/to/key.pem", "certificate_path": "/path/to/certificate.pem" }, "multiplex": { "enabled": true } } ] } ``` === ":material-auto-fix: With ACME" ```json { "inbounds": [ { "type": "trojan", "listen": "::", "listen_port": 8080, "users": [ { "name": "example", "password": "password" } ], "tls": { "enabled": true, "server_name": "example.org", "acme": { "domain": "example.org", "email": "admin@example.org" } }, "multiplex": { "enabled": true } } ] } ``` === ":material-cloud: With ACME and Cloudflare API" ```json { "inbounds": [ { "type": "trojan", "listen": "::", "listen_port": 8080, "users": [ { "name": "example", "password": "password" } ], "tls": { "enabled": true, "server_name": "example.org", "acme": { "domain": "example.org", "email": "admin@example.org", "dns01_challenge": { "provider": "cloudflare", "api_token": "my_token" } } }, "multiplex": { "enabled": true } } ] } ``` ## :material-cellphone-link: Client Example === ":material-web-check: With valid certificate" ```json { "outbounds": [ { "type": "trojan", "server": "127.0.0.1", "server_port": 8080, "password": "password", "tls": { "enabled": true, "server_name": "example.org" }, "multiplex": { "enabled": true } } ] } ``` === ":material-check: With self-sign certificate" !!! info "Tip" Use `sing-box merge` command to merge configuration and certificate into one file. ```json { "outbounds": [ { "type": "trojan", "server": "127.0.0.1", "server_port": 8080, "password": "password", "tls": { "enabled": true, "server_name": "example.org", "certificate_path": "/path/to/certificate.pem" }, "multiplex": { "enabled": true } } ] } ``` === ":material-alert: Ignore certificate verification" ```json { "outbounds": [ { "type": "trojan", "server": "127.0.0.1", "server_port": 8080, "password": "password", "tls": { "enabled": true, "server_name": "example.org", "insecure": true }, "multiplex": { "enabled": true } } ] } ``` ================================================ FILE: docs/migration.md ================================================ --- 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" } } ] } ``` ### 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. 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`. !!! 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" } ] } } ``` ### 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 [`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 DNS servers are refactored for better performance and scalability. !!! info "References" [DNS Server](/configuration/dns/server/) / [Legacy DNS Server](/configuration/dns/server/legacy/) === "Local" === ":material-card-remove: Deprecated" ```json { "dns": { "servers": [ { "address": "local" } ] } } ``` === ":material-card-multiple: New" ```json { "dns": { "servers": [ { "type": "local" } ] } } ``` === "TCP" === ":material-card-remove: Deprecated" ```json { "dns": { "servers": [ { "address": "tcp://1.1.1.1" } ] } } ``` === ":material-card-multiple: New" ```json { "dns": { "servers": [ { "type": "tcp", "server": "1.1.1.1" } ] } } ``` === "UDP" === ":material-card-remove: Deprecated" ```json { "dns": { "servers": [ { "address": "1.1.1.1" } ] } } ``` === ":material-card-multiple: New" ```json { "dns": { "servers": [ { "type": "udp", "server": "1.1.1.1" } ] } } ``` === "TLS" === ":material-card-remove: Deprecated" ```json { "dns": { "servers": [ { "address": "tls://1.1.1.1" } ] } } ``` === ":material-card-multiple: New" ```json { "dns": { "servers": [ { "type": "tls", "server": "1.1.1.1" } ] } } ``` === "HTTPS" === ":material-card-remove: Deprecated" ```json { "dns": { "servers": [ { "address": "https://1.1.1.1/dns-query" } ] } } ``` === ":material-card-multiple: New" ```json { "dns": { "servers": [ { "type": "https", "server": "1.1.1.1" } ] } } ``` === "QUIC" === ":material-card-remove: Deprecated" ```json { "dns": { "servers": [ { "address": "quic://1.1.1.1" } ] } } ``` === ":material-card-multiple: New" ```json { "dns": { "servers": [ { "type": "quic", "server": "1.1.1.1" } ] } } ``` === "HTTP3" === ":material-card-remove: Deprecated" ```json { "dns": { "servers": [ { "address": "h3://1.1.1.1/dns-query" } ] } } ``` === ":material-card-multiple: New" ```json { "dns": { "servers": [ { "type": "h3", "server": "1.1.1.1" } ] } } ``` === "DHCP" === ":material-card-remove: Deprecated" ```json { "dns": { "servers": [ { "address": "dhcp://auto" }, { "address": "dhcp://en0" } ] } } ``` === ":material-card-multiple: New" ```json { "dns": { "servers": [ { "type": "dhcp", }, { "type": "dhcp", "interface": "en0" } ] } } ``` === "FakeIP" === ":material-card-remove: Deprecated" ```json { "dns": { "servers": [ { "address": "1.1.1.1" }, { "address": "fakeip", "tag": "fakeip" } ], "rules": [ { "query_type": [ "A", "AAAA" ], "server": "fakeip" } ], "fakeip": { "enabled": true, "inet4_range": "198.18.0.0/15", "inet6_range": "fc00::/18" } } } ``` === ":material-card-multiple: New" ```json { "dns": { "servers": [ { "type": "udp", "server": "1.1.1.1" }, { "type": "fakeip", "tag": "fakeip", "inet4_range": "198.18.0.0/15", "inet6_range": "fc00::/18" } ], "rules": [ { "query_type": [ "A", "AAAA" ], "server": "fakeip" } ] } } ``` === "RCode" === ":material-card-remove: Deprecated" ```json { "dns": { "servers": [ { "address": "rcode://refused" } ] } } ``` === ":material-card-multiple: New" ```json { "dns": { "rules": [ { "domain": [ "example.com" ], // other rules "action": "predefined", "rcode": "REFUSED" } ] } } ``` === "Servers with domain address" === ":material-card-remove: Deprecated" ```json { "dns": { "servers": [ { "address": "https://dns.google/dns-query", "address_resolver": "google" }, { "tag": "google", "address": "1.1.1.1" } ] } } ``` === ":material-card-multiple: New" ```json { "dns": { "servers": [ { "type": "https", "server": "dns.google", "domain_resolver": "google" }, { "type": "udp", "tag": "google", "server": "1.1.1.1" } ] } } ``` === "Servers with strategy" === ":material-card-remove: Deprecated" ```json { "dns": { "servers": [ { "address": "1.1.1.1", "strategy": "ipv4_only" }, { "tag": "google", "address": "8.8.8.8", "strategy": "prefer_ipv6" } ], "rules": [ { "domain": "google.com", "server": "google" } ] } } ``` === ":material-card-multiple: New" ```json { "dns": { "servers": [ { "type": "udp", "server": "1.1.1.1" }, { "type": "udp", "tag": "google", "server": "8.8.8.8" } ], "rules": [ { "domain": "google.com", "server": "google", "strategy": "prefer_ipv6" } ], "strategy": "ipv4_only" } } ``` === "Servers with client subnet" === ":material-card-remove: Deprecated" ```json { "dns": { "servers": [ { "address": "1.1.1.1" }, { "tag": "google", "address": "8.8.8.8", "client_subnet": "1.1.1.1" } ] } } ``` === ":material-card-multiple: New" ```json { "dns": { "servers": [ { "type": "udp", "server": "1.1.1.1" }, { "type": "udp", "tag": "google", "server": "8.8.8.8" } ], "rules": [ { "domain": "google.com", "server": "google", "client_subnet": "1.1.1.1" } ] } } ``` ### Migrate outbound DNS rule items to domain resolver The legacy outbound DNS rules are deprecated and can be replaced by new domain resolver options. !!! info "References" [DNS rule](/configuration/dns/rule/#outbound) / [Dial Fields](/configuration/shared/dial/#domain_resolver) / [Route](/configuration/route/#domain_resolver) === ":material-card-remove: Deprecated" ```json { "dns": { "servers": [ { "address": "local", "tag": "local" } ], "rules": [ { "outbound": "any", "server": "local" } ] }, "outbounds": [ { "type": "socks", "server": "example.org", "server_port": 2080 } ] } ``` === ":material-card-multiple: New" ```json { "dns": { "servers": [ { "type": "local", "tag": "local" } ] }, "outbounds": [ { "type": "socks", "server": "example.org", "server_port": 2080, "domain_resolver": { "server": "local", "rewrite_ttl": 60, "client_subnet": "1.1.1.1" }, // or "domain_resolver": "local", } ], // or "route": { "default_domain_resolver": { "server": "local", "rewrite_ttl": 60, "client_subnet": "1.1.1.1" } } } ``` ### Migrate outbound domain strategy option to domain resolver !!! info "References" [Dial Fields](/configuration/shared/dial/#domain_strategy) The `domain_strategy` option in Dial Fields has been deprecated and can be replaced with the new domain resolver option. Note that due to the use of Dial Fields by some of the new DNS servers introduced in sing-box 1.12, some people mistakenly believe that `domain_strategy` is the same feature as in the legacy DNS servers. === ":material-card-remove: Deprecated" ```json { "outbounds": [ { "type": "socks", "server": "example.org", "server_port": 2080, "domain_strategy": "prefer_ipv4", } ] } ``` === ":material-card-multiple: New" ```json { "dns": { "servers": [ { "type": "local", "tag": "local" } ] }, "outbounds": [ { "type": "socks", "server": "example.org", "server_port": 2080, "domain_resolver": { "server": "local", "strategy": "prefer_ipv4" } } ] } ``` ## 1.11.0 ### Migrate legacy special outbounds to rule actions Legacy special outbounds are deprecated and can be replaced by rule actions. !!! info "References" [Rule Action](/configuration/route/rule_action/) / [Block](/configuration/outbound/block/) / [DNS](/configuration/outbound/dns) === "Block" === ":material-card-remove: Deprecated" ```json { "outbounds": [ { "type": "block", "tag": "block" } ], "route": { "rules": [ { ..., "outbound": "block" } ] } } ``` === ":material-card-multiple: New" ```json { "route": { "rules": [ { ..., "action": "reject" } ] } } ``` === "DNS" === ":material-card-remove: Deprecated" ```json { "inbound": [ { ..., "sniff": true } ], "outbounds": [ { "tag": "dns", "type": "dns" } ], "route": { "rules": [ { "protocol": "dns", "outbound": "dns" } ] } } ``` === ":material-card-multiple: New" ```json { "route": { "rules": [ { "action": "sniff" }, { "protocol": "dns", "action": "hijack-dns" } ] } } ``` ### Migrate legacy inbound fields to rule actions Inbound fields are deprecated and can be replaced by rule actions. !!! info "References" [Listen Fields](/configuration/shared/listen/) / [Rule](/configuration/route/rule/) / [Rule Action](/configuration/route/rule_action/) / [DNS Rule](/configuration/dns/rule/) / [DNS Rule Action](/configuration/dns/rule_action/) === ":material-card-remove: Deprecated" ```json { "inbounds": [ { "type": "mixed", "sniff": true, "sniff_timeout": "1s", "domain_strategy": "prefer_ipv4" } ] } ``` === ":material-card-multiple: New" ```json { "inbounds": [ { "type": "mixed", "tag": "in" } ], "route": { "rules": [ { "inbound": "in", "action": "resolve", "strategy": "prefer_ipv4" }, { "inbound": "in", "action": "sniff", "timeout": "1s" } ] } } ``` ### Migrate destination override fields to route options Destination override fields in direct outbound are deprecated and can be replaced by route options. !!! info "References" [Rule Action](/configuration/route/rule_action/) / [Direct](/configuration/outbound/direct/) === ":material-card-remove: Deprecated" ```json { "outbounds": [ { "type": "direct", "override_address": "1.1.1.1", "override_port": 443 } ] } ``` === ":material-card-multiple: New" ```json { "route": { "rules": [ { "action": "route-options", // or route "override_address": "1.1.1.1", "override_port": 443 } ] } ``` ### Migrate WireGuard outbound to endpoint WireGuard outbound is deprecated and can be replaced by endpoint. !!! info "References" [Endpoint](/configuration/endpoint/) / [WireGuard Endpoint](/configuration/endpoint/wireguard/) / [WireGuard Outbound](/configuration/outbound/wireguard/) === ":material-card-remove: Deprecated" ```json { "outbounds": [ { "type": "wireguard", "tag": "wg-out", "server": "127.0.0.1", "server_port": 10001, "system_interface": true, "gso": true, "interface_name": "wg0", "local_address": [ "10.0.0.1/32" ], "private_key": "", "peer_public_key": "", "pre_shared_key": "", "reserved": [0, 0, 0], "mtu": 1408 } ] } ``` === ":material-card-multiple: New" ```json { "endpoints": [ { "type": "wireguard", "tag": "wg-ep", "system": true, "name": "wg0", "mtu": 1408, "address": [ "10.0.0.2/32" ], "private_key": "", "listen_port": 10000, "peers": [ { "address": "127.0.0.1", "port": 10001, "public_key": "", "pre_shared_key": "", "allowed_ips": [ "0.0.0.0/0" ], "persistent_keepalive_interval": 30, "reserved": [0, 0, 0] } ] } ] } ``` ## 1.10.0 ### TUN address fields are merged `inet4_address` and `inet6_address` are merged into `address`, `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`. !!! info "References" [TUN](/configuration/inbound/tun/) === ":material-card-remove: Deprecated" ```json { "inbounds": [ { "type": "tun", "inet4_address": "172.19.0.1/30", "inet6_address": "fdfe:dcba:9876::1/126", "inet4_route_address": [ "0.0.0.0/1", "128.0.0.0/1" ], "inet6_route_address": [ "::/1", "8000::/1" ], "inet4_route_exclude_address": [ "192.168.0.0/16" ], "inet6_route_exclude_address": [ "fc00::/7" ] } ] } ``` === ":material-card-multiple: New" ```json { "inbounds": [ { "type": "tun", "address": [ "172.19.0.1/30", "fdfe:dcba:9876::1/126" ], "route_address": [ "0.0.0.0/1", "128.0.0.0/1", "::/1", "8000::/1" ], "route_exclude_address": [ "192.168.0.0/16", "fc00::/7" ] } ] } ``` ## 1.9.5 ### Bundle Identifier updates in Apple platform clients Due to problems with our old Apple developer account, we can only change Bundle Identifiers to re-list sing-box apps, which means the data will not be automatically inherited. For iOS, you need to back up your old data yourself (if you still have access to it); for tvOS, you need to re-import profiles from your iPhone or iPad or create it manually; for macOS, you can migrate the data folder using the following command: ```bash cd ~/Library/Group\ Containers && \ mv group.io.nekohasekai.sfa group.io.nekohasekai.sfavt ``` ## 1.9.0 ### `domain_suffix` behavior update For historical reasons, sing-box's `domain_suffix` rule matches literal prefixes instead of the same as other projects. sing-box 1.9.0 modifies the behavior of `domain_suffix`: If the rule value is prefixed with `.`, the behavior is unchanged, otherwise it matches `(domain|.+\.domain)` instead. ### `process_path` format update on Windows The `process_path` rule of sing-box is inherited from Clash, the original code uses the local system's path format (e.g. `\Device\HarddiskVolume1\folder\program.exe`), but when the device has multiple disks, the HarddiskVolume serial number is not stable. sing-box 1.9.0 make QueryFullProcessImageNameW output a Win32 path (such as `C:\folder\program.exe`), which will disrupt the existing `process_path` use cases in Windows. ## 1.8.0 ### :material-close-box: Migrate cache file from Clash API to independent options !!! info "References" [Clash API](/configuration/experimental/clash-api/) / [Cache File](/configuration/experimental/cache-file/) === ":material-card-remove: Deprecated" ```json { "experimental": { "clash_api": { "cache_file": "cache.db", // default value "cahce_id": "my_profile2", "store_mode": true, "store_selected": true, "store_fakeip": true } } } ``` === ":material-card-multiple: New" ```json { "experimental" : { "cache_file": { "enabled": true, "path": "cache.db", // default value "cache_id": "my_profile2", "store_fakeip": true } } } ``` ### :material-checkbox-intermediate: Migrate GeoIP to rule-sets !!! info "References" [GeoIP](/configuration/route/geoip/) / [Route](/configuration/route/) / [Route Rule](/configuration/route/rule/) / [DNS Rule](/configuration/dns/rule/) / [rule-set](/configuration/rule-set/) !!! tip `sing-box geoip` commands can help you convert custom GeoIP into rule-sets. === ":material-card-remove: Deprecated" ```json { "route": { "rules": [ { "geoip": "private", "outbound": "direct" }, { "geoip": "cn", "outbound": "direct" }, { "source_geoip": "cn", "outbound": "block" } ], "geoip": { "download_detour": "proxy" } } } ``` === ":material-card-multiple: New" ```json { "route": { "rules": [ { "ip_is_private": true, "outbound": "direct" }, { "rule_set": "geoip-cn", "outbound": "direct" }, { "rule_set": "geoip-us", "rule_set_ipcidr_match_source": true, "outbound": "block" } ], "rule_set": [ { "tag": "geoip-cn", "type": "remote", "format": "binary", "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs", "download_detour": "proxy" }, { "tag": "geoip-us", "type": "remote", "format": "binary", "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-us.srs", "download_detour": "proxy" } ] }, "experimental": { "cache_file": { "enabled": true // required to save rule-set cache } } } ``` ### :material-checkbox-intermediate: Migrate Geosite to rule-sets !!! info "References" [Geosite](/configuration/route/geosite/) / [Route](/configuration/route/) / [Route Rule](/configuration/route/rule/) / [DNS Rule](/configuration/dns/rule/) / [rule-set](/configuration/rule-set/) !!! tip `sing-box geosite` commands can help you convert custom Geosite into rule-sets. === ":material-card-remove: Deprecated" ```json { "route": { "rules": [ { "geosite": "cn", "outbound": "direct" } ], "geosite": { "download_detour": "proxy" } } } ``` === ":material-card-multiple: New" ```json { "route": { "rules": [ { "rule_set": "geosite-cn", "outbound": "direct" } ], "rule_set": [ { "tag": "geosite-cn", "type": "remote", "format": "binary", "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs", "download_detour": "proxy" } ] }, "experimental": { "cache_file": { "enabled": true // required to save rule-set cache } } } ``` ================================================ FILE: docs/migration.zh.md ================================================ --- 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" } } ] } ``` ### 迁移地址筛选字段到响应匹配 旧版地址筛选字段(不使用 `match_response` 的 `ip_cidr`、`ip_is_private`)已废弃, 旧版 `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` 显式匹配。 !!! 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" } ] } } ``` ### 迁移 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 规则中的 [`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 服务器格式 DNS 服务器已经重构。 !!! info "引用" [DNS 服务器](/zh/configuration/dns/server/) / [旧 DNS 服务器](/zh/configuration/dns/server/legacy/) === "Local" === ":material-card-remove: 弃用的" ```json { "dns": { "servers": [ { "address": "local" } ] } } ``` === ":material-card-multiple: 新的" ```json { "dns": { "servers": [ { "type": "local" } ] } } ``` === "TCP" === ":material-card-remove: 弃用的" ```json { "dns": { "servers": [ { "address": "tcp://1.1.1.1" } ] } } ``` === ":material-card-multiple: 新的" ```json { "dns": { "servers": [ { "type": "tcp", "server": "1.1.1.1" } ] } } ``` === "UDP" === ":material-card-remove: 弃用的" ```json { "dns": { "servers": [ { "address": "1.1.1.1" } ] } } ``` === ":material-card-multiple: 新的" ```json { "dns": { "servers": [ { "type": "udp", "server": "1.1.1.1" } ] } } ``` === "TLS" === ":material-card-remove: 弃用的" ```json { "dns": { "servers": [ { "address": "tls://1.1.1.1" } ] } } ``` === ":material-card-multiple: 新的" ```json { "dns": { "servers": [ { "type": "tls", "server": "1.1.1.1" } ] } } ``` === "HTTPS" === ":material-card-remove: 弃用的" ```json { "dns": { "servers": [ { "address": "https://1.1.1.1/dns-query" } ] } } ``` === ":material-card-multiple: 新的" ```json { "dns": { "servers": [ { "type": "https", "server": "1.1.1.1" } ] } } ``` === "QUIC" === ":material-card-remove: 弃用的" ```json { "dns": { "servers": [ { "address": "quic://1.1.1.1" } ] } } ``` === ":material-card-multiple: 新的" ```json { "dns": { "servers": [ { "type": "quic", "server": "1.1.1.1" } ] } } ``` === "HTTP3" === ":material-card-remove: 弃用的" ```json { "dns": { "servers": [ { "address": "h3://1.1.1.1/dns-query" } ] } } ``` === ":material-card-multiple: 新的" ```json { "dns": { "servers": [ { "type": "h3", "server": "1.1.1.1" } ] } } ``` === "DHCP" === ":material-card-remove: 弃用的" ```json { "dns": { "servers": [ { "address": "dhcp://auto" }, { "address": "dhcp://en0" } ] } } ``` === ":material-card-multiple: 新的" ```json { "dns": { "servers": [ { "type": "dhcp", }, { "type": "dhcp", "interface": "en0" } ] } } ``` === "FakeIP" === ":material-card-remove: 弃用的" ```json { "dns": { "servers": [ { "address": "1.1.1.1" }, { "address": "fakeip", "tag": "fakeip" } ], "rules": [ { "query_type": [ "A", "AAAA" ], "server": "fakeip" } ], "fakeip": { "enabled": true, "inet4_range": "198.18.0.0/15", "inet6_range": "fc00::/18" } } } ``` === ":material-card-multiple: 新的" ```json { "dns": { "servers": [ { "type": "udp", "server": "1.1.1.1" }, { "type": "fakeip", "tag": "fakeip", "inet4_range": "198.18.0.0/15", "inet6_range": "fc00::/18" } ], "rules": [ { "query_type": [ "A", "AAAA" ], "server": "fakeip" } ] } } ``` === "RCode" === ":material-card-remove: 弃用的" ```json { "dns": { "servers": [ { "address": "rcode://refused" } ] } } ``` === ":material-card-multiple: 新的" ```json { "dns": { "rules": [ { "domain": [ "example.com" ], // 其它规则 "action": "predefined", "rcode": "REFUSED" } ] } } ``` === "带有域名地址的服务器" === ":material-card-remove: 弃用的" ```json { "dns": { "servers": [ { "address": "https://dns.google/dns-query", "address_resolver": "google" }, { "tag": "google", "address": "1.1.1.1" } ] } } ``` === ":material-card-multiple: 新的" ```json { "dns": { "servers": [ { "type": "https", "server": "dns.google", "domain_resolver": "google" }, { "type": "udp", "tag": "google", "server": "1.1.1.1" } ] } } ``` === "带有域策略的服务器" === ":material-card-remove: 弃用的" ```json { "dns": { "servers": [ { "address": "1.1.1.1", "strategy": "ipv4_only" }, { "tag": "google", "address": "8.8.8.8", "strategy": "prefer_ipv6" } ], "rules": [ { "domain": "google.com", "server": "google" } ] } } ``` === ":material-card-multiple: 新的" ```json { "dns": { "servers": [ { "type": "udp", "server": "1.1.1.1" }, { "type": "udp", "tag": "google", "server": "8.8.8.8" } ], "rules": [ { "domain": "google.com", "server": "google", "strategy": "prefer_ipv6" } ], "strategy": "ipv4_only" } } ``` === "带有客户端子网的服务器" === ":material-card-remove: 弃用的" ```json { "dns": { "servers": [ { "address": "1.1.1.1" }, { "tag": "google", "address": "8.8.8.8", "client_subnet": "1.1.1.1" } ] } } ``` === ":material-card-multiple: 新的" ```json { "dns": { "servers": [ { "type": "udp", "server": "1.1.1.1" }, { "type": "udp", "tag": "google", "server": "8.8.8.8" } ], "rules": [ { "domain": "google.com", "server": "google", "client_subnet": "1.1.1.1" } ] } } ``` ### 迁移 outbound DNS 规则项到域解析选项 旧的 `outbound` DNS 规则已废弃,且可新的域解析选项代替。 !!! info "参考" [DNS 规则](/zh/configuration/dns/rule/#outbound) / [拨号字段](/zh/configuration/shared/dial/#domain_resolver) / [路由](/zh/configuration/route/#default_domain_resolver) === ":material-card-remove: 废弃的" ```json { "dns": { "servers": [ { "address": "local", "tag": "local" } ], "rules": [ { "outbound": "any", "server": "local" } ] }, "outbounds": [ { "type": "socks", "server": "example.org", "server_port": 2080 } ] } ``` === ":material-card-multiple: 新的" ```json { "dns": { "servers": [ { "type": "local", "tag": "local" } ] }, "outbounds": [ { "type": "socks", "server": "example.org", "server_port": 2080, "domain_resolver": { "server": "local", "rewrite_ttl": 60, "client_subnet": "1.1.1.1" }, // 或 "domain_resolver": "local", } ], // 或 "route": { "default_domain_resolver": { "server": "local", "rewrite_ttl": 60, "client_subnet": "1.1.1.1" } } } ``` ### 迁移出站域名策略选项到域名解析器 拨号字段中的 `domain_strategy` 选项已被弃用,可以用新的域名解析器选项替代。 请注意,由于 sing-box 1.12 中引入的一些新 DNS 服务器使用了拨号字段,一些人错误地认为 `domain_strategy` 与旧 DNS 服务器中的功能相同。 !!! info "参考" [拨号字段](/zh/configuration/shared/dial/#domain_strategy) === ":material-card-remove: 弃用的" ```json { "outbounds": [ { "type": "socks", "server": "example.org", "server_port": 2080, "domain_strategy": "prefer_ipv4", } ] } ``` === ":material-card-multiple: 新的" ```json { "dns": { "servers": [ { "type": "local", "tag": "local" } ] }, "outbounds": [ { "type": "socks", "server": "example.org", "server_port": 2080, "domain_resolver": { "server": "local", "strategy": "prefer_ipv4" } } ] } ``` ## 1.11.0 ### 迁移旧的特殊出站到规则动作 旧的特殊出站已被弃用,且可以被规则动作替代。 !!! info "参考" [规则动作](/zh/configuration/route/rule_action/) / [Block](/zh/configuration/outbound/block/) / [DNS](/zh/configuration/outbound/dns) === "Block" === ":material-card-remove: 弃用的" ```json { "outbounds": [ { "type": "block", "tag": "block" } ], "route": { "rules": [ { ..., "outbound": "block" } ] } } ``` === ":material-card-multiple: 新的" ```json { "route": { "rules": [ { ..., "action": "reject" } ] } } ``` === "DNS" === ":material-card-remove: 弃用的" ```json { "inbound": [ { ..., "sniff": true } ], "outbounds": [ { "tag": "dns", "type": "dns" } ], "route": { "rules": [ { "protocol": "dns", "outbound": "dns" } ] } } ``` === ":material-card-multiple: 新的" ```json { "route": { "rules": [ { "action": "sniff" }, { "protocol": "dns", "action": "hijack-dns" } ] } } ``` ### 迁移旧的入站字段到规则动作 入站选项已被弃用,且可以被规则动作替代。 !!! info "参考" [监听字段](/zh/configuration/shared/listen/) / [规则](/zh/configuration/route/rule/) / [规则动作](/zh/configuration/route/rule_action/) / [DNS 规则](/zh/configuration/dns/rule/) / [DNS 规则动作](/zh/configuration/dns/rule_action/) === ":material-card-remove: 弃用的" ```json { "inbounds": [ { "type": "mixed", "sniff": true, "sniff_timeout": "1s", "domain_strategy": "prefer_ipv4" } ] } ``` === ":material-card-multiple: 新的" ```json { "inbounds": [ { "type": "mixed", "tag": "in" } ], "route": { "rules": [ { "inbound": "in", "action": "resolve", "strategy": "prefer_ipv4" }, { "inbound": "in", "action": "sniff", "timeout": "1s" } ] } } ``` ### 迁移 direct 出站中的目标地址覆盖字段到路由字段 direct 出站中的目标地址覆盖字段已废弃,且可以被路由字段替代。 !!! info "参考" [Rule Action](/zh/configuration/route/rule_action/) / [Direct](/zh/configuration/outbound/direct/) === ":material-card-remove: 弃用的" ```json { "outbounds": [ { "type": "direct", "override_address": "1.1.1.1", "override_port": 443 } ] } ``` === ":material-card-multiple: 新的" ```json { "route": { "rules": [ { "action": "route-options", // 或 route "override_address": "1.1.1.1", "override_port": 443 } ] } } ``` ### 迁移 WireGuard 出站到端点 WireGuard 出站已被弃用,且可以被端点替代。 !!! info "参考" [端点](/zh/configuration/endpoint/) / [WireGuard 端点](/zh/configuration/endpoint/wireguard/) / [WireGuard 出站](/zh/configuration/outbound/wireguard/) === ":material-card-remove: 弃用的" ```json { "outbounds": [ { "type": "wireguard", "tag": "wg-out", "server": "127.0.0.1", "server_port": 10001, "system_interface": true, "gso": true, "interface_name": "wg0", "local_address": [ "10.0.0.1/32" ], "private_key": "", "peer_public_key": "", "pre_shared_key": "", "reserved": [0, 0, 0], "mtu": 1408 } ] } ``` === ":material-card-multiple: 新的" ```json { "endpoints": [ { "type": "wireguard", "tag": "wg-ep", "system": true, "name": "wg0", "mtu": 1408, "address": [ "10.0.0.2/32" ], "private_key": "", "listen_port": 10000, "peers": [ { "address": "127.0.0.1", "port": 10001, "public_key": "", "pre_shared_key": "", "allowed_ips": [ "0.0.0.0/0" ], "persistent_keepalive_interval": 30, "reserved": [0, 0, 0] } ] } ] } ``` ## 1.10.0 ### TUN 地址字段已合并 `inet4_address` 和 `inet6_address` 已合并为 `address`, `inet4_route_address` 和 `inet6_route_address` 已合并为 `route_address`, `inet4_route_exclude_address` 和 `inet6_route_exclude_address` 已合并为 `route_exclude_address`。 !!! info "参考" [TUN](/zh/configuration/inbound/tun/) === ":material-card-remove: 弃用的" ```json { "inbounds": [ { "type": "tun", "inet4_address": "172.19.0.1/30", "inet6_address": "fdfe:dcba:9876::1/126", "inet4_route_address": [ "0.0.0.0/1", "128.0.0.0/1" ], "inet6_route_address": [ "::/1", "8000::/1" ], "inet4_route_exclude_address": [ "192.168.0.0/16" ], "inet6_route_exclude_address": [ "fc00::/7" ] } ] } ``` === ":material-card-multiple: 新的" ```json { "inbounds": [ { "type": "tun", "address": [ "172.19.0.1/30", "fdfe:dcba:9876::1/126" ], "route_address": [ "0.0.0.0/1", "128.0.0.0/1", "::/1", "8000::/1" ], "route_exclude_address": [ "192.168.0.0/16", "fc00::/7" ] } ] } ``` ## 1.9.5 ### Apple 平台客户端的 Bundle Identifier 更新 由于我们旧的苹果开发者账户存在问题,我们只能通过更新 Bundle Identifiers 来重新上架 sing-box 应用, 这意味着数据不会自动继承。 对于 iOS,您需要自行备份旧的数据(如果您仍然可以访问); 对于 Apple tvOS,您需要从 iPhone 或 iPad 重新导入配置或者手动创建; 对于 macOS,您可以使用以下命令迁移数据文件夹: ```bash cd ~/Library/Group\ Containers && \ mv group.io.nekohasekai.sfa group.io.nekohasekai.sfavt ``` ## 1.9.0 ### `domain_suffix` 行为更新 由于历史原因,sing-box 的 `domain_suffix` 规则匹配字面前缀,而不与其他项目相同。 sing-box 1.9.0 修改了 `domain_suffix` 的行为:如果规则值以 `.` 为前缀则行为不变,否则改为匹配 `(domain|.+\.domain)`。 ### 对 Windows 上 `process_path` 格式的更新 sing-box 的 `process_path` 规则继承自Clash, 原始代码使用本地系统的路径格式(例如 `\Device\HarddiskVolume1\folder\program.exe`), 但是当设备有多个硬盘时,该 HarddiskVolume 系列号并不稳定。 sing-box 1.9.0 使 QueryFullProcessImageNameW 输出 Win32 路径(如 `C:\folder\program.exe`), 这将会破坏现有的 Windows `process_path` 用例。 ## 1.8.0 ### :material-close-box: 将缓存文件从 Clash API 迁移到独立选项 !!! info "参考" [Clash API](/zh/configuration/experimental/clash-api/) / [Cache File](/zh/configuration/experimental/cache-file/) === ":material-card-remove: 弃用的" ```json { "experimental": { "clash_api": { "cache_file": "cache.db", // 默认值 "cahce_id": "my_profile2", "store_mode": true, "store_selected": true, "store_fakeip": true } } } ``` === ":material-card-multiple: 新的" ```json { "experimental" : { "cache_file": { "enabled": true, "path": "cache.db", // 默认值 "cache_id": "my_profile2", "store_fakeip": true } } } ``` ### :material-checkbox-intermediate: 迁移 GeoIP 到规则集 !!! info "参考" [GeoIP](/zh/configuration/route/geoip/) / [路由](/zh/configuration/route/) / [路由规则](/zh/configuration/route/rule/) / [DNS 规则](/zh/configuration/dns/rule/) / [规则集](/zh/configuration/rule-set/) !!! tip `sing-box geoip` 命令可以帮助您将自定义 GeoIP 转换为规则集。 === ":material-card-remove: 弃用的" ```json { "route": { "rules": [ { "geoip": "private", "outbound": "direct" }, { "geoip": "cn", "outbound": "direct" }, { "source_geoip": "cn", "outbound": "block" } ], "geoip": { "download_detour": "proxy" } } } ``` === ":material-card-multiple: 新的" ```json { "route": { "rules": [ { "ip_is_private": true, "outbound": "direct" }, { "rule_set": "geoip-cn", "outbound": "direct" }, { "rule_set": "geoip-us", "rule_set_ipcidr_match_source": true, "outbound": "block" } ], "rule_set": [ { "tag": "geoip-cn", "type": "remote", "format": "binary", "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs", "download_detour": "proxy" }, { "tag": "geoip-us", "type": "remote", "format": "binary", "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-us.srs", "download_detour": "proxy" } ] }, "experimental": { "cache_file": { "enabled": true // required to save rule-set cache } } } ``` ### :material-checkbox-intermediate: 迁移 Geosite 到规则集 !!! info "参考" [Geosite](/zh/configuration/route/geosite/) / [路由](/zh/configuration/route/) / [路由规则](/zh/configuration/route/rule/) / [DNS 规则](/zh/configuration/dns/rule/) / [规则集](/zh/configuration/rule-set/) !!! tip `sing-box geosite` 命令可以帮助您将自定义 Geosite 转换为规则集。 === ":material-card-remove: 弃用的" ```json { "route": { "rules": [ { "geosite": "cn", "outbound": "direct" } ], "geosite": { "download_detour": "proxy" } } } ``` === ":material-card-multiple: 新的" ```json { "route": { "rules": [ { "rule_set": "geosite-cn", "outbound": "direct" } ], "rule_set": [ { "tag": "geosite-cn", "type": "remote", "format": "binary", "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs", "download_detour": "proxy" } ] }, "experimental": { "cache_file": { "enabled": true // required to save rule-set cache } } } ``` ================================================ FILE: docs/sponsors.md ================================================ --- icon: material/hand-coin --- # Sponsors Do you or your friends use sing-box? You can help keep the project bug-free and feature rich by sponsoring the project maintainer via [GitHub Sponsors](https://github.com/sponsors/nekohasekai). ![](https://nekohasekai.github.io/sponsor-images/sponsors.svg) ## Special Sponsors > Viral Tech, Inc. Helping us re-list sing-box apps on the Apple Store. --- > [JetBrains](https://www.jetbrains.com) Free license for the amazing IDEs. [![JetBrains logo](https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg)](https://www.jetbrains.com) ================================================ FILE: docs/support.md ================================================ --- icon: material/forum --- # Support | Channel | Link | | :---------------------------- | :------------------------------------------ | | GitHub Issues | https://github.com/SagerNet/sing-box/issues | | Telegram notification channel | https://t.me/yapnc | | Telegram user group | https://t.me/yapug | | Email | contact@sagernet.org | ================================================ FILE: docs/support.zh.md ================================================ --- icon: material/forum --- # 支持 | 通道 | 链接 | | :---------------- | :------------------------------------------ | | GitHub Issues | https://github.com/SagerNet/sing-box/issues | | Telegram 通知频道 | https://t.me/yapnc | | Telegram 用户组 | https://t.me/yapug | | 邮件 | contact@sagernet.org | ================================================ FILE: experimental/cachefile/cache.go ================================================ package cachefile import ( "context" "errors" "net/netip" "os" "strings" "sync" "time" "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" ) var ( bucketSelected = []byte("selected") bucketExpand = []byte("group_expand") bucketMode = []byte("clash_mode") bucketRuleSet = []byte("rule_set") bucketNameList = []string{ string(bucketSelected), string(bucketExpand), string(bucketMode), string(bucketRuleSet), string(bucketRDRC), string(bucketDNSCache), } cacheIDDefault = []byte("default") ) var _ adapter.CacheFile = (*CacheFile)(nil) type CacheFile struct { 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 saveCacheKey struct { TransportName string QuestionName string QType uint16 } 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 } else { path = "cache.db" } var cacheIDBytes []byte 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 { rdrcTimeout = time.Duration(options.RDRCTimeout) } else { rdrcTimeout = 7 * 24 * time.Hour } } 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[saveCacheKey]bool), saveDNSCache: make(map[saveCacheKey]saveDNSCacheEntry), } } func (c *CacheFile) Name() string { return "cache-file" } 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 { 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 ( db *bbolt.DB err error ) for range 10 { db, err = bbolt.Open(c.path, fileMode, &options) if err == nil { break } if errors.Is(err, bboltErrors.ErrTimeout) { continue } if E.IsMulti(err, bboltErrors.ErrInvalid, bboltErrors.ErrChecksum, bboltErrors.ErrVersionMismatch) { rmErr := os.Remove(c.path) if rmErr != nil { return err } } time.Sleep(100 * time.Millisecond) } if err != nil { return err } err = filemanager.Chown(c.ctx, c.path) if err != nil { db.Close() return E.Cause(err, "platform chown") } err = db.Batch(func(tx *bbolt.Tx) error { return tx.ForEach(func(name []byte, b *bbolt.Bucket) error { if name[0] == 0 { return b.ForEachBucket(func(k []byte) error { bucketName := string(k) if !(common.Contains(bucketNameList, bucketName)) { _ = b.DeleteBucket(name) } return nil }) } else { bucketName := string(name) if !(common.Contains(bucketNameList, bucketName) || strings.HasPrefix(bucketName, fakeipBucketPrefix)) { _ = tx.DeleteBucket(name) } } return nil }) }) if err != nil { db.Close() return err } c.DB = db return nil } func (c *CacheFile) Close() error { if c.DB == nil { return nil } return c.DB.Close() } func (c *CacheFile) view(fn func(tx *bbolt.Tx) error) (err error) { defer func() { if r := recover(); r != nil { c.resetDB() err = E.New("database corrupted: ", r) } }() return c.DB.View(fn) } func (c *CacheFile) batch(fn func(tx *bbolt.Tx) error) (err error) { defer func() { if r := recover(); r != nil { c.resetDB() err = E.New("database corrupted: ", r) } }() return c.DB.Batch(fn) } func (c *CacheFile) update(fn func(tx *bbolt.Tx) error) (err error) { defer func() { if r := recover(); r != nil { c.resetDB() err = E.New("database corrupted: ", r) } }() return c.DB.Update(fn) } func (c *CacheFile) resetDB() { c.resetAccess.Lock() defer c.resetAccess.Unlock() c.DB.Close() os.Remove(c.path) db, err := bbolt.Open(c.path, 0o666, &bbolt.Options{Timeout: time.Second}) if err == nil { _ = filemanager.Chown(c.ctx, c.path) c.DB = db } } func (c *CacheFile) StoreFakeIP() bool { return c.storeFakeIP } func (c *CacheFile) LoadMode() string { var mode string c.view(func(t *bbolt.Tx) error { bucket := t.Bucket(bucketMode) if bucket == nil { return nil } var modeBytes []byte if len(c.cacheID) > 0 { modeBytes = bucket.Get(c.cacheID) } else { modeBytes = bucket.Get(cacheIDDefault) } mode = string(modeBytes) return nil }) return mode } func (c *CacheFile) StoreMode(mode string) error { return c.batch(func(t *bbolt.Tx) error { bucket, err := t.CreateBucketIfNotExists(bucketMode) if err != nil { return err } if len(c.cacheID) > 0 { return bucket.Put(c.cacheID, []byte(mode)) } else { return bucket.Put(cacheIDDefault, []byte(mode)) } }) } func (c *CacheFile) bucket(t *bbolt.Tx, key []byte) *bbolt.Bucket { if c.cacheID == nil { return t.Bucket(key) } bucket := t.Bucket(c.cacheID) if bucket == nil { return nil } return bucket.Bucket(key) } func (c *CacheFile) createBucket(t *bbolt.Tx, key []byte) (*bbolt.Bucket, error) { if c.cacheID == nil { return t.CreateBucketIfNotExists(key) } bucket, err := t.CreateBucketIfNotExists(c.cacheID) if bucket == nil { return nil, err } return bucket.CreateBucketIfNotExists(key) } func (c *CacheFile) LoadSelected(group string) string { var selected string c.view(func(t *bbolt.Tx) error { bucket := c.bucket(t, bucketSelected) if bucket == nil { return nil } selectedBytes := bucket.Get([]byte(group)) if len(selectedBytes) > 0 { selected = string(selectedBytes) } return nil }) return selected } func (c *CacheFile) StoreSelected(group, selected string) error { return c.batch(func(t *bbolt.Tx) error { bucket, err := c.createBucket(t, bucketSelected) if err != nil { return err } return bucket.Put([]byte(group), []byte(selected)) }) } func (c *CacheFile) LoadGroupExpand(group string) (isExpand bool, loaded bool) { c.view(func(t *bbolt.Tx) error { bucket := c.bucket(t, bucketExpand) if bucket == nil { return nil } expandBytes := bucket.Get([]byte(group)) if len(expandBytes) == 1 { isExpand = expandBytes[0] == 1 loaded = true } return nil }) return } func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error { return c.batch(func(t *bbolt.Tx) error { bucket, err := c.createBucket(t, bucketExpand) if err != nil { return err } if isExpand { return bucket.Put([]byte(group), []byte{1}) } else { return bucket.Put([]byte(group), []byte{0}) } }) } func (c *CacheFile) LoadRuleSet(tag string) *adapter.SavedBinary { var savedSet adapter.SavedBinary err := c.view(func(t *bbolt.Tx) error { bucket := c.bucket(t, bucketRuleSet) if bucket == nil { return os.ErrNotExist } setBinary := bucket.Get([]byte(tag)) if len(setBinary) == 0 { return os.ErrInvalid } return savedSet.UnmarshalBinary(setBinary) }) if err != nil { return nil } return &savedSet } func (c *CacheFile) SaveRuleSet(tag string, set *adapter.SavedBinary) error { return c.batch(func(t *bbolt.Tx) error { bucket, err := c.createBucket(t, bucketRuleSet) if err != nil { return err } setBinary, err := set.MarshalBinary() if err != nil { return err } return bucket.Put([]byte(tag), setBinary) }) } ================================================ FILE: experimental/cachefile/dns_cache.go ================================================ 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 { 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 { 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) 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) } } ================================================ FILE: experimental/cachefile/fakeip.go ================================================ package cachefile import ( "net/netip" "os" "time" "github.com/sagernet/bbolt" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" ) const fakeipBucketPrefix = "fakeip_" var ( bucketFakeIP = []byte(fakeipBucketPrefix + "address") bucketFakeIPDomain4 = []byte(fakeipBucketPrefix + "domain4") bucketFakeIPDomain6 = []byte(fakeipBucketPrefix + "domain6") keyMetadata = []byte(fakeipBucketPrefix + "metadata") ) func (c *CacheFile) FakeIPMetadata() *adapter.FakeIPMetadata { var metadata adapter.FakeIPMetadata err := c.batch(func(tx *bbolt.Tx) error { bucket := tx.Bucket(bucketFakeIP) if bucket == nil { return os.ErrNotExist } metadataBinary := bucket.Get(keyMetadata) if len(metadataBinary) == 0 { return os.ErrInvalid } err := bucket.Delete(keyMetadata) if err != nil { return err } return metadata.UnmarshalBinary(metadataBinary) }) if err != nil { return nil } return &metadata } func (c *CacheFile) FakeIPSaveMetadata(metadata *adapter.FakeIPMetadata) error { return c.batch(func(tx *bbolt.Tx) error { bucket, err := tx.CreateBucketIfNotExists(bucketFakeIP) if err != nil { return err } metadataBinary, err := metadata.MarshalBinary() if err != nil { return err } return bucket.Put(keyMetadata, metadataBinary) }) } func (c *CacheFile) FakeIPSaveMetadataAsync(metadata *adapter.FakeIPMetadata) { if c.saveMetadataTimer == nil { c.saveMetadataTimer = time.AfterFunc(C.FakeIPMetadataSaveInterval, func() { _ = c.FakeIPSaveMetadata(metadata) }) } else { c.saveMetadataTimer.Reset(C.FakeIPMetadataSaveInterval) } } func (c *CacheFile) FakeIPStore(address netip.Addr, domain string) error { return c.batch(func(tx *bbolt.Tx) error { bucket, err := tx.CreateBucketIfNotExists(bucketFakeIP) if err != nil { return err } oldDomain := bucket.Get(address.AsSlice()) err = bucket.Put(address.AsSlice(), []byte(domain)) if err != nil { return err } if address.Is4() { bucket, err = tx.CreateBucketIfNotExists(bucketFakeIPDomain4) } else { bucket, err = tx.CreateBucketIfNotExists(bucketFakeIPDomain6) } if err != nil { return err } if oldDomain != nil { if err := bucket.Delete(oldDomain); err != nil { return err } } return bucket.Put([]byte(domain), address.AsSlice()) }) } func (c *CacheFile) FakeIPStoreAsync(address netip.Addr, domain string, logger logger.Logger) { c.saveFakeIPAccess.Lock() if oldDomain, loaded := c.saveDomain[address]; loaded { if address.Is4() { delete(c.saveAddress4, oldDomain) } else { delete(c.saveAddress6, oldDomain) } } c.saveDomain[address] = domain if address.Is4() { c.saveAddress4[domain] = address } else { c.saveAddress6[domain] = address } c.saveFakeIPAccess.Unlock() go func() { err := c.FakeIPStore(address, domain) if err != nil { logger.Warn("save FakeIP cache: ", err) } c.saveFakeIPAccess.Lock() delete(c.saveDomain, address) if address.Is4() { delete(c.saveAddress4, domain) } else { delete(c.saveAddress6, domain) } c.saveFakeIPAccess.Unlock() }() } func (c *CacheFile) FakeIPLoad(address netip.Addr) (string, bool) { c.saveFakeIPAccess.RLock() cachedDomain, cached := c.saveDomain[address] c.saveFakeIPAccess.RUnlock() if cached { return cachedDomain, true } var domain string _ = c.view(func(tx *bbolt.Tx) error { bucket := tx.Bucket(bucketFakeIP) if bucket == nil { return nil } domain = string(bucket.Get(address.AsSlice())) return nil }) return domain, domain != "" } func (c *CacheFile) FakeIPLoadDomain(domain string, isIPv6 bool) (netip.Addr, bool) { var ( cachedAddress netip.Addr cached bool ) c.saveFakeIPAccess.RLock() if !isIPv6 { cachedAddress, cached = c.saveAddress4[domain] } else { cachedAddress, cached = c.saveAddress6[domain] } c.saveFakeIPAccess.RUnlock() if cached { return cachedAddress, true } var address netip.Addr _ = c.view(func(tx *bbolt.Tx) error { var bucket *bbolt.Bucket if isIPv6 { bucket = tx.Bucket(bucketFakeIPDomain6) } else { bucket = tx.Bucket(bucketFakeIPDomain4) } if bucket == nil { return nil } address = M.AddrFromIP(bucket.Get([]byte(domain))) return nil }) return address, address.IsValid() } func (c *CacheFile) FakeIPReset() error { return c.batch(func(tx *bbolt.Tx) error { err := tx.DeleteBucket(bucketFakeIP) if err != nil { return err } err = tx.DeleteBucket(bucketFakeIPDomain4) if err != nil { return err } return tx.DeleteBucket(bucketFakeIPDomain6) }) } ================================================ FILE: experimental/cachefile/rdrc.go ================================================ package cachefile import ( "encoding/binary" "time" "github.com/sagernet/bbolt" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/logger" ) var bucketRDRC = []byte("rdrc2") func (c *CacheFile) StoreRDRC() bool { return c.storeRDRC } func (c *CacheFile) RDRCTimeout() time.Duration { return c.rdrcTimeout } func (c *CacheFile) LoadRDRC(transportName string, qName string, qType uint16) (rejected bool) { c.saveRDRCAccess.RLock() rejected, cached := c.saveRDRC[saveCacheKey{transportName, qName, qType}] c.saveRDRCAccess.RUnlock() if cached { return } key := buf.Get(2 + len(qName)) binary.BigEndian.PutUint16(key, qType) copy(key[2:], qName) defer buf.Put(key) var deleteCache bool err := c.view(func(tx *bbolt.Tx) error { bucket := c.bucket(tx, bucketRDRC) if bucket == nil { return nil } bucket = bucket.Bucket([]byte(transportName)) if bucket == nil { return nil } content := bucket.Get(key) if content == nil { return nil } expiresAt := time.Unix(int64(binary.BigEndian.Uint64(content)), 0) if time.Now().After(expiresAt) { deleteCache = true return nil } rejected = true return nil }) if err != nil { return } if deleteCache { c.update(func(tx *bbolt.Tx) error { bucket := c.bucket(tx, bucketRDRC) if bucket == nil { return nil } bucket = bucket.Bucket([]byte(transportName)) if bucket == nil { return nil } return bucket.Delete(key) }) } return } func (c *CacheFile) SaveRDRC(transportName string, qName string, qType uint16) error { expiresAt := buf.Get(8) defer buf.Put(expiresAt) binary.BigEndian.PutUint64(expiresAt, uint64(time.Now().Add(c.rdrcTimeout).Unix())) return c.batch(func(tx *bbolt.Tx) error { bucket, err := c.createBucket(tx, bucketRDRC) 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) return bucket.Put(key, expiresAt) }) } func (c *CacheFile) SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger) { saveKey := saveCacheKey{transportName, qName, qType} c.saveRDRCAccess.Lock() c.saveRDRC[saveKey] = true c.saveRDRCAccess.Unlock() go func() { err := c.SaveRDRC(transportName, qName, qType) if err != nil { logger.Warn("save RDRC: ", err) } c.saveRDRCAccess.Lock() delete(c.saveRDRC, saveKey) c.saveRDRCAccess.Unlock() }() } ================================================ FILE: experimental/clashapi/api_meta.go ================================================ package clashapi import ( "bytes" "context" "net" "net/http" "runtime/debug" "time" "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" "github.com/sagernet/sing/common/json" "github.com/sagernet/ws" "github.com/sagernet/ws/wsutil" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/render" ) // API created by Clash.Meta func (s *Server) setupMetaAPI(r chi.Router) { if s.logDebug { r := chi.NewRouter() r.Put("/gc", func(w http.ResponseWriter, r *http.Request) { debug.FreeOSMemory() }) r.Mount("/", middleware.Profiler()) } r.Get("/memory", memory(s.ctx, s.trafficManager)) r.Mount("/group", groupRouter(s)) r.Mount("/upgrade", upgradeRouter(s)) } type Memory struct { Inuse uint64 `json:"inuse"` OSLimit uint64 `json:"oslimit"` // maybe we need it in the future } func memory(ctx context.Context, trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { var conn net.Conn if r.Header.Get("Upgrade") == "websocket" { var err error conn, _, _, err = ws.UpgradeHTTP(r, w) if err != nil { return } defer conn.Close() } if conn == nil { w.Header().Set("Content-Type", "application/json") render.Status(r, http.StatusOK) } tick := time.NewTicker(time.Second) defer tick.Stop() buf := &bytes.Buffer{} var err error first := true for { select { case <-ctx.Done(): return case <-tick.C: } buf.Reset() inuse := trafficManager.Snapshot().Memory // make chat.js begin with zero // this is shit var,but we need output 0 for first time if first { first = false inuse = 0 } if err := json.NewEncoder(buf).Encode(Memory{ Inuse: inuse, OSLimit: 0, }); err != nil { break } if conn == nil { _, err = w.Write(buf.Bytes()) w.(http.Flusher).Flush() } else { err = wsutil.WriteServerText(conn, buf.Bytes()) } if err != nil { break } } } } ================================================ FILE: experimental/clashapi/api_meta_group.go ================================================ package clashapi import ( "context" "net/http" "strconv" "strings" "sync" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/urltest" "github.com/sagernet/sing-box/protocol/group" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/batch" "github.com/sagernet/sing/common/json/badjson" "github.com/go-chi/chi/v5" "github.com/go-chi/render" ) func groupRouter(server *Server) http.Handler { r := chi.NewRouter() r.Get("/", getGroups(server)) r.Route("/{name}", func(r chi.Router) { r.Use(parseProxyName, findProxyByName(server)) r.Get("/", getGroup(server)) r.Get("/delay", getGroupDelay(server)) }) return r } func getGroups(server *Server) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { groups := common.Map(common.Filter(server.outbound.Outbounds(), func(it adapter.Outbound) bool { _, isGroup := it.(adapter.OutboundGroup) return isGroup }), func(it adapter.Outbound) *badjson.JSONObject { return proxyInfo(server, it) }) render.JSON(w, r, render.M{ "proxies": groups, }) } } func getGroup(server *Server) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound) if _, ok := proxy.(adapter.OutboundGroup); ok { render.JSON(w, r, proxyInfo(server, proxy)) return } render.Status(r, http.StatusNotFound) render.JSON(w, r, ErrNotFound) } } func getGroupDelay(server *Server) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound) outboundGroup, ok := proxy.(adapter.OutboundGroup) if !ok { render.Status(r, http.StatusNotFound) render.JSON(w, r, ErrNotFound) return } query := r.URL.Query() url := query.Get("url") if strings.HasPrefix(url, "http://") { url = "" } timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 32) if err != nil { render.Status(r, http.StatusBadRequest) render.JSON(w, r, ErrBadRequest) return } ctx, cancel := context.WithTimeout(r.Context(), time.Millisecond*time.Duration(timeout)) defer cancel() var result map[string]uint16 if urlTestGroup, isURLTestGroup := outboundGroup.(adapter.URLTestGroup); isURLTestGroup { result, err = urlTestGroup.URLTest(ctx) } else { outbounds := common.FilterNotNil(common.Map(outboundGroup.All(), func(it string) adapter.Outbound { itOutbound, _ := server.outbound.Outbound(it) return itOutbound })) b, _ := batch.New(ctx, batch.WithConcurrencyNum[any](10)) checked := make(map[string]bool) result = make(map[string]uint16) var resultAccess sync.Mutex for _, detour := range outbounds { tag := detour.Tag() realTag := group.RealTag(detour) if checked[realTag] { continue } checked[realTag] = true p, loaded := server.outbound.Outbound(realTag) if !loaded { continue } b.Go(realTag, func() (any, error) { t, err := urltest.URLTest(ctx, url, p) if err != nil { server.logger.Debug("outbound ", tag, " unavailable: ", err) server.urlTestHistory.DeleteURLTestHistory(realTag) } else { server.logger.Debug("outbound ", tag, " available: ", t, "ms") server.urlTestHistory.StoreURLTestHistory(realTag, &adapter.URLTestHistory{ Time: time.Now(), Delay: t, }) resultAccess.Lock() result[tag] = t resultAccess.Unlock() } return nil, nil }) } b.Wait() } if err != nil { render.Status(r, http.StatusGatewayTimeout) render.JSON(w, r, newError(err.Error())) return } render.JSON(w, r, result) } } ================================================ FILE: experimental/clashapi/api_meta_upgrade.go ================================================ package clashapi import ( "net/http" E "github.com/sagernet/sing/common/exceptions" "github.com/go-chi/chi/v5" "github.com/go-chi/render" ) func upgradeRouter(server *Server) http.Handler { r := chi.NewRouter() r.Post("/ui", updateExternalUI(server)) return r } func updateExternalUI(server *Server) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { if server.externalUI == "" { render.Status(r, http.StatusNotFound) render.JSON(w, r, newError("external UI not enabled")) return } server.logger.Info("upgrading external UI") err := server.downloadExternalUI() if err != nil { server.logger.Error(E.Cause(err, "upgrade external ui")) render.Status(r, http.StatusInternalServerError) render.JSON(w, r, newError(err.Error())) return } server.logger.Info("updated external UI") render.JSON(w, r, render.M{"status": "ok"}) } } ================================================ FILE: experimental/clashapi/cache.go ================================================ package clashapi import ( "context" "net/http" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing/service" "github.com/go-chi/chi/v5" "github.com/go-chi/render" ) func cacheRouter(ctx context.Context) http.Handler { r := chi.NewRouter() r.Post("/fakeip/flush", flushFakeip(ctx)) r.Post("/dns/flush", flushDNS(ctx)) return r } func flushFakeip(ctx context.Context) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { cacheFile := service.FromContext[adapter.CacheFile](ctx) if cacheFile != nil { err := cacheFile.FakeIPReset() if err != nil { render.Status(r, http.StatusInternalServerError) render.JSON(w, r, newError(err.Error())) return } } render.NoContent(w, r) } } func flushDNS(ctx context.Context) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { dnsRouter := service.FromContext[adapter.DNSRouter](ctx) if dnsRouter != nil { dnsRouter.ClearCache() } render.NoContent(w, r) } } ================================================ FILE: experimental/clashapi/common.go ================================================ package clashapi import ( "net/http" "net/url" "github.com/go-chi/chi/v5" ) // When name is composed of a partial escape string, Golang does not unescape it func getEscapeParam(r *http.Request, paramName string) string { param := chi.URLParam(r, paramName) if newParam, err := url.PathUnescape(param); err == nil { param = newParam } return param } ================================================ FILE: experimental/clashapi/configs.go ================================================ package clashapi import ( "net/http" "github.com/sagernet/sing-box/log" "github.com/go-chi/chi/v5" "github.com/go-chi/render" ) func configRouter(server *Server, logFactory log.Factory) http.Handler { r := chi.NewRouter() r.Get("/", getConfigs(server, logFactory)) r.Put("/", updateConfigs) r.Patch("/", patchConfigs(server)) return r } type configSchema struct { Port int `json:"port"` SocksPort int `json:"socks-port"` RedirPort int `json:"redir-port"` TProxyPort int `json:"tproxy-port"` MixedPort int `json:"mixed-port"` AllowLan bool `json:"allow-lan"` BindAddress string `json:"bind-address"` Mode string `json:"mode"` // sing-box added ModeList []string `json:"mode-list"` LogLevel string `json:"log-level"` IPv6 bool `json:"ipv6"` Tun map[string]any `json:"tun"` } func getConfigs(server *Server, logFactory log.Factory) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { logLevel := logFactory.Level() if logLevel == log.LevelTrace { logLevel = log.LevelDebug } else if logLevel < log.LevelError { logLevel = log.LevelError } render.JSON(w, r, &configSchema{ Mode: server.mode, ModeList: server.modeList, BindAddress: "*", LogLevel: log.FormatLevel(logLevel), }) } } func patchConfigs(server *Server) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { var newConfig configSchema err := render.DecodeJSON(r.Body, &newConfig) if err != nil { render.Status(r, http.StatusBadRequest) render.JSON(w, r, ErrBadRequest) return } if newConfig.Mode != "" { server.SetMode(newConfig.Mode) } render.NoContent(w, r) } } func updateConfigs(w http.ResponseWriter, r *http.Request) { render.NoContent(w, r) } ================================================ FILE: experimental/clashapi/connections.go ================================================ package clashapi import ( "bytes" "context" "net/http" "strconv" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" "github.com/sagernet/sing/common/json" "github.com/sagernet/ws" "github.com/sagernet/ws/wsutil" "github.com/go-chi/chi/v5" "github.com/go-chi/render" "github.com/gofrs/uuid/v5" ) func connectionRouter(ctx context.Context, network adapter.NetworkManager, trafficManager *trafficontrol.Manager) http.Handler { r := chi.NewRouter() r.Get("/", getConnections(ctx, trafficManager)) r.Delete("/", closeAllConnections(network, trafficManager)) r.Delete("/{id}", closeConnection(trafficManager)) return r } func getConnections(ctx context.Context, trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Upgrade") != "websocket" { snapshot := trafficManager.Snapshot() render.JSON(w, r, snapshot) return } conn, _, _, err := ws.UpgradeHTTP(r, w) if err != nil { return } defer conn.Close() intervalStr := r.URL.Query().Get("interval") interval := 1000 if intervalStr != "" { t, err := strconv.Atoi(intervalStr) if err != nil { render.Status(r, http.StatusBadRequest) render.JSON(w, r, ErrBadRequest) return } interval = t } buf := &bytes.Buffer{} sendSnapshot := func() error { buf.Reset() snapshot := trafficManager.Snapshot() if err := json.NewEncoder(buf).Encode(snapshot); err != nil { return err } return wsutil.WriteServerText(conn, buf.Bytes()) } if err = sendSnapshot(); err != nil { return } tick := time.NewTicker(time.Millisecond * time.Duration(interval)) defer tick.Stop() for { select { case <-ctx.Done(): return case <-tick.C: } if err = sendSnapshot(); err != nil { break } } } } func closeConnection(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { id := uuid.FromStringOrNil(chi.URLParam(r, "id")) snapshot := trafficManager.Snapshot() for _, c := range snapshot.Connections { if id == c.Metadata().ID { c.Close() break } } render.NoContent(w, r) } } 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() } network.ResetNetwork() render.NoContent(w, r) } } ================================================ FILE: experimental/clashapi/ctxkeys.go ================================================ package clashapi var ( CtxKeyProxyName = contextKey("proxy name") CtxKeyProviderName = contextKey("provider name") CtxKeyProxy = contextKey("proxy") CtxKeyProvider = contextKey("provider") ) type contextKey string func (c contextKey) String() string { return "clash context key " + string(c) } ================================================ FILE: experimental/clashapi/dns.go ================================================ package clashapi import ( "context" "net/http" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common" "github.com/go-chi/chi/v5" "github.com/go-chi/render" "github.com/miekg/dns" ) func dnsRouter(router adapter.DNSRouter) http.Handler { r := chi.NewRouter() r.Get("/query", queryDNS(router)) return r } func queryDNS(router adapter.DNSRouter) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { name := r.URL.Query().Get("name") qTypeStr := r.URL.Query().Get("type") if qTypeStr == "" { qTypeStr = "A" } qType, exist := dns.StringToType[qTypeStr] if !exist { render.Status(r, http.StatusBadRequest) render.JSON(w, r, newError("invalid query type")) return } ctx, cancel := context.WithTimeout(context.Background(), C.DNSTimeout) defer cancel() msg := dns.Msg{} msg.SetQuestion(dns.Fqdn(name), qType) resp, err := router.Exchange(ctx, &msg, adapter.DNSQueryOptions{}) if err != nil { render.Status(r, http.StatusInternalServerError) render.JSON(w, r, newError(err.Error())) return } responseData := render.M{ "Status": resp.Rcode, "Question": resp.Question, "Server": "internal", "TC": resp.Truncated, "RD": resp.RecursionDesired, "RA": resp.RecursionAvailable, "AD": resp.AuthenticatedData, "CD": resp.CheckingDisabled, } rr2Json := func(rr dns.RR) render.M { header := rr.Header() return render.M{ "name": header.Name, "type": header.Rrtype, "TTL": header.Ttl, "data": rr.String()[len(header.String()):], } } if len(resp.Answer) > 0 { responseData["Answer"] = common.Map(resp.Answer, rr2Json) } if len(resp.Ns) > 0 { responseData["Authority"] = common.Map(resp.Ns, rr2Json) } if len(resp.Extra) > 0 { responseData["Additional"] = common.Map(resp.Extra, rr2Json) } render.JSON(w, r, responseData) } } ================================================ FILE: experimental/clashapi/errors.go ================================================ package clashapi var ( ErrUnauthorized = newError("Unauthorized") ErrBadRequest = newError("Body invalid") ErrForbidden = newError("Forbidden") ErrNotFound = newError("Resource not found") ErrRequestTimeout = newError("Timeout") ) // HTTPError is custom HTTP error for API type HTTPError struct { Message string `json:"message"` } func (e *HTTPError) Error() string { return e.Message } func newError(msg string) *HTTPError { return &HTTPError{Message: msg} } ================================================ FILE: experimental/clashapi/profile.go ================================================ package clashapi import ( "net/http" "github.com/go-chi/chi/v5" "github.com/go-chi/render" ) func profileRouter() http.Handler { r := chi.NewRouter() r.Get("/tracing", subscribeTracing) return r } func subscribeTracing(w http.ResponseWriter, r *http.Request) { // if !profile.Tracing.Load() { render.Status(r, http.StatusNotFound) render.JSON(w, r, ErrNotFound) //return //} /*wsConn, err := upgrader.Upgrade(w, r, nil) if err != nil { return } ch := make(chan map[string]any, 1024) sub := event.Subscribe() defer event.UnSubscribe(sub) buf := &bytes.Buffer{} go func() { for elm := range sub { select { case ch <- elm: default: } } close(ch) }() for elm := range ch { buf.Reset() if err := json.NewEncoder(buf).Encode(elm); err != nil { break } if err := wsConn.WriteMessage(websocket.TextMessage, buf.Bytes()); err != nil { break } }*/ } ================================================ FILE: experimental/clashapi/provider.go ================================================ package clashapi import ( "context" "net/http" "github.com/go-chi/chi/v5" "github.com/go-chi/render" ) func proxyProviderRouter() http.Handler { r := chi.NewRouter() r.Get("/", getProviders) r.Route("/{name}", func(r chi.Router) { r.Use(parseProviderName, findProviderByName) r.Get("/", getProvider) r.Put("/", updateProvider) r.Get("/healthcheck", healthCheckProvider) }) return r } func getProviders(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, render.M{ "providers": render.M{}, }) } func getProvider(w http.ResponseWriter, r *http.Request) { /*provider := r.Context().Value(CtxKeyProvider).(provider.ProxyProvider) render.JSON(w, r, provider)*/ render.NoContent(w, r) } func updateProvider(w http.ResponseWriter, r *http.Request) { /*provider := r.Context().Value(CtxKeyProvider).(provider.ProxyProvider) if err := provider.Update(); err != nil { render.Status(r, http.StatusServiceUnavailable) render.JSON(w, r, newError(err.Error())) return }*/ render.NoContent(w, r) } func healthCheckProvider(w http.ResponseWriter, r *http.Request) { /*provider := r.Context().Value(CtxKeyProvider).(provider.ProxyProvider) provider.HealthCheck()*/ render.NoContent(w, r) } func parseProviderName(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { name := getEscapeParam(r, "name") ctx := context.WithValue(r.Context(), CtxKeyProviderName, name) next.ServeHTTP(w, r.WithContext(ctx)) }) } func findProviderByName(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { /*name := r.Context().Value(CtxKeyProviderName).(string) providers := tunnel.ProxyProviders() provider, exist := providers[name] if !exist {*/ render.Status(r, http.StatusNotFound) render.JSON(w, r, ErrNotFound) //return //} // ctx := context.WithValue(r.Context(), CtxKeyProvider, provider) // next.ServeHTTP(w, r.WithContext(ctx)) }) } ================================================ FILE: experimental/clashapi/proxies.go ================================================ package clashapi import ( "context" "net/http" "sort" "strconv" "strings" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/urltest" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/protocol/group" "github.com/sagernet/sing/common" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json/badjson" N "github.com/sagernet/sing/common/network" "github.com/go-chi/chi/v5" "github.com/go-chi/render" ) func proxyRouter(server *Server, router adapter.Router) http.Handler { r := chi.NewRouter() r.Get("/", getProxies(server)) r.Route("/{name}", func(r chi.Router) { r.Use(parseProxyName, findProxyByName(server)) r.Get("/", getProxy(server)) r.Get("/delay", getProxyDelay(server)) r.Put("/", updateProxy) }) return r } func parseProxyName(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { name := getEscapeParam(r, "name") ctx := context.WithValue(r.Context(), CtxKeyProxyName, name) next.ServeHTTP(w, r.WithContext(ctx)) }) } func findProxyByName(server *Server) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { name := r.Context().Value(CtxKeyProxyName).(string) proxy, exist := server.outbound.Outbound(name) if !exist { render.Status(r, http.StatusNotFound) render.JSON(w, r, ErrNotFound) return } ctx := context.WithValue(r.Context(), CtxKeyProxy, proxy) next.ServeHTTP(w, r.WithContext(ctx)) }) } } func proxyInfo(server *Server, detour adapter.Outbound) *badjson.JSONObject { var info badjson.JSONObject var clashType string switch detour.Type() { case C.TypeBlock: clashType = "Reject" default: clashType = C.ProxyDisplayName(detour.Type()) } info.Put("type", clashType) info.Put("name", detour.Tag()) info.Put("udp", common.Contains(detour.Network(), N.NetworkUDP)) delayHistory := server.urlTestHistory.LoadURLTestHistory(adapter.OutboundTag(detour)) if delayHistory != nil { info.Put("history", []*adapter.URLTestHistory{delayHistory}) } else { info.Put("history", []*adapter.URLTestHistory{}) } if group, isGroup := detour.(adapter.OutboundGroup); isGroup { info.Put("now", group.Now()) info.Put("all", group.All()) } return &info } func getProxies(server *Server) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { var proxyMap badjson.JSONObject outbounds := common.Filter(server.outbound.Outbounds(), func(detour adapter.Outbound) bool { return detour.Tag() != "" }) outbounds = append(outbounds, common.Map(common.Filter(server.endpoint.Endpoints(), func(detour adapter.Endpoint) bool { return detour.Tag() != "" }), func(it adapter.Endpoint) adapter.Outbound { return it })...) allProxies := make([]string, 0, len(outbounds)) for _, detour := range outbounds { switch detour.Type() { case C.TypeDirect, C.TypeBlock, C.TypeDNS: continue } allProxies = append(allProxies, detour.Tag()) } defaultTag := server.outbound.Default().Tag() sort.SliceStable(allProxies, func(i, j int) bool { return allProxies[i] == defaultTag }) // fix clash dashboard proxyMap.Put("GLOBAL", map[string]any{ "type": "Fallback", "name": "GLOBAL", "udp": true, "history": []*adapter.URLTestHistory{}, "all": allProxies, "now": defaultTag, }) for i, detour := range outbounds { var tag string if detour.Tag() == "" { tag = F.ToString(i) } else { tag = detour.Tag() } proxyMap.Put(tag, proxyInfo(server, detour)) } var responseMap badjson.JSONObject responseMap.Put("proxies", &proxyMap) response, err := responseMap.MarshalJSON() if err != nil { render.Status(r, http.StatusInternalServerError) render.JSON(w, r, newError(err.Error())) return } w.Write(response) } } func getProxy(server *Server) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound) response, err := proxyInfo(server, proxy).MarshalJSON() if err != nil { render.Status(r, http.StatusInternalServerError) render.JSON(w, r, newError(err.Error())) return } w.Write(response) } } type UpdateProxyRequest struct { Name string `json:"name"` } func updateProxy(w http.ResponseWriter, r *http.Request) { req := UpdateProxyRequest{} if err := render.DecodeJSON(r.Body, &req); err != nil { render.Status(r, http.StatusBadRequest) render.JSON(w, r, ErrBadRequest) return } proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound) selector, ok := proxy.(*group.Selector) if !ok { render.Status(r, http.StatusBadRequest) render.JSON(w, r, newError("Must be a Selector")) return } if !selector.SelectOutbound(req.Name) { render.Status(r, http.StatusBadRequest) render.JSON(w, r, newError("Selector update error: not found")) return } render.NoContent(w, r) } func getProxyDelay(server *Server) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() url := query.Get("url") if strings.HasPrefix(url, "http://") { url = "" } timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 16) if err != nil { render.Status(r, http.StatusBadRequest) render.JSON(w, r, ErrBadRequest) return } proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound) ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(timeout)) defer cancel() delay, err := urltest.URLTest(ctx, url, proxy) defer func() { realTag := group.RealTag(proxy) if err != nil { server.urlTestHistory.DeleteURLTestHistory(realTag) } else { server.urlTestHistory.StoreURLTestHistory(realTag, &adapter.URLTestHistory{ Time: time.Now(), Delay: delay, }) } }() if ctx.Err() != nil { render.Status(r, http.StatusGatewayTimeout) render.JSON(w, r, ErrRequestTimeout) return } if err != nil || delay == 0 { render.Status(r, http.StatusServiceUnavailable) render.JSON(w, r, newError("An error occurred in the delay test")) return } render.JSON(w, r, render.M{ "delay": delay, }) } } ================================================ FILE: experimental/clashapi/ruleprovider.go ================================================ package clashapi import ( "net/http" "github.com/go-chi/chi/v5" "github.com/go-chi/render" ) func ruleProviderRouter() http.Handler { r := chi.NewRouter() r.Get("/", getRuleProviders) r.Route("/{name}", func(r chi.Router) { r.Use(parseProviderName, findRuleProviderByName) r.Get("/", getRuleProvider) r.Put("/", updateRuleProvider) }) return r } func getRuleProviders(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, render.M{ "providers": []string{}, }) } func getRuleProvider(w http.ResponseWriter, r *http.Request) { // provider := r.Context().Value(CtxKeyProvider).(provider.RuleProvider) // render.JSON(w, r, provider) render.NoContent(w, r) } func updateRuleProvider(w http.ResponseWriter, r *http.Request) { /*provider := r.Context().Value(CtxKeyProvider).(provider.RuleProvider) if err := provider.Update(); err != nil { render.Status(r, http.StatusServiceUnavailable) render.JSON(w, r, newError(err.Error())) return }*/ render.NoContent(w, r) } func findRuleProviderByName(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { /*name := r.Context().Value(CtxKeyProviderName).(string) providers := tunnel.RuleProviders() provider, exist := providers[name] if !exist {*/ render.Status(r, http.StatusNotFound) render.JSON(w, r, ErrNotFound) //return //} // ctx := context.WithValue(r.Context(), CtxKeyProvider, provider) // next.ServeHTTP(w, r.WithContext(ctx)) }) } ================================================ FILE: experimental/clashapi/rules.go ================================================ package clashapi import ( "net/http" "github.com/sagernet/sing-box/adapter" "github.com/go-chi/chi/v5" "github.com/go-chi/render" ) func ruleRouter(router adapter.Router) http.Handler { r := chi.NewRouter() r.Get("/", getRules(router)) return r } type Rule struct { Type string `json:"type"` Payload string `json:"payload"` Proxy string `json:"proxy"` } func getRules(router adapter.Router) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { rawRules := router.Rules() var rules []Rule for _, rule := range rawRules { rules = append(rules, Rule{ Type: rule.Type(), Payload: rule.String(), Proxy: rule.Action().String(), }) } render.JSON(w, r, render.M{ "rules": rules, }) } } ================================================ FILE: experimental/clashapi/script.go ================================================ package clashapi import ( "net/http" "github.com/go-chi/chi/v5" "github.com/go-chi/render" ) func scriptRouter() http.Handler { r := chi.NewRouter() r.Post("/", testScript) r.Patch("/", patchScript) return r } /*type TestScriptRequest struct { Script *string `json:"script"` Metadata C.Metadata `json:"metadata"` }*/ func testScript(w http.ResponseWriter, r *http.Request) { /* req := TestScriptRequest{} if err := render.DecodeJSON(r.Body, &req); err != nil { render.Status(r, http.StatusBadRequest) render.JSON(w, r, ErrBadRequest) return } fn := tunnel.ScriptFn() if req.Script == nil && fn == nil { render.Status(r, http.StatusBadRequest) render.JSON(w, r, newError("should send `script`")) return } if !req.Metadata.Valid() { render.Status(r, http.StatusBadRequest) render.JSON(w, r, newError("metadata not valid")) return } if req.Script != nil { var err error fn, err = script.ParseScript(*req.Script) if err != nil { render.Status(r, http.StatusBadRequest) render.JSON(w, r, newError(err.Error())) return } } ctx, _ := script.MakeContext(tunnel.ProxyProviders(), tunnel.RuleProviders()) thread := &starlark.Thread{} ret, err := starlark.Call(thread, fn, starlark.Tuple{ctx, script.MakeMetadata(&req.Metadata)}, nil) if err != nil { render.Status(r, http.StatusBadRequest) render.JSON(w, r, newError(err.Error())) return } elm, ok := ret.(starlark.String) if !ok { render.Status(r, http.StatusBadRequest) render.JSON(w, r, "script fn must return a string") return } render.JSON(w, r, render.M{ "result": string(elm), })*/ render.Status(r, http.StatusBadRequest) render.JSON(w, r, newError("not implemented")) } type PatchScriptRequest struct { Script string `json:"script"` } func patchScript(w http.ResponseWriter, r *http.Request) { /*req := PatchScriptRequest{} if err := render.DecodeJSON(r.Body, &req); err != nil { render.Status(r, http.StatusBadRequest) render.JSON(w, r, ErrBadRequest) return } fn, err := script.ParseScript(req.Script) if err != nil { render.Status(r, http.StatusBadRequest) render.JSON(w, r, newError(err.Error())) return } tunnel.UpdateScript(fn)*/ render.NoContent(w, r) } ================================================ FILE: experimental/clashapi/server.go ================================================ package clashapi import ( "bytes" "context" "errors" "net" "net/http" "os" "runtime" "strings" "syscall" "time" "github.com/sagernet/cors" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/urltest" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental" "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" "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" "github.com/sagernet/sing/common/observable" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/filemanager" "github.com/sagernet/ws" "github.com/sagernet/ws/wsutil" "github.com/go-chi/chi/v5" "github.com/go-chi/render" ) func init() { experimental.RegisterClashServerConstructor(NewServer) } var _ adapter.ClashServer = (*Server)(nil) type Server struct { ctx context.Context network adapter.NetworkManager router adapter.Router dnsRouter adapter.DNSRouter outbound adapter.OutboundManager endpoint adapter.EndpointManager logger log.Logger httpServer *http.Server trafficManager *trafficontrol.Manager urlTestHistory adapter.URLTestHistoryStorage logDebug bool cleaner *cleanup.Cleaner mode string modeList []string modeUpdateHook *observable.Subscriber[struct{}] externalController bool externalUI string externalUIDownloadURL string externalUIDownloadDetour string } func NewServer(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) { trafficManager := trafficontrol.NewManager() 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), endpoint: service.FromContext[adapter.EndpointManager](ctx), logger: logFactory.NewLogger("clash-api"), httpServer: &http.Server{ Addr: options.ExternalController, Handler: chiRouter, }, trafficManager: trafficManager, logDebug: logFactory.Level() >= log.LevelDebug, modeList: options.ModeList, 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 { s.urlTestHistory = urltest.NewHistoryStorage() } defaultMode := "Rule" if options.DefaultMode != "" { defaultMode = options.DefaultMode } if !common.Contains(s.modeList, defaultMode) { s.modeList = append([]string{defaultMode}, s.modeList...) } s.mode = defaultMode //goland:noinspection GoDeprecation //nolint:staticcheck if options.StoreMode || options.StoreSelected || options.StoreFakeIP || options.CacheFile != "" || options.CacheID != "" { return nil, E.New("cache_file and related fields in Clash API is deprecated in sing-box 1.8.0, use experimental.cache_file instead.") } allowedOrigins := options.AccessControlAllowOrigin if len(allowedOrigins) == 0 { allowedOrigins = []string{"*"} } cors := cors.New(cors.Options{ AllowedOrigins: allowedOrigins, AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, AllowedHeaders: []string{"Content-Type", "Authorization"}, AllowPrivateNetwork: options.AccessControlAllowPrivateNetwork, MaxAge: 300, }) chiRouter.Use(cors.Handler) chiRouter.Group(func(r chi.Router) { r.Use(authentication(options.Secret)) r.Get("/", hello(options.ExternalUI != "")) r.Get("/logs", getLogs(s.ctx, logFactory)) r.Get("/traffic", traffic(s.ctx, trafficManager)) r.Get("/version", version) 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.network, trafficManager)) r.Mount("/providers/proxies", proxyProviderRouter()) r.Mount("/providers/rules", ruleProviderRouter()) r.Mount("/script", scriptRouter()) r.Mount("/profile", profileRouter()) r.Mount("/cache", cacheRouter(ctx)) r.Mount("/dns", dnsRouter(s.dnsRouter)) s.setupMetaAPI(r) }) if options.ExternalUI != "" { s.externalUI = filemanager.BasePath(ctx, os.ExpandEnv(options.ExternalUI)) chiRouter.Group(func(r chi.Router) { r.Get("/ui", http.RedirectHandler("/ui/", http.StatusMovedPermanently).ServeHTTP) r.Handle("/ui/*", http.StripPrefix("/ui/", http.FileServer(Dir(s.externalUI)))) }) } return s, nil } func (s *Server) Name() string { return "clash server" } func (s *Server) Start(stage adapter.StartStage) error { switch stage { case adapter.StartStateStart: cacheFile := service.FromContext[adapter.CacheFile](s.ctx) if cacheFile != nil { mode := cacheFile.LoadMode() if common.Any(s.modeList, func(it string) bool { return strings.EqualFold(it, mode) }) { s.mode = mode } } case adapter.StartStateStarted: if s.externalController { s.checkAndDownloadExternalUI() var ( listener net.Listener err error ) for range 3 { listener, err = net.Listen("tcp", s.httpServer.Addr) if runtime.GOOS == "android" && errors.Is(err, syscall.EADDRINUSE) { time.Sleep(100 * time.Millisecond) continue } break } if err != nil { return E.Cause(err, "external controller listen error") } s.logger.Info("restful api listening at ", listener.Addr()) go func() { err = s.httpServer.Serve(listener) if err != nil && !errors.Is(err, http.ErrServerClosed) { s.logger.Error("external controller serve error: ", err) } }() } } return nil } func (s *Server) Close() error { return common.Close( common.PtrOrNil(s.httpServer), s.trafficManager, s.urlTestHistory, common.PtrOrNil(s.cleaner), ) } func (s *Server) Mode() string { return s.mode } func (s *Server) ModeList() []string { return s.modeList } func (s *Server) SetModeUpdateHook(hook *observable.Subscriber[struct{}]) { s.modeUpdateHook = hook } func (s *Server) SetMode(newMode string) { if !common.Contains(s.modeList, newMode) { newMode = common.Find(s.modeList, func(it string) bool { return strings.EqualFold(it, newMode) }) } if !common.Contains(s.modeList, newMode) { return } if newMode == s.mode { return } s.mode = newMode if s.modeUpdateHook != nil { s.modeUpdateHook.Emit(struct{}{}) } s.dnsRouter.ClearCache() cacheFile := service.FromContext[adapter.CacheFile](s.ctx) if cacheFile != nil { err := cacheFile.StoreMode(newMode) if err != nil { s.logger.Error(E.Cause(err, "save mode")) } } s.logger.Info("updated mode: ", newMode) } func (s *Server) HistoryStorage() adapter.URLTestHistoryStorage { return s.urlTestHistory } func (s *Server) TrafficManager() *trafficontrol.Manager { return s.trafficManager } func (s *Server) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) net.Conn { return trafficontrol.NewTCPTracker(conn, s.trafficManager, metadata, s.outbound, matchedRule, matchOutbound) } func (s *Server) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) N.PacketConn { return trafficontrol.NewUDPTracker(conn, s.trafficManager, metadata, s.outbound, matchedRule, matchOutbound) } func authentication(serverSecret string) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { if serverSecret == "" { next.ServeHTTP(w, r) return } // Browser websocket not support custom header if r.Header.Get("Upgrade") == "websocket" && r.URL.Query().Get("token") != "" { token := r.URL.Query().Get("token") if token != serverSecret { render.Status(r, http.StatusUnauthorized) render.JSON(w, r, ErrUnauthorized) return } next.ServeHTTP(w, r) return } header := r.Header.Get("Authorization") bearer, token, found := strings.Cut(header, " ") hasInvalidHeader := bearer != "Bearer" hasInvalidSecret := !found || token != serverSecret if hasInvalidHeader || hasInvalidSecret { render.Status(r, http.StatusUnauthorized) render.JSON(w, r, ErrUnauthorized) return } next.ServeHTTP(w, r) } return http.HandlerFunc(fn) } } func hello(redirect bool) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { contentType := r.Header.Get("Content-Type") if !redirect || contentType == "application/json" { render.JSON(w, r, render.M{"hello": "clash"}) } else { http.Redirect(w, r, "/ui/", http.StatusTemporaryRedirect) } } } type Traffic struct { Up int64 `json:"up"` Down int64 `json:"down"` } func traffic(ctx context.Context, trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { var conn net.Conn if r.Header.Get("Upgrade") == "websocket" { var err error conn, _, _, err = ws.UpgradeHTTP(r, w) if err != nil { return } defer conn.Close() } if conn == nil { w.Header().Set("Content-Type", "application/json") render.Status(r, http.StatusOK) } tick := time.NewTicker(time.Second) defer tick.Stop() buf := &bytes.Buffer{} uploadTotal, downloadTotal := trafficManager.Total() for { select { case <-ctx.Done(): return case <-tick.C: } buf.Reset() uploadTotalNew, downloadTotalNew := trafficManager.Total() err := json.NewEncoder(buf).Encode(Traffic{ Up: uploadTotalNew - uploadTotal, Down: downloadTotalNew - downloadTotal, }) if err != nil { break } if conn == nil { _, err = w.Write(buf.Bytes()) w.(http.Flusher).Flush() } else { err = wsutil.WriteServerText(conn, buf.Bytes()) } if err != nil { break } uploadTotal = uploadTotalNew downloadTotal = downloadTotalNew } } } type Log struct { Type string `json:"type"` Payload string `json:"payload"` } func getLogs(ctx context.Context, logFactory log.ObservableFactory) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { levelText := r.URL.Query().Get("level") if levelText == "" { levelText = "info" } level, ok := log.ParseLevel(levelText) if ok != nil { render.Status(r, http.StatusBadRequest) render.JSON(w, r, ErrBadRequest) return } subscription, done, err := logFactory.Subscribe() if err != nil { render.Status(r, http.StatusNoContent) return } defer logFactory.UnSubscribe(subscription) var conn net.Conn if r.Header.Get("Upgrade") == "websocket" { conn, _, _, err = ws.UpgradeHTTP(r, w) if err != nil { return } defer conn.Close() } if conn == nil { w.Header().Set("Content-Type", "application/json") render.Status(r, http.StatusOK) } buf := &bytes.Buffer{} var logEntry log.Entry for { select { case <-ctx.Done(): return case <-done: return case logEntry = <-subscription: } if logEntry.Level > level { continue } buf.Reset() err = json.NewEncoder(buf).Encode(Log{ Type: log.FormatLevel(logEntry.Level), Payload: logEntry.Message, }) if err != nil { break } if conn == nil { _, err = w.Write(buf.Bytes()) w.(http.Flusher).Flush() } else { err = wsutil.WriteServerText(conn, buf.Bytes()) } if err != nil { break } } } } func version(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, render.M{"version": "sing-box " + C.Version, "premium": true, "meta": true}) } ================================================ FILE: experimental/clashapi/server_fs.go ================================================ package clashapi import "net/http" type Dir http.Dir func (d Dir) Open(name string) (http.File, error) { file, err := http.Dir(d).Open(name) if err != nil { return nil, err } return &fileWrapper{file}, nil } // workaround for #2345 #2596 type fileWrapper struct { http.File } ================================================ FILE: experimental/clashapi/server_resources.go ================================================ package clashapi import ( "archive/zip" "context" "crypto/tls" "io" "net" "net/http" "os" "path/filepath" "strings" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "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/filemanager" ) func (s *Server) checkAndDownloadExternalUI() { if s.externalUI == "" { return } entries, err := os.ReadDir(s.externalUI) if err != nil { os.MkdirAll(s.externalUI, 0o755) } if len(entries) == 0 { err = s.downloadExternalUI() if err != nil { s.logger.Error("download external ui error: ", err) } } } func (s *Server) downloadExternalUI() error { var downloadURL string if s.externalUIDownloadURL != "" { downloadURL = s.externalUIDownloadURL } else { downloadURL = "https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip" } var detour adapter.Outbound if s.externalUIDownloadDetour != "" { outbound, loaded := s.outbound.Outbound(s.externalUIDownloadDetour) if !loaded { return E.New("detour outbound not found: ", s.externalUIDownloadDetour) } detour = outbound } else { outbound := s.outbound.Default() detour = outbound } s.logger.Info("downloading external ui using outbound/", detour.Type(), "[", detour.Tag(), "]") httpClient := &http.Client{ Transport: &http.Transport{ ForceAttemptHTTP2: true, TLSHandshakeTimeout: C.TCPTimeout, DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { return detour.DialContext(ctx, network, M.ParseSocksaddr(addr)) }, TLSClientConfig: &tls.Config{ Time: ntp.TimeFuncFromContext(s.ctx), RootCAs: adapter.RootPoolFromContext(s.ctx), }, }, } defer httpClient.CloseIdleConnections() response, err := httpClient.Get(downloadURL) if err != nil { return err } defer response.Body.Close() if response.StatusCode != http.StatusOK { return E.New("download external ui failed: ", response.Status) } err = s.downloadZIP(response.Body, s.externalUI) if err != nil { removeAllInDirectory(s.externalUI) } return err } func (s *Server) downloadZIP(body io.Reader, output string) error { tempFile, err := filemanager.CreateTemp(s.ctx, "external-ui.zip") if err != nil { return err } defer os.Remove(tempFile.Name()) _, err = io.Copy(tempFile, body) tempFile.Close() if err != nil { return err } reader, err := zip.OpenReader(tempFile.Name()) if err != nil { return err } defer reader.Close() trimDir := zipIsInSingleDirectory(reader.File) for _, file := range reader.File { if file.FileInfo().IsDir() { continue } pathElements := strings.Split(file.Name, "/") if trimDir { pathElements = pathElements[1:] } saveDirectory := output if len(pathElements) > 1 { saveDirectory = filepath.Join(saveDirectory, filepath.Join(pathElements[:len(pathElements)-1]...)) } err = os.MkdirAll(saveDirectory, 0o755) if err != nil { return err } savePath := filepath.Join(saveDirectory, pathElements[len(pathElements)-1]) err = downloadZIPEntry(s.ctx, file, savePath) if err != nil { return err } } return nil } func downloadZIPEntry(ctx context.Context, zipFile *zip.File, savePath string) error { saveFile, err := filemanager.Create(ctx, savePath) if err != nil { return err } defer saveFile.Close() reader, err := zipFile.Open() if err != nil { return err } defer reader.Close() return common.Error(io.Copy(saveFile, reader)) } func removeAllInDirectory(directory string) { dirEntries, err := os.ReadDir(directory) if err != nil { return } for _, dirEntry := range dirEntries { os.RemoveAll(filepath.Join(directory, dirEntry.Name())) } } func zipIsInSingleDirectory(files []*zip.File) bool { var singleDirectory string for _, file := range files { if file.FileInfo().IsDir() { continue } pathElements := strings.Split(file.Name, "/") if len(pathElements) == 0 { return false } if singleDirectory == "" { singleDirectory = pathElements[0] } else if singleDirectory != pathElements[0] { return false } } return true } ================================================ FILE: experimental/clashapi/trafficontrol/manager.go ================================================ package trafficontrol import ( "runtime" "sync" "sync/atomic" "time" "github.com/sagernet/sing-box/common/compatible" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/observable" "github.com/sagernet/sing/common/x/list" "github.com/gofrs/uuid/v5" ) type ConnectionEventType int const ( ConnectionEventNew ConnectionEventType = iota ConnectionEventUpdate ConnectionEventClosed ) type ConnectionEvent struct { Type ConnectionEventType ID uuid.UUID Metadata *TrackerMetadata UplinkDelta int64 DownlinkDelta int64 ClosedAt time.Time } const closedConnectionsLimit = 1000 type Manager struct { uploadTotal atomic.Int64 downloadTotal atomic.Int64 connections compatible.Map[uuid.UUID, Tracker] closedConnectionsAccess sync.Mutex closedConnections list.List[TrackerMetadata] memory uint64 eventSubscriber *observable.Subscriber[ConnectionEvent] } func NewManager() *Manager { return &Manager{} } func (m *Manager) SetEventHook(subscriber *observable.Subscriber[ConnectionEvent]) { m.eventSubscriber = subscriber } func (m *Manager) Join(c Tracker) { metadata := c.Metadata() m.connections.Store(metadata.ID, c) if m.eventSubscriber != nil { m.eventSubscriber.Emit(ConnectionEvent{ Type: ConnectionEventNew, ID: metadata.ID, Metadata: metadata, }) } } func (m *Manager) Leave(c Tracker) { metadata := c.Metadata() _, loaded := m.connections.LoadAndDelete(metadata.ID) if loaded { closedAt := time.Now() metadata.ClosedAt = closedAt metadataCopy := *metadata m.closedConnectionsAccess.Lock() if m.closedConnections.Len() >= closedConnectionsLimit { m.closedConnections.PopFront() } m.closedConnections.PushBack(metadataCopy) m.closedConnectionsAccess.Unlock() if m.eventSubscriber != nil { m.eventSubscriber.Emit(ConnectionEvent{ Type: ConnectionEventClosed, ID: metadata.ID, Metadata: &metadataCopy, ClosedAt: closedAt, }) } } } func (m *Manager) PushUploaded(size int64) { m.uploadTotal.Add(size) } func (m *Manager) PushDownloaded(size int64) { m.downloadTotal.Add(size) } func (m *Manager) Total() (up int64, down int64) { return m.uploadTotal.Load(), m.downloadTotal.Load() } func (m *Manager) ConnectionsLen() int { return m.connections.Len() } func (m *Manager) Connections() []*TrackerMetadata { var connections []*TrackerMetadata m.connections.Range(func(_ uuid.UUID, value Tracker) bool { connections = append(connections, value.Metadata()) return true }) return connections } func (m *Manager) ClosedConnections() []*TrackerMetadata { m.closedConnectionsAccess.Lock() values := m.closedConnections.Array() m.closedConnectionsAccess.Unlock() if len(values) == 0 { return nil } connections := make([]*TrackerMetadata, len(values)) for i := range values { connections[i] = &values[i] } return connections } func (m *Manager) Connection(id uuid.UUID) Tracker { connection, loaded := m.connections.Load(id) if !loaded { return nil } return connection } func (m *Manager) Snapshot() *Snapshot { var connections []Tracker m.connections.Range(func(_ uuid.UUID, value Tracker) bool { if value.Metadata().OutboundType != C.TypeDNS { connections = append(connections, value) } return true }) var memStats runtime.MemStats runtime.ReadMemStats(&memStats) m.memory = memStats.StackInuse + memStats.HeapInuse + memStats.HeapIdle - memStats.HeapReleased return &Snapshot{ Upload: m.uploadTotal.Load(), Download: m.downloadTotal.Load(), Connections: connections, Memory: m.memory, } } func (m *Manager) Clear() { m.closedConnectionsAccess.Lock() defer m.closedConnectionsAccess.Unlock() m.closedConnections.Init() } type Snapshot struct { Download int64 Upload int64 Connections []Tracker Memory uint64 } func (s *Snapshot) MarshalJSON() ([]byte, error) { return json.Marshal(map[string]any{ "downloadTotal": s.Download, "uploadTotal": s.Upload, "connections": common.Map(s.Connections, func(t Tracker) *TrackerMetadata { return t.Metadata() }), "memory": s.Memory, }) } ================================================ FILE: experimental/clashapi/trafficontrol/tracker.go ================================================ package trafficontrol import ( "net" "sync/atomic" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/bufio" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json" N "github.com/sagernet/sing/common/network" "github.com/gofrs/uuid/v5" ) type TrackerMetadata struct { ID uuid.UUID Metadata adapter.InboundContext CreatedAt time.Time ClosedAt time.Time Upload *atomic.Int64 Download *atomic.Int64 Chain []string Rule adapter.Rule Outbound string OutboundType string } func (t TrackerMetadata) MarshalJSON() ([]byte, error) { var inbound string if t.Metadata.Inbound != "" { inbound = t.Metadata.InboundType + "/" + t.Metadata.Inbound } else { inbound = t.Metadata.InboundType } var domain string if t.Metadata.Domain != "" { domain = t.Metadata.Domain } else { domain = t.Metadata.Destination.Fqdn } var processPath string if t.Metadata.ProcessInfo != nil { if t.Metadata.ProcessInfo.ProcessPath != "" { processPath = t.Metadata.ProcessInfo.ProcessPath } else if len(t.Metadata.ProcessInfo.AndroidPackageNames) > 0 { processPath = t.Metadata.ProcessInfo.AndroidPackageNames[0] } if processPath == "" { if t.Metadata.ProcessInfo.UserId != -1 { processPath = F.ToString(t.Metadata.ProcessInfo.UserId) } } else if t.Metadata.ProcessInfo.UserName != "" { processPath = F.ToString(processPath, " (", t.Metadata.ProcessInfo.UserName, ")") } else if t.Metadata.ProcessInfo.UserId != -1 { processPath = F.ToString(processPath, " (", t.Metadata.ProcessInfo.UserId, ")") } } var rule string if t.Rule != nil { rule = F.ToString(t.Rule, " => ", t.Rule.Action()) } else { rule = "final" } return json.Marshal(map[string]any{ "id": t.ID, "metadata": map[string]any{ "network": t.Metadata.Network, "type": inbound, "sourceIP": t.Metadata.Source.Addr, "destinationIP": t.Metadata.Destination.Addr, "sourcePort": F.ToString(t.Metadata.Source.Port), "destinationPort": F.ToString(t.Metadata.Destination.Port), "host": domain, "dnsMode": "normal", "processPath": processPath, }, "upload": t.Upload.Load(), "download": t.Download.Load(), "start": t.CreatedAt, "chains": t.Chain, "rule": rule, "rulePayload": "", }) } type Tracker interface { Metadata() *TrackerMetadata Close() error } type TCPConn struct { N.ExtendedConn metadata TrackerMetadata manager *Manager } func (tt *TCPConn) Metadata() *TrackerMetadata { return &tt.metadata } func (tt *TCPConn) Close() error { tt.manager.Leave(tt) return tt.ExtendedConn.Close() } func (tt *TCPConn) Upstream() any { return tt.ExtendedConn } func (tt *TCPConn) ReaderReplaceable() bool { return true } func (tt *TCPConn) WriterReplaceable() bool { return true } func NewTCPTracker(conn net.Conn, manager *Manager, metadata adapter.InboundContext, outboundManager adapter.OutboundManager, matchRule adapter.Rule, matchOutbound adapter.Outbound) *TCPConn { id, _ := uuid.NewV4() var ( chain []string next string outbound string outboundType string ) if matchOutbound != nil { next = matchOutbound.Tag() } else { next = outboundManager.Default().Tag() } for { detour, loaded := outboundManager.Outbound(next) if !loaded { break } chain = append(chain, next) outbound = detour.Tag() outboundType = detour.Type() group, isGroup := detour.(adapter.OutboundGroup) if !isGroup { break } next = group.Now() } upload := new(atomic.Int64) download := new(atomic.Int64) tracker := &TCPConn{ ExtendedConn: bufio.NewCounterConn(conn, []N.CountFunc{func(n int64) { upload.Add(n) manager.PushUploaded(n) }}, []N.CountFunc{func(n int64) { download.Add(n) manager.PushDownloaded(n) }}), metadata: TrackerMetadata{ ID: id, Metadata: metadata, CreatedAt: time.Now(), Upload: upload, Download: download, Chain: common.Reverse(chain), Rule: matchRule, Outbound: outbound, OutboundType: outboundType, }, manager: manager, } manager.Join(tracker) return tracker } type UDPConn struct { N.PacketConn `json:"-"` metadata TrackerMetadata manager *Manager } func (ut *UDPConn) Metadata() *TrackerMetadata { return &ut.metadata } func (ut *UDPConn) Close() error { ut.manager.Leave(ut) return ut.PacketConn.Close() } func (ut *UDPConn) Upstream() any { return ut.PacketConn } func (ut *UDPConn) ReaderReplaceable() bool { return true } func (ut *UDPConn) WriterReplaceable() bool { return true } func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata adapter.InboundContext, outboundManager adapter.OutboundManager, matchRule adapter.Rule, matchOutbound adapter.Outbound) *UDPConn { id, _ := uuid.NewV4() var ( chain []string next string outbound string outboundType string ) if matchOutbound != nil { next = matchOutbound.Tag() } else { next = outboundManager.Default().Tag() } for { detour, loaded := outboundManager.Outbound(next) if !loaded { break } chain = append(chain, next) outbound = detour.Tag() outboundType = detour.Type() group, isGroup := detour.(adapter.OutboundGroup) if !isGroup { break } next = group.Now() } upload := new(atomic.Int64) download := new(atomic.Int64) trackerConn := &UDPConn{ PacketConn: bufio.NewCounterPacketConn(conn, []N.CountFunc{func(n int64) { upload.Add(n) manager.PushUploaded(n) }}, []N.CountFunc{func(n int64) { download.Add(n) manager.PushDownloaded(n) }}), metadata: TrackerMetadata{ ID: id, Metadata: metadata, CreatedAt: time.Now(), Upload: upload, Download: download, Chain: common.Reverse(chain), Rule: matchRule, Outbound: outbound, OutboundType: outboundType, }, manager: manager, } manager.Join(trackerConn) return trackerConn } ================================================ FILE: experimental/clashapi.go ================================================ package experimental import ( "context" "os" "sort" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" ) type ClashServerConstructor = func(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) var clashServerConstructor ClashServerConstructor func RegisterClashServerConstructor(constructor ClashServerConstructor) { clashServerConstructor = constructor } func NewClashServer(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) { if clashServerConstructor == nil { return nil, os.ErrInvalid } return clashServerConstructor(ctx, logFactory, options) } func CalculateClashModeList(options option.Options) []string { var clashModes []string clashModes = append(clashModes, extraClashModeFromRule(common.PtrValueOrDefault(options.Route).Rules)...) clashModes = append(clashModes, extraClashModeFromDNSRule(common.PtrValueOrDefault(options.DNS).Rules)...) clashModes = common.FilterNotDefault(common.Uniq(clashModes)) predefinedOrder := []string{ "Rule", "Global", "Direct", } var newClashModes []string for _, mode := range clashModes { if !common.Contains(predefinedOrder, mode) { newClashModes = append(newClashModes, mode) } } sort.Strings(newClashModes) for _, mode := range predefinedOrder { if common.Contains(clashModes, mode) { newClashModes = append(newClashModes, mode) } } return newClashModes } func extraClashModeFromRule(rules []option.Rule) []string { var clashMode []string for _, rule := range rules { switch rule.Type { case C.RuleTypeDefault: if rule.DefaultOptions.ClashMode != "" { clashMode = append(clashMode, rule.DefaultOptions.ClashMode) } case C.RuleTypeLogical: clashMode = append(clashMode, extraClashModeFromRule(rule.LogicalOptions.Rules)...) } } return clashMode } func extraClashModeFromDNSRule(rules []option.DNSRule) []string { var clashMode []string for _, rule := range rules { switch rule.Type { case C.RuleTypeDefault: if rule.DefaultOptions.ClashMode != "" { clashMode = append(clashMode, rule.DefaultOptions.ClashMode) } case C.RuleTypeLogical: clashMode = append(clashMode, extraClashModeFromDNSRule(rule.LogicalOptions.Rules)...) } } return clashMode } ================================================ FILE: experimental/deprecated/constants.go ================================================ package deprecated import ( "fmt" "github.com/sagernet/sing-box/common/badversion" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental/locale" F "github.com/sagernet/sing/common/format" "golang.org/x/mod/semver" ) type Note struct { Name string Description string DeprecatedVersion string ScheduledVersion string EnvName string MigrationLink string } func (n Note) Impending() bool { if n.ScheduledVersion == "" { return false } if !semver.IsValid("v" + C.Version) { return false } versionCurrent := badversion.Parse(C.Version) versionMinor := badversion.Parse(n.ScheduledVersion).Minor - versionCurrent.Minor if versionCurrent.PreReleaseIdentifier == "" && versionMinor < 0 { panic("invalid deprecated note: " + n.Name) } return versionMinor <= 1 } func (n Note) Message() string { if n.MigrationLink != "" { return fmt.Sprintf(locale.Current().DeprecatedMessage, n.Description, n.DeprecatedVersion, n.ScheduledVersion) } else { return fmt.Sprintf(locale.Current().DeprecatedMessageNoLink, n.Description, n.DeprecatedVersion, n.ScheduledVersion) } } func (n Note) MessageWithLink() string { if n.MigrationLink != "" { return F.ToString( n.Description, " is deprecated in sing-box ", n.DeprecatedVersion, " and will be removed in sing-box ", n.ScheduledVersion, ", checkout documentation for migration: ", n.MigrationLink, ) } else { return F.ToString( n.Description, " is deprecated in sing-box ", n.DeprecatedVersion, " and will be removed in sing-box ", n.ScheduledVersion, ".", ) } } var OptionOutboundDNSRuleItem = Note{ Name: "outbound-dns-rule-item", Description: "outbound DNS rule item", DeprecatedVersion: "1.12.0", ScheduledVersion: "1.14.0", EnvName: "OUTBOUND_DNS_RULE_ITEM", MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-outbound-dns-rule-items-to-domain-resolver", } var OptionMissingDomainResolver = Note{ Name: "missing-domain-resolver", Description: "missing `route.default_domain_resolver` or `domain_resolver` in dial fields", DeprecatedVersion: "1.12.0", ScheduledVersion: "1.14.0", EnvName: "MISSING_DOMAIN_RESOLVER", MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-outbound-dns-rule-items-to-domain-resolver", } var OptionLegacyDomainStrategyOptions = Note{ Name: "legacy-domain-strategy-options", Description: "legacy domain strategy options", DeprecatedVersion: "1.12.0", ScheduledVersion: "1.14.0", EnvName: "LEGACY_DOMAIN_STRATEGY_OPTIONS", 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 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", 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 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 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, } ================================================ FILE: experimental/deprecated/manager.go ================================================ package deprecated import ( "context" "github.com/sagernet/sing/service" ) type Manager interface { ReportDeprecated(feature Note) } func Report(ctx context.Context, feature Note) { manager := service.FromContext[Manager](ctx) if manager == nil { return } manager.ReportDeprecated(feature) } ================================================ FILE: experimental/deprecated/stderr.go ================================================ 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 } func NewStderrManager(logger logger.Logger) Manager { return &stderrManager{ logger: logger, reported: make(map[string]bool), } } func (f *stderrManager) ReportDeprecated(feature Note) { f.access.Lock() defer f.access.Unlock() if f.reported[feature.Name] { return } f.reported[feature.Name] = true if !feature.Impending() { f.logger.Warn(feature.MessageWithLink()) return } if feature.EnvName != "" { enable, enableErr := strconv.ParseBool(os.Getenv("ENABLE_DEPRECATED_" + feature.EnvName)) if enableErr == nil && enable { f.logger.Warn(feature.MessageWithLink()) return } f.logger.Error(feature.MessageWithLink()) f.logger.Fatal("to continuing using this feature, set environment variable ENABLE_DEPRECATED_" + feature.EnvName + "=true") } else { f.logger.Error(feature.MessageWithLink()) } } ================================================ FILE: experimental/libbox/build_info.go ================================================ //go:build android package libbox import ( "archive/zip" "bytes" "debug/buildinfo" "io" "runtime/debug" "strings" "github.com/sagernet/sing/common" ) const ( androidVPNCoreTypeOpenVPN = "OpenVPN" androidVPNCoreTypeShadowsocks = "Shadowsocks" androidVPNCoreTypeClash = "Clash" androidVPNCoreTypeV2Ray = "V2Ray" androidVPNCoreTypeWireGuard = "WireGuard" androidVPNCoreTypeSingBox = "sing-box" androidVPNCoreTypeUnknown = "Unknown" ) type AndroidVPNType struct { CoreType string CorePath string GoVersion string } func ReadAndroidVPNType(publicSourceDirList StringIterator) (*AndroidVPNType, error) { apkPathList := iteratorToArray[string](publicSourceDirList) var lastError error for _, apkPath := range apkPathList { androidVPNType, err := readAndroidVPNType(apkPath) if androidVPNType == nil { if err != nil { lastError = err } continue } return androidVPNType, nil } return nil, lastError } func readAndroidVPNType(publicSourceDir string) (*AndroidVPNType, error) { reader, err := zip.OpenReader(publicSourceDir) if err != nil { return nil, err } defer reader.Close() var lastError error for _, file := range reader.File { if !strings.HasPrefix(file.Name, "lib/") { continue } vpnType, err := readAndroidVPNTypeEntry(file) if err != nil { lastError = err continue } return vpnType, nil } for _, file := range reader.File { if !strings.HasPrefix(file.Name, "lib/") { continue } if strings.Contains(file.Name, androidVPNCoreTypeOpenVPN) || strings.Contains(file.Name, "ovpn") { return &AndroidVPNType{CoreType: androidVPNCoreTypeOpenVPN}, nil } if strings.Contains(file.Name, androidVPNCoreTypeShadowsocks) { return &AndroidVPNType{CoreType: androidVPNCoreTypeShadowsocks}, nil } } return nil, lastError } func readAndroidVPNTypeEntry(zipFile *zip.File) (*AndroidVPNType, error) { readCloser, err := zipFile.Open() if err != nil { return nil, err } libContent := make([]byte, zipFile.UncompressedSize64) _, err = io.ReadFull(readCloser, libContent) readCloser.Close() if err != nil { return nil, err } buildInfo, err := buildinfo.Read(bytes.NewReader(libContent)) if err != nil { return nil, err } var vpnType AndroidVPNType vpnType.GoVersion = buildInfo.GoVersion if !strings.HasPrefix(vpnType.GoVersion, "go") { vpnType.GoVersion = "obfuscated" } else { vpnType.GoVersion = vpnType.GoVersion[2:] } vpnType.CoreType = androidVPNCoreTypeUnknown if len(buildInfo.Deps) == 0 { vpnType.CoreType = "obfuscated" return &vpnType, nil } dependencies := make(map[string]bool) dependencies[buildInfo.Path] = true for _, module := range buildInfo.Deps { dependencies[module.Path] = true if module.Replace != nil { dependencies[module.Replace.Path] = true } } for dependency := range dependencies { pkgType, loaded := determinePkgType(dependency) if loaded { vpnType.CoreType = pkgType } } if vpnType.CoreType == androidVPNCoreTypeUnknown { for dependency := range dependencies { pkgType, loaded := determinePkgTypeSecondary(dependency) if loaded { vpnType.CoreType = pkgType return &vpnType, nil } } } if vpnType.CoreType != androidVPNCoreTypeUnknown { vpnType.CorePath, _ = determineCorePath(buildInfo, vpnType.CoreType) return &vpnType, nil } if dependencies["github.com/golang/protobuf"] && dependencies["github.com/v2fly/ss-bloomring"] { vpnType.CoreType = androidVPNCoreTypeV2Ray return &vpnType, nil } return &vpnType, nil } func determinePkgType(pkgName string) (string, bool) { pkgNameLower := strings.ToLower(pkgName) if strings.Contains(pkgNameLower, "clash") { return androidVPNCoreTypeClash, true } if strings.Contains(pkgNameLower, "v2ray") || strings.Contains(pkgNameLower, "xray") { return androidVPNCoreTypeV2Ray, true } if strings.Contains(pkgNameLower, "sing-box") { return androidVPNCoreTypeSingBox, true } return "", false } func determinePkgTypeSecondary(pkgName string) (string, bool) { pkgNameLower := strings.ToLower(pkgName) if strings.Contains(pkgNameLower, "wireguard") { return androidVPNCoreTypeWireGuard, true } return "", false } func determineCorePath(pkgInfo *buildinfo.BuildInfo, pkgType string) (string, bool) { switch pkgType { case androidVPNCoreTypeClash: return determineCorePathForPkgs(pkgInfo, []string{"github.com/Dreamacro/clash"}, []string{"clash"}) case androidVPNCoreTypeV2Ray: if v2rayVersion, loaded := determineCorePathForPkgs(pkgInfo, []string{ "github.com/v2fly/v2ray-core", "github.com/v2fly/v2ray-core/v4", "github.com/v2fly/v2ray-core/v5", }, []string{ "v2ray", }); loaded { return v2rayVersion, true } if xrayVersion, loaded := determineCorePathForPkgs(pkgInfo, []string{ "github.com/xtls/xray-core", }, []string{ "xray", }); loaded { return xrayVersion, true } return "", false case androidVPNCoreTypeSingBox: return determineCorePathForPkgs(pkgInfo, []string{"github.com/sagernet/sing-box"}, []string{"sing-box"}) case androidVPNCoreTypeWireGuard: return determineCorePathForPkgs(pkgInfo, []string{"golang.zx2c4.com/wireguard"}, []string{"wireguard"}) default: return "", false } } func determineCorePathForPkgs(pkgInfo *buildinfo.BuildInfo, pkgs []string, names []string) (string, bool) { for _, pkg := range pkgs { if pkgInfo.Path == pkg { return pkg, true } strictDependency := common.Find(pkgInfo.Deps, func(module *debug.Module) bool { return module.Path == pkg }) if strictDependency != nil { if isValidVersion(strictDependency.Version) { return strictDependency.Path + " " + strictDependency.Version, true } else { return strictDependency.Path, true } } } for _, name := range names { if strings.Contains(pkgInfo.Path, name) { return pkgInfo.Path, true } looseDependency := common.Find(pkgInfo.Deps, func(module *debug.Module) bool { return strings.Contains(module.Path, name) || (module.Replace != nil && strings.Contains(module.Replace.Path, name)) }) if looseDependency != nil { return looseDependency.Path, true } } return "", false } func isValidVersion(version string) bool { if version == "(devel)" { return false } if strings.Contains(version, "v0.0.0") { return false } return true } ================================================ FILE: experimental/libbox/command.go ================================================ package libbox const ( CommandLog int32 = iota CommandStatus CommandGroup CommandClashMode CommandConnections CommandOutbounds ) ================================================ FILE: experimental/libbox/command_client.go ================================================ package libbox import ( "context" "net" "os" "path/filepath" "strconv" "sync" "time" "github.com/sagernet/sing-box/daemon" "github.com/sagernet/sing/common" 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" ) type CommandClient struct { handler CommandClientHandler grpcConn *grpc.ClientConn grpcClient daemon.StartedServiceClient options CommandClientOptions ctx context.Context cancel context.CancelFunc clientMutex sync.RWMutex standalone bool } type CommandClientOptions struct { commands []int32 StatusInterval int64 } func (o *CommandClientOptions) AddCommand(command int32) { o.commands = append(o.commands, command) } type CommandClientHandler interface { Connected() Disconnected(message string) SetDefaultLogLevel(level int32) ClearLogs() WriteLogs(messageList LogIterator) WriteStatus(message *StatusMessage) WriteGroups(message OutboundGroupIterator) WriteOutbounds(message OutboundGroupItemIterator) InitializeClashMode(modeList StringIterator, currentMode string) UpdateClashMode(newMode string) WriteConnectionEvents(events *ConnectionEvents) } type LogEntry struct { Level int32 Message string } type LogIterator interface { Len() int32 HasNext() bool Next() *LogEntry } type XPCDialer interface { DialXPC() (int32, error) } var sXPCDialer XPCDialer func SetXPCDialer(dialer XPCDialer) { sXPCDialer = dialer } func NewStandaloneCommandClient() *CommandClient { return &CommandClient{standalone: true} } func NewCommandClient(handler CommandClientHandler, options *CommandClientOptions) *CommandClient { return &CommandClient{ handler: handler, options: common.PtrValueOrDefault(options), } } func unaryClientAuthInterceptor(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { if sCommandServerSecret != "" { ctx = metadata.AppendToOutgoingContext(ctx, "x-command-secret", sCommandServerSecret) } return invoker(ctx, method, req, reply, cc, opts...) } func streamClientAuthInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) { if sCommandServerSecret != "" { ctx = metadata.AppendToOutgoingContext(ctx, "x-command-secret", sCommandServerSecret) } return streamer(ctx, desc, cc, method, opts...) } const ( commandClientDialAttempts = 10 commandClientDialBaseDelay = 100 * time.Millisecond commandClientDialStepDelay = 50 * time.Millisecond ) func commandClientDialDelay(attempt int) time.Duration { return commandClientDialBaseDelay + time.Duration(attempt)*commandClientDialStepDelay } func dialTarget() (string, func(context.Context, string) (net.Conn, error)) { if sXPCDialer != nil { return "passthrough:///xpc", func(ctx context.Context, _ string) (net.Conn, error) { fileDescriptor, err := sXPCDialer.DialXPC() if err != nil { return nil, E.Cause(err, "dial xpc") } return networkConnectionFromFileDescriptor(fileDescriptor) } } if sCommandServerListenPort == 0 { socketPath := filepath.Join(sBasePath, "command.sock") return "passthrough:///command-socket", func(ctx context.Context, _ string) (net.Conn, error) { var networkDialer net.Dialer return networkDialer.DialContext(ctx, "unix", socketPath) } } return net.JoinHostPort("127.0.0.1", strconv.Itoa(int(sCommandServerListenPort))), nil } func networkConnectionFromFileDescriptor(fileDescriptor int32) (net.Conn, error) { file := os.NewFile(uintptr(fileDescriptor), "xpc-command-socket") if file == nil { return nil, E.New("invalid file descriptor") } networkConnection, err := net.FileConn(file) if err != nil { file.Close() return nil, E.Cause(err, "create connection from fd") } file.Close() return networkConnection, nil } func (c *CommandClient) dialWithRetry(target string, contextDialer func(context.Context, string) (net.Conn, error), retryDial bool) (*grpc.ClientConn, daemon.StartedServiceClient, error) { var connection *grpc.ClientConn var client daemon.StartedServiceClient var lastError error for attempt := range commandClientDialAttempts { if connection == nil { options := []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithUnaryInterceptor(unaryClientAuthInterceptor), grpc.WithStreamInterceptor(streamClientAuthInterceptor), } if contextDialer != nil { options = append(options, grpc.WithContextDialer(contextDialer)) } var err error connection, err = grpc.NewClient(target, options...) if err != nil { lastError = err if !retryDial { return nil, nil, E.Cause(err, "create command client") } time.Sleep(commandClientDialDelay(attempt)) continue } client = daemon.NewStartedServiceClient(connection) } waitDuration := commandClientDialDelay(attempt) ctx, cancel := context.WithTimeout(context.Background(), waitDuration) _, err := client.GetStartedAt(ctx, &emptypb.Empty{}, grpc.WaitForReady(true)) cancel() if err == nil { return connection, client, nil } lastError = err } if connection != nil { connection.Close() } return nil, nil, E.Cause(lastError, "probe command server") } func (c *CommandClient) Connect() error { c.clientMutex.Lock() common.Close(common.PtrOrNil(c.grpcConn)) target, contextDialer := dialTarget() connection, client, err := c.dialWithRetry(target, contextDialer, true) if err != nil { c.clientMutex.Unlock() return err } c.grpcConn = connection c.grpcClient = client c.ctx, c.cancel = context.WithCancel(context.Background()) c.clientMutex.Unlock() c.handler.Connected() return c.dispatchCommands() } func (c *CommandClient) ConnectWithFD(fd int32) error { c.clientMutex.Lock() common.Close(common.PtrOrNil(c.grpcConn)) networkConnection, err := networkConnectionFromFileDescriptor(fd) if err != nil { c.clientMutex.Unlock() return err } connection, client, err := c.dialWithRetry("passthrough:///xpc", func(ctx context.Context, _ string) (net.Conn, error) { return networkConnection, nil }, false) if err != nil { networkConnection.Close() c.clientMutex.Unlock() return err } c.grpcConn = connection c.grpcClient = client c.ctx, c.cancel = context.WithCancel(context.Background()) c.clientMutex.Unlock() c.handler.Connected() return c.dispatchCommands() } func (c *CommandClient) dispatchCommands() error { for _, command := range c.options.commands { switch command { case CommandLog: go c.handleLogStream() case CommandStatus: go c.handleStatusStream() case CommandGroup: go c.handleGroupStream() case CommandClashMode: go c.handleClashModeStream() case CommandConnections: go c.handleConnectionsStream() case CommandOutbounds: go c.handleOutboundsStream() default: return E.New("unknown command: ", command) } } return nil } func (c *CommandClient) Disconnect() error { c.clientMutex.Lock() defer c.clientMutex.Unlock() if c.cancel != nil { c.cancel() } return common.Close(common.PtrOrNil(c.grpcConn)) } func (c *CommandClient) getClientForCall() (daemon.StartedServiceClient, error) { c.clientMutex.RLock() if c.grpcClient != nil { defer c.clientMutex.RUnlock() return c.grpcClient, nil } c.clientMutex.RUnlock() c.clientMutex.Lock() defer c.clientMutex.Unlock() if c.grpcClient != nil { return c.grpcClient, nil } target, contextDialer := dialTarget() connection, client, err := c.dialWithRetry(target, contextDialer, true) if err != nil { return nil, E.Cause(err, "get command client") } c.grpcConn = connection c.grpcClient = client if c.ctx == nil { c.ctx, c.cancel = context.WithCancel(context.Background()) } return c.grpcClient, nil } func (c *CommandClient) closeConnection() { c.clientMutex.Lock() defer c.clientMutex.Unlock() if c.grpcConn != nil { c.grpcConn.Close() c.grpcConn = nil c.grpcClient = nil } } func callWithResult[T any](c *CommandClient, call func(client daemon.StartedServiceClient) (T, error)) (T, error) { client, err := c.getClientForCall() if err != nil { var zero T return zero, err } if c.standalone { defer c.closeConnection() } return call(client) } func (c *CommandClient) getStreamContext() (daemon.StartedServiceClient, context.Context) { c.clientMutex.RLock() defer c.clientMutex.RUnlock() return c.grpcClient, c.ctx } func (c *CommandClient) handleLogStream() { client, ctx := c.getStreamContext() stream, err := client.SubscribeLog(ctx, &emptypb.Empty{}) if err != nil { c.handler.Disconnected(E.Cause(err, "subscribe log").Error()) return } defaultLogLevel, err := client.GetDefaultLogLevel(ctx, &emptypb.Empty{}) if err != nil { 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(E.Cause(err, "log stream recv").Error()) return } if logMessage.Reset_ { c.handler.ClearLogs() } var messages []*LogEntry for _, msg := range logMessage.Messages { messages = append(messages, &LogEntry{ Level: int32(msg.Level), Message: msg.Message, }) } c.handler.WriteLogs(newIterator(messages)) } } func (c *CommandClient) handleStatusStream() { client, ctx := c.getStreamContext() interval := c.options.StatusInterval stream, err := client.SubscribeStatus(ctx, &daemon.SubscribeStatusRequest{ Interval: interval, }) if err != nil { c.handler.Disconnected(E.Cause(err, "subscribe status").Error()) return } for { status, err := stream.Recv() if err != nil { c.handler.Disconnected(E.Cause(err, "status stream recv").Error()) return } c.handler.WriteStatus(statusMessageFromGRPC(status)) } } func (c *CommandClient) handleGroupStream() { client, ctx := c.getStreamContext() stream, err := client.SubscribeGroups(ctx, &emptypb.Empty{}) if err != nil { c.handler.Disconnected(E.Cause(err, "subscribe groups").Error()) return } for { groups, err := stream.Recv() if err != nil { c.handler.Disconnected(E.Cause(err, "groups stream recv").Error()) return } c.handler.WriteGroups(outboundGroupIteratorFromGRPC(groups)) } } func (c *CommandClient) handleClashModeStream() { client, ctx := c.getStreamContext() modeStatus, err := client.GetClashModeStatus(ctx, &emptypb.Empty{}) if err != nil { c.handler.Disconnected(E.Cause(err, "get clash mode status").Error()) return } if sFixAndroidStack { go func() { c.handler.InitializeClashMode(newIterator(modeStatus.ModeList), modeStatus.CurrentMode) if len(modeStatus.ModeList) == 0 { 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(E.Cause(os.ErrInvalid, "empty clash mode list").Error()) return } } if len(modeStatus.ModeList) == 0 { return } stream, err := client.SubscribeClashMode(ctx, &emptypb.Empty{}) if err != nil { c.handler.Disconnected(E.Cause(err, "subscribe clash mode").Error()) return } for { mode, err := stream.Recv() if err != nil { c.handler.Disconnected(E.Cause(err, "clash mode stream recv").Error()) return } c.handler.UpdateClashMode(mode.Mode) } } func (c *CommandClient) handleConnectionsStream() { client, ctx := c.getStreamContext() interval := c.options.StatusInterval stream, err := client.SubscribeConnections(ctx, &daemon.SubscribeConnectionsRequest{ Interval: interval, }) if err != nil { c.handler.Disconnected(E.Cause(err, "subscribe connections").Error()) return } for { events, err := stream.Recv() if err != nil { c.handler.Disconnected(E.Cause(err, "connections stream recv").Error()) return } libboxEvents := connectionEventsFromGRPC(events) c.handler.WriteConnectionEvents(libboxEvents) } } func (c *CommandClient) handleOutboundsStream() { client, ctx := c.getStreamContext() stream, err := client.SubscribeOutbounds(ctx, &emptypb.Empty{}) if err != nil { c.handler.Disconnected(E.Cause(err, "subscribe outbounds").Error()) return } for { list, err := stream.Recv() if err != nil { c.handler.Disconnected(E.Cause(err, "outbounds stream recv").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{ GroupTag: groupTag, OutboundTag: outboundTag, }) }) if err != nil { return E.Cause(err, "select outbound") } return nil } func (c *CommandClient) URLTest(groupTag string) error { _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { return client.URLTest(context.Background(), &daemon.URLTestRequest{ OutboundTag: groupTag, }) }) if err != nil { return E.Cause(err, "url test") } return nil } func (c *CommandClient) SetClashMode(newMode string) error { _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { return client.SetClashMode(context.Background(), &daemon.ClashMode{ Mode: newMode, }) }) if err != nil { return E.Cause(err, "set clash mode") } return nil } func (c *CommandClient) CloseConnection(connId string) error { _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { return client.CloseConnection(context.Background(), &daemon.CloseConnectionRequest{ Id: connId, }) }) 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{}) }) 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{}) }) 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{}) }) 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{}) }) 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, E.Cause(err, "get system proxy status") } return systemProxyStatusFromGRPC(status), nil }) } func (c *CommandClient) SetSystemProxyEnabled(isEnabled bool) error { _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { return client.SetSystemProxyEnabled(context.Background(), &daemon.SetSystemProxyEnabledRequest{ Enabled: isEnabled, }) }) if err != nil { return E.Cause(err, "set system proxy enabled") } return nil } 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, }) }) if err != nil { return E.Cause(err, "trigger debug crash") } return nil } 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, }) }) 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{}) }) 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, E.Cause(err, "get deprecated warnings") } var notes []*DeprecatedNote for _, warning := range warnings.Warnings { notes = append(notes, &DeprecatedNote{ Description: warning.Description, DeprecatedVersion: warning.DeprecatedVersion, ScheduledVersion: warning.ScheduledVersion, MigrationLink: warning.MigrationLink, }) } return newIterator(notes), nil }) } 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, E.Cause(err, "get started at") } return startedAt.StartedAt, nil }) } func (c *CommandClient) SetGroupExpand(groupTag string, isExpand bool) error { _, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) { return client.SetGroupExpand(context.Background(), &daemon.SetGroupExpandRequest{ GroupTag: groupTag, IsExpand: isExpand, }) }) 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 E.Cause(err, "start network quality test") } 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 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 } 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 E.Cause(err, "start stun test") } if c.standalone { defer c.closeConnection() } stream, err := client.StartSTUNTest(context.Background(), &daemon.STUNTestRequest{ Server: server, OutboundTag: outboundTag, }) if err != nil { 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 } 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)) } } func (c *CommandClient) SubscribeTailscaleStatus(handler TailscaleStatusHandler) error { client, err := c.getClientForCall() if err != nil { 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 E.Cause(err, "subscribe tailscale status") } for { event, recvErr := stream.Recv() if recvErr != nil { if status.Code(recvErr) == codes.NotFound || status.Code(recvErr) == codes.Unavailable { return nil } recvErr = E.Cause(recvErr, "tailscale status recv") 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 E.Cause(err, "start tailscale ping") } if c.standalone { defer c.closeConnection() } stream, err := client.StartTailscalePing(context.Background(), &daemon.TailscalePingRequest{ EndpointTag: endpointTag, PeerIP: peerIP, }) if err != nil { 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 } handler.OnPingResult(tailscalePingResultFromGRPC(event)) } } ================================================ FILE: experimental/libbox/command_server.go ================================================ package libbox import ( "context" "errors" "net" "os" "path/filepath" "strconv" "syscall" "time" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/daemon" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/service" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" ) type CommandServer struct { *daemon.StartedService handler CommandServerHandler platformInterface PlatformInterface platformWrapper *platformInterfaceWrapper grpcServer *grpc.Server listener net.Listener endPauseTimer *time.Timer } type CommandServerHandler interface { ServiceStop() error ServiceReload() error GetSystemProxyStatus() (*SystemProxyStatus, error) SetSystemProxyEnabled(enabled bool) error TriggerNativeCrash() error WriteDebugMessage(message string) } func NewCommandServer(handler CommandServerHandler, platformInterface PlatformInterface) (*CommandServer, error) { ctx := baseContext(platformInterface) platformWrapper := &platformInterfaceWrapper{ iif: platformInterface, useProcFS: platformInterface.UseProcFS(), } service.MustRegister[adapter.PlatformInterface](ctx, platformWrapper) server := &CommandServer{ handler: handler, platformInterface: platformInterface, platformWrapper: platformWrapper, } server.StartedService = daemon.NewStartedService(daemon.ServiceOptions{ Context: ctx, // Platform: platformWrapper, Handler: (*platformHandler)(server), Debug: sDebug, LogMaxLines: sLogMaxLines, OOMKillerEnabled: sOOMKillerEnabled, OOMKillerDisabled: sOOMKillerDisabled, OOMMemoryLimit: uint64(sOOMMemoryLimit), // WorkingDirectory: sWorkingPath, // TempDirectory: sTempPath, // UserID: sUserID, // GroupID: sGroupID, // SystemProxyEnabled: false, }) return server, nil } func unaryAuthInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { if sCommandServerSecret == "" { return handler(ctx, req) } md, ok := metadata.FromIncomingContext(ctx) if !ok { return nil, status.Error(codes.Unauthenticated, "missing metadata") } values := md.Get("x-command-secret") if len(values) == 0 { return nil, status.Error(codes.Unauthenticated, "missing authentication secret") } if values[0] != sCommandServerSecret { return nil, status.Error(codes.Unauthenticated, "invalid authentication secret") } return handler(ctx, req) } func streamAuthInterceptor(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { if sCommandServerSecret == "" { return handler(srv, ss) } md, ok := metadata.FromIncomingContext(ss.Context()) if !ok { return status.Error(codes.Unauthenticated, "missing metadata") } values := md.Get("x-command-secret") if len(values) == 0 { return status.Error(codes.Unauthenticated, "missing authentication secret") } if values[0] != sCommandServerSecret { return status.Error(codes.Unauthenticated, "invalid authentication secret") } return handler(srv, ss) } func (s *CommandServer) Start() error { var ( listener net.Listener err error ) if sCommandServerListenPort == 0 { sockPath := filepath.Join(sBasePath, "command.sock") os.Remove(sockPath) for range 30 { listener, err = net.ListenUnix("unix", &net.UnixAddr{ Name: sockPath, Net: "unix", }) if err == nil { break } if !errors.Is(err, syscall.EROFS) { break } time.Sleep(time.Second) } if err != nil { return E.Cause(err, "listen command server") } if sUserID != os.Getuid() { err = os.Chown(sockPath, sUserID, sGroupID) if err != nil { listener.Close() os.Remove(sockPath) return E.Cause(err, "chown") } } } else { listener, err = net.Listen("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(int(sCommandServerListenPort)))) if err != nil { return E.Cause(err, "listen command server") } } s.listener = listener serverOptions := []grpc.ServerOption{ grpc.UnaryInterceptor(unaryAuthInterceptor), grpc.StreamInterceptor(streamAuthInterceptor), } s.grpcServer = grpc.NewServer(serverOptions...) daemon.RegisterStartedServiceServer(s.grpcServer, s.StartedService) go s.grpcServer.Serve(listener) return nil } func (s *CommandServer) Close() { if s.grpcServer != nil { s.grpcServer.Stop() } common.Close(s.listener) s.StartedService.Close() } type OverrideOptions struct { AutoRedirect bool IncludePackage StringIterator ExcludePackage StringIterator } func (s *CommandServer) StartOrReloadService(configContent string, options *OverrideOptions) error { saveConfigSnapshot(configContent) err := s.StartedService.StartOrReloadService(configContent, &daemon.OverrideOptions{ AutoRedirect: options.AutoRedirect, IncludePackage: iteratorToArray(options.IncludePackage), ExcludePackage: iteratorToArray(options.ExcludePackage), }) if err != nil { return E.Cause(err, "start or reload service") } return nil } func (s *CommandServer) CloseService() error { return s.StartedService.CloseService() } func (s *CommandServer) WriteMessage(level int32, message string) { s.StartedService.WriteMessage(log.Level(level), message) } func (s *CommandServer) SetError(message string) { s.StartedService.SetError(E.New(message)) } func (s *CommandServer) NeedWIFIState() bool { instance := s.StartedService.Instance() if instance == nil || instance.Box() == nil { return false } return instance.Box().Network().NeedWIFIState() } func (s *CommandServer) NeedFindProcess() bool { instance := s.StartedService.Instance() if instance == nil || instance.Box() == nil { return false } return instance.Box().Router().NeedFindProcess() } func (s *CommandServer) Pause() { instance := s.StartedService.Instance() if instance == nil || instance.PauseManager() == nil { return } instance.PauseManager().DevicePause() if C.IsIos { if s.endPauseTimer == nil { s.endPauseTimer = time.AfterFunc(time.Minute, instance.PauseManager().DeviceWake) } else { s.endPauseTimer.Reset(time.Minute) } } } func (s *CommandServer) Wake() { instance := s.StartedService.Instance() if instance == nil || instance.PauseManager() == nil { return } if !C.IsIos { instance.PauseManager().DeviceWake() } } func (s *CommandServer) ResetNetwork() { instance := s.StartedService.Instance() if instance == nil || instance.Box() == nil { return } instance.Box().Network().ResetNetwork() } func (s *CommandServer) UpdateWIFIState() { instance := s.StartedService.Instance() if instance == nil || instance.Box() == nil { return } instance.Box().Network().UpdateWIFIState() } type platformHandler CommandServer func (h *platformHandler) ServiceStop() error { return (*CommandServer)(h).handler.ServiceStop() } func (h *platformHandler) ServiceReload() error { return (*CommandServer)(h).handler.ServiceReload() } func (h *platformHandler) SystemProxyStatus() (*daemon.SystemProxyStatus, error) { status, err := (*CommandServer)(h).handler.GetSystemProxyStatus() if err != nil { return nil, E.Cause(err, "get system proxy status") } return &daemon.SystemProxyStatus{ Enabled: status.Enabled, Available: status.Available, }, nil } 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) } ================================================ FILE: experimental/libbox/command_types.go ================================================ package libbox import ( "slices" "strings" "time" "github.com/sagernet/sing-box/daemon" M "github.com/sagernet/sing/common/metadata" ) type StatusMessage struct { Memory int64 Goroutines int32 ConnectionsIn int32 ConnectionsOut int32 TrafficAvailable bool Uplink int64 Downlink int64 UplinkTotal int64 DownlinkTotal int64 } type SystemProxyStatus struct { Available bool Enabled bool } type OutboundGroup struct { Tag string Type string Selectable bool Selected string IsExpand bool itemList []*OutboundGroupItem } func (g *OutboundGroup) GetItems() OutboundGroupItemIterator { return newIterator(g.itemList) } type OutboundGroupIterator interface { Next() *OutboundGroup HasNext() bool } type OutboundGroupItem struct { Tag string Type string URLTestTime int64 URLTestDelay int32 } type OutboundGroupItemIterator interface { Next() *OutboundGroupItem HasNext() bool } const ( ConnectionStateAll = iota ConnectionStateActive ConnectionStateClosed ) const ( ConnectionEventNew = iota ConnectionEventUpdate ConnectionEventClosed ) const ( closedConnectionMaxAge = int64((5 * time.Minute) / time.Millisecond) ) type ConnectionEvent struct { Type int32 ID string Connection *Connection UplinkDelta int64 DownlinkDelta int64 ClosedAt int64 } type ConnectionEvents struct { Reset bool events []*ConnectionEvent } func (c *ConnectionEvents) Iterator() ConnectionEventIterator { return newIterator(c.events) } type ConnectionEventIterator interface { Next() *ConnectionEvent HasNext() bool } type Connections struct { connectionMap map[string]*Connection input []Connection filtered []Connection filterState int32 filterApplied bool } func NewConnections() *Connections { return &Connections{ connectionMap: make(map[string]*Connection), } } func (c *Connections) ApplyEvents(events *ConnectionEvents) { if events == nil { return } if events.Reset { c.connectionMap = make(map[string]*Connection) } for _, event := range events.events { switch event.Type { case ConnectionEventNew: if event.Connection != nil { conn := *event.Connection c.connectionMap[event.ID] = &conn } case ConnectionEventUpdate: if conn, ok := c.connectionMap[event.ID]; ok { conn.Uplink = event.UplinkDelta conn.Downlink = event.DownlinkDelta conn.UplinkTotal += event.UplinkDelta conn.DownlinkTotal += event.DownlinkDelta } case ConnectionEventClosed: if event.Connection != nil { conn := *event.Connection conn.ClosedAt = event.ClosedAt conn.Uplink = 0 conn.Downlink = 0 c.connectionMap[event.ID] = &conn continue } if conn, ok := c.connectionMap[event.ID]; ok { conn.ClosedAt = event.ClosedAt conn.Uplink = 0 conn.Downlink = 0 } } } c.evictClosedConnections(time.Now().UnixMilli()) c.input = c.input[:0] for _, conn := range c.connectionMap { c.input = append(c.input, *conn) } if c.filterApplied { c.FilterState(c.filterState) } else { c.filtered = c.filtered[:0] c.filtered = append(c.filtered, c.input...) } } func (c *Connections) evictClosedConnections(nowMilliseconds int64) { for id, conn := range c.connectionMap { if conn.ClosedAt == 0 { continue } if nowMilliseconds-conn.ClosedAt > closedConnectionMaxAge { delete(c.connectionMap, id) } } } func (c *Connections) FilterState(state int32) { c.filterApplied = true c.filterState = state c.filtered = c.filtered[:0] switch state { case ConnectionStateAll: c.filtered = append(c.filtered, c.input...) case ConnectionStateActive: for _, connection := range c.input { if connection.ClosedAt == 0 { c.filtered = append(c.filtered, connection) } } case ConnectionStateClosed: for _, connection := range c.input { if connection.ClosedAt != 0 { c.filtered = append(c.filtered, connection) } } } } func (c *Connections) SortByDate() { slices.SortStableFunc(c.filtered, func(x, y Connection) int { if x.CreatedAt < y.CreatedAt { return 1 } else if x.CreatedAt > y.CreatedAt { return -1 } else { return strings.Compare(y.ID, x.ID) } }) } func (c *Connections) SortByTraffic() { slices.SortStableFunc(c.filtered, func(x, y Connection) int { xTraffic := x.Uplink + x.Downlink yTraffic := y.Uplink + y.Downlink if xTraffic < yTraffic { return 1 } else if xTraffic > yTraffic { return -1 } else { return strings.Compare(y.ID, x.ID) } }) } func (c *Connections) SortByTrafficTotal() { slices.SortStableFunc(c.filtered, func(x, y Connection) int { xTraffic := x.UplinkTotal + x.DownlinkTotal yTraffic := y.UplinkTotal + y.DownlinkTotal if xTraffic < yTraffic { return 1 } else if xTraffic > yTraffic { return -1 } else { return strings.Compare(y.ID, x.ID) } }) } func (c *Connections) Iterator() ConnectionIterator { return newPtrIterator(c.filtered) } type ProcessInfo struct { ProcessID int64 UserID int32 UserName string ProcessPath string packageNames []string } func (p *ProcessInfo) PackageNames() StringIterator { return newIterator(p.packageNames) } type Connection struct { ID string Inbound string InboundType string IPVersion int32 Network string Source string Destination string Domain string Protocol string User string FromOutbound string CreatedAt int64 ClosedAt int64 Uplink int64 Downlink int64 UplinkTotal int64 DownlinkTotal int64 Rule string Outbound string OutboundType string chainList []string ProcessInfo *ProcessInfo } func (c *Connection) Chain() StringIterator { return newIterator(c.chainList) } func (c *Connection) DisplayDestination() string { destination := M.ParseSocksaddr(c.Destination) if destination.IsIP() && c.Domain != "" { destination = M.Socksaddr{ Fqdn: c.Domain, Port: destination.Port, } return destination.String() } return c.Destination } type ConnectionIterator interface { Next() *Connection HasNext() bool } func statusMessageFromGRPC(status *daemon.Status) *StatusMessage { if status == nil { return nil } return &StatusMessage{ Memory: int64(status.Memory), Goroutines: status.Goroutines, ConnectionsIn: status.ConnectionsIn, ConnectionsOut: status.ConnectionsOut, TrafficAvailable: status.TrafficAvailable, Uplink: status.Uplink, Downlink: status.Downlink, UplinkTotal: status.UplinkTotal, DownlinkTotal: status.DownlinkTotal, } } func outboundGroupIteratorFromGRPC(groups *daemon.Groups) OutboundGroupIterator { if groups == nil || len(groups.Group) == 0 { return newIterator([]*OutboundGroup{}) } var libboxGroups []*OutboundGroup for _, g := range groups.Group { libboxGroup := &OutboundGroup{ Tag: g.Tag, Type: g.Type, Selectable: g.Selectable, Selected: g.Selected, IsExpand: g.IsExpand, } for _, item := range g.Items { libboxGroup.itemList = append(libboxGroup.itemList, &OutboundGroupItem{ Tag: item.Tag, Type: item.Type, URLTestTime: item.UrlTestTime, URLTestDelay: item.UrlTestDelay, }) } libboxGroups = append(libboxGroups, libboxGroup) } 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 { processInfo = &ProcessInfo{ ProcessID: int64(conn.ProcessInfo.ProcessId), UserID: conn.ProcessInfo.UserId, UserName: conn.ProcessInfo.UserName, ProcessPath: conn.ProcessInfo.ProcessPath, packageNames: conn.ProcessInfo.PackageNames, } } return Connection{ ID: conn.Id, Inbound: conn.Inbound, InboundType: conn.InboundType, IPVersion: conn.IpVersion, Network: conn.Network, Source: conn.Source, Destination: conn.Destination, Domain: conn.Domain, Protocol: conn.Protocol, User: conn.User, FromOutbound: conn.FromOutbound, CreatedAt: conn.CreatedAt, ClosedAt: conn.ClosedAt, Uplink: conn.Uplink, Downlink: conn.Downlink, UplinkTotal: conn.UplinkTotal, DownlinkTotal: conn.DownlinkTotal, Rule: conn.Rule, Outbound: conn.Outbound, OutboundType: conn.OutboundType, chainList: conn.ChainList, ProcessInfo: processInfo, } } func connectionEventFromGRPC(event *daemon.ConnectionEvent) *ConnectionEvent { if event == nil { return nil } libboxEvent := &ConnectionEvent{ Type: int32(event.Type), ID: event.Id, UplinkDelta: event.UplinkDelta, DownlinkDelta: event.DownlinkDelta, ClosedAt: event.ClosedAt, } if event.Connection != nil { conn := connectionFromGRPC(event.Connection) libboxEvent.Connection = &conn } return libboxEvent } func connectionEventsFromGRPC(events *daemon.ConnectionEvents) *ConnectionEvents { if events == nil { return nil } libboxEvents := &ConnectionEvents{ Reset: events.Reset_, } for _, event := range events.Events { if libboxEvent := connectionEventFromGRPC(event); libboxEvent != nil { libboxEvents.events = append(libboxEvents.events, libboxEvent) } } return libboxEvents } func systemProxyStatusFromGRPC(status *daemon.SystemProxyStatus) *SystemProxyStatus { if status == nil { return nil } return &SystemProxyStatus{ Available: status.Available, Enabled: status.Enabled, } } ================================================ FILE: experimental/libbox/command_types_nq.go ================================================ 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, } } ================================================ FILE: experimental/libbox/command_types_stun.go ================================================ 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, } } ================================================ FILE: experimental/libbox/command_types_tailscale.go ================================================ 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, } } ================================================ FILE: experimental/libbox/command_types_tailscale_ping.go ================================================ 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, } } ================================================ FILE: experimental/libbox/config.go ================================================ package libbox import ( "bytes" "context" "net/netip" "os" box "github.com/sagernet/sing-box" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/dns" "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" "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/filemanager" ) var sOOMReporter oomkiller.OOMReporter func baseContext(platformInterface PlatformInterface) context.Context { dnsRegistry := include.DNSTransportRegistry() if platformInterface != nil { if localTransport := platformInterface.LocalDNSTransport(); localTransport != nil { dns.RegisterTransport[option.LocalDNSServerOptions](dnsRegistry, C.DNSTypeLocal, func(ctx context.Context, logger log.ContextLogger, tag string, options option.LocalDNSServerOptions) (adapter.DNSTransport, error) { return newPlatformTransport(localTransport, tag, options), nil }) } } 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()) } func parseConfig(ctx context.Context, configContent string) (option.Options, error) { options, err := json.UnmarshalExtendedContext[option.Options](ctx, []byte(configContent)) if err != nil { return option.Options{}, E.Cause(err, "decode config") } return options, nil } func CheckConfig(configContent string) error { ctx := baseContext(nil) options, err := parseConfig(ctx, configContent) if err != nil { return err } ctx, cancel := context.WithCancel(ctx) defer cancel() ctx = service.ContextWith[adapter.PlatformInterface](ctx, (*platformInterfaceStub)(nil)) instance, err := box.New(box.Options{ Context: ctx, Options: options, }) if err == nil { instance.Close() } return err } type platformInterfaceStub struct{} func (s *platformInterfaceStub) Initialize(networkManager adapter.NetworkManager) error { return nil } func (s *platformInterfaceStub) UsePlatformAutoDetectInterfaceControl() bool { return true } func (s *platformInterfaceStub) AutoDetectInterfaceControl(fd int) error { return nil } func (s *platformInterfaceStub) UsePlatformInterface() bool { return false } func (s *platformInterfaceStub) OpenInterface(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error) { return nil, os.ErrInvalid } func (s *platformInterfaceStub) UsePlatformDefaultInterfaceMonitor() bool { return true } func (s *platformInterfaceStub) CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor { return (*interfaceMonitorStub)(nil) } func (s *platformInterfaceStub) UsePlatformNetworkInterfaces() bool { return false } func (s *platformInterfaceStub) NetworkInterfaces() ([]adapter.NetworkInterface, error) { return nil, os.ErrInvalid } func (s *platformInterfaceStub) UnderNetworkExtension() bool { return false } func (s *platformInterfaceStub) NetworkExtensionIncludeAllNetworks() bool { return false } func (s *platformInterfaceStub) ClearDNSCache() { } func (s *platformInterfaceStub) RequestPermissionForWIFIState() error { return nil } func (s *platformInterfaceStub) UsePlatformWIFIMonitor() bool { return false } func (s *platformInterfaceStub) ReadWIFIState() adapter.WIFIState { return adapter.WIFIState{} } func (s *platformInterfaceStub) SystemCertificates() []string { return nil } func (s *platformInterfaceStub) UsePlatformConnectionOwnerFinder() bool { return false } func (s *platformInterfaceStub) FindConnectionOwner(request *adapter.FindConnectionOwnerRequest) (*adapter.ConnectionOwner, error) { return nil, os.ErrInvalid } func (s *platformInterfaceStub) UsePlatformNotification() bool { return false } func (s *platformInterfaceStub) SendNotification(notification *adapter.Notification) error { return nil } 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 } func (s *platformInterfaceStub) LocalDNSTransport() dns.TransportConstructorFunc[option.LocalDNSServerOptions] { return nil } type interfaceMonitorStub struct{} func (s *interfaceMonitorStub) Start() error { return os.ErrInvalid } func (s *interfaceMonitorStub) Close() error { return os.ErrInvalid } func (s *interfaceMonitorStub) DefaultInterface() *control.Interface { return nil } func (s *interfaceMonitorStub) OverrideAndroidVPN() bool { return false } func (s *interfaceMonitorStub) AndroidVPNEnabled() bool { return false } func (s *interfaceMonitorStub) RegisterCallback(callback tun.DefaultInterfaceUpdateCallback) *list.Element[tun.DefaultInterfaceUpdateCallback] { return nil } func (s *interfaceMonitorStub) UnregisterCallback(element *list.Element[tun.DefaultInterfaceUpdateCallback]) { } func (s *interfaceMonitorStub) RegisterMyInterface(interfaceName string) { } func (s *interfaceMonitorStub) MyInterface() string { return "" } func FormatConfig(configContent string) (*StringBox, error) { options, err := parseConfig(baseContext(nil), configContent) if err != nil { return nil, err } var buffer bytes.Buffer encoder := json.NewEncoder(&buffer) encoder.SetIndent("", " ") err = encoder.Encode(options) if err != nil { return nil, err } return wrapString(buffer.String()), nil } ================================================ FILE: experimental/libbox/connection_owner_darwin.go ================================================ package libbox import ( "net/netip" "os/user" "syscall" "github.com/sagernet/sing-box/common/process" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" ) func FindConnectionOwner(ipProtocol int32, sourceAddress string, sourcePort int32, destinationAddress string, destinationPort int32) (*ConnectionOwner, error) { source, err := parseConnectionOwnerAddrPort(sourceAddress, sourcePort) if err != nil { return nil, E.Cause(err, "parse source") } destination, err := parseConnectionOwnerAddrPort(destinationAddress, destinationPort) if err != nil { return nil, E.Cause(err, "parse destination") } var network string switch ipProtocol { case syscall.IPPROTO_TCP: network = "tcp" case syscall.IPPROTO_UDP: network = "udp" default: return nil, E.New("unknown protocol: ", ipProtocol) } owner, err := process.FindDarwinConnectionOwner(network, source, destination) if err != nil { return nil, err } result := &ConnectionOwner{ UserId: owner.UserId, ProcessPath: owner.ProcessPath, } if owner.UserId != -1 && owner.UserName == "" { osUser, _ := user.LookupId(F.ToString(owner.UserId)) if osUser != nil { result.UserName = osUser.Username } } return result, nil } func parseConnectionOwnerAddrPort(address string, port int32) (netip.AddrPort, error) { if port < 0 || port > 65535 { return netip.AddrPort{}, E.New("invalid port: ", port) } addr, err := netip.ParseAddr(address) if err != nil { return netip.AddrPort{}, err } return netip.AddrPortFrom(addr.Unmap(), uint16(port)), nil } ================================================ FILE: experimental/libbox/debug.go ================================================ package libbox import ( "time" "unsafe" ) func TriggerGoPanic() { time.AfterFunc(200*time.Millisecond, func() { *(*int)(unsafe.Pointer(uintptr(0))) = 0 }) } ================================================ FILE: experimental/libbox/deprecated.go ================================================ package libbox import ( "github.com/sagernet/sing-box/experimental/deprecated" ) var _ = deprecated.Note(DeprecatedNote{}) type DeprecatedNote struct { Name string Description string DeprecatedVersion string ScheduledVersion string EnvName string MigrationLink string } func (n DeprecatedNote) Impending() bool { return deprecated.Note(n).Impending() } func (n DeprecatedNote) Message() string { return deprecated.Note(n).Message() } func (n DeprecatedNote) MessageWithLink() string { return deprecated.Note(n).MessageWithLink() } type DeprecatedNoteIterator interface { HasNext() bool Next() *DeprecatedNote } ================================================ FILE: experimental/libbox/dns.go ================================================ package libbox import ( "context" "net/netip" "strings" "syscall" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/dns" "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/task" mDNS "github.com/miekg/dns" ) type LocalDNSTransport interface { Raw() bool Lookup(ctx *ExchangeContext, network string, domain string) error Exchange(ctx *ExchangeContext, message []byte) error } var _ adapter.DNSTransport = (*platformTransport)(nil) type platformTransport struct { dns.TransportAdapter iif LocalDNSTransport } func newPlatformTransport(iif LocalDNSTransport, tag string, options option.LocalDNSServerOptions) *platformTransport { return &platformTransport{ TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options), iif: iif, } } func (p *platformTransport) Start(stage adapter.StartStage) error { return nil } func (p *platformTransport) Close() error { return nil } func (p *platformTransport) Reset() { } func (p *platformTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { response := &ExchangeContext{ context: ctx, } if p.iif.Raw() { messageBytes, err := message.Pack() if err != nil { return nil, err } var responseMessage *mDNS.Msg var group task.Group group.Append0(func(ctx context.Context) error { err = p.iif.Exchange(response, messageBytes) if err != nil { return err } if response.error != nil { return response.error } responseMessage = &response.message return nil }) err = group.Run(ctx) if err != nil { return nil, err } return responseMessage, nil } else { question := message.Question[0] var network string switch question.Qtype { case mDNS.TypeA: network = "ip4" case mDNS.TypeAAAA: network = "ip6" default: return nil, E.New("only IP queries are supported by current version of Android") } var responseAddrs []netip.Addr var group task.Group group.Append0(func(ctx context.Context) error { err := p.iif.Lookup(response, network, question.Name) if err != nil { return err } if response.error != nil { return response.error } responseAddrs = response.addresses return nil }) err := group.Run(ctx) if err != nil { return nil, err } return dns.FixedResponse(message.Id, question, responseAddrs, C.DefaultDNSTTL), nil } } type Func interface { Invoke() error } type ExchangeContext struct { context context.Context message mDNS.Msg addresses []netip.Addr error error } func (c *ExchangeContext) OnCancel(callback Func) { go func() { <-c.context.Done() callback.Invoke() }() } func (c *ExchangeContext) Success(result string) { c.addresses = common.Map(common.Filter(strings.Split(result, "\n"), func(it string) bool { return !common.IsEmpty(it) }), func(it string) netip.Addr { return M.ParseSocksaddrHostPort(it, 0).Unwrap().Addr }) } func (c *ExchangeContext) RawSuccess(result []byte) { err := c.message.Unpack(result) if err != nil { c.error = E.Cause(err, "parse response") } } func (c *ExchangeContext) ErrorCode(code int32) { c.error = dns.RcodeError(code) } func (c *ExchangeContext) ErrnoCode(code int32) { c.error = syscall.Errno(code) } ================================================ FILE: experimental/libbox/fdroid.go ================================================ package libbox import ( "archive/zip" "bytes" "crypto/tls" "encoding/json" "io" "net" "net/http" "net/url" "os" "path/filepath" "sort" "strconv" "strings" "sync" "time" E "github.com/sagernet/sing/common/exceptions" ) const fdroidUserAgent = "F-Droid 1.21.1" type FDroidUpdateInfo struct { VersionCode int32 VersionName string DownloadURL string FileSize int64 FileSHA256 string } type FDroidPingResult struct { URL string LatencyMs int32 Error string } type FDroidPingResultIterator interface { Len() int32 HasNext() bool Next() *FDroidPingResult } type fdroidAPIResponse struct { PackageName string `json:"packageName"` SuggestedVersionCode int32 `json:"suggestedVersionCode"` Packages []fdroidAPIPackage `json:"packages"` } type fdroidAPIPackage struct { VersionName string `json:"versionName"` VersionCode int32 `json:"versionCode"` } type fdroidEntry struct { Timestamp int64 `json:"timestamp"` Version int `json:"version"` Index fdroidEntryFile `json:"index"` Diffs map[string]fdroidEntryFile `json:"diffs"` } type fdroidEntryFile struct { Name string `json:"name"` SHA256 string `json:"sha256"` Size int64 `json:"size"` NumPackages int `json:"numPackages"` } type fdroidIndexV2 struct { Packages map[string]fdroidV2Package `json:"packages"` } type fdroidV2Package struct { Versions map[string]fdroidV2Version `json:"versions"` } type fdroidV2Version struct { Manifest fdroidV2Manifest `json:"manifest"` File fdroidV2File `json:"file"` } type fdroidV2Manifest struct { VersionCode int32 `json:"versionCode"` VersionName string `json:"versionName"` } type fdroidV2File struct { Name string `json:"name"` SHA256 string `json:"sha256"` Size int64 `json:"size"` } type fdroidIndexV1 struct { Packages map[string][]fdroidV1Package `json:"packages"` } type fdroidV1Package struct { VersionCode int32 `json:"versionCode"` VersionName string `json:"versionName"` ApkName string `json:"apkName"` Size int64 `json:"size"` Hash string `json:"hash"` HashType string `json:"hashType"` } type fdroidCache struct { MirrorURL string `json:"mirrorURL"` Timestamp int64 `json:"timestamp"` ETag string `json:"etag"` IsV1 bool `json:"isV1,omitempty"` } func CheckFDroidUpdate(mirrorURL, packageName string, currentVersionCode int32, cachePath string) (*FDroidUpdateInfo, error) { mirrorURL = strings.TrimRight(mirrorURL, "/") if strings.Contains(mirrorURL, "f-droid.org") { return checkFDroidAPI(mirrorURL, packageName, currentVersionCode) } client := newFDroidHTTPClient() defer client.CloseIdleConnections() cache := loadFDroidCache(cachePath, mirrorURL) if cache != nil && cache.IsV1 { return checkFDroidV1(client, mirrorURL, packageName, currentVersionCode, cachePath, cache) } return checkFDroidV2(client, mirrorURL, packageName, currentVersionCode, cachePath, cache) } func PingFDroidMirrors(mirrorURLs string) (FDroidPingResultIterator, error) { urls := strings.Split(mirrorURLs, ",") results := make([]*FDroidPingResult, len(urls)) var waitGroup sync.WaitGroup for i, rawURL := range urls { waitGroup.Add(1) go func(index int, target string) { defer waitGroup.Done() target = strings.TrimSpace(target) result := &FDroidPingResult{URL: target} latency, err := pingTLS(target) if err != nil { result.LatencyMs = -1 result.Error = err.Error() } else { result.LatencyMs = int32(latency.Milliseconds()) } results[index] = result }(i, rawURL) } waitGroup.Wait() sort.Slice(results, func(i, j int) bool { if results[i].LatencyMs < 0 { return false } if results[j].LatencyMs < 0 { return true } return results[i].LatencyMs < results[j].LatencyMs }) return newIterator(results), nil } func PingFDroidMirror(mirrorURL string) *FDroidPingResult { mirrorURL = strings.TrimSpace(mirrorURL) result := &FDroidPingResult{URL: mirrorURL} latency, err := pingTLS(mirrorURL) if err != nil { result.LatencyMs = -1 result.Error = err.Error() } else { result.LatencyMs = int32(latency.Milliseconds()) } return result } func newFDroidHTTPClient() *http.Client { return &http.Client{ Timeout: 30 * time.Second, } } func newFDroidRequest(requestURL string) (*http.Request, error) { request, err := http.NewRequest("GET", requestURL, nil) if err != nil { return nil, err } request.Header.Set("User-Agent", fdroidUserAgent) return request, nil } func checkFDroidAPI(mirrorURL, packageName string, currentVersionCode int32) (*FDroidUpdateInfo, error) { client := newFDroidHTTPClient() defer client.CloseIdleConnections() apiURL := "https://f-droid.org/api/v1/packages/" + packageName request, err := newFDroidRequest(apiURL) if err != nil { return nil, err } response, err := client.Do(request) if err != nil { return nil, err } defer response.Body.Close() if response.StatusCode != http.StatusOK { return nil, E.New("HTTP ", response.Status) } body, err := io.ReadAll(response.Body) if err != nil { return nil, err } var apiResponse fdroidAPIResponse err = json.Unmarshal(body, &apiResponse) if err != nil { return nil, err } var bestCode int32 var bestName string for _, pkg := range apiResponse.Packages { if pkg.VersionCode > currentVersionCode && pkg.VersionCode > bestCode { bestCode = pkg.VersionCode bestName = pkg.VersionName } } if bestCode == 0 { return nil, nil } return &FDroidUpdateInfo{ VersionCode: bestCode, VersionName: bestName, DownloadURL: "https://f-droid.org/repo/" + packageName + "_" + strconv.FormatInt(int64(bestCode), 10) + ".apk", }, nil } func checkFDroidV2(client *http.Client, mirrorURL, packageName string, currentVersionCode int32, cachePath string, cache *fdroidCache) (*FDroidUpdateInfo, error) { entryURL := mirrorURL + "/entry.jar" request, err := newFDroidRequest(entryURL) if err != nil { return nil, err } if cache != nil && cache.ETag != "" { request.Header.Set("If-None-Match", cache.ETag) } response, err := client.Do(request) if err != nil { return nil, err } defer response.Body.Close() if response.StatusCode == http.StatusNotModified { return nil, nil } if response.StatusCode == http.StatusNotFound { writeFDroidCache(cachePath, mirrorURL, 0, "", true) return checkFDroidV1(client, mirrorURL, packageName, currentVersionCode, cachePath, nil) } if response.StatusCode != http.StatusOK { return nil, E.New("HTTP ", response.Status, ": ", entryURL) } jarData, err := io.ReadAll(response.Body) if err != nil { return nil, err } etag := response.Header.Get("ETag") var entry fdroidEntry err = readJSONFromJar(jarData, "entry.json", &entry) if err != nil { return nil, E.Cause(err, "read entry.jar") } if entry.Timestamp == 0 { return nil, E.New("entry.json not found in entry.jar") } if cache != nil && cache.Timestamp == entry.Timestamp { writeFDroidCache(cachePath, mirrorURL, entry.Timestamp, etag, false) return nil, nil } var indexURL string if cache != nil { cachedTimestamp := strconv.FormatInt(cache.Timestamp, 10) if diff, ok := entry.Diffs[cachedTimestamp]; ok { indexURL = mirrorURL + "/" + diff.Name } } if indexURL == "" { indexURL = mirrorURL + "/" + entry.Index.Name } indexRequest, err := newFDroidRequest(indexURL) if err != nil { return nil, err } indexResponse, err := client.Do(indexRequest) if err != nil { return nil, err } defer indexResponse.Body.Close() if indexResponse.StatusCode != http.StatusOK { return nil, E.New("HTTP ", indexResponse.Status, ": ", indexURL) } indexData, err := io.ReadAll(indexResponse.Body) if err != nil { return nil, err } var index fdroidIndexV2 err = json.Unmarshal(indexData, &index) if err != nil { return nil, err } writeFDroidCache(cachePath, mirrorURL, entry.Timestamp, etag, false) pkg, ok := index.Packages[packageName] if !ok { return nil, nil } var bestCode int32 var bestVersion fdroidV2Version for _, version := range pkg.Versions { if version.Manifest.VersionCode > currentVersionCode && version.Manifest.VersionCode > bestCode { bestCode = version.Manifest.VersionCode bestVersion = version } } if bestCode == 0 { return nil, nil } return &FDroidUpdateInfo{ VersionCode: bestCode, VersionName: bestVersion.Manifest.VersionName, DownloadURL: mirrorURL + "/" + bestVersion.File.Name, FileSize: bestVersion.File.Size, FileSHA256: bestVersion.File.SHA256, }, nil } func checkFDroidV1(client *http.Client, mirrorURL, packageName string, currentVersionCode int32, cachePath string, cache *fdroidCache) (*FDroidUpdateInfo, error) { indexURL := mirrorURL + "/index-v1.jar" request, err := newFDroidRequest(indexURL) if err != nil { return nil, err } if cache != nil && cache.ETag != "" { request.Header.Set("If-None-Match", cache.ETag) } response, err := client.Do(request) if err != nil { return nil, err } defer response.Body.Close() if response.StatusCode == http.StatusNotModified { return nil, nil } if response.StatusCode != http.StatusOK { return nil, E.New("HTTP ", response.Status, ": ", indexURL) } jarData, err := io.ReadAll(response.Body) if err != nil { return nil, err } etag := response.Header.Get("ETag") var index fdroidIndexV1 err = readJSONFromJar(jarData, "index-v1.json", &index) if err != nil { return nil, E.Cause(err, "read index-v1.jar") } writeFDroidCache(cachePath, mirrorURL, 0, etag, true) packages, ok := index.Packages[packageName] if !ok { return nil, nil } var bestCode int32 var bestPackage fdroidV1Package for _, pkg := range packages { if pkg.VersionCode > currentVersionCode && pkg.VersionCode > bestCode { bestCode = pkg.VersionCode bestPackage = pkg } } if bestCode == 0 { return nil, nil } return &FDroidUpdateInfo{ VersionCode: bestCode, VersionName: bestPackage.VersionName, DownloadURL: mirrorURL + "/" + bestPackage.ApkName, FileSize: bestPackage.Size, FileSHA256: bestPackage.Hash, }, nil } func readJSONFromJar(jarData []byte, fileName string, destination any) error { zipReader, err := zip.NewReader(bytes.NewReader(jarData), int64(len(jarData))) if err != nil { return err } for _, file := range zipReader.File { if file.Name != fileName { continue } reader, err := file.Open() if err != nil { return err } data, err := io.ReadAll(reader) reader.Close() if err != nil { return err } return json.Unmarshal(data, destination) } return nil } func pingTLS(mirrorURL string) (time.Duration, error) { parsed, err := url.Parse(mirrorURL) if err != nil { return 0, err } host := parsed.Host if !strings.Contains(host, ":") { host = host + ":443" } dialer := &net.Dialer{Timeout: 5 * time.Second} start := time.Now() conn, err := tls.DialWithDialer(dialer, "tcp", host, &tls.Config{}) if err != nil { return 0, err } latency := time.Since(start) conn.Close() return latency, nil } func loadFDroidCache(cachePath, mirrorURL string) *fdroidCache { cacheFile := filepath.Join(cachePath, "fdroid_cache.json") data, err := os.ReadFile(cacheFile) if err != nil { return nil } var cache fdroidCache err = json.Unmarshal(data, &cache) if err != nil { return nil } if cache.MirrorURL != mirrorURL { return nil } return &cache } func writeFDroidCache(cachePath, mirrorURL string, timestamp int64, etag string, isV1 bool) { cache := fdroidCache{ MirrorURL: mirrorURL, Timestamp: timestamp, ETag: etag, IsV1: isV1, } data, err := json.Marshal(cache) if err != nil { return } os.MkdirAll(cachePath, 0o755) os.WriteFile(filepath.Join(cachePath, "fdroid_cache.json"), data, 0o644) } ================================================ FILE: experimental/libbox/fdroid_mirrors.go ================================================ package libbox type FDroidMirror struct { URL string Country string Name string } type FDroidMirrorIterator interface { Len() int32 HasNext() bool Next() *FDroidMirror } var builtinFDroidMirrors = []FDroidMirror{ // Official {URL: "https://f-droid.org/repo", Country: "Official", Name: "f-droid.org"}, {URL: "https://cloudflare.f-droid.org/repo", Country: "Official", Name: "Cloudflare CDN"}, // China {URL: "https://mirrors.tuna.tsinghua.edu.cn/fdroid/repo", Country: "China", Name: "Tsinghua TUNA"}, {URL: "https://mirrors.nju.edu.cn/fdroid/repo", Country: "China", Name: "Nanjing University"}, {URL: "https://mirror.iscas.ac.cn/fdroid/repo", Country: "China", Name: "ISCAS"}, {URL: "https://mirror.nyist.edu.cn/fdroid/repo", Country: "China", Name: "NYIST"}, {URL: "https://mirrors.cqupt.edu.cn/fdroid/repo", Country: "China", Name: "CQUPT"}, {URL: "https://mirrors.shanghaitech.edu.cn/fdroid/repo", Country: "China", Name: "ShanghaiTech"}, // India {URL: "https://mirror.hyd.albony.in/fdroid/repo", Country: "India", Name: "Albony Hyderabad"}, {URL: "https://mirror.del2.albony.in/fdroid/repo", Country: "India", Name: "Albony Delhi"}, // Taiwan {URL: "https://mirror.ossplanet.net/fdroid/repo", Country: "Taiwan", Name: "OSSPlanet"}, // France {URL: "https://fdroid.tetaneutral.net/fdroid/repo", Country: "France", Name: "tetaneutral.net"}, {URL: "https://mirror.freedif.org/fdroid/repo", Country: "France", Name: "FreeDif"}, // Germany {URL: "https://ftp.fau.de/fdroid/repo", Country: "Germany", Name: "FAU Erlangen"}, {URL: "https://ftp.agdsn.de/fdroid/repo", Country: "Germany", Name: "AGDSN Dresden"}, {URL: "https://ftp.gwdg.de/pub/android/fdroid/repo", Country: "Germany", Name: "GWDG"}, {URL: "https://mirror.level66.network/fdroid/repo", Country: "Germany", Name: "Level66"}, {URL: "https://mirror.mci-1.serverforge.org/fdroid/repo", Country: "Germany", Name: "ServerForge"}, // Netherlands {URL: "https://ftp.snt.utwente.nl/pub/software/fdroid/repo", Country: "Netherlands", Name: "University of Twente"}, // Sweden {URL: "https://ftp.lysator.liu.se/pub/fdroid/repo", Country: "Sweden", Name: "Lysator"}, // Denmark {URL: "https://mirrors.dotsrc.org/fdroid/repo", Country: "Denmark", Name: "dotsrc.org"}, // Austria {URL: "https://mirror.kumi.systems/fdroid/repo", Country: "Austria", Name: "Kumi Systems"}, // Switzerland {URL: "https://mirror.init7.net/fdroid/repo", Country: "Switzerland", Name: "Init7"}, // Romania {URL: "https://mirrors.hostico.ro/fdroid/repo", Country: "Romania", Name: "Hostico"}, {URL: "https://mirrors.chroot.ro/fdroid/repo", Country: "Romania", Name: "Chroot"}, {URL: "https://ftp.lug.ro/fdroid/repo", Country: "Romania", Name: "LUG Romania"}, // US {URL: "https://plug-mirror.rcac.purdue.edu/fdroid/repo", Country: "US", Name: "Purdue"}, {URL: "https://mirror.fcix.net/fdroid/repo", Country: "US", Name: "FCIX"}, {URL: "https://opencolo.mm.fcix.net/fdroid/repo", Country: "US", Name: "OpenColo"}, {URL: "https://forksystems.mm.fcix.net/fdroid/repo", Country: "US", Name: "Fork Systems"}, {URL: "https://southfront.mm.fcix.net/fdroid/repo", Country: "US", Name: "South Front"}, {URL: "https://ziply.mm.fcix.net/fdroid/repo", Country: "US", Name: "Ziply"}, // Canada {URL: "https://mirror.quantum5.ca/fdroid/repo", Country: "Canada", Name: "Quantum5"}, // Australia {URL: "https://mirror.aarnet.edu.au/fdroid/repo", Country: "Australia", Name: "AARNet"}, // Other {URL: "https://mirror.cyberbits.eu/fdroid/repo", Country: "Europe", Name: "Cyberbits EU"}, {URL: "https://mirror.eu.ossplanet.net/fdroid/repo", Country: "Europe", Name: "OSSPlanet EU"}, {URL: "https://mirror.cyberbits.asia/fdroid/repo", Country: "Asia", Name: "Cyberbits Asia"}, {URL: "https://mirrors.jevincanders.net/fdroid/repo", Country: "US", Name: "Jevincanders"}, {URL: "https://mirrors.komogoto.com/fdroid/repo", Country: "US", Name: "Komogoto"}, {URL: "https://fdroid.rasp.sh/fdroid/repo", Country: "Europe", Name: "rasp.sh"}, {URL: "https://mirror.gofoss.xyz/fdroid/repo", Country: "Europe", Name: "GoFOSS"}, } func GetFDroidMirrors() FDroidMirrorIterator { return newPtrIterator(builtinFDroidMirrors) } ================================================ FILE: experimental/libbox/ffi.json ================================================ { "version": 1, "variables": { "VERSION": "$(go run github.com/sagernet/sing-box/cmd/internal/read_tag@latest)", "WORKSPACE_ROOT": "../../..", "DEPLOY_ANDROID": "${WORKSPACE_ROOT}/sing-box-for-android/app/libs", "DEPLOY_APPLE": "${WORKSPACE_ROOT}/sing-box-for-apple", "DEPLOY_WINDOWS": "${WORKSPACE_ROOT}/sing-box-for-windows/local-packages" }, "packages": [ { "id": "libbox", "path": ".", "java_package": "io.nekohasekai.libbox", "csharp_namespace": "SagerNet", "csharp_entrypoint": "Libbox", "apple_prefix": "Libbox" } ], "builds": [ { "id": "android-main", "packages": ["libbox"], "default": { "tags": [ "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_naive_outbound", "with_clash_api", "badlinkname", "tfogo_checklinkname0", "with_tailscale", "ts_omit_logtail", "ts_omit_ssh", "ts_omit_drive", "ts_omit_taildrop", "ts_omit_webclient", "ts_omit_doctor", "ts_omit_capture", "ts_omit_kube", "ts_omit_aws", "ts_omit_synology", "ts_omit_bird" ], "ldflags": "-X github.com/sagernet/sing-box/constant.Version=${VERSION} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0", "trimpath": true } }, { "id": "android-legacy", "packages": ["libbox"], "default": { "tags": [ "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "badlinkname", "tfogo_checklinkname0", "with_tailscale", "ts_omit_logtail", "ts_omit_ssh", "ts_omit_drive", "ts_omit_taildrop", "ts_omit_webclient", "ts_omit_doctor", "ts_omit_capture", "ts_omit_kube", "ts_omit_aws", "ts_omit_synology", "ts_omit_bird" ], "ldflags": "-X github.com/sagernet/sing-box/constant.Version=${VERSION} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0", "trimpath": true } }, { "id": "apple", "packages": ["libbox"], "default": { "tags": [ "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_naive_outbound", "with_clash_api", "badlinkname", "tfogo_checklinkname0", "with_dhcp", "grpcnotrace", "with_tailscale", "ts_omit_logtail", "ts_omit_ssh", "ts_omit_drive", "ts_omit_taildrop", "ts_omit_webclient", "ts_omit_doctor", "ts_omit_capture", "ts_omit_kube", "ts_omit_aws", "ts_omit_synology", "ts_omit_bird" ], "ldflags": "-X github.com/sagernet/sing-box/constant.Version=${VERSION} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0", "trimpath": true }, "overrides": [ { "match": { "os": "ios" }, "tags_append": ["with_low_memory"] }, { "match": { "os": "tvos" }, "tags_append": ["with_low_memory"] } ] }, { "id": "windows", "packages": ["libbox"], "default": { "tags": [ "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_naive_outbound", "with_purego", "with_clash_api", "badlinkname", "tfogo_checklinkname0", "with_tailscale", "ts_omit_logtail", "ts_omit_ssh", "ts_omit_drive", "ts_omit_taildrop", "ts_omit_webclient", "ts_omit_doctor", "ts_omit_capture", "ts_omit_kube", "ts_omit_aws", "ts_omit_synology", "ts_omit_bird" ], "ldflags": "-X github.com/sagernet/sing-box/constant.Version=${VERSION} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0", "trimpath": true } } ], "platforms": [ { "type": "android", "build": "android-main", "min_sdk": 23, "ndk_version": "28.0.13004108", "lib_name": "box", "languages": [{ "type": "java" }], "artifacts": [ { "type": "aar", "output_path": "libbox.aar", "execute_after": [ "if [ -d \"${DEPLOY_ANDROID}\" ]; then", " rm -f \"${DEPLOY_ANDROID}/$$(basename \"${OUTPUT_PATH}\")\"", " mv \"${OUTPUT_PATH}\" \"${DEPLOY_ANDROID}/\"", "fi" ] } ] }, { "type": "android", "build": "android-legacy", "min_sdk": 21, "ndk_version": "28.0.13004108", "lib_name": "box", "languages": [{ "type": "java" }], "artifacts": [ { "type": "aar", "output_path": "libbox-legacy.aar", "execute_after": [ "if [ -d \"${DEPLOY_ANDROID}\" ]; then", " rm -f \"${DEPLOY_ANDROID}/$$(basename \"${OUTPUT_PATH}\")\"", " mv \"${OUTPUT_PATH}\" \"${DEPLOY_ANDROID}/\"", "fi" ] } ] }, { "type": "apple", "build": "apple", "targets": [ "ios/arm64", "ios/simulator/arm64", "ios/simulator/amd64", "tvos/arm64", "tvos/simulator/arm64", "tvos/simulator/amd64", "macos/arm64", "macos/amd64" ], "languages": [{ "type": "objc" }], "artifacts": [ { "type": "xcframework", "module_name": "Libbox", "execute_after": [ "if [ -d \"${DEPLOY_APPLE}\" ]; then", " rm -rf \"${DEPLOY_APPLE}/${MODULE_NAME}.xcframework\"", " mv \"${OUTPUT_PATH}\" \"${DEPLOY_APPLE}/\"", "fi" ] } ] }, { "type": "csharp", "build": "windows", "targets": [ "windows/amd64" ], "languages": [{ "type": "csharp" }], "artifacts": [ { "type": "nuget", "package_id": "SagerNet.Libbox", "package_version": "0.0.0-local", "execute_after": { "windows": [ "$$deployPath = '${DEPLOY_WINDOWS}'", "if (Test-Path $$deployPath) {", " Remove-Item \"$$deployPath\\${PACKAGE_ID}.*.nupkg\" -ErrorAction SilentlyContinue", " Move-Item -Force '${OUTPUT_PATH}' \"$$deployPath\\\"", " $$cachePath = if ($$env:NUGET_PACKAGES) { $$env:NUGET_PACKAGES } else { \"$$env:USERPROFILE\\.nuget\\packages\" }", " Remove-Item -Recurse -Force \"$$cachePath\\sagernet.libbox\\${PACKAGE_VERSION}\" -ErrorAction SilentlyContinue", "}" ], "default": [ "if [ -d \"${DEPLOY_WINDOWS}\" ]; then", " rm -f \"${DEPLOY_WINDOWS}/${PACKAGE_ID}.*.nupkg\"", " mv \"${OUTPUT_PATH}\" \"${DEPLOY_WINDOWS}/\"", " cache_path=\"$${NUGET_PACKAGES:-$${HOME}/.nuget/packages}\"", " rm -rf \"$${cache_path}/sagernet.libbox/${PACKAGE_VERSION}\"", "fi" ] } } ] } ] } ================================================ FILE: experimental/libbox/http.go ================================================ package libbox import ( "bytes" "context" "crypto/sha256" "crypto/tls" "crypto/x509" "encoding/hex" "errors" "fmt" "io" "math/rand" "net" "net/http" "net/url" "os" "strconv" "sync" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/protocol/socks" "github.com/sagernet/sing/protocol/socks/socks5" ) type HTTPClient interface { RestrictedTLS() ModernTLS() PinnedTLS12() PinnedSHA256(sumHex string) TrySocks5(port int32) KeepAlive() NewRequest() HTTPRequest Close() } type HTTPRequest interface { SetURL(link string) error SetMethod(method string) SetHeader(key string, value string) SetContent(content []byte) SetContentString(content string) RandomUserAgent() SetUserAgent(userAgent string) Execute() (HTTPResponse, error) } type HTTPResponse interface { GetContent() (*StringBox, error) WriteTo(path string) error WriteToWithProgress(path string, handler HTTPResponseWriteToProgressHandler) error } type HTTPResponseWriteToProgressHandler interface { Update(progress int64, total int64) } var ( _ HTTPClient = (*httpClient)(nil) _ HTTPRequest = (*httpRequest)(nil) _ HTTPResponse = (*httpResponse)(nil) ) type httpClient struct { tls tls.Config client http.Client transport http.Transport } func NewHTTPClient() HTTPClient { client := new(httpClient) client.client.Transport = &client.transport client.transport.ForceAttemptHTTP2 = true client.transport.TLSHandshakeTimeout = C.TCPTimeout client.transport.TLSClientConfig = &client.tls client.transport.DisableKeepAlives = true return client } func (c *httpClient) ModernTLS() { c.setTLSVersion(tls.VersionTLS12, 0, func(suite *tls.CipherSuite) bool { return true }) } func (c *httpClient) RestrictedTLS() { c.setTLSVersion(tls.VersionTLS13, 0, func(suite *tls.CipherSuite) bool { return common.Contains(suite.SupportedVersions, uint16(tls.VersionTLS13)) }) } func (c *httpClient) setTLSVersion(minVersion, maxVersion uint16, filter func(*tls.CipherSuite) bool) { c.tls.MinVersion = minVersion if maxVersion != 0 { c.tls.MaxVersion = maxVersion } c.tls.CipherSuites = common.Map(common.Filter(tls.CipherSuites(), filter), func(it *tls.CipherSuite) uint16 { return it.ID }) } func (c *httpClient) PinnedTLS12() { c.setTLSVersion(tls.VersionTLS12, tls.VersionTLS12, func(suite *tls.CipherSuite) bool { return true }) } func (c *httpClient) PinnedSHA256(sumHex string) { c.tls.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { for _, rawCert := range rawCerts { certSum := sha256.Sum256(rawCert) if sumHex == hex.EncodeToString(certSum[:]) { return nil } } return E.New("pinned sha256 sum mismatch") } } func (c *httpClient) TrySocks5(port int32) { dialer := new(net.Dialer) c.transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { for { socksConn, err := dialer.DialContext(ctx, "tcp", "127.0.0.1:"+strconv.Itoa(int(port))) if err != nil { break } _, err = socks.ClientHandshake5(socksConn, socks5.CommandConnect, M.ParseSocksaddr(addr), "", "") if err != nil { break } //nolint:staticcheck return socksConn, err } return dialer.DialContext(ctx, network, addr) } } func (c *httpClient) KeepAlive() { c.transport.DisableKeepAlives = false } func (c *httpClient) NewRequest() HTTPRequest { req := &httpRequest{httpClient: c} req.request = http.Request{ Method: "GET", Header: http.Header{}, } return req } func (c *httpClient) Close() { c.transport.CloseIdleConnections() } type httpRequest struct { *httpClient request http.Request } func (r *httpRequest) SetURL(link string) (err error) { r.request.URL, err = url.Parse(link) if err != nil { return } if r.request.URL.User != nil { user := r.request.URL.User.Username() password, _ := r.request.URL.User.Password() r.request.SetBasicAuth(user, password) } return } func (r *httpRequest) SetMethod(method string) { r.request.Method = method } func (r *httpRequest) SetHeader(key string, value string) { r.request.Header.Set(key, value) } func (r *httpRequest) RandomUserAgent() { r.request.Header.Set("User-Agent", fmt.Sprintf("curl/7.%d.%d", rand.Int()%54, rand.Int()%2)) } func (r *httpRequest) SetUserAgent(userAgent string) { r.request.Header.Set("User-Agent", userAgent) } func (r *httpRequest) SetContent(content []byte) { r.request.Body = io.NopCloser(bytes.NewReader(content)) r.request.ContentLength = int64(len(content)) } func (r *httpRequest) SetContentString(content string) { r.SetContent([]byte(content)) } func (r *httpRequest) Execute() (HTTPResponse, error) { response, err := r.client.Do(&r.request) if err != nil { return nil, err } httpResp := &httpResponse{Response: response} if response.StatusCode != http.StatusOK { return nil, errors.New(httpResp.errorString()) } return httpResp, nil } type httpResponse struct { *http.Response getContentOnce sync.Once content []byte contentError error } func (h *httpResponse) errorString() string { content, err := h.GetContent() if err != nil { return fmt.Sprint("HTTP ", h.Status) } return fmt.Sprint("HTTP ", h.Status, ": ", content) } func (h *httpResponse) GetContent() (*StringBox, error) { h.getContentOnce.Do(func() { defer h.Body.Close() h.content, h.contentError = io.ReadAll(h.Body) }) if h.contentError != nil { return nil, h.contentError } return wrapString(string(h.content)), nil } func (h *httpResponse) WriteTo(path string) error { defer h.Body.Close() file, err := os.Create(path) if err != nil { return err } defer file.Close() return common.Error(bufio.Copy(file, h.Body)) } func (h *httpResponse) WriteToWithProgress(path string, handler HTTPResponseWriteToProgressHandler) error { defer h.Body.Close() file, err := os.Create(path) if err != nil { return err } defer file.Close() return common.Error(bufio.Copy(&progressWriter{ writer: file, handler: handler, total: h.ContentLength, }, h.Body)) } type progressWriter struct { writer io.Writer handler HTTPResponseWriteToProgressHandler total int64 written int64 } func (w *progressWriter) Write(p []byte) (int, error) { n, err := w.writer.Write(p) w.written += int64(n) w.handler.Update(w.written, w.total) return n, err } ================================================ FILE: experimental/libbox/internal/oomprofile/builder.go ================================================ //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) 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, }) } ================================================ FILE: experimental/libbox/internal/oomprofile/defs_darwin_amd64.go ================================================ //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 ) ================================================ FILE: experimental/libbox/internal/oomprofile/defs_darwin_arm64.go ================================================ //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 ) ================================================ FILE: experimental/libbox/internal/oomprofile/linkname.go ================================================ //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) ================================================ FILE: experimental/libbox/internal/oomprofile/mapping_darwin.go ================================================ //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() { added := machVMInfo(func(lo, hi, offset uint64, file, buildID string) { b.addMappingEntry(lo, hi, offset, file, buildID, false) }) if !added { 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 ================================================ FILE: experimental/libbox/internal/oomprofile/mapping_linux.go ================================================ //go:build linux package oomprofile import "os" func (b *profileBuilder) readMapping() { data, _ := os.ReadFile("/proc/self/maps") stdParseProcSelfMaps(data, func(lo, hi, offset uint64, file, buildID string) { b.addMappingEntry(lo, hi, offset, file, buildID, false) }) if len(b.mem) == 0 { b.addMappingEntry(0, 0, 0, "", "", true) } } ================================================ FILE: experimental/libbox/internal/oomprofile/mapping_windows.go ================================================ //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() } ================================================ FILE: experimental/libbox/internal/oomprofile/oomprofile.go ================================================ //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, _ := runtimeMemProfileInternal(nil, true) var ok bool 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, _ := fetch(nil, nil) var ok bool 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, _ := fetch(nil) var ok bool 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 range 2 { 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 } ================================================ FILE: experimental/libbox/internal/oomprofile/protobuf.go ================================================ //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) 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-- } ================================================ FILE: experimental/libbox/internal/procfs/procfs.go ================================================ package procfs import ( "bufio" "encoding/binary" "encoding/hex" "fmt" "net" "net/netip" "os" "strconv" "strings" "unsafe" N "github.com/sagernet/sing/common/network" ) var ( netIndexOfLocal = -1 netIndexOfUid = -1 nativeEndian binary.ByteOrder ) func init() { var x uint32 = 0x01020304 if *(*byte)(unsafe.Pointer(&x)) == 0x01 { nativeEndian = binary.BigEndian } else { nativeEndian = binary.LittleEndian } } func ResolveSocketByProcSearch(network string, source, _ netip.AddrPort) int32 { if netIndexOfLocal < 0 || netIndexOfUid < 0 { return -1 } path := "/proc/net/" if network == N.NetworkTCP { path += "tcp" } else { path += "udp" } if source.Addr().Is6() { path += "6" } sIP := source.Addr().AsSlice() if len(sIP) == 0 { return -1 } var bytes [2]byte binary.BigEndian.PutUint16(bytes[:], source.Port()) local := fmt.Sprintf("%s:%s", hex.EncodeToString(nativeEndianIP(sIP)), hex.EncodeToString(bytes[:])) file, err := os.Open(path) if err != nil { return -1 } defer file.Close() reader := bufio.NewReader(file) for { row, _, err := reader.ReadLine() if err != nil { return -1 } fields := strings.Fields(string(row)) if len(fields) <= netIndexOfLocal || len(fields) <= netIndexOfUid { continue } if strings.EqualFold(local, fields[netIndexOfLocal]) { uid, err := strconv.Atoi(fields[netIndexOfUid]) if err != nil { return -1 } return int32(uid) } } } func nativeEndianIP(ip net.IP) []byte { result := make([]byte, len(ip)) for i := 0; i < len(ip); i += 4 { value := binary.BigEndian.Uint32(ip[i:]) nativeEndian.PutUint32(result[i:], value) } return result } func init() { file, err := os.Open("/proc/net/tcp") if err != nil { return } defer file.Close() reader := bufio.NewReader(file) header, _, err := reader.ReadLine() if err != nil { return } columns := strings.Fields(string(header)) var txQueue, rxQueue, tr, tmWhen bool for idx, col := range columns { offset := 0 if txQueue && rxQueue { offset-- } if tr && tmWhen { offset-- } switch col { case "tx_queue": txQueue = true case "rx_queue": rxQueue = true case "tr": tr = true case "tm->when": tmWhen = true case "local_address": netIndexOfLocal = idx + offset case "uid": netIndexOfUid = idx + offset } } } ================================================ FILE: experimental/libbox/iterator.go ================================================ package libbox import "github.com/sagernet/sing/common" type StringIterator interface { Len() int32 HasNext() bool Next() string } type Int32Iterator interface { Len() int32 HasNext() bool Next() int32 } var _ StringIterator = (*iterator[string])(nil) type iterator[T any] struct { values []T } func newIterator[T any](values []T) *iterator[T] { return &iterator[T]{values} } //go:noinline func newPtrIterator[T any](values []T) *iterator[*T] { return &iterator[*T]{common.Map(values, func(value T) *T { return &value })} } func (i *iterator[T]) Len() int32 { return int32(len(i.values)) } func (i *iterator[T]) HasNext() bool { return len(i.values) > 0 } func (i *iterator[T]) Next() T { if len(i.values) == 0 { return common.DefaultValue[T]() } nextValue := i.values[0] i.values = i.values[1:] return nextValue } type abstractIterator[T any] interface { Next() T HasNext() bool } func iteratorToArray[T any](iterator abstractIterator[T]) []T { if iterator == nil { return nil } var values []T for iterator.HasNext() { values = append(values, iterator.Next()) } return values } ================================================ FILE: experimental/libbox/link_flags_stub.go ================================================ //go:build !unix package libbox import ( "net" ) func linkFlags(rawFlags uint32) net.Flags { panic("stub!") } ================================================ FILE: experimental/libbox/link_flags_unix.go ================================================ //go:build unix package libbox import ( "net" "syscall" ) // copied from net.linkFlags func linkFlags(rawFlags uint32) net.Flags { var f net.Flags if rawFlags&syscall.IFF_UP != 0 { f |= net.FlagUp } if rawFlags&syscall.IFF_RUNNING != 0 { f |= net.FlagRunning } if rawFlags&syscall.IFF_BROADCAST != 0 { f |= net.FlagBroadcast } if rawFlags&syscall.IFF_LOOPBACK != 0 { f |= net.FlagLoopback } if rawFlags&syscall.IFF_POINTOPOINT != 0 { f |= net.FlagPointToPoint } if rawFlags&syscall.IFF_MULTICAST != 0 { f |= net.FlagMulticast } return f } ================================================ FILE: experimental/libbox/log.go ================================================ //go:build darwin || linux || windows package libbox import ( "archive/zip" "io" "io/fs" "os" "path/filepath" "runtime" "runtime/debug" "time" ) 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) 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" && runtime.GOOS != "windows" { err = outputFile.Chown(sUserID, sGroupID) if err != nil { outputFile.Close() os.Remove(outputFile.Name()) return err } } err = debug.SetCrashOutput(outputFile, debug.CrashOptions{}) if err != nil { outputFile.Close() os.Remove(outputFile.Name()) return err } _ = 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() } ================================================ FILE: experimental/libbox/monitor.go ================================================ package libbox import ( tun "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/x/list" ) var ( _ tun.DefaultInterfaceMonitor = (*platformDefaultInterfaceMonitor)(nil) _ InterfaceUpdateListener = (*platformDefaultInterfaceMonitor)(nil) ) type platformDefaultInterfaceMonitor struct { *platformInterfaceWrapper logger logger.Logger callbacks list.List[tun.DefaultInterfaceUpdateCallback] myInterface string } func (m *platformDefaultInterfaceMonitor) Start() error { return m.iif.StartDefaultInterfaceMonitor(m) } func (m *platformDefaultInterfaceMonitor) Close() error { return m.iif.CloseDefaultInterfaceMonitor(m) } func (m *platformDefaultInterfaceMonitor) DefaultInterface() *control.Interface { m.defaultInterfaceAccess.Lock() defer m.defaultInterfaceAccess.Unlock() return m.defaultInterface } func (m *platformDefaultInterfaceMonitor) OverrideAndroidVPN() bool { return false } func (m *platformDefaultInterfaceMonitor) AndroidVPNEnabled() bool { return false } func (m *platformDefaultInterfaceMonitor) RegisterCallback(callback tun.DefaultInterfaceUpdateCallback) *list.Element[tun.DefaultInterfaceUpdateCallback] { m.defaultInterfaceAccess.Lock() defer m.defaultInterfaceAccess.Unlock() return m.callbacks.PushBack(callback) } func (m *platformDefaultInterfaceMonitor) UnregisterCallback(element *list.Element[tun.DefaultInterfaceUpdateCallback]) { m.defaultInterfaceAccess.Lock() defer m.defaultInterfaceAccess.Unlock() m.callbacks.Remove(element) } func (m *platformDefaultInterfaceMonitor) UpdateDefaultInterface(interfaceName string, interfaceIndex32 int32, isExpensive bool, isConstrained bool) { if sFixAndroidStack { done := make(chan struct{}) go func() { m.updateDefaultInterface(interfaceName, interfaceIndex32, isExpensive, isConstrained) close(done) }() <-done } else { m.updateDefaultInterface(interfaceName, interfaceIndex32, isExpensive, isConstrained) } } func (m *platformDefaultInterfaceMonitor) updateDefaultInterface(interfaceName string, interfaceIndex32 int32, isExpensive bool, isConstrained bool) { m.isExpensive = isExpensive m.isConstrained = isConstrained err := m.networkManager.UpdateInterfaces() if err != nil { m.logger.Error(E.Cause(err, "update interfaces")) } m.defaultInterfaceAccess.Lock() if interfaceIndex32 == -1 { m.defaultInterface = nil callbacks := m.callbacks.Array() m.defaultInterfaceAccess.Unlock() for _, callback := range callbacks { callback(nil, 0) } return } oldInterface := m.defaultInterface newInterface, err := m.networkManager.InterfaceFinder().ByIndex(int(interfaceIndex32)) if err != nil { m.defaultInterfaceAccess.Unlock() m.logger.Error(E.Cause(err, "find updated interface: ", interfaceName)) return } m.defaultInterface = newInterface if oldInterface != nil && oldInterface.Name == m.defaultInterface.Name && oldInterface.Index == m.defaultInterface.Index { m.defaultInterfaceAccess.Unlock() return } callbacks := m.callbacks.Array() m.defaultInterfaceAccess.Unlock() for _, callback := range callbacks { callback(newInterface, 0) } } func (m *platformDefaultInterfaceMonitor) RegisterMyInterface(interfaceName string) { m.defaultInterfaceAccess.Lock() defer m.defaultInterfaceAccess.Unlock() m.myInterface = interfaceName } func (m *platformDefaultInterfaceMonitor) MyInterface() string { m.defaultInterfaceAccess.Lock() defer m.defaultInterfaceAccess.Unlock() return m.myInterface } ================================================ FILE: experimental/libbox/neighbor.go ================================================ package libbox type NeighborEntry struct { Address string MacAddress string Hostname string } type NeighborEntryIterator interface { Next() *NeighborEntry HasNext() bool } type NeighborSubscription struct { done chan struct{} } func (s *NeighborSubscription) Close() { close(s.done) } ================================================ FILE: experimental/libbox/neighbor_darwin.go ================================================ //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} } ================================================ FILE: experimental/libbox/neighbor_linux.go ================================================ //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)) } } } ================================================ FILE: experimental/libbox/neighbor_stub.go ================================================ //go:build !linux && !darwin package libbox import "os" func SubscribeNeighborTable(_ NeighborUpdateListener) (*NeighborSubscription, error) { return nil, os.ErrInvalid } ================================================ FILE: experimental/libbox/neighbor_unix.go ================================================ //go:build linux || darwin package libbox import ( "net" "net/netip" ) 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 } ================================================ FILE: experimental/libbox/networkquality.go ================================================ 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() } ================================================ FILE: experimental/libbox/oom_report.go ================================================ //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 { 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) if err != nil { return err } chownReport(reportsDir) destPath, err := nextAvailableReportPath(reportsDir, time.Now().UTC()) if err != nil { return err } 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 } 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) } 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) } ================================================ FILE: experimental/libbox/panic.go ================================================ package libbox // https://github.com/golang/go/issues/46893 // TODO: remove after `bulkBarrierPreWrite: unaligned arguments` fixed type StringBox struct { Value string } func wrapString(value string) *StringBox { return &StringBox{Value: value} } ================================================ FILE: experimental/libbox/pidfd_android.go ================================================ package libbox import ( "os" _ "unsafe" ) // https://github.com/SagerNet/sing-box/issues/3233 // https://github.com/golang/go/issues/70508 // https://github.com/tailscale/tailscale/issues/13452 //go:linkname checkPidfdOnce os.checkPidfdOnce var checkPidfdOnce func() error func init() { checkPidfdOnce = func() error { return os.ErrInvalid } } ================================================ FILE: experimental/libbox/platform.go ================================================ package libbox import C "github.com/sagernet/sing-box/constant" type PlatformInterface interface { LocalDNSTransport() LocalDNSTransport UsePlatformAutoDetectInterfaceControl() bool AutoDetectInterfaceControl(fd int32) error OpenTun(options TunOptions) (int32, error) UseProcFS() bool FindConnectionOwner(ipProtocol int32, sourceAddress string, sourcePort int32, destinationAddress string, destinationPort int32) (*ConnectionOwner, error) StartDefaultInterfaceMonitor(listener InterfaceUpdateListener) error CloseDefaultInterfaceMonitor(listener InterfaceUpdateListener) error GetInterfaces() (NetworkInterfaceIterator, error) UnderNetworkExtension() bool IncludeAllNetworks() bool ReadWIFIState() *WIFIState SystemCertificates() StringIterator ClearDNSCache() SendNotification(notification *Notification) error StartNeighborMonitor(listener NeighborUpdateListener) error CloseNeighborMonitor(listener NeighborUpdateListener) error RegisterMyInterface(name string) } type NeighborUpdateListener interface { UpdateNeighborTable(entries NeighborEntryIterator) } type ConnectionOwner struct { UserId int32 UserName string ProcessPath string androidPackageNames []string } func (c *ConnectionOwner) SetAndroidPackageNames(names StringIterator) { c.androidPackageNames = iteratorToArray[string](names) } func (c *ConnectionOwner) AndroidPackageNames() StringIterator { return newIterator(c.androidPackageNames) } type InterfaceUpdateListener interface { UpdateDefaultInterface(interfaceName string, interfaceIndex int32, isExpensive bool, isConstrained bool) } const ( InterfaceTypeWIFI = int32(C.InterfaceTypeWIFI) InterfaceTypeCellular = int32(C.InterfaceTypeCellular) InterfaceTypeEthernet = int32(C.InterfaceTypeEthernet) InterfaceTypeOther = int32(C.InterfaceTypeOther) ) type NetworkInterface struct { Index int32 MTU int32 Name string Addresses StringIterator Flags int32 Type int32 DNSServer StringIterator Metered bool } type WIFIState struct { SSID string BSSID string } func NewWIFIState(wifiSSID string, wifiBSSID string) *WIFIState { return &WIFIState{wifiSSID, wifiBSSID} } type NetworkInterfaceIterator interface { Next() *NetworkInterface HasNext() bool } type Notification struct { Identifier string TypeName string TypeID int32 Title string Subtitle string Body string OpenURL string } type OnDemandRule interface { Target() int32 DNSSearchDomainMatch() StringIterator DNSServerAddressMatch() StringIterator InterfaceTypeMatch() int32 SSIDMatch() StringIterator ProbeURL() string } type OnDemandRuleIterator interface { Next() OnDemandRule HasNext() bool } ================================================ FILE: experimental/libbox/pprof.go ================================================ package libbox import ( "net" "net/http" _ "net/http/pprof" "strconv" ) type PProfServer struct { server *http.Server } func NewPProfServer(port int) *PProfServer { return &PProfServer{ &http.Server{ Addr: ":" + strconv.Itoa(port), }, } } func (s *PProfServer) Start() error { ln, err := net.Listen("tcp", s.server.Addr) if err != nil { return err } go s.server.Serve(ln) return nil } func (s *PProfServer) Close() error { return s.server.Close() } ================================================ FILE: experimental/libbox/profile_import.go ================================================ package libbox import ( "bufio" "bytes" "compress/gzip" "encoding/binary" "io" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/varbin" ) func EncodeChunkedMessage(data []byte) []byte { var buffer bytes.Buffer binary.Write(&buffer, binary.BigEndian, uint16(len(data))) buffer.Write(data) return buffer.Bytes() } func DecodeLengthChunk(data []byte) int32 { return int32(binary.BigEndian.Uint16(data)) } const ( MessageTypeError = iota MessageTypeProfileList MessageTypeProfileContentRequest MessageTypeProfileContent ) type ErrorMessage struct { Message string } func (e *ErrorMessage) Encode() []byte { var buffer bytes.Buffer buffer.WriteByte(MessageTypeError) writeString(&buffer, e.Message) return buffer.Bytes() } func DecodeErrorMessage(data []byte) (*ErrorMessage, error) { reader := bytes.NewReader(data) messageType, err := reader.ReadByte() if err != nil { return nil, err } if messageType != MessageTypeError { return nil, E.New("invalid message") } var message ErrorMessage message.Message, err = readString(reader) if err != nil { return nil, err } return &message, nil } const ( ProfileTypeLocal int32 = iota ProfileTypeiCloud ProfileTypeRemote ) type ProfilePreview struct { ProfileID int64 Name string Type int32 } type ProfilePreviewIterator interface { Next() *ProfilePreview HasNext() bool } type ProfileEncoder struct { profiles []ProfilePreview } func (e *ProfileEncoder) Append(profile *ProfilePreview) { e.profiles = append(e.profiles, *profile) } func (e *ProfileEncoder) Encode() []byte { var buffer bytes.Buffer buffer.WriteByte(MessageTypeProfileList) binary.Write(&buffer, binary.BigEndian, uint16(len(e.profiles))) for _, preview := range e.profiles { binary.Write(&buffer, binary.BigEndian, preview.ProfileID) writeString(&buffer, preview.Name) binary.Write(&buffer, binary.BigEndian, preview.Type) } return buffer.Bytes() } type ProfileDecoder struct { profiles []*ProfilePreview } func (d *ProfileDecoder) Decode(data []byte) error { reader := bytes.NewReader(data) messageType, err := reader.ReadByte() if err != nil { return err } if messageType != MessageTypeProfileList { return E.New("invalid message") } var profileCount uint16 err = binary.Read(reader, binary.BigEndian, &profileCount) if err != nil { return err } for i := 0; i < int(profileCount); i++ { var profile ProfilePreview err = binary.Read(reader, binary.BigEndian, &profile.ProfileID) if err != nil { return err } profile.Name, err = readString(reader) if err != nil { return err } err = binary.Read(reader, binary.BigEndian, &profile.Type) if err != nil { return err } d.profiles = append(d.profiles, &profile) } return nil } func (d *ProfileDecoder) Iterator() ProfilePreviewIterator { return newIterator(d.profiles) } type ProfileContentRequest struct { ProfileID int64 } func (r *ProfileContentRequest) Encode() []byte { var buffer bytes.Buffer buffer.WriteByte(MessageTypeProfileContentRequest) binary.Write(&buffer, binary.BigEndian, r.ProfileID) return buffer.Bytes() } func DecodeProfileContentRequest(data []byte) (*ProfileContentRequest, error) { reader := bytes.NewReader(data) messageType, err := reader.ReadByte() if err != nil { return nil, err } if messageType != MessageTypeProfileContentRequest { return nil, E.New("invalid message") } var request ProfileContentRequest err = binary.Read(reader, binary.BigEndian, &request.ProfileID) if err != nil { return nil, err } return &request, nil } type ProfileContent struct { Name string Type int32 Config string RemotePath string AutoUpdate bool AutoUpdateInterval int32 LastUpdated int64 } func (c *ProfileContent) Encode() []byte { buffer := new(bytes.Buffer) buffer.WriteByte(MessageTypeProfileContent) buffer.WriteByte(1) gWriter := gzip.NewWriter(buffer) writer := bufio.NewWriter(gWriter) writeStringBuffered(writer, c.Name) binary.Write(writer, binary.BigEndian, c.Type) writeStringBuffered(writer, c.Config) if c.Type != ProfileTypeLocal { writeStringBuffered(writer, c.RemotePath) } if c.Type == ProfileTypeRemote { binary.Write(writer, binary.BigEndian, c.AutoUpdate) binary.Write(writer, binary.BigEndian, c.AutoUpdateInterval) binary.Write(writer, binary.BigEndian, c.LastUpdated) } writer.Flush() gWriter.Flush() gWriter.Close() return buffer.Bytes() } func DecodeProfileContent(data []byte) (*ProfileContent, error) { reader := bytes.NewReader(data) messageType, err := reader.ReadByte() if err != nil { return nil, err } if messageType != MessageTypeProfileContent { return nil, E.New("invalid message") } version, err := reader.ReadByte() if err != nil { return nil, err } gReader, err := gzip.NewReader(reader) if err != nil { return nil, E.Cause(err, "unsupported profile") } bReader := varbin.StubReader(gReader) var content ProfileContent content.Name, err = readString(bReader) if err != nil { return nil, err } err = binary.Read(bReader, binary.BigEndian, &content.Type) if err != nil { return nil, err } content.Config, err = readString(bReader) if err != nil { return nil, err } if content.Type != ProfileTypeLocal { content.RemotePath, err = readString(bReader) if err != nil { return nil, err } } if content.Type == ProfileTypeRemote || (version == 0 && content.Type != ProfileTypeLocal) { err = binary.Read(bReader, binary.BigEndian, &content.AutoUpdate) if err != nil { return nil, err } if version >= 1 { err = binary.Read(bReader, binary.BigEndian, &content.AutoUpdateInterval) if err != nil { return nil, err } } err = binary.Read(bReader, binary.BigEndian, &content.LastUpdated) if err != nil { return nil, err } } return &content, nil } func readString(reader io.ByteReader) (string, error) { length, err := binary.ReadUvarint(reader) if err != nil { return "", err } buf := make([]byte, length) for i := range buf { buf[i], err = reader.ReadByte() if err != nil { return "", err } } return string(buf), nil } func writeString(buffer *bytes.Buffer, value string) { varbin.WriteUvarint(buffer, uint64(len(value))) buffer.WriteString(value) } func writeStringBuffered(writer *bufio.Writer, value string) { varbin.WriteUvarint(writer, uint64(len(value))) writer.WriteString(value) } ================================================ FILE: experimental/libbox/remote_profile.go ================================================ package libbox import ( "net/url" ) func GenerateRemoteProfileImportLink(name string, remoteURL string) string { importLink := &url.URL{ Scheme: "sing-box", Host: "import-remote-profile", RawQuery: url.Values{"url": []string{remoteURL}}.Encode(), Fragment: name, } return importLink.String() } type ImportRemoteProfile struct { Name string URL string Host string } func ParseRemoteProfileImportLink(importLink string) (*ImportRemoteProfile, error) { importURL, err := url.Parse(importLink) if err != nil { return nil, err } remoteURL, err := url.Parse(importURL.Query().Get("url")) if err != nil { return nil, err } name := importURL.Fragment if name == "" { name = remoteURL.Host } return &ImportRemoteProfile{ Name: name, URL: remoteURL.String(), Host: remoteURL.Host, }, nil } ================================================ FILE: experimental/libbox/report.go ================================================ //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) } ================================================ FILE: experimental/libbox/semver.go ================================================ package libbox import ( "strings" "golang.org/x/mod/semver" ) func CompareSemver(left string, right string) bool { normalizedLeft := normalizeSemver(left) if !semver.IsValid(normalizedLeft) { return false } normalizedRight := normalizeSemver(right) if !semver.IsValid(normalizedRight) { return false } return semver.Compare(normalizedLeft, normalizedRight) > 0 } func normalizeSemver(version string) string { trimmedVersion := strings.TrimSpace(version) if strings.HasPrefix(trimmedVersion, "v") { return trimmedVersion } return "v" + trimmedVersion } ================================================ FILE: experimental/libbox/semver_test.go ================================================ package libbox import ( "testing" "github.com/stretchr/testify/require" ) func TestCompareSemver(t *testing.T) { t.Parallel() require.False(t, CompareSemver("1.13.0-rc.4", "1.13.0")) require.True(t, CompareSemver("1.13.1", "1.13.0")) require.False(t, CompareSemver("v1.13.0", "1.13.0")) require.False(t, CompareSemver("1.13.0-", "1.13.0")) } ================================================ FILE: experimental/libbox/service.go ================================================ package libbox import ( "crypto/rand" "encoding/hex" "errors" "net" "net/netip" "runtime" "strconv" "sync" "syscall" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental/libbox/internal/procfs" "github.com/sagernet/sing-box/option" tun "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" ) var _ adapter.PlatformInterface = (*platformInterfaceWrapper)(nil) type platformInterfaceWrapper struct { iif PlatformInterface useProcFS bool networkManager adapter.NetworkManager myTunName string myTunAddress []netip.Addr defaultInterfaceAccess sync.Mutex defaultInterface *control.Interface isExpensive bool isConstrained bool } func (w *platformInterfaceWrapper) Initialize(networkManager adapter.NetworkManager) error { w.networkManager = networkManager return nil } func (w *platformInterfaceWrapper) UsePlatformAutoDetectInterfaceControl() bool { return w.iif.UsePlatformAutoDetectInterfaceControl() } func (w *platformInterfaceWrapper) AutoDetectInterfaceControl(fd int) error { return w.iif.AutoDetectInterfaceControl(int32(fd)) } func (w *platformInterfaceWrapper) UsePlatformInterface() bool { return true } func (w *platformInterfaceWrapper) OpenInterface(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error) { if len(options.IncludeUID) > 0 || len(options.ExcludeUID) > 0 { return nil, E.New("platform: unsupported uid options") } if len(options.IncludeAndroidUser) > 0 { return nil, E.New("platform: unsupported android_user option") } routeRanges, err := options.BuildAutoRouteRanges(true) if err != nil { return nil, err } tunFd, err := w.iif.OpenTun(&tunOptions{options, routeRanges, platformOptions}) if err != nil { return nil, err } options.Name, err = getTunnelName(tunFd) if err != nil { return nil, E.Cause(err, "query tun name") } options.InterfaceMonitor.RegisterMyInterface(options.Name) dupFd, err := dup(int(tunFd)) if err != nil { return nil, E.Cause(err, "dup tun file descriptor") } options.FileDescriptor = dupFd w.myTunName = options.Name w.myTunAddress = myTunAddress(options) w.iif.RegisterMyInterface(options.Name) return tun.New(*options) } func myTunAddress(options *tun.Options) []netip.Addr { addresses := make([]netip.Addr, 0, len(options.Inet4Address)+len(options.Inet6Address)) for _, prefix := range options.Inet4Address { addresses = append(addresses, prefix.Addr()) } for _, prefix := range options.Inet6Address { addresses = append(addresses, prefix.Addr()) } return addresses } func (w *platformInterfaceWrapper) MyInterfaceAddress() []netip.Addr { return w.myTunAddress } func (w *platformInterfaceWrapper) UsePlatformDefaultInterfaceMonitor() bool { return true } func (w *platformInterfaceWrapper) CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor { return &platformDefaultInterfaceMonitor{ platformInterfaceWrapper: w, logger: logger, } } func (w *platformInterfaceWrapper) UsePlatformNetworkInterfaces() bool { return true } func (w *platformInterfaceWrapper) NetworkInterfaces() ([]adapter.NetworkInterface, error) { interfaceIterator, err := w.iif.GetInterfaces() if err != nil { return nil, err } var interfaces []adapter.NetworkInterface for _, netInterface := range iteratorToArray[*NetworkInterface](interfaceIterator) { w.defaultInterfaceAccess.Lock() // (GOOS=windows) SA4006: this value of `isDefault` is never used // Why not used? //nolint:staticcheck isDefault := netInterface.Name != w.myTunName && w.defaultInterface != nil && int(netInterface.Index) == w.defaultInterface.Index w.defaultInterfaceAccess.Unlock() interfaces = append(interfaces, adapter.NetworkInterface{ Interface: control.Interface{ Index: int(netInterface.Index), MTU: int(netInterface.MTU), Name: netInterface.Name, Addresses: common.Map(iteratorToArray[string](netInterface.Addresses), netip.MustParsePrefix), Flags: linkFlags(uint32(netInterface.Flags)), }, Type: C.InterfaceType(netInterface.Type), DNSServers: iteratorToArray[string](netInterface.DNSServer), Expensive: netInterface.Metered || isDefault && w.isExpensive, Constrained: isDefault && w.isConstrained, }) } interfaces = common.UniqBy(interfaces, func(it adapter.NetworkInterface) string { return it.Name }) return interfaces, nil } func (w *platformInterfaceWrapper) UnderNetworkExtension() bool { return w.iif.UnderNetworkExtension() } func (w *platformInterfaceWrapper) NetworkExtensionIncludeAllNetworks() bool { return w.iif.IncludeAllNetworks() } func (w *platformInterfaceWrapper) ClearDNSCache() { w.iif.ClearDNSCache() } func (w *platformInterfaceWrapper) RequestPermissionForWIFIState() error { return nil } func (w *platformInterfaceWrapper) UsePlatformWIFIMonitor() bool { return true } func (w *platformInterfaceWrapper) ReadWIFIState() adapter.WIFIState { wifiState := w.iif.ReadWIFIState() if wifiState == nil { return adapter.WIFIState{} } return (adapter.WIFIState)(*wifiState) } func (w *platformInterfaceWrapper) SystemCertificates() []string { return iteratorToArray[string](w.iif.SystemCertificates()) } func (w *platformInterfaceWrapper) UsePlatformConnectionOwnerFinder() bool { return true } func (w *platformInterfaceWrapper) FindConnectionOwner(request *adapter.FindConnectionOwnerRequest) (*adapter.ConnectionOwner, error) { if w.useProcFS { var source netip.AddrPort var destination netip.AddrPort sourceAddr, _ := netip.ParseAddr(request.SourceAddress) source = netip.AddrPortFrom(sourceAddr, uint16(request.SourcePort)) destAddr, _ := netip.ParseAddr(request.DestinationAddress) destination = netip.AddrPortFrom(destAddr, uint16(request.DestinationPort)) var network string switch request.IpProtocol { case int32(syscall.IPPROTO_TCP): network = "tcp" case int32(syscall.IPPROTO_UDP): network = "udp" default: return nil, E.New("unknown protocol: ", request.IpProtocol) } uid := procfs.ResolveSocketByProcSearch(network, source, destination) if uid == -1 { return nil, E.New("procfs: not found") } return &adapter.ConnectionOwner{ UserId: uid, }, nil } result, err := w.iif.FindConnectionOwner(request.IpProtocol, request.SourceAddress, request.SourcePort, request.DestinationAddress, request.DestinationPort) if err != nil { return nil, err } return &adapter.ConnectionOwner{ UserId: result.UserId, UserName: result.UserName, ProcessPath: result.ProcessPath, AndroidPackageNames: result.androidPackageNames, }, nil } func (w *platformInterfaceWrapper) DisableColors() bool { return runtime.GOOS != "android" } func (w *platformInterfaceWrapper) UsePlatformNotification() bool { return true } func (w *platformInterfaceWrapper) SendNotification(notification *adapter.Notification) error { 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() if entry == nil { continue } 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 { return 0, E.New("no available port found") } listener, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(int(port)))) if err != nil { if errors.Is(err, syscall.EADDRINUSE) { continue } return 0, E.Cause(err, "find available port") } err = listener.Close() if err != nil { return 0, E.Cause(err, "close listener") } return int32(port), nil } } func RandomHex(length int32) *StringBox { bytes := make([]byte, length) common.Must1(rand.Read(bytes)) return wrapString(hex.EncodeToString(bytes)) } ================================================ FILE: experimental/libbox/service_other.go ================================================ //go:build !windows package libbox import "syscall" func dup(fd int) (nfd int, err error) { return syscall.Dup(fd) } ================================================ FILE: experimental/libbox/service_windows.go ================================================ package libbox import "os" func dup(fd int) (nfd int, err error) { return 0, os.ErrInvalid } ================================================ FILE: experimental/libbox/setup.go ================================================ package libbox import ( "math" "os" "path/filepath" "runtime" "runtime/debug" "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/dns" "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" E "github.com/sagernet/sing/common/exceptions" ) var ( sBasePath string sWorkingPath string sTempPath string sUserID int sGroupID int sFixAndroidStack bool sCommandServerListenPort uint16 sCommandServerSecret string sLogMaxLines int sDebug bool sCrashReportSource string sOOMKillerEnabled bool sOOMKillerDisabled bool sOOMMemoryLimit int64 ) func init() { debug.SetPanicOnFault(true) debug.SetTraceback("all") } type SetupOptions struct { BasePath string WorkingPath string TempPath string FixAndroidStack bool CommandServerListenPort int32 CommandServerSecret string LogMaxLines int Debug bool CrashReportSource string OomKillerEnabled bool OomKillerDisabled bool OomMemoryLimit int64 } func applySetupOptions(options *SetupOptions) { sBasePath = options.BasePath sWorkingPath = options.WorkingPath sTempPath = options.TempPath sUserID = os.Getuid() sGroupID = os.Getgid() // TODO: remove after fixed // https://github.com/golang/go/issues/68760 sFixAndroidStack = options.FixAndroidStack sCommandServerListenPort = uint16(options.CommandServerListenPort) 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 redirectStderr(filepath.Join(sWorkingPath, "CrashReport-"+sCrashReportSource+".log")) } 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 { return C.Version } func GoVersion() string { return runtime.Version() + ", " + runtime.GOOS + "/" + runtime.GOARCH } func FormatBytes(length int64) string { return byteformats.FormatKBytes(uint64(length)) } func FormatMemoryBytes(length int64) string { return byteformats.FormatMemoryKBytes(uint64(length)) } 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 FormatFQDN(fqdn string) string { return dns.FqdnToDomain(fqdn) } func ProxyDisplayType(proxyType string) string { return C.ProxyDisplayName(proxyType) } ================================================ FILE: experimental/libbox/signal_handler_darwin.go ================================================ //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() } ================================================ FILE: experimental/libbox/signal_handler_stub.go ================================================ //go:build !darwin || !badlinkname package libbox func PrepareCrashSignalHandlers() {} func ReinstallCrashSignalHandlers() {} ================================================ FILE: experimental/libbox/stun.go ================================================ 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() } ================================================ FILE: experimental/libbox/tun.go ================================================ package libbox import ( "net" "net/netip" "github.com/sagernet/sing-box/option" tun "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" ) const ( DNSModeDisabled = tun.DNSModeDisabled DNSModeNative = tun.DNSModeNative DNSModeHijack = tun.DNSModeHijack ) type TunOptions interface { GetInet4Address() RoutePrefixIterator GetInet6Address() RoutePrefixIterator GetDNSMode() *StringBox GetDNSServerAddress() (StringIterator, error) GetMTU() int32 GetAutoRoute() bool GetStrictRoute() bool GetInet4RouteAddress() RoutePrefixIterator GetInet6RouteAddress() RoutePrefixIterator GetInet4RouteExcludeAddress() RoutePrefixIterator GetInet6RouteExcludeAddress() RoutePrefixIterator GetInet4RouteRange() RoutePrefixIterator GetInet6RouteRange() RoutePrefixIterator GetIncludePackage() StringIterator GetExcludePackage() StringIterator IsHTTPProxyEnabled() bool GetHTTPProxyServer() string GetHTTPProxyServerPort() int32 GetHTTPProxyBypassDomain() StringIterator GetHTTPProxyMatchDomain() StringIterator } type RoutePrefix struct { address netip.Addr prefix int } func (p *RoutePrefix) Address() string { return p.address.String() } func (p *RoutePrefix) Prefix() int32 { return int32(p.prefix) } func (p *RoutePrefix) Mask() string { var bits int if p.address.Is6() { bits = 128 } else { bits = 32 } return net.IP(net.CIDRMask(p.prefix, bits)).String() } func (p *RoutePrefix) String() string { return netip.PrefixFrom(p.address, p.prefix).String() } type RoutePrefixIterator interface { Next() *RoutePrefix HasNext() bool } func mapRoutePrefix(prefixes []netip.Prefix) RoutePrefixIterator { return newIterator(common.Map(prefixes, func(prefix netip.Prefix) *RoutePrefix { return &RoutePrefix{ address: prefix.Addr(), prefix: prefix.Bits(), } })) } var _ TunOptions = (*tunOptions)(nil) type tunOptions struct { *tun.Options routeRanges []netip.Prefix option.TunPlatformOptions } func (o *tunOptions) GetInet4Address() RoutePrefixIterator { return mapRoutePrefix(o.Inet4Address) } func (o *tunOptions) GetInet6Address() RoutePrefixIterator { return mapRoutePrefix(o.Inet6Address) } 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 newIterator(common.Map(dnsServers, netip.Addr.String)), nil } func (o *tunOptions) GetMTU() int32 { return int32(o.MTU) } func (o *tunOptions) GetAutoRoute() bool { return o.AutoRoute } func (o *tunOptions) GetStrictRoute() bool { return o.StrictRoute } func (o *tunOptions) GetInet4RouteAddress() RoutePrefixIterator { return mapRoutePrefix(o.Inet4RouteAddress) } func (o *tunOptions) GetInet6RouteAddress() RoutePrefixIterator { return mapRoutePrefix(o.Inet6RouteAddress) } func (o *tunOptions) GetInet4RouteExcludeAddress() RoutePrefixIterator { return mapRoutePrefix(o.Inet4RouteExcludeAddress) } func (o *tunOptions) GetInet6RouteExcludeAddress() RoutePrefixIterator { return mapRoutePrefix(o.Inet6RouteExcludeAddress) } func (o *tunOptions) GetInet4RouteRange() RoutePrefixIterator { return mapRoutePrefix(common.Filter(o.routeRanges, func(it netip.Prefix) bool { return it.Addr().Is4() })) } func (o *tunOptions) GetInet6RouteRange() RoutePrefixIterator { return mapRoutePrefix(common.Filter(o.routeRanges, func(it netip.Prefix) bool { return it.Addr().Is6() })) } func (o *tunOptions) GetIncludePackage() StringIterator { return newIterator(o.IncludePackage) } func (o *tunOptions) GetExcludePackage() StringIterator { return newIterator(o.ExcludePackage) } func (o *tunOptions) IsHTTPProxyEnabled() bool { if o.TunPlatformOptions.HTTPProxy == nil { return false } return o.TunPlatformOptions.HTTPProxy.Enabled } func (o *tunOptions) GetHTTPProxyServer() string { return o.TunPlatformOptions.HTTPProxy.Server } func (o *tunOptions) GetHTTPProxyServerPort() int32 { return int32(o.TunPlatformOptions.HTTPProxy.ServerPort) } func (o *tunOptions) GetHTTPProxyBypassDomain() StringIterator { return newIterator(o.TunPlatformOptions.HTTPProxy.BypassDomain) } func (o *tunOptions) GetHTTPProxyMatchDomain() StringIterator { return newIterator(o.TunPlatformOptions.HTTPProxy.MatchDomain) } ================================================ FILE: experimental/libbox/tun_darwin.go ================================================ package libbox import ( "golang.org/x/sys/unix" ) // kanged from wireauard-apple const utunControlName = "com.apple.net.utun_control" func GetTunnelFileDescriptor() int32 { ctlInfo := &unix.CtlInfo{} copy(ctlInfo.Name[:], utunControlName) for fd := range 1024 { addr, err := unix.Getpeername(fd) if err != nil { continue } addrCTL, loaded := addr.(*unix.SockaddrCtl) if !loaded { continue } if ctlInfo.Id == 0 { err = unix.IoctlCtlInfo(fd, ctlInfo) if err != nil { continue } } if addrCTL.ID == ctlInfo.Id { return int32(fd) } } return -1 } ================================================ FILE: experimental/libbox/tun_name_darwin.go ================================================ package libbox import "golang.org/x/sys/unix" func getTunnelName(fd int32) (string, error) { return unix.GetsockoptString( int(fd), 2, /* #define SYSPROTO_CONTROL 2 */ 2, /* #define UTUN_OPT_IFNAME 2 */ ) } ================================================ FILE: experimental/libbox/tun_name_linux.go ================================================ package libbox import ( "fmt" "syscall" "unsafe" "golang.org/x/sys/unix" ) const ifReqSize = unix.IFNAMSIZ + 64 func getTunnelName(fd int32) (string, error) { var ifr [ifReqSize]byte var errno syscall.Errno _, _, errno = unix.Syscall( unix.SYS_IOCTL, uintptr(fd), uintptr(unix.TUNGETIFF), uintptr(unsafe.Pointer(&ifr[0])), ) if errno != 0 { return "", fmt.Errorf("failed to get name of TUN device: %w", errno) } return unix.ByteSliceToString(ifr[:]), nil } ================================================ FILE: experimental/libbox/tun_name_other.go ================================================ //go:build !(darwin || linux) package libbox import "os" func getTunnelName(fd int32) (string, error) { return "", os.ErrInvalid } ================================================ FILE: experimental/locale/locale.go ================================================ package locale var ( localeRegistry = make(map[string]*Locale) current = defaultLocal ) type Locale struct { // deprecated messages for graphical clients Locale string DeprecatedMessage string DeprecatedMessageNoLink string } var defaultLocal = &Locale{ Locale: "en_US", DeprecatedMessage: "%s is deprecated in sing-box %s and will be removed in sing-box %s please checkout documentation for migration.", DeprecatedMessageNoLink: "%s is deprecated in sing-box %s and will be removed in sing-box %s.", } func Current() *Locale { return current } func Set(localeId string) bool { locale, loaded := localeRegistry[localeId] if !loaded { return false } current = locale return true } ================================================ FILE: experimental/locale/locale_zh_CN.go ================================================ package locale var warningMessageForEndUsers = "\n\n如果您不明白此消息意味着什么:您的配置文件已过时,且将很快不可用。请联系您的配置提供者以更新配置。" func init() { localeRegistry["zh_CN"] = &Locale{ Locale: "zh_CN", DeprecatedMessage: "%s 已在 sing-box %s 中被弃用,且将在 sing-box %s 中被移除,请参阅迁移指南。" + warningMessageForEndUsers, DeprecatedMessageNoLink: "%s 已在 sing-box %s 中被弃用,且将在 sing-box %s 中被移除。" + warningMessageForEndUsers, } } ================================================ FILE: experimental/v2rayapi/server.go ================================================ package v2rayapi import ( "errors" "net" "net/http" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/experimental" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) func init() { experimental.RegisterV2RayServerConstructor(NewServer) } var _ adapter.V2RayServer = (*Server)(nil) type Server struct { logger log.Logger listen string tcpListener net.Listener grpcServer *grpc.Server statsService *StatsService } func NewServer(logger log.Logger, options option.V2RayAPIOptions) (adapter.V2RayServer, error) { grpcServer := grpc.NewServer(grpc.Creds(insecure.NewCredentials())) statsService := NewStatsService(common.PtrValueOrDefault(options.Stats)) if statsService != nil { RegisterStatsServiceServer(grpcServer, statsService) } server := &Server{ logger: logger, listen: options.Listen, grpcServer: grpcServer, statsService: statsService, } return server, nil } func (s *Server) Name() string { return "v2ray server" } func (s *Server) Start(stage adapter.StartStage) error { if stage != adapter.StartStatePostStart { return nil } listener, err := net.Listen("tcp", s.listen) if err != nil { return err } s.logger.Info("grpc server started at ", listener.Addr()) s.tcpListener = listener go func() { err = s.grpcServer.Serve(listener) if err != nil && !errors.Is(err, http.ErrServerClosed) { s.logger.Error(err) } }() return nil } func (s *Server) Close() error { if s.grpcServer != nil { s.grpcServer.Stop() } return common.Close( common.PtrOrNil(s.grpcServer), s.tcpListener, ) } func (s *Server) StatsService() adapter.ConnectionTracker { return s.statsService } ================================================ FILE: experimental/v2rayapi/stats.go ================================================ package v2rayapi import ( "context" "net" "regexp" "runtime" "strings" "sync" "sync/atomic" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" N "github.com/sagernet/sing/common/network" ) func init() { StatsService_ServiceDesc.ServiceName = "v2ray.core.app.stats.command.StatsService" } var ( _ adapter.ConnectionTracker = (*StatsService)(nil) _ StatsServiceServer = (*StatsService)(nil) ) type StatsService struct { createdAt time.Time inbounds map[string]bool outbounds map[string]bool users map[string]bool access sync.Mutex counters map[string]*atomic.Int64 } func NewStatsService(options option.V2RayStatsServiceOptions) *StatsService { if !options.Enabled { return nil } inbounds := make(map[string]bool) outbounds := make(map[string]bool) users := make(map[string]bool) for _, inbound := range options.Inbounds { inbounds[inbound] = true } for _, outbound := range options.Outbounds { outbounds[outbound] = true } for _, user := range options.Users { users[user] = true } return &StatsService{ createdAt: time.Now(), inbounds: inbounds, outbounds: outbounds, users: users, counters: make(map[string]*atomic.Int64), } } func (s *StatsService) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) net.Conn { inbound := metadata.Inbound user := metadata.User outbound := matchOutbound.Tag() var readCounter []*atomic.Int64 var writeCounter []*atomic.Int64 countInbound := inbound != "" && s.inbounds[inbound] countOutbound := outbound != "" && s.outbounds[outbound] countUser := user != "" && s.users[user] if !countInbound && !countOutbound && !countUser { return conn } s.access.Lock() if countInbound { readCounter = append(readCounter, s.loadOrCreateCounter("inbound>>>"+inbound+">>>traffic>>>uplink")) writeCounter = append(writeCounter, s.loadOrCreateCounter("inbound>>>"+inbound+">>>traffic>>>downlink")) } if countOutbound { readCounter = append(readCounter, s.loadOrCreateCounter("outbound>>>"+outbound+">>>traffic>>>uplink")) writeCounter = append(writeCounter, s.loadOrCreateCounter("outbound>>>"+outbound+">>>traffic>>>downlink")) } if countUser { readCounter = append(readCounter, s.loadOrCreateCounter("user>>>"+user+">>>traffic>>>uplink")) writeCounter = append(writeCounter, s.loadOrCreateCounter("user>>>"+user+">>>traffic>>>downlink")) } s.access.Unlock() return bufio.NewInt64CounterConn(conn, readCounter, writeCounter) } func (s *StatsService) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) N.PacketConn { inbound := metadata.Inbound user := metadata.User outbound := matchOutbound.Tag() var readCounter []*atomic.Int64 var writeCounter []*atomic.Int64 countInbound := inbound != "" && s.inbounds[inbound] countOutbound := outbound != "" && s.outbounds[outbound] countUser := user != "" && s.users[user] if !countInbound && !countOutbound && !countUser { return conn } s.access.Lock() if countInbound { readCounter = append(readCounter, s.loadOrCreateCounter("inbound>>>"+inbound+">>>traffic>>>uplink")) writeCounter = append(writeCounter, s.loadOrCreateCounter("inbound>>>"+inbound+">>>traffic>>>downlink")) } if countOutbound { readCounter = append(readCounter, s.loadOrCreateCounter("outbound>>>"+outbound+">>>traffic>>>uplink")) writeCounter = append(writeCounter, s.loadOrCreateCounter("outbound>>>"+outbound+">>>traffic>>>downlink")) } if countUser { readCounter = append(readCounter, s.loadOrCreateCounter("user>>>"+user+">>>traffic>>>uplink")) writeCounter = append(writeCounter, s.loadOrCreateCounter("user>>>"+user+">>>traffic>>>downlink")) } s.access.Unlock() return bufio.NewInt64CounterPacketConn(conn, readCounter, nil, writeCounter, nil) } func (s *StatsService) GetStats(ctx context.Context, request *GetStatsRequest) (*GetStatsResponse, error) { s.access.Lock() counter, loaded := s.counters[request.Name] s.access.Unlock() if !loaded { return nil, E.New(request.Name, " not found.") } var value int64 if request.Reset_ { value = counter.Swap(0) } else { value = counter.Load() } return &GetStatsResponse{Stat: &Stat{Name: request.Name, Value: value}}, nil } func (s *StatsService) QueryStats(ctx context.Context, request *QueryStatsRequest) (*QueryStatsResponse, error) { var response QueryStatsResponse s.access.Lock() defer s.access.Unlock() if len(request.Patterns) == 0 { for name, counter := range s.counters { var value int64 if request.Reset_ { value = counter.Swap(0) } else { value = counter.Load() } response.Stat = append(response.Stat, &Stat{Name: name, Value: value}) } } else if request.Regexp { matchers := make([]*regexp.Regexp, 0, len(request.Patterns)) for _, pattern := range request.Patterns { matcher, err := regexp.Compile(pattern) if err != nil { return nil, err } matchers = append(matchers, matcher) } for name, counter := range s.counters { for _, matcher := range matchers { if matcher.MatchString(name) { var value int64 if request.Reset_ { value = counter.Swap(0) } else { value = counter.Load() } response.Stat = append(response.Stat, &Stat{Name: name, Value: value}) } } } } else { for name, counter := range s.counters { for _, matcher := range request.Patterns { if strings.Contains(name, matcher) { var value int64 if request.Reset_ { value = counter.Swap(0) } else { value = counter.Load() } response.Stat = append(response.Stat, &Stat{Name: name, Value: value}) } } } } return &response, nil } func (s *StatsService) GetSysStats(ctx context.Context, request *SysStatsRequest) (*SysStatsResponse, error) { var rtm runtime.MemStats runtime.ReadMemStats(&rtm) response := &SysStatsResponse{ Uptime: uint32(time.Since(s.createdAt).Seconds()), NumGoroutine: uint32(runtime.NumGoroutine()), Alloc: rtm.Alloc, TotalAlloc: rtm.TotalAlloc, Sys: rtm.Sys, Mallocs: rtm.Mallocs, Frees: rtm.Frees, LiveObjects: rtm.Mallocs - rtm.Frees, NumGC: rtm.NumGC, PauseTotalNs: rtm.PauseTotalNs, } return response, nil } func (s *StatsService) mustEmbedUnimplementedStatsServiceServer() { } //nolint:staticcheck func (s *StatsService) loadOrCreateCounter(name string) *atomic.Int64 { counter, loaded := s.counters[name] if loaded { return counter } counter = &atomic.Int64{} s.counters[name] = counter return counter } ================================================ FILE: experimental/v2rayapi/stats.pb.go ================================================ package v2rayapi import ( reflect "reflect" sync "sync" unsafe "unsafe" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) type GetStatsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Name of the stat counter. Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // Whether or not to reset the counter to fetching its value. Reset_ bool `protobuf:"varint,2,opt,name=reset,proto3" json:"reset,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *GetStatsRequest) Reset() { *x = GetStatsRequest{} mi := &file_experimental_v2rayapi_stats_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *GetStatsRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*GetStatsRequest) ProtoMessage() {} func (x *GetStatsRequest) ProtoReflect() protoreflect.Message { mi := &file_experimental_v2rayapi_stats_proto_msgTypes[0] 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 GetStatsRequest.ProtoReflect.Descriptor instead. func (*GetStatsRequest) Descriptor() ([]byte, []int) { return file_experimental_v2rayapi_stats_proto_rawDescGZIP(), []int{0} } func (x *GetStatsRequest) GetName() string { if x != nil { return x.Name } return "" } func (x *GetStatsRequest) GetReset_() bool { if x != nil { return x.Reset_ } return false } type Stat struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` Value int64 `protobuf:"varint,2,opt,name=value,proto3" json:"value,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Stat) Reset() { *x = Stat{} mi := &file_experimental_v2rayapi_stats_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Stat) String() string { return protoimpl.X.MessageStringOf(x) } func (*Stat) ProtoMessage() {} func (x *Stat) ProtoReflect() protoreflect.Message { mi := &file_experimental_v2rayapi_stats_proto_msgTypes[1] 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 Stat.ProtoReflect.Descriptor instead. func (*Stat) Descriptor() ([]byte, []int) { return file_experimental_v2rayapi_stats_proto_rawDescGZIP(), []int{1} } func (x *Stat) GetName() string { if x != nil { return x.Name } return "" } func (x *Stat) GetValue() int64 { if x != nil { return x.Value } return 0 } type GetStatsResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Stat *Stat `protobuf:"bytes,1,opt,name=stat,proto3" json:"stat,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *GetStatsResponse) Reset() { *x = GetStatsResponse{} mi := &file_experimental_v2rayapi_stats_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *GetStatsResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*GetStatsResponse) ProtoMessage() {} func (x *GetStatsResponse) ProtoReflect() protoreflect.Message { mi := &file_experimental_v2rayapi_stats_proto_msgTypes[2] 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 GetStatsResponse.ProtoReflect.Descriptor instead. func (*GetStatsResponse) Descriptor() ([]byte, []int) { return file_experimental_v2rayapi_stats_proto_rawDescGZIP(), []int{2} } func (x *GetStatsResponse) GetStat() *Stat { if x != nil { return x.Stat } return nil } type QueryStatsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Deprecated, use Patterns instead Pattern string `protobuf:"bytes,1,opt,name=pattern,proto3" json:"pattern,omitempty"` Reset_ bool `protobuf:"varint,2,opt,name=reset,proto3" json:"reset,omitempty"` Patterns []string `protobuf:"bytes,3,rep,name=patterns,proto3" json:"patterns,omitempty"` Regexp bool `protobuf:"varint,4,opt,name=regexp,proto3" json:"regexp,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *QueryStatsRequest) Reset() { *x = QueryStatsRequest{} mi := &file_experimental_v2rayapi_stats_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *QueryStatsRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*QueryStatsRequest) ProtoMessage() {} func (x *QueryStatsRequest) ProtoReflect() protoreflect.Message { mi := &file_experimental_v2rayapi_stats_proto_msgTypes[3] 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 QueryStatsRequest.ProtoReflect.Descriptor instead. func (*QueryStatsRequest) Descriptor() ([]byte, []int) { return file_experimental_v2rayapi_stats_proto_rawDescGZIP(), []int{3} } func (x *QueryStatsRequest) GetPattern() string { if x != nil { return x.Pattern } return "" } func (x *QueryStatsRequest) GetReset_() bool { if x != nil { return x.Reset_ } return false } func (x *QueryStatsRequest) GetPatterns() []string { if x != nil { return x.Patterns } return nil } func (x *QueryStatsRequest) GetRegexp() bool { if x != nil { return x.Regexp } return false } type QueryStatsResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Stat []*Stat `protobuf:"bytes,1,rep,name=stat,proto3" json:"stat,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *QueryStatsResponse) Reset() { *x = QueryStatsResponse{} mi := &file_experimental_v2rayapi_stats_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *QueryStatsResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*QueryStatsResponse) ProtoMessage() {} func (x *QueryStatsResponse) ProtoReflect() protoreflect.Message { mi := &file_experimental_v2rayapi_stats_proto_msgTypes[4] 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 QueryStatsResponse.ProtoReflect.Descriptor instead. func (*QueryStatsResponse) Descriptor() ([]byte, []int) { return file_experimental_v2rayapi_stats_proto_rawDescGZIP(), []int{4} } func (x *QueryStatsResponse) GetStat() []*Stat { if x != nil { return x.Stat } return nil } type SysStatsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SysStatsRequest) Reset() { *x = SysStatsRequest{} mi := &file_experimental_v2rayapi_stats_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SysStatsRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*SysStatsRequest) ProtoMessage() {} func (x *SysStatsRequest) ProtoReflect() protoreflect.Message { mi := &file_experimental_v2rayapi_stats_proto_msgTypes[5] 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 SysStatsRequest.ProtoReflect.Descriptor instead. func (*SysStatsRequest) Descriptor() ([]byte, []int) { return file_experimental_v2rayapi_stats_proto_rawDescGZIP(), []int{5} } type SysStatsResponse struct { state protoimpl.MessageState `protogen:"open.v1"` NumGoroutine uint32 `protobuf:"varint,1,opt,name=NumGoroutine,proto3" json:"NumGoroutine,omitempty"` NumGC uint32 `protobuf:"varint,2,opt,name=NumGC,proto3" json:"NumGC,omitempty"` Alloc uint64 `protobuf:"varint,3,opt,name=Alloc,proto3" json:"Alloc,omitempty"` TotalAlloc uint64 `protobuf:"varint,4,opt,name=TotalAlloc,proto3" json:"TotalAlloc,omitempty"` Sys uint64 `protobuf:"varint,5,opt,name=Sys,proto3" json:"Sys,omitempty"` Mallocs uint64 `protobuf:"varint,6,opt,name=Mallocs,proto3" json:"Mallocs,omitempty"` Frees uint64 `protobuf:"varint,7,opt,name=Frees,proto3" json:"Frees,omitempty"` LiveObjects uint64 `protobuf:"varint,8,opt,name=LiveObjects,proto3" json:"LiveObjects,omitempty"` PauseTotalNs uint64 `protobuf:"varint,9,opt,name=PauseTotalNs,proto3" json:"PauseTotalNs,omitempty"` Uptime uint32 `protobuf:"varint,10,opt,name=Uptime,proto3" json:"Uptime,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SysStatsResponse) Reset() { *x = SysStatsResponse{} mi := &file_experimental_v2rayapi_stats_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *SysStatsResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*SysStatsResponse) ProtoMessage() {} func (x *SysStatsResponse) ProtoReflect() protoreflect.Message { mi := &file_experimental_v2rayapi_stats_proto_msgTypes[6] 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 SysStatsResponse.ProtoReflect.Descriptor instead. func (*SysStatsResponse) Descriptor() ([]byte, []int) { return file_experimental_v2rayapi_stats_proto_rawDescGZIP(), []int{6} } func (x *SysStatsResponse) GetNumGoroutine() uint32 { if x != nil { return x.NumGoroutine } return 0 } func (x *SysStatsResponse) GetNumGC() uint32 { if x != nil { return x.NumGC } return 0 } func (x *SysStatsResponse) GetAlloc() uint64 { if x != nil { return x.Alloc } return 0 } func (x *SysStatsResponse) GetTotalAlloc() uint64 { if x != nil { return x.TotalAlloc } return 0 } func (x *SysStatsResponse) GetSys() uint64 { if x != nil { return x.Sys } return 0 } func (x *SysStatsResponse) GetMallocs() uint64 { if x != nil { return x.Mallocs } return 0 } func (x *SysStatsResponse) GetFrees() uint64 { if x != nil { return x.Frees } return 0 } func (x *SysStatsResponse) GetLiveObjects() uint64 { if x != nil { return x.LiveObjects } return 0 } func (x *SysStatsResponse) GetPauseTotalNs() uint64 { if x != nil { return x.PauseTotalNs } return 0 } func (x *SysStatsResponse) GetUptime() uint32 { if x != nil { return x.Uptime } return 0 } var File_experimental_v2rayapi_stats_proto protoreflect.FileDescriptor const file_experimental_v2rayapi_stats_proto_rawDesc = "" + "\n" + "!experimental/v2rayapi/stats.proto\x12\x15experimental.v2rayapi\";\n" + "\x0fGetStatsRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + "\x05reset\x18\x02 \x01(\bR\x05reset\"0\n" + "\x04Stat\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + "\x05value\x18\x02 \x01(\x03R\x05value\"C\n" + "\x10GetStatsResponse\x12/\n" + "\x04stat\x18\x01 \x01(\v2\x1b.experimental.v2rayapi.StatR\x04stat\"w\n" + "\x11QueryStatsRequest\x12\x18\n" + "\apattern\x18\x01 \x01(\tR\apattern\x12\x14\n" + "\x05reset\x18\x02 \x01(\bR\x05reset\x12\x1a\n" + "\bpatterns\x18\x03 \x03(\tR\bpatterns\x12\x16\n" + "\x06regexp\x18\x04 \x01(\bR\x06regexp\"E\n" + "\x12QueryStatsResponse\x12/\n" + "\x04stat\x18\x01 \x03(\v2\x1b.experimental.v2rayapi.StatR\x04stat\"\x11\n" + "\x0fSysStatsRequest\"\xa2\x02\n" + "\x10SysStatsResponse\x12\"\n" + "\fNumGoroutine\x18\x01 \x01(\rR\fNumGoroutine\x12\x14\n" + "\x05NumGC\x18\x02 \x01(\rR\x05NumGC\x12\x14\n" + "\x05Alloc\x18\x03 \x01(\x04R\x05Alloc\x12\x1e\n" + "\n" + "TotalAlloc\x18\x04 \x01(\x04R\n" + "TotalAlloc\x12\x10\n" + "\x03Sys\x18\x05 \x01(\x04R\x03Sys\x12\x18\n" + "\aMallocs\x18\x06 \x01(\x04R\aMallocs\x12\x14\n" + "\x05Frees\x18\a \x01(\x04R\x05Frees\x12 \n" + "\vLiveObjects\x18\b \x01(\x04R\vLiveObjects\x12\"\n" + "\fPauseTotalNs\x18\t \x01(\x04R\fPauseTotalNs\x12\x16\n" + "\x06Uptime\x18\n" + " \x01(\rR\x06Uptime2\xb4\x02\n" + "\fStatsService\x12]\n" + "\bGetStats\x12&.experimental.v2rayapi.GetStatsRequest\x1a'.experimental.v2rayapi.GetStatsResponse\"\x00\x12c\n" + "\n" + "QueryStats\x12(.experimental.v2rayapi.QueryStatsRequest\x1a).experimental.v2rayapi.QueryStatsResponse\"\x00\x12`\n" + "\vGetSysStats\x12&.experimental.v2rayapi.SysStatsRequest\x1a'.experimental.v2rayapi.SysStatsResponse\"\x00B4Z2github.com/sagernet/sing-box/experimental/v2rayapib\x06proto3" var ( file_experimental_v2rayapi_stats_proto_rawDescOnce sync.Once file_experimental_v2rayapi_stats_proto_rawDescData []byte ) func file_experimental_v2rayapi_stats_proto_rawDescGZIP() []byte { file_experimental_v2rayapi_stats_proto_rawDescOnce.Do(func() { file_experimental_v2rayapi_stats_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_experimental_v2rayapi_stats_proto_rawDesc), len(file_experimental_v2rayapi_stats_proto_rawDesc))) }) return file_experimental_v2rayapi_stats_proto_rawDescData } var ( file_experimental_v2rayapi_stats_proto_msgTypes = make([]protoimpl.MessageInfo, 7) file_experimental_v2rayapi_stats_proto_goTypes = []any{ (*GetStatsRequest)(nil), // 0: experimental.v2rayapi.GetStatsRequest (*Stat)(nil), // 1: experimental.v2rayapi.Stat (*GetStatsResponse)(nil), // 2: experimental.v2rayapi.GetStatsResponse (*QueryStatsRequest)(nil), // 3: experimental.v2rayapi.QueryStatsRequest (*QueryStatsResponse)(nil), // 4: experimental.v2rayapi.QueryStatsResponse (*SysStatsRequest)(nil), // 5: experimental.v2rayapi.SysStatsRequest (*SysStatsResponse)(nil), // 6: experimental.v2rayapi.SysStatsResponse } ) var file_experimental_v2rayapi_stats_proto_depIdxs = []int32{ 1, // 0: experimental.v2rayapi.GetStatsResponse.stat:type_name -> experimental.v2rayapi.Stat 1, // 1: experimental.v2rayapi.QueryStatsResponse.stat:type_name -> experimental.v2rayapi.Stat 0, // 2: experimental.v2rayapi.StatsService.GetStats:input_type -> experimental.v2rayapi.GetStatsRequest 3, // 3: experimental.v2rayapi.StatsService.QueryStats:input_type -> experimental.v2rayapi.QueryStatsRequest 5, // 4: experimental.v2rayapi.StatsService.GetSysStats:input_type -> experimental.v2rayapi.SysStatsRequest 2, // 5: experimental.v2rayapi.StatsService.GetStats:output_type -> experimental.v2rayapi.GetStatsResponse 4, // 6: experimental.v2rayapi.StatsService.QueryStats:output_type -> experimental.v2rayapi.QueryStatsResponse 6, // 7: experimental.v2rayapi.StatsService.GetSysStats:output_type -> experimental.v2rayapi.SysStatsResponse 5, // [5:8] is the sub-list for method output_type 2, // [2:5] is the sub-list for method input_type 2, // [2:2] is the sub-list for extension type_name 2, // [2:2] is the sub-list for extension extendee 0, // [0:2] is the sub-list for field type_name } func init() { file_experimental_v2rayapi_stats_proto_init() } func file_experimental_v2rayapi_stats_proto_init() { if File_experimental_v2rayapi_stats_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_experimental_v2rayapi_stats_proto_rawDesc), len(file_experimental_v2rayapi_stats_proto_rawDesc)), NumEnums: 0, NumMessages: 7, NumExtensions: 0, NumServices: 1, }, GoTypes: file_experimental_v2rayapi_stats_proto_goTypes, DependencyIndexes: file_experimental_v2rayapi_stats_proto_depIdxs, MessageInfos: file_experimental_v2rayapi_stats_proto_msgTypes, }.Build() File_experimental_v2rayapi_stats_proto = out.File file_experimental_v2rayapi_stats_proto_goTypes = nil file_experimental_v2rayapi_stats_proto_depIdxs = nil } ================================================ FILE: experimental/v2rayapi/stats.proto ================================================ syntax = "proto3"; package experimental.v2rayapi; option go_package = "github.com/sagernet/sing-box/experimental/v2rayapi"; message GetStatsRequest { // Name of the stat counter. string name = 1; // Whether or not to reset the counter to fetching its value. bool reset = 2; } message Stat { string name = 1; int64 value = 2; } message GetStatsResponse { Stat stat = 1; } message QueryStatsRequest { // Deprecated, use Patterns instead string pattern = 1; bool reset = 2; repeated string patterns = 3; bool regexp = 4; } message QueryStatsResponse { repeated Stat stat = 1; } message SysStatsRequest {} message SysStatsResponse { uint32 NumGoroutine = 1; uint32 NumGC = 2; uint64 Alloc = 3; uint64 TotalAlloc = 4; uint64 Sys = 5; uint64 Mallocs = 6; uint64 Frees = 7; uint64 LiveObjects = 8; uint64 PauseTotalNs = 9; uint32 Uptime = 10; } service StatsService { rpc GetStats(GetStatsRequest) returns (GetStatsResponse) {} rpc QueryStats(QueryStatsRequest) returns (QueryStatsResponse) {} rpc GetSysStats(SysStatsRequest) returns (SysStatsResponse) {} } ================================================ FILE: experimental/v2rayapi/stats_grpc.pb.go ================================================ package v2rayapi import ( context "context" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" ) // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. // Requires gRPC-Go v1.64.0 or later. const _ = grpc.SupportPackageIsVersion9 const ( StatsService_GetStats_FullMethodName = "/experimental.v2rayapi.StatsService/GetStats" StatsService_QueryStats_FullMethodName = "/experimental.v2rayapi.StatsService/QueryStats" StatsService_GetSysStats_FullMethodName = "/experimental.v2rayapi.StatsService/GetSysStats" ) // StatsServiceClient is the client API for StatsService service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type StatsServiceClient interface { GetStats(ctx context.Context, in *GetStatsRequest, opts ...grpc.CallOption) (*GetStatsResponse, error) QueryStats(ctx context.Context, in *QueryStatsRequest, opts ...grpc.CallOption) (*QueryStatsResponse, error) GetSysStats(ctx context.Context, in *SysStatsRequest, opts ...grpc.CallOption) (*SysStatsResponse, error) } type statsServiceClient struct { cc grpc.ClientConnInterface } func NewStatsServiceClient(cc grpc.ClientConnInterface) StatsServiceClient { return &statsServiceClient{cc} } func (c *statsServiceClient) GetStats(ctx context.Context, in *GetStatsRequest, opts ...grpc.CallOption) (*GetStatsResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(GetStatsResponse) err := c.cc.Invoke(ctx, StatsService_GetStats_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *statsServiceClient) QueryStats(ctx context.Context, in *QueryStatsRequest, opts ...grpc.CallOption) (*QueryStatsResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(QueryStatsResponse) err := c.cc.Invoke(ctx, StatsService_QueryStats_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } func (c *statsServiceClient) GetSysStats(ctx context.Context, in *SysStatsRequest, opts ...grpc.CallOption) (*SysStatsResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(SysStatsResponse) err := c.cc.Invoke(ctx, StatsService_GetSysStats_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } // StatsServiceServer is the server API for StatsService service. // All implementations must embed UnimplementedStatsServiceServer // for forward compatibility. type StatsServiceServer interface { GetStats(context.Context, *GetStatsRequest) (*GetStatsResponse, error) QueryStats(context.Context, *QueryStatsRequest) (*QueryStatsResponse, error) GetSysStats(context.Context, *SysStatsRequest) (*SysStatsResponse, error) mustEmbedUnimplementedStatsServiceServer() } // UnimplementedStatsServiceServer must be embedded to have // forward compatible implementations. // // NOTE: this should be embedded by value instead of pointer to avoid a nil // pointer dereference when methods are called. type UnimplementedStatsServiceServer struct{} func (UnimplementedStatsServiceServer) GetStats(context.Context, *GetStatsRequest) (*GetStatsResponse, error) { return nil, status.Error(codes.Unimplemented, "method GetStats not implemented") } func (UnimplementedStatsServiceServer) QueryStats(context.Context, *QueryStatsRequest) (*QueryStatsResponse, error) { return nil, status.Error(codes.Unimplemented, "method QueryStats not implemented") } func (UnimplementedStatsServiceServer) GetSysStats(context.Context, *SysStatsRequest) (*SysStatsResponse, error) { return nil, status.Error(codes.Unimplemented, "method GetSysStats not implemented") } func (UnimplementedStatsServiceServer) mustEmbedUnimplementedStatsServiceServer() {} func (UnimplementedStatsServiceServer) testEmbeddedByValue() {} // UnsafeStatsServiceServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to StatsServiceServer will // result in compilation errors. type UnsafeStatsServiceServer interface { mustEmbedUnimplementedStatsServiceServer() } func RegisterStatsServiceServer(s grpc.ServiceRegistrar, srv StatsServiceServer) { // If the following call panics, it indicates UnimplementedStatsServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { t.testEmbeddedByValue() } s.RegisterService(&StatsService_ServiceDesc, srv) } func _StatsService_GetStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetStatsRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(StatsServiceServer).GetStats(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: StatsService_GetStats_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(StatsServiceServer).GetStats(ctx, req.(*GetStatsRequest)) } return interceptor(ctx, in, info, handler) } func _StatsService_QueryStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(QueryStatsRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(StatsServiceServer).QueryStats(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: StatsService_QueryStats_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(StatsServiceServer).QueryStats(ctx, req.(*QueryStatsRequest)) } return interceptor(ctx, in, info, handler) } func _StatsService_GetSysStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(SysStatsRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(StatsServiceServer).GetSysStats(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: StatsService_GetSysStats_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(StatsServiceServer).GetSysStats(ctx, req.(*SysStatsRequest)) } return interceptor(ctx, in, info, handler) } // StatsService_ServiceDesc is the grpc.ServiceDesc for StatsService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) var StatsService_ServiceDesc = grpc.ServiceDesc{ ServiceName: "experimental.v2rayapi.StatsService", HandlerType: (*StatsServiceServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "GetStats", Handler: _StatsService_GetStats_Handler, }, { MethodName: "QueryStats", Handler: _StatsService_QueryStats_Handler, }, { MethodName: "GetSysStats", Handler: _StatsService_GetSysStats_Handler, }, }, Streams: []grpc.StreamDesc{}, Metadata: "experimental/v2rayapi/stats.proto", } ================================================ FILE: experimental/v2rayapi.go ================================================ package experimental import ( "os" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" ) type V2RayServerConstructor = func(logger log.Logger, options option.V2RayAPIOptions) (adapter.V2RayServer, error) var v2rayServerConstructor V2RayServerConstructor func RegisterV2RayServerConstructor(constructor V2RayServerConstructor) { v2rayServerConstructor = constructor } func NewV2RayServer(logger log.Logger, options option.V2RayAPIOptions) (adapter.V2RayServer, error) { if v2rayServerConstructor == nil { return nil, os.ErrInvalid } return v2rayServerConstructor(logger, options) } ================================================ FILE: go.mod ================================================ module github.com/sagernet/sing-box 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.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 github.com/database64128/tfo-go/v2 v2.3.2 github.com/go-chi/chi/v5 v5.2.5 github.com/go-chi/render v1.0.3 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/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 github.com/mholt/acmez/v3 v3.1.6 github.com/miekg/dns v1.1.72 github.com/openai/openai-go/v3 v3.26.0 github.com/oschwald/maxminddb-golang v1.13.1 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-20260513071958-2faf34666c2c github.com/sagernet/cronet-go/all v0.0.0-20260513071958-2faf34666c2c 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 github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 github.com/sagernet/sing v0.8.11-0.20260514083559-7297f9541547 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.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 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 github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 github.com/vishvananda/netns v0.0.5 go.uber.org/zap v1.27.1 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba golang.org/x/crypto v0.48.0 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 google.golang.org/protobuf v1.36.11 howett.net/plist v1.0.1 ) require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/ajg/form v1.5.1 // indirect github.com/akutz/memconn v0.1.0 // indirect github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect 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 github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 // 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 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 github.com/gobwas/pool v0.2.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/btree v1.1.3 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect github.com/google/uuid v1.6.0 // indirect 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/klauspost/compress v1.18.0 // indirect 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 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-20260513071149-ade33496efb8 // indirect github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260513071149-ade33496efb8 // indirect github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260513071149-ade33496efb8 // indirect github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260513071149-ade33496efb8 // indirect github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260513071149-ade33496efb8 // indirect github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260513071149-ade33496efb8 // indirect github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260513071149-ade33496efb8 // indirect github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260513071149-ade33496efb8 // indirect github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260513071149-ade33496efb8 // indirect github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260513071149-ade33496efb8 // indirect github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260513071149-ade33496efb8 // indirect github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260513071149-ade33496efb8 // indirect github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260513071149-ade33496efb8 // indirect github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260513071149-ade33496efb8 // indirect github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260513071149-ade33496efb8 // indirect github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260513071149-ade33496efb8 // indirect github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260513071149-ade33496efb8 // indirect github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260513071149-ade33496efb8 // indirect github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260513071149-ade33496efb8 // indirect github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260513071149-ade33496efb8 // indirect github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260513071149-ade33496efb8 // indirect github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260513071149-ade33496efb8 // indirect github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260513071149-ade33496efb8 // indirect github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260513071149-ade33496efb8 // indirect github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260513071149-ade33496efb8 // indirect github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260513071149-ade33496efb8 // indirect github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260513071149-ade33496efb8 // indirect github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260513071149-ade33496efb8 // indirect github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260513071149-ade33496efb8 // 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 github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 // indirect github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/blake3 v0.2.4 // indirect go.uber.org/multierr v1.11.0 // indirect 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/term v0.40.0 // indirect golang.org/x/text v0.34.0 // indirect golang.org/x/time v0.11.0 // indirect golang.org/x/tools v0.42.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard/windows v0.5.3 // indirect 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 ) ================================================ FILE: go.sum ================================================ code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE= code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A= github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/anthropics/anthropic-sdk-go v1.26.0 h1:oUTzFaUpAevfuELAP1sjL6CQJ9HHAfT7CoSYSac11PY= 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.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= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk= github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso= github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0= 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= github.com/database64128/netx-go v0.1.1 h1:dT5LG7Gs7zFZBthFBbzWE6K8wAHjSNAaK7wCYZT7NzM= github.com/database64128/netx-go v0.1.1/go.mod h1:LNlYVipaYkQArRFDNNJ02VkNV+My9A5XR/IGS7sIBQc= github.com/database64128/tfo-go/v2 v2.3.2 h1:UhZMKiMq3swZGUiETkLBDzQnZBPSAeBMClpJGlnJ5Fw= github.com/database64128/tfo-go/v2 v2.3.2/go.mod h1:GC3uB5oa4beGpCUbRb2ZOWP73bJJFmMyAVgQSO7r724= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk= github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ= github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 h1:CaO/zOnF8VvUfEbhRatPcwKVWamvbYd8tQGRWacE9kU= github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4= github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/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= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/gaissmai/bart v0.18.0 h1:jQLBT/RduJu0pv/tLwXE+xKPgtWJejbxuXAR+wLJafo= github.com/gaissmai/bart v0.18.0/go.mod h1:JJzMAhNF5Rjo4SF4jWBrANuJfqY+FvsFhW7t1UZJ+XY= github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I= github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo= github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced h1:Q311OHjMh/u5E2TITc++WlTP5We0xNseRMkHDyvhW7I= github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0= github.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI= github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/insomniacslk/dhcp v0.0.0-20260220084031-5adc3eb26f91 h1:u9i04mGE3iliBh0EFuWaKsmcwrLacqGmq1G3XoaM7gY= github.com/insomniacslk/dhcp v0.0.0-20260220084031-5adc3eb26f91/go.mod h1:qfvBmyDNp+/liLEYWRvqny/PEz9hGe2Dz833eXILSmo= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I= github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 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= github.com/letsencrypt/pebble/v2 v2.10.0/go.mod h1:Sk8cmUIPcIdv2nINo+9PB4L+ZBhzY+F9A1a/h/xmWiQ= github.com/libdns/acmedns v0.5.0 h1:5pRtmUj4Lb/QkNJSl1xgOGBUJTWW7RjpNaIhjpDXjPE= github.com/libdns/acmedns v0.5.0/go.mod h1:X7UAFP1Ep9NpTwWpVlrZzJLR7epynAy0wrIxSPFgKjQ= github.com/libdns/alidns v1.0.6 h1:/Ii428ty6WHFJmE24rZxq2taq++gh7rf9jhgLfp8PmM= github.com/libdns/alidns v1.0.6/go.mod h1:RECwyQ88e9VqQVtSrvX76o1ux3gQUKGzMgxICi+u7Ec= github.com/libdns/cloudflare v0.2.2 h1:XWHv+C1dDcApqazlh08Q6pjytYLgR2a+Y3xrXFu0vsI= github.com/libdns/cloudflare v0.2.2/go.mod h1:w9uTmRCDlAoafAsTPnn2nJ0XHK/eaUMh86DUk8BWi60= github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U= github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/mdlayher/netlink v1.9.0 h1:G8+GLq2x3v4D4MVIqDdNUhTUC7TKiCy/6MDkmItfKco= github.com/mdlayher/netlink v1.9.0/go.mod h1:YBnl5BXsCoRuwBjKKlZ+aYmEoq0r12FDA/3JC+94KDg= github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= github.com/metacubex/utls v1.8.4 h1:HmL9nUApDdWSkgUyodfwF6hSjtiwCGGdyhaSpEejKpg= github.com/metacubex/utls v1.8.4/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko= github.com/mholt/acmez/v3 v3.1.6 h1:eGVQNObP0pBN4sxqrXeg7MYqTOWyoiYpQqITVWlrevk= github.com/mholt/acmez/v3 v3.1.6/go.mod h1:5nTPosTGosLxF3+LU4ygbgMRFDhbAVpqMI4+a4aHLBY= github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/openai/openai-go/v3 v3.26.0 h1:bRt6H/ozMNt/dDkN4gobnLqaEGrRGBzmbVs0xxJEnQE= 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= github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4= github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1 h1:qi+ijeREa0yfAaO+NOcZ81gv4uzOfALUIdhkiIFvmG4= github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1/go.mod h1:JULDuzTMn2gyZFcjpTVZP4/UuwAdbHJ0bum2RdjXojU= github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkkD2QgdTuzQG263YZ+2emfpeyGqW0= 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-20260513071958-2faf34666c2c h1:JatMWK/reVa5Y+x3D3l49SVtHB/EQUEtQnAFTxPBNxY= github.com/sagernet/cronet-go v0.0.0-20260513071958-2faf34666c2c/go.mod h1:T/mwtrpC4JlWfScw73CmSBvHzIvc7BatQ1MhRr+cYNw= github.com/sagernet/cronet-go/all v0.0.0-20260513071958-2faf34666c2c h1:F/tL+VzLZ2F4SNZZze6SRSRL/jcX7LwIsuL1+hECiz0= github.com/sagernet/cronet-go/all v0.0.0-20260513071958-2faf34666c2c/go.mod h1:GGE1tBbFgHq8kV99AKX1JXFY+9FvgNSK/W6Z5j24Ihc= github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260513071149-ade33496efb8 h1:NCKxyAnEkwsEueAEbuuUUjs2FEZAIflr+WN3Mwbvsdg= github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260513071149-ade33496efb8/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw= github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260513071149-ade33496efb8 h1:o3AGm7/L/zAdBvPu0u1dFgDR/tH086qyuXZkjLNJ7/E= github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260513071149-ade33496efb8/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM= github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260513071149-ade33496efb8 h1:AeO8yHQj7aNj16fiJNU797alyuM3T+3VASnETHeV220= github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260513071149-ade33496efb8/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc= github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260513071149-ade33496efb8 h1:ZgW2/Qq/5Q6eTlW80QXLokU56kfjvbLJSEGYTkcG3hU= github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260513071149-ade33496efb8/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ= github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260513071149-ade33496efb8 h1:orYgvX5X9aUa+sRrAuuqA6PXiiBUI2D367ZJqan4lIU= github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260513071149-ade33496efb8/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs= github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260513071149-ade33496efb8 h1:2w1s3wEk7qW2w4IGwlJflxwXBM97UChNiqAErKpvHr0= github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260513071149-ade33496efb8/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0= github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260513071149-ade33496efb8 h1:22k6CB3d4gHT+SARUh2bgNyGU4QwYupcCdP8cGuwygY= github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260513071149-ade33496efb8/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0= github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260513071149-ade33496efb8 h1:PkJ5EaqLrv6bNR+MHx1/joJXoRcoYcV7JA4NtXbFQsc= github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260513071149-ade33496efb8/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4= github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260513071149-ade33496efb8 h1:V629H+OQ9yOR2d0Jkq5y42j5btpvoSWJbUaBH7FCGPI= github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260513071149-ade33496efb8/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo= github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260513071149-ade33496efb8 h1:gfObF5uoqJslCdMRRm2Yo+gmPJQPVlrci5Myrki0Kzk= github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260513071149-ade33496efb8/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ= github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260513071149-ade33496efb8 h1:JRPN0RBKvoOBEHezJh/54KD9ftWL7YadtcCgOf/vRnw= github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260513071149-ade33496efb8/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU= github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260513071149-ade33496efb8 h1:mM8gNdFlXSpjZFs9kgaMgW94oTRF8YdEEQgdOp/OEUA= github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260513071149-ade33496efb8/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI= github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260513071149-ade33496efb8 h1:ZtCH0fH07giTK6wqkenA9fdFYt7krjWiyOvC8z9nPwk= github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260513071149-ade33496efb8/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ= github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260513071149-ade33496efb8 h1:Uviqmw+Q4No9kCxJWJ5CYcq6PNHB9f0jQhd15j39+no= github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260513071149-ade33496efb8/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0= github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260513071149-ade33496efb8 h1:la4zRTE9zpZCmsixwzKT2LnHuo0e439EmGwOlB1An9Q= github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260513071149-ade33496efb8/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s= github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260513071149-ade33496efb8 h1:KodFGMqn+X2dqET0O3xww3iemAGmpoC8U4JW8gwt0x4= github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260513071149-ade33496efb8/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ= github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260513071149-ade33496efb8 h1:QTk1RXNLOIcorZYcF0rBrwLpCIZCKEA2Jr69eFrt8xg= github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260513071149-ade33496efb8/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow= github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260513071149-ade33496efb8 h1:SXqSlM/GjZFvNdUV3IvHq5gqHfW4iWlQHMGzEsgXGXE= github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260513071149-ade33496efb8/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk= github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260513071149-ade33496efb8 h1:aAgLWpfESvy7rfDVH7ioOZQ7u2kmRsbUqJVrwJtkFWs= github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260513071149-ade33496efb8/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E= github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260513071149-ade33496efb8 h1:oTLUyhLckc8TZQ8SRCapgTYyRbz1pBpIvzjMCLMPFu8= github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260513071149-ade33496efb8/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8= github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260513071149-ade33496efb8 h1:LHm/85Y3zN0kNgG+li5qHvP3dzvavEytCYzdLtrfrrg= github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260513071149-ade33496efb8/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w= github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260513071149-ade33496efb8 h1:Pom5TSHV8Cln73uOgQlJ+JtmEu9xh+OuLHWq57dBaVg= github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260513071149-ade33496efb8/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0= github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260513071149-ade33496efb8 h1:1pPcb15BonaFl4153tRo7zOJ7U2zD1vjH+5JipSfJ3g= github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260513071149-ade33496efb8/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs= github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260513071149-ade33496efb8 h1:3Dy4exYQ/IVJGcnTtvW3LmjfjDaxFgJT1hn/ALBpd2M= github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260513071149-ade33496efb8/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc= github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260513071149-ade33496efb8 h1:mo9YMCYTGCRUiWNKtPVQb+qEetufxnch372xUOh9q3M= github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260513071149-ade33496efb8/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4= github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260513071149-ade33496efb8 h1:mhh3JEDDx68oKT4kfqKlWp5QTyzVR84OS/qgqHYIbq0= github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260513071149-ade33496efb8/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc= github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260513071149-ade33496efb8 h1:04KOo38hZojV3bJ5Vqwbpj48ZQy6o7aliYXLN/TNX6g= github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260513071149-ade33496efb8/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc= github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260513071149-ade33496efb8 h1:p535QakpDZEeBz/BfFZGZo0D+Pdn74TE8UTr6c6MSog= github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260513071149-ade33496efb8/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260513071149-ade33496efb8 h1:dovTyKHh3toBIUOS70P4Yx+3Baw6Gppsfy1sJbXoAy0= github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260513071149-ade33496efb8/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= github.com/sagernet/gomobile v0.1.12/go.mod h1:A8l3FlHi2D/+mfcd4HHvk5DGFPW/ShFb9jHP5VmSiDY= github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 h1:AzCE2RhBjLJ4WIWc/GejpNh+z30d5H1hwaB0nD9eY3o= github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1/go.mod h1:NJKBtm9nVEK3iyOYWsUlrDQuoGh4zJ4KOPhSYVidvQ4= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/nftables v0.3.0-mod.2 h1:ck2KMU02OxL1eDFgGaWYglMDpoOZ7OHzxje+vW5Q0OQ= 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.11-0.20260514083559-7297f9541547 h1:dsbM8zLH9DwFM4hSFwXXuUU5TQwMEnsWVsGv4BDshuU= github.com/sagernet/sing v0.8.11-0.20260514083559-7297f9541547/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= github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= 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= 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.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= github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8= github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7 h1:8zc1Aph1+ElqF9/7aSPkO0o4vTd+AfQC+CO324mLWGg= github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7/go.mod h1:m87GAn4UcesHQF3leaPFEINZETO5za1LGn1GJdNDgNc= github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c h1:f9cXNB+IOOPnR8DOLMTpr42jf7naxh5Un5Y09BBf5Cg= github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c/go.mod h1:WUxgxUDZoCF2sxVmW+STSxatP02Qn3FcafTiI2BLtE0= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ= github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4= github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4= github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg= github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio= github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8= github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw= github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8= github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU= github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0= github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA= github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc= github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:UBPHPtv8+nEAy2PD8RyAhOYvau1ek0HDJqLS/Pysi14= github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ= github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 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= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek= go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w= golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ= golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM= howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= 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= ================================================ FILE: include/acme.go ================================================ //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) } ================================================ FILE: include/acme_stub.go ================================================ //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`) }) } ================================================ FILE: include/ccm.go ================================================ //go:build with_ccm && (!darwin || cgo) package include import ( "github.com/sagernet/sing-box/adapter/service" "github.com/sagernet/sing-box/service/ccm" ) func registerCCMService(registry *service.Registry) { ccm.RegisterService(registry) } ================================================ FILE: include/ccm_stub.go ================================================ //go:build !with_ccm package include import ( "context" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/service" 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 registerCCMService(registry *service.Registry) { service.Register[option.CCMServiceOptions](registry, C.TypeCCM, func(ctx context.Context, logger log.ContextLogger, tag string, options option.CCMServiceOptions) (adapter.Service, error) { return nil, E.New(`CCM is not included in this build, rebuild with -tags with_CCM`) }) } ================================================ FILE: include/ccm_stub_darwin.go ================================================ //go:build with_ccm && darwin && !cgo package include import ( "context" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/service" 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 registerCCMService(registry *service.Registry) { service.Register[option.CCMServiceOptions](registry, C.TypeCCM, func(ctx context.Context, logger log.ContextLogger, tag string, options option.CCMServiceOptions) (adapter.Service, error) { return nil, E.New(`CCM requires CGO on darwin, rebuild with CGO_ENABLED=1`) }) } ================================================ FILE: include/clashapi.go ================================================ //go:build with_clash_api package include import _ "github.com/sagernet/sing-box/experimental/clashapi" ================================================ FILE: include/clashapi_stub.go ================================================ //go:build !with_clash_api package include import ( "context" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/experimental" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" ) func init() { experimental.RegisterClashServerConstructor(func(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) { return nil, E.New(`clash api is not included in this build, rebuild with -tags with_clash_api`) }) } ================================================ FILE: include/cloudflared.go ================================================ //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) } ================================================ FILE: include/cloudflared_stub.go ================================================ //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`) }) } ================================================ FILE: include/dhcp.go ================================================ //go:build with_dhcp package include import ( "github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/dns/transport/dhcp" ) func registerDHCPTransport(registry *dns.TransportRegistry) { dhcp.RegisterTransport(registry) } ================================================ FILE: include/dhcp_stub.go ================================================ //go:build !with_dhcp package include 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/log" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" ) func registerDHCPTransport(registry *dns.TransportRegistry) { dns.RegisterTransport[option.DHCPDNSServerOptions](registry, C.DNSTypeDHCP, func(ctx context.Context, logger log.ContextLogger, tag string, options option.DHCPDNSServerOptions) (adapter.DNSTransport, error) { return nil, E.New(`DHCP is not included in this build, rebuild with -tags with_dhcp`) }) } ================================================ FILE: include/naive_outbound.go ================================================ //go:build with_naive_outbound package include import ( "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/protocol/naive" ) func registerNaiveOutbound(registry *outbound.Registry) { naive.RegisterOutbound(registry) } ================================================ FILE: include/naive_outbound_stub.go ================================================ //go:build !with_naive_outbound package include import ( "context" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/outbound" 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 registerNaiveOutbound(registry *outbound.Registry) { outbound.Register[option.NaiveOutboundOptions](registry, C.TypeNaive, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.NaiveOutboundOptions) (adapter.Outbound, error) { return nil, E.New(`naive outbound is not included in this build, rebuild with -tags with_naive_outbound`) }) } ================================================ FILE: include/ocm.go ================================================ //go:build with_ocm package include import ( "github.com/sagernet/sing-box/adapter/service" "github.com/sagernet/sing-box/service/ocm" ) func registerOCMService(registry *service.Registry) { ocm.RegisterService(registry) } ================================================ FILE: include/ocm_stub.go ================================================ //go:build !with_ocm package include import ( "context" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/service" 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 registerOCMService(registry *service.Registry) { service.Register[option.OCMServiceOptions](registry, C.TypeOCM, func(ctx context.Context, logger log.ContextLogger, tag string, options option.OCMServiceOptions) (adapter.Service, error) { return nil, E.New(`OCM is not included in this build, rebuild with -tags with_ocm`) }) } ================================================ FILE: include/oom_killer.go ================================================ package include import ( "github.com/sagernet/sing-box/adapter/service" "github.com/sagernet/sing-box/service/oomkiller" ) func registerOOMKillerService(registry *service.Registry) { oomkiller.RegisterService(registry) } ================================================ FILE: include/quic.go ================================================ //go:build with_quic 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" "github.com/sagernet/sing-box/protocol/hysteria2" _ "github.com/sagernet/sing-box/protocol/naive/quic" "github.com/sagernet/sing-box/protocol/tuic" _ "github.com/sagernet/sing-box/transport/v2rayquic" ) func registerQUICInbounds(registry *inbound.Registry) { hysteria.RegisterInbound(registry) tuic.RegisterInbound(registry) hysteria2.RegisterInbound(registry) } func registerQUICOutbounds(registry *outbound.Registry) { hysteria.RegisterOutbound(registry) tuic.RegisterOutbound(registry) hysteria2.RegisterOutbound(registry) } func registerQUICTransports(registry *dns.TransportRegistry) { quic.RegisterTransport(registry) quic.RegisterHTTP3Transport(registry) } func registerQUICServices(registry *service.Registry) { hysteria2.RegisterRealmService(registry) } ================================================ FILE: include/quic_stub.go ================================================ //go:build !with_quic package include import ( "context" "io" "net/http" "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" "github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/protocol/naive" "github.com/sagernet/sing-box/transport/v2ray" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) func init() { v2ray.RegisterQUICConstructor( func(ctx context.Context, logger logger.ContextLogger, options option.V2RayQUICOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (adapter.V2RayServerTransport, error) { return nil, C.ErrQUICNotIncluded }, func(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayQUICOptions, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) { return nil, C.ErrQUICNotIncluded }, ) } func registerQUICInbounds(registry *inbound.Registry) { inbound.Register[option.HysteriaInboundOptions](registry, C.TypeHysteria, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaInboundOptions) (adapter.Inbound, error) { return nil, C.ErrQUICNotIncluded }) inbound.Register[option.TUICInboundOptions](registry, C.TypeTUIC, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICInboundOptions) (adapter.Inbound, error) { return nil, C.ErrQUICNotIncluded }) inbound.Register[option.Hysteria2InboundOptions](registry, C.TypeHysteria2, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2InboundOptions) (adapter.Inbound, error) { return nil, C.ErrQUICNotIncluded }) naive.ConfigureHTTP3ListenerFunc = func(ctx context.Context, logger logger.Logger, listener *listener.Listener, handler http.Handler, tlsConfig tls.ServerConfig, options option.NaiveInboundOptions) (io.Closer, error) { return nil, C.ErrQUICNotIncluded } } func registerQUICOutbounds(registry *outbound.Registry) { outbound.Register[option.HysteriaOutboundOptions](registry, C.TypeHysteria, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaOutboundOptions) (adapter.Outbound, error) { return nil, C.ErrQUICNotIncluded }) outbound.Register[option.TUICOutboundOptions](registry, C.TypeTUIC, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICOutboundOptions) (adapter.Outbound, error) { return nil, C.ErrQUICNotIncluded }) outbound.Register[option.Hysteria2OutboundOptions](registry, C.TypeHysteria2, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2OutboundOptions) (adapter.Outbound, error) { return nil, C.ErrQUICNotIncluded }) } func registerQUICTransports(registry *dns.TransportRegistry) { dns.RegisterTransport[option.RemoteTLSDNSServerOptions](registry, C.DNSTypeQUIC, func(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteTLSDNSServerOptions) (adapter.DNSTransport, error) { return nil, C.ErrQUICNotIncluded }) dns.RegisterTransport[option.RemoteHTTPSDNSServerOptions](registry, C.DNSTypeHTTP3, func(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) { 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 }) } ================================================ FILE: include/registry.go ================================================ package include import ( "context" "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" "github.com/sagernet/sing-box/adapter/service" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/dns/transport" "github.com/sagernet/sing-box/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" "github.com/sagernet/sing-box/protocol/block" "github.com/sagernet/sing-box/protocol/direct" "github.com/sagernet/sing-box/protocol/group" "github.com/sagernet/sing-box/protocol/http" "github.com/sagernet/sing-box/protocol/mixed" "github.com/sagernet/sing-box/protocol/naive" "github.com/sagernet/sing-box/protocol/redirect" "github.com/sagernet/sing-box/protocol/shadowsocks" "github.com/sagernet/sing-box/protocol/shadowtls" "github.com/sagernet/sing-box/protocol/socks" "github.com/sagernet/sing-box/protocol/ssh" "github.com/sagernet/sing-box/protocol/tor" "github.com/sagernet/sing-box/protocol/trojan" "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(), CertificateProviderRegistry()) } func InboundRegistry() *inbound.Registry { registry := inbound.NewRegistry() tun.RegisterInbound(registry) redirect.RegisterRedirect(registry) redirect.RegisterTProxy(registry) direct.RegisterInbound(registry) socks.RegisterInbound(registry) http.RegisterInbound(registry) mixed.RegisterInbound(registry) shadowsocks.RegisterInbound(registry) vmess.RegisterInbound(registry) trojan.RegisterInbound(registry) naive.RegisterInbound(registry) shadowtls.RegisterInbound(registry) vless.RegisterInbound(registry) anytls.RegisterInbound(registry) registerQUICInbounds(registry) registerCloudflaredInbound(registry) registerStubForRemovedInbounds(registry) return registry } func OutboundRegistry() *outbound.Registry { registry := outbound.NewRegistry() direct.RegisterOutbound(registry) block.RegisterOutbound(registry) group.RegisterSelector(registry) group.RegisterURLTest(registry) socks.RegisterOutbound(registry) http.RegisterOutbound(registry) shadowsocks.RegisterOutbound(registry) vmess.RegisterOutbound(registry) trojan.RegisterOutbound(registry) registerNaiveOutbound(registry) tor.RegisterOutbound(registry) ssh.RegisterOutbound(registry) shadowtls.RegisterOutbound(registry) vless.RegisterOutbound(registry) anytls.RegisterOutbound(registry) registerQUICOutbounds(registry) registerStubForRemovedOutbounds(registry) return registry } func EndpointRegistry() *endpoint.Registry { registry := endpoint.NewRegistry() registerWireGuardEndpoint(registry) registerTailscaleEndpoint(registry) return registry } func DNSTransportRegistry() *dns.TransportRegistry { registry := dns.NewTransportRegistry() transport.RegisterTCP(registry) transport.RegisterUDP(registry) transport.RegisterTLS(registry) transport.RegisterHTTPS(registry) hosts.RegisterTransport(registry) local.RegisterTransport(registry) mdns.RegisterTransport(registry) fakeip.RegisterTransport(registry) resolved.RegisterTransport(registry) registerQUICTransports(registry) registerDHCPTransport(registry) registerTailscaleTransport(registry) return registry } func ServiceRegistry() *service.Registry { registry := service.NewRegistry() resolved.RegisterService(registry) ssmapi.RegisterService(registry) registerQUICServices(registry) registerDERPService(registry) registerCCMService(registry) registerOCMService(registry) registerOOMKillerService(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") }) } func registerStubForRemovedOutbounds(registry *outbound.Registry) { outbound.Register[option.ShadowsocksROutboundOptions](registry, C.TypeShadowsocksR, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksROutboundOptions) (adapter.Outbound, error) { return nil, E.New("ShadowsocksR is deprecated and removed in sing-box 1.6.0") }) outbound.Register[option.StubOptions](registry, C.TypeWireGuard, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.StubOptions) (adapter.Outbound, error) { return nil, E.New("WireGuard outbound is deprecated in sing-box 1.11.0 and removed in sing-box 1.13.0, use WireGuard endpoint instead") }) } ================================================ FILE: include/tailscale.go ================================================ //go:build with_tailscale 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" "github.com/sagernet/sing-box/protocol/tailscale" "github.com/sagernet/sing-box/service/derp" ) func registerTailscaleEndpoint(registry *endpoint.Registry) { tailscale.RegisterEndpoint(registry) } func registerTailscaleTransport(registry *dns.TransportRegistry) { tailscale.RegistryTransport(registry) } func registerTailscaleCertificateProvider(registry *certificate.Registry) { tailscale.RegisterCertificateProvider(registry) } func registerDERPService(registry *service.Registry) { derp.Register(registry) } ================================================ FILE: include/tailscale_stub.go ================================================ //go:build !with_tailscale package include 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" "github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" ) func registerTailscaleEndpoint(registry *endpoint.Registry) { endpoint.Register[option.TailscaleEndpointOptions](registry, C.TypeTailscale, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TailscaleEndpointOptions) (adapter.Endpoint, error) { return nil, E.New(`Tailscale is not included in this build, rebuild with -tags with_tailscale`) }) } func registerTailscaleTransport(registry *dns.TransportRegistry) { dns.RegisterTransport[option.TailscaleDNSServerOptions](registry, C.DNSTypeTailscale, func(ctx context.Context, logger log.ContextLogger, tag string, options option.TailscaleDNSServerOptions) (adapter.DNSTransport, error) { return nil, E.New(`Tailscale is not included in this build, rebuild with -tags with_tailscale`) }) } 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`) }) } ================================================ FILE: include/tz_android.go ================================================ // Copyright 2014 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // kanged from https://github.com/golang/mobile/blob/c713f31d574bb632a93f169b2cc99c9e753fef0e/app/android.go#L89 package include // #include import "C" import "time" func init() { var currentT C.time_t var currentTM C.struct_tm C.time(¤tT) C.localtime_r(¤tT, ¤tTM) tzOffset := int(currentTM.tm_gmtoff) tz := C.GoString(currentTM.tm_zone) time.Local = time.FixedZone(tz, tzOffset) } ================================================ FILE: include/tz_ios.go ================================================ package include /* #cgo CFLAGS: -x objective-c #cgo LDFLAGS: -framework Foundation #import const char* getSystemTimeZone() { NSTimeZone *timeZone = [NSTimeZone systemTimeZone]; NSString *timeZoneName = [timeZone description]; return [timeZoneName UTF8String]; } */ import "C" import ( "strings" "time" ) func init() { tzDescription := C.GoString(C.getSystemTimeZone()) if len(tzDescription) == 0 { return } location, err := time.LoadLocation(strings.Split(tzDescription, " ")[0]) if err != nil { return } time.Local = location } ================================================ FILE: include/v2rayapi.go ================================================ //go:build with_v2ray_api package include import _ "github.com/sagernet/sing-box/experimental/v2rayapi" ================================================ FILE: include/v2rayapi_stub.go ================================================ //go:build !with_v2ray_api package include import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/experimental" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" ) func init() { experimental.RegisterV2RayServerConstructor(func(logger log.Logger, options option.V2RayAPIOptions) (adapter.V2RayServer, error) { return nil, E.New(`v2ray api is not included in this build, rebuild with -tags with_v2ray_api`) }) } ================================================ FILE: include/wireguard.go ================================================ //go:build with_wireguard package include import ( "github.com/sagernet/sing-box/adapter/endpoint" "github.com/sagernet/sing-box/protocol/wireguard" ) func registerWireGuardEndpoint(registry *endpoint.Registry) { wireguard.RegisterEndpoint(registry) } ================================================ FILE: include/wireguard_stub.go ================================================ //go:build !with_wireguard package include import ( "context" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/endpoint" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" ) func registerWireGuardEndpoint(registry *endpoint.Registry) { endpoint.Register[option.WireGuardEndpointOptions](registry, C.TypeWireGuard, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.WireGuardEndpointOptions) (adapter.Endpoint, error) { return nil, E.New(`WireGuard is not included in this build, rebuild with -tags with_wireguard`) }) } ================================================ FILE: log/export.go ================================================ package log import ( "context" "os" "time" ) var std ContextLogger func init() { std = NewDefaultFactory( context.Background(), Formatter{BaseTime: time.Now()}, os.Stderr, "", nil, false, ).Logger() } func StdLogger() ContextLogger { return std } func SetStdLogger(logger ContextLogger) { std = logger } func Trace(args ...any) { std.Trace(args...) } func Debug(args ...any) { std.Debug(args...) } func Info(args ...any) { std.Info(args...) } func Warn(args ...any) { std.Warn(args...) } func Error(args ...any) { std.Error(args...) } func Fatal(args ...any) { std.Fatal(args...) } func Panic(args ...any) { std.Panic(args...) } func TraceContext(ctx context.Context, args ...any) { std.TraceContext(ctx, args...) } func DebugContext(ctx context.Context, args ...any) { std.DebugContext(ctx, args...) } func InfoContext(ctx context.Context, args ...any) { std.InfoContext(ctx, args...) } func WarnContext(ctx context.Context, args ...any) { std.WarnContext(ctx, args...) } func ErrorContext(ctx context.Context, args ...any) { std.ErrorContext(ctx, args...) } func FatalContext(ctx context.Context, args ...any) { std.FatalContext(ctx, args...) } func PanicContext(ctx context.Context, args ...any) { std.PanicContext(ctx, args...) } ================================================ FILE: log/factory.go ================================================ package log import ( "github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/observable" ) type ( Logger logger.Logger ContextLogger logger.ContextLogger ) type Factory interface { Start() error Close() error Level() Level SetLevel(level Level) Logger() ContextLogger NewLogger(tag string) ContextLogger } type ObservableFactory interface { Factory observable.Observable[Entry] } type Entry struct { Level Level Message string } ================================================ FILE: log/format.go ================================================ package log import ( "context" "strconv" "strings" "time" F "github.com/sagernet/sing/common/format" "github.com/logrusorgru/aurora" ) type Formatter struct { BaseTime time.Time DisableColors bool DisableTimestamp bool FullTimestamp bool TimestampFormat string DisableLineBreak bool } func (f Formatter) Format(ctx context.Context, level Level, tag string, message string, timestamp time.Time) string { levelString := strings.ToUpper(FormatLevel(level)) if !f.DisableColors { switch level { case LevelDebug, LevelTrace: levelString = aurora.White(levelString).String() case LevelInfo: levelString = aurora.Cyan(levelString).String() case LevelWarn: levelString = aurora.Yellow(levelString).String() case LevelError, LevelFatal, LevelPanic: levelString = aurora.Red(levelString).String() } } if tag != "" { message = tag + ": " + message } var id ID var hasId bool if ctx != nil { id, hasId = IDFromContext(ctx) } if hasId { activeDuration := FormatDuration(time.Since(id.CreatedAt)) if !f.DisableColors { var color aurora.Color color = aurora.Color(uint8(id.ID)) color %= 215 row := uint(color / 36) column := uint(color % 36) var r, g, b float32 r = float32(row * 51) g = float32(column / 6 * 51) b = float32((column % 6) * 51) luma := 0.2126*r + 0.7152*g + 0.0722*b if luma < 60 { row = 5 - row column = 35 - column color = aurora.Color(row*36 + column) } color += 16 color = color << 16 color |= 1 << 14 message = F.ToString("[", aurora.Colorize(id.ID, color).String(), " ", activeDuration, "] ", message) } else { message = F.ToString("[", id.ID, " ", activeDuration, "] ", message) } } switch { case f.DisableTimestamp: message = levelString + " " + message case f.FullTimestamp: message = timestamp.Format(f.TimestampFormat) + " " + levelString + " " + message default: message = levelString + "[" + xd(int(timestamp.Sub(f.BaseTime)/time.Second), 4) + "] " + message } if f.DisableLineBreak { if message[len(message)-1] == '\n' { message = message[:len(message)-1] } } else { if message[len(message)-1] != '\n' { message += "\n" } } return message } func (f Formatter) FormatWithSimple(ctx context.Context, level Level, tag string, message string, timestamp time.Time) (string, string) { levelString := strings.ToUpper(FormatLevel(level)) if !f.DisableColors { switch level { case LevelDebug, LevelTrace: levelString = aurora.White(levelString).String() case LevelInfo: levelString = aurora.Cyan(levelString).String() case LevelWarn: levelString = aurora.Yellow(levelString).String() case LevelError, LevelFatal, LevelPanic: levelString = aurora.Red(levelString).String() } } if tag != "" { message = tag + ": " + message } messageSimple := message var id ID var hasId bool if ctx != nil { id, hasId = IDFromContext(ctx) } if hasId { activeDuration := FormatDuration(time.Since(id.CreatedAt)) if !f.DisableColors { var color aurora.Color color = aurora.Color(uint8(id.ID)) color %= 215 row := uint(color / 36) column := uint(color % 36) var r, g, b float32 r = float32(row * 51) g = float32(column / 6 * 51) b = float32((column % 6) * 51) luma := 0.2126*r + 0.7152*g + 0.0722*b if luma < 60 { row = 5 - row column = 35 - column color = aurora.Color(row*36 + column) } color += 16 color = color << 16 color |= 1 << 14 message = F.ToString("[", aurora.Colorize(id.ID, color).String(), " ", activeDuration, "] ", message) } else { message = F.ToString("[", id.ID, " ", activeDuration, "] ", message) } messageSimple = F.ToString("[", id.ID, " ", activeDuration, "] ", messageSimple) } switch { case f.DisableTimestamp: message = levelString + " " + message case f.FullTimestamp: message = timestamp.Format(f.TimestampFormat) + " " + levelString + " " + message default: message = levelString + "[" + xd(int(timestamp.Sub(f.BaseTime)/time.Second), 4) + "] " + message } if message[len(message)-1] != '\n' { message += "\n" } return message, messageSimple } func xd(value int, x int) string { message := strconv.Itoa(value) for len(message) < x { message = "0" + message } return message } func FormatDuration(duration time.Duration) string { if duration < time.Second { return F.ToString(duration.Milliseconds(), "ms") } else if duration < time.Minute { return F.ToString(int64(duration.Seconds()), ".", int64(duration.Seconds()*100)%100, "s") } else { return F.ToString(int64(duration.Minutes()), "m", int64(duration.Seconds())%60, "s") } } ================================================ FILE: log/id.go ================================================ package log import ( "context" "math/rand" "time" ) type idKey struct{} type ID struct { ID uint32 CreatedAt time.Time } func ContextWithNewID(ctx context.Context) context.Context { return ContextWithID(ctx, ID{ ID: rand.Uint32(), CreatedAt: time.Now(), }) } func ContextWithID(ctx context.Context, id ID) context.Context { return context.WithValue(ctx, (*idKey)(nil), id) } func IDFromContext(ctx context.Context) (ID, bool) { id, loaded := ctx.Value((*idKey)(nil)).(ID) return id, loaded } ================================================ FILE: log/level.go ================================================ package log import ( E "github.com/sagernet/sing/common/exceptions" ) type Level = uint8 const ( LevelPanic Level = iota LevelFatal LevelError LevelWarn LevelInfo LevelDebug LevelTrace ) func FormatLevel(level Level) string { switch level { case LevelTrace: return "trace" case LevelDebug: return "debug" case LevelInfo: return "info" case LevelWarn: return "warn" case LevelError: return "error" case LevelFatal: return "fatal" case LevelPanic: return "panic" default: return "unknown" } } func ParseLevel(level string) (Level, error) { switch level { case "trace": return LevelTrace, nil case "debug": return LevelDebug, nil case "info": return LevelInfo, nil case "warn", "warning": return LevelWarn, nil case "error": return LevelError, nil case "fatal": return LevelFatal, nil case "panic": return LevelPanic, nil default: return LevelTrace, E.New("unknown log level: ", level) } } ================================================ FILE: log/log.go ================================================ package log import ( "context" "io" "os" "time" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" ) type Options struct { Context context.Context Options option.LogOptions Observable bool DefaultWriter io.Writer BaseTime time.Time PlatformWriter PlatformWriter } func New(options Options) (Factory, error) { logOptions := options.Options if logOptions.Disabled { return NewNOPFactory(), nil } var logWriter io.Writer var logFilePath string switch logOptions.Output { case "": logWriter = options.DefaultWriter if logWriter == nil { logWriter = os.Stderr } case "stderr": logWriter = os.Stderr case "stdout": logWriter = os.Stdout default: logWriter = io.Discard logFilePath = logOptions.Output } logFormatter := Formatter{ BaseTime: options.BaseTime, DisableColors: logOptions.DisableColor || logFilePath != "", DisableTimestamp: !logOptions.Timestamp && logFilePath != "", FullTimestamp: logOptions.Timestamp, TimestampFormat: "-0700 2006-01-02 15:04:05", } factory := NewDefaultFactory( options.Context, logFormatter, logWriter, logFilePath, options.PlatformWriter, options.Observable, ) if logOptions.Level != "" { logLevel, err := ParseLevel(logOptions.Level) if err != nil { return nil, E.Cause(err, "parse log level") } factory.SetLevel(logLevel) } else { factory.SetLevel(LevelTrace) } return factory, nil } ================================================ FILE: log/nop.go ================================================ package log import ( "context" "os" "github.com/sagernet/sing/common/observable" ) var _ ObservableFactory = (*nopFactory)(nil) type nopFactory struct{} func NewNOPFactory() ObservableFactory { return (*nopFactory)(nil) } func (f *nopFactory) Start() error { return nil } func (f *nopFactory) Close() error { return nil } func (f *nopFactory) Level() Level { return LevelTrace } func (f *nopFactory) SetLevel(level Level) { } func (f *nopFactory) Logger() ContextLogger { return f } func (f *nopFactory) NewLogger(tag string) ContextLogger { return f } func (f *nopFactory) Trace(args ...any) { } func (f *nopFactory) Debug(args ...any) { } func (f *nopFactory) Info(args ...any) { } func (f *nopFactory) Warn(args ...any) { } func (f *nopFactory) Error(args ...any) { } func (f *nopFactory) Fatal(args ...any) { } func (f *nopFactory) Panic(args ...any) { } func (f *nopFactory) TraceContext(ctx context.Context, args ...any) { } func (f *nopFactory) DebugContext(ctx context.Context, args ...any) { } func (f *nopFactory) InfoContext(ctx context.Context, args ...any) { } func (f *nopFactory) WarnContext(ctx context.Context, args ...any) { } func (f *nopFactory) ErrorContext(ctx context.Context, args ...any) { } func (f *nopFactory) FatalContext(ctx context.Context, args ...any) { } func (f *nopFactory) PanicContext(ctx context.Context, args ...any) { } func (f *nopFactory) Subscribe() (subscription observable.Subscription[Entry], done <-chan struct{}, err error) { return nil, nil, os.ErrInvalid } func (f *nopFactory) UnSubscribe(subscription observable.Subscription[Entry]) { } ================================================ FILE: log/observable.go ================================================ package log import ( "context" "io" "os" "time" "github.com/sagernet/sing/common" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/observable" "github.com/sagernet/sing/service/filemanager" ) var _ Factory = (*defaultFactory)(nil) type defaultFactory struct { ctx context.Context formatter Formatter platformFormatter Formatter writer io.Writer file *os.File filePath string platformWriter PlatformWriter needObservable bool level Level subscriber *observable.Subscriber[Entry] observer *observable.Observer[Entry] } func NewDefaultFactory( ctx context.Context, formatter Formatter, writer io.Writer, filePath string, platformWriter PlatformWriter, needObservable bool, ) ObservableFactory { factory := &defaultFactory{ ctx: ctx, formatter: formatter, platformFormatter: Formatter{ BaseTime: formatter.BaseTime, DisableLineBreak: true, }, writer: writer, filePath: filePath, platformWriter: platformWriter, needObservable: needObservable, level: LevelTrace, subscriber: observable.NewSubscriber[Entry](128), } /*if platformWriter != nil { factory.platformFormatter.DisableColors = platformWriter.DisableColors() }*/ if needObservable { factory.observer = observable.NewObserver[Entry](factory.subscriber, 64) } return factory } func (f *defaultFactory) Start() error { if f.filePath != "" { logFile, err := filemanager.OpenFile(f.ctx, f.filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { return err } f.writer = logFile f.file = logFile } return nil } func (f *defaultFactory) Close() error { return common.Close( common.PtrOrNil(f.file), f.subscriber, ) } func (f *defaultFactory) Level() Level { return f.level } func (f *defaultFactory) SetLevel(level Level) { f.level = level } func (f *defaultFactory) Logger() ContextLogger { return f.NewLogger("") } func (f *defaultFactory) NewLogger(tag string) ContextLogger { return &observableLogger{f, tag} } func (f *defaultFactory) Subscribe() (subscription observable.Subscription[Entry], done <-chan struct{}, err error) { return f.observer.Subscribe() } func (f *defaultFactory) UnSubscribe(sub observable.Subscription[Entry]) { f.observer.UnSubscribe(sub) } var _ ContextLogger = (*observableLogger)(nil) type observableLogger struct { *defaultFactory tag string } func (l *observableLogger) Log(ctx context.Context, level Level, args []any) { level = OverrideLevelFromContext(level, ctx) if level > l.level && l.platformWriter == nil { return } nowTime := time.Now() if level <= l.level { if l.needObservable { message, messageSimple := l.formatter.FormatWithSimple(ctx, level, l.tag, F.ToString(args...), nowTime) if level == LevelPanic { panic(message) } l.writer.Write([]byte(message)) if level == LevelFatal { os.Exit(1) } l.subscriber.Emit(Entry{level, messageSimple}) } else { message := l.formatter.Format(ctx, level, l.tag, F.ToString(args...), nowTime) if level == LevelPanic { panic(message) } l.writer.Write([]byte(message)) if level == LevelFatal { os.Exit(1) } } } if l.platformWriter != nil { l.platformWriter.WriteMessage(level, l.platformFormatter.Format(ctx, level, l.tag, F.ToString(args...), nowTime)) } } func (l *observableLogger) Trace(args ...any) { l.TraceContext(context.Background(), args...) } func (l *observableLogger) Debug(args ...any) { l.DebugContext(context.Background(), args...) } func (l *observableLogger) Info(args ...any) { l.InfoContext(context.Background(), args...) } func (l *observableLogger) Warn(args ...any) { l.WarnContext(context.Background(), args...) } func (l *observableLogger) Error(args ...any) { l.ErrorContext(context.Background(), args...) } func (l *observableLogger) Fatal(args ...any) { l.FatalContext(context.Background(), args...) } func (l *observableLogger) Panic(args ...any) { l.PanicContext(context.Background(), args...) } func (l *observableLogger) TraceContext(ctx context.Context, args ...any) { l.Log(ctx, LevelTrace, args) } func (l *observableLogger) DebugContext(ctx context.Context, args ...any) { l.Log(ctx, LevelDebug, args) } func (l *observableLogger) InfoContext(ctx context.Context, args ...any) { l.Log(ctx, LevelInfo, args) } func (l *observableLogger) WarnContext(ctx context.Context, args ...any) { l.Log(ctx, LevelWarn, args) } func (l *observableLogger) ErrorContext(ctx context.Context, args ...any) { l.Log(ctx, LevelError, args) } func (l *observableLogger) FatalContext(ctx context.Context, args ...any) { l.Log(ctx, LevelFatal, args) } func (l *observableLogger) PanicContext(ctx context.Context, args ...any) { l.Log(ctx, LevelPanic, args) } ================================================ FILE: log/override.go ================================================ package log import ( "context" ) type overrideLevelKey struct{} func ContextWithOverrideLevel(ctx context.Context, level Level) context.Context { return context.WithValue(ctx, (*overrideLevelKey)(nil), level) } func OverrideLevelFromContext(origin Level, ctx context.Context) Level { level, loaded := ctx.Value((*overrideLevelKey)(nil)).(Level) if !loaded || origin > level { return origin } return level } ================================================ FILE: log/platform.go ================================================ package log type PlatformWriter interface { WriteMessage(level Level, message string) } ================================================ FILE: mkdocs.yml ================================================ site_name: sing-box site_url: https://sing-box.sagernet.org/ site_author: nekohasekai repo_url: https://github.com/SagerNet/sing-box repo_name: SagerNet/sing-box copyright: Copyright © 2022 nekohasekai site_description: The universal proxy platform. remote_branch: docs edit_uri: "" theme: name: material logo: assets/icon.svg favicon: assets/icon.svg palette: - media: "(prefers-color-scheme)" toggle: icon: material/link name: Switch to light mode - media: "(prefers-color-scheme: light)" scheme: default primary: white toggle: icon: material/toggle-switch name: Switch to dark mode - media: "(prefers-color-scheme: dark)" scheme: slate primary: black toggle: icon: material/toggle-switch-off name: Switch to system preference features: # - navigation.instant - navigation.tracking - navigation.tabs - navigation.indexes - navigation.expand - navigation.sections - header.autohide - content.code.copy - content.code.select - content.code.annotate icon: admonition: question: material/new-box nav: - Home: - index.md - Change Log: changelog.md - Migration: migration.md - Deprecated: deprecated.md - Support: support.md - Sponsors: sponsors.md - Installation: - Package Manager: installation/package-manager.md - Docker: installation/docker.md - Build from source: installation/build-from-source.md - Graphical Clients: - clients/index.md - Android: - clients/android/index.md - Features: clients/android/features.md - Apple platforms: - clients/apple/index.md - Features: clients/apple/features.md - General: clients/general.md - Privacy policy: clients/privacy.md - Manual: - Proxy: - Server: manual/proxy/server.md - Client: manual/proxy/client.md # - TUN: manual/proxy/tun.md - Proxy Protocol: - Shadowsocks: manual/proxy-protocol/shadowsocks.md - Trojan: manual/proxy-protocol/trojan.md - Hysteria 2: manual/proxy-protocol/hysteria2.md - Misc: - TunnelVision: manual/misc/tunnelvision.md - Configuration: - configuration/index.md - Log: - configuration/log/index.md - DNS: - configuration/dns/index.md - DNS Server: - configuration/dns/server/index.md - Legacy: configuration/dns/server/legacy.md - Local: configuration/dns/server/local.md - Hosts: configuration/dns/server/hosts.md - TCP: configuration/dns/server/tcp.md - UDP: configuration/dns/server/udp.md - TLS: configuration/dns/server/tls.md - QUIC: configuration/dns/server/quic.md - 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 - DNS Rule: configuration/dns/rule.md - DNS Rule Action: configuration/dns/rule_action.md - FakeIP: configuration/dns/fakeip.md - NTP: configuration/ntp/index.md - Certificate: configuration/certificate/index.md - Route: - configuration/route/index.md - GeoIP: configuration/route/geoip.md - Geosite: configuration/route/geosite.md - Route Rule: configuration/route/rule.md - Rule Action: configuration/route/rule_action.md - Protocol Sniff: configuration/route/sniff.md - Rule Set: - configuration/rule-set/index.md - Source Format: configuration/rule-set/source-format.md - Headless Rule: configuration/rule-set/headless-rule.md - AdGuard DNS Filer: configuration/rule-set/adguard.md - Experimental: - configuration/experimental/index.md - Cache File: configuration/experimental/cache-file.md - Clash API: configuration/experimental/clash-api.md - V2Ray API: configuration/experimental/v2ray-api.md - Shared: - 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 - 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 - V2Ray Transport: configuration/shared/v2ray-transport.md - 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 - Tailscale: configuration/endpoint/tailscale.md - Inbound: - configuration/inbound/index.md - Direct: configuration/inbound/direct.md - Mixed: configuration/inbound/mixed.md - SOCKS: configuration/inbound/socks.md - HTTP: configuration/inbound/http.md - Shadowsocks: configuration/inbound/shadowsocks.md - VMess: configuration/inbound/vmess.md - Trojan: configuration/inbound/trojan.md - Naive: configuration/inbound/naive.md - Hysteria: configuration/inbound/hysteria.md - ShadowTLS: configuration/inbound/shadowtls.md - VLESS: configuration/inbound/vless.md - TUIC: configuration/inbound/tuic.md - Hysteria2: configuration/inbound/hysteria2.md - AnyTLS: configuration/inbound/anytls.md - 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 - Block: configuration/outbound/block.md - SOCKS: configuration/outbound/socks.md - HTTP: configuration/outbound/http.md - Shadowsocks: configuration/outbound/shadowsocks.md - VMess: configuration/outbound/vmess.md - Trojan: configuration/outbound/trojan.md - Naive: configuration/outbound/naive.md - WireGuard: configuration/outbound/wireguard.md - Hysteria: configuration/outbound/hysteria.md - ShadowTLS: configuration/outbound/shadowtls.md - VLESS: configuration/outbound/vless.md - TUIC: configuration/outbound/tuic.md - Hysteria2: configuration/outbound/hysteria2.md - AnyTLS: configuration/outbound/anytls.md - Tor: configuration/outbound/tor.md - SSH: configuration/outbound/ssh.md - DNS: configuration/outbound/dns.md - Selector: configuration/outbound/selector.md - URLTest: configuration/outbound/urltest.md - Service: - configuration/service/index.md - DERP: configuration/service/derp.md - Resolved: configuration/service/resolved.md - 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 kwds: case: lower - pymdownx.inlinehilite - pymdownx.snippets - pymdownx.superfences - pymdownx.details - pymdownx.critic - pymdownx.caret - pymdownx.keys - pymdownx.mark - pymdownx.tilde - pymdownx.magiclink - admonition - attr_list - md_in_html - footnotes - def_list - pymdownx.highlight: anchor_linenums: true - pymdownx.tabbed: alternate_style: true - pymdownx.tasklist: custom_checkbox: true - pymdownx.emoji: emoji_index: !!python/name:material.extensions.emoji.twemoji emoji_generator: !!python/name:material.extensions.emoji.to_svg - pymdownx.superfences: custom_fences: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format extra: social: - icon: fontawesome/brands/github link: https://github.com/SagerNet/sing-box generator: false plugins: - search - i18n: docs_structure: suffix fallback_to_default: true languages: - build: true default: true locale: en name: English - build: true default: false locale: zh name: 简体中文 nav_translations: Home: 开始 Change Log: 更新日志 Migration: 迁移指南 Deprecated: 废弃功能列表 Support: 支持 Installation: 安装 Package Manager: 包管理器 Build from source: 从源代码构建 Graphical Clients: 图形界面客户端 Features: 特性 Apple platforms: Apple 平台 General: 通用 Privacy policy: 隐私政策 Configuration: 配置 Log: 日志 DNS Server: DNS 服务器 DNS Rule: DNS 规则 DNS Rule Action: DNS 规则动作 Route: 路由 Route Rule: 路由规则 Rule Action: 规则动作 Protocol Sniff: 协议探测 Rule Set: 规则集 Source Format: 源文件格式 Headless Rule: 无头规则 Experimental: 实验性 Cache File: 缓存文件 Shared: 通用 Listen Fields: 监听字段 Dial Fields: 拨号字段 Certificate Provider Fields: 证书提供者字段 DNS01 Challenge Fields: DNS01 验证字段 Multiplex: 多路复用 V2Ray Transport: V2Ray 传输层 Wi-Fi State: Wi-Fi 状态 Endpoint: 端点 Inbound: 入站 Outbound: 出站 Certificate Provider: 证书提供者 Manual: 手册 reconfigure_material: true reconfigure_search: true ================================================ FILE: option/acme.go ================================================ 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"` Profile string `json:"profile,omitempty"` HTTPClient *HTTPClientOptions `json:"http_client,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 } ================================================ FILE: option/anytls.go ================================================ package option import "github.com/sagernet/sing/common/json/badoption" type AnyTLSInboundOptions struct { ListenOptions InboundTLSOptionsContainer Users []AnyTLSUser `json:"users,omitempty"` PaddingScheme badoption.Listable[string] `json:"padding_scheme,omitempty"` } type AnyTLSUser struct { Name string `json:"name,omitempty"` Password string `json:"password,omitempty"` } type AnyTLSOutboundOptions struct { DialerOptions ServerOptions OutboundTLSOptionsContainer Password string `json:"password,omitempty"` IdleSessionCheckInterval badoption.Duration `json:"idle_session_check_interval,omitempty"` IdleSessionTimeout badoption.Duration `json:"idle_session_timeout,omitempty"` MinIdleSession int `json:"min_idle_session,omitempty"` } ================================================ FILE: option/ccm.go ================================================ package option import ( "github.com/sagernet/sing/common/json/badoption" ) type CCMServiceOptions struct { ListenOptions InboundTLSOptionsContainer CredentialPath string `json:"credential_path,omitempty"` Users []CCMUser `json:"users,omitempty"` Headers badoption.HTTPHeader `json:"headers,omitempty"` Detour string `json:"detour,omitempty"` UsagesPath string `json:"usages_path,omitempty"` } type CCMUser struct { Name string `json:"name,omitempty"` Token string `json:"token,omitempty"` } ================================================ FILE: option/certificate.go ================================================ package option import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json/badoption" ) type _CertificateOptions struct { Store string `json:"store,omitempty"` Certificate badoption.Listable[string] `json:"certificate,omitempty"` CertificatePath badoption.Listable[string] `json:"certificate_path,omitempty"` CertificateDirectoryPath badoption.Listable[string] `json:"certificate_directory_path,omitempty"` } type CertificateOptions _CertificateOptions func (o CertificateOptions) MarshalJSON() ([]byte, error) { switch o.Store { case C.CertificateStoreSystem: o.Store = "" } return json.Marshal((*_CertificateOptions)(&o)) } func (o *CertificateOptions) UnmarshalJSON(data []byte) error { err := json.Unmarshal(data, (*_CertificateOptions)(o)) if err != nil { return err } switch o.Store { case C.CertificateStoreSystem, "": o.Store = C.CertificateStoreSystem } return nil } ================================================ FILE: option/certificate_provider.go ================================================ 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 != "" } ================================================ FILE: option/cloudflared.go ================================================ 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"` } ================================================ FILE: option/debug.go ================================================ package option import "github.com/sagernet/sing/common/byteformats" type DebugOptions struct { Listen string `json:"listen,omitempty"` GCPercent *int `json:"gc_percent,omitempty"` MaxStack *int `json:"max_stack,omitempty"` MaxThreads *int `json:"max_threads,omitempty"` PanicOnFault *bool `json:"panic_on_fault,omitempty"` TraceBack string `json:"trace_back,omitempty"` MemoryLimit *byteformats.MemoryBytes `json:"memory_limit,omitempty"` OOMKiller *bool `json:"oom_killer,omitempty"` } ================================================ FILE: option/direct.go ================================================ package option import ( "context" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" ) type DirectInboundOptions struct { ListenOptions Network NetworkList `json:"network,omitempty"` OverrideAddress string `json:"override_address,omitempty"` OverridePort uint16 `json:"override_port,omitempty"` } type _DirectOutboundOptions struct { DialerOptions // Deprecated: Use Route Action instead OverrideAddress string `json:"override_address,omitempty"` // Deprecated: Use Route Action instead OverridePort uint16 `json:"override_port,omitempty"` // Deprecated: removed ProxyProtocol uint8 `json:"proxy_protocol,omitempty"` } type DirectOutboundOptions _DirectOutboundOptions func (d *DirectOutboundOptions) UnmarshalJSONContext(ctx context.Context, content []byte) error { err := json.UnmarshalDisallowUnknownFields(content, (*_DirectOutboundOptions)(d)) if err != nil { return err } //nolint:staticcheck if d.OverrideAddress != "" || d.OverridePort != 0 { return E.New("destination override fields in direct outbound are deprecated in sing-box 1.11.0 and removed in sing-box 1.13.0, use route options instead") } return nil } ================================================ FILE: option/dns.go ================================================ package option import ( "context" "net/netip" 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" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/service" ) type RawDNSOptions struct { Servers []DNSServerOptions `json:"servers,omitempty"` Rules []DNSRule `json:"rules,omitempty"` Final string `json:"final,omitempty"` ReverseMapping bool `json:"reverse_mapping,omitempty"` DNSClientOptions } type DNSOptions struct { RawDNSOptions } 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" ) type removedLegacyDNSOptions struct { FakeIP json.RawMessage `json:"fakeip,omitempty"` } func (o *DNSOptions) UnmarshalJSONContext(ctx context.Context, content []byte) error { var legacyOptions removedLegacyDNSOptions err := json.UnmarshalContext(ctx, content, &legacyOptions) if err != nil { return err } if len(legacyOptions.FakeIP) != 0 { return E.New(legacyDNSFakeIPRemovedMessage) } return badjson.UnmarshallExcludedContext(ctx, content, legacyOptions, &o.RawDNSOptions) } 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"` 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) } type _DNSServerOptions struct { Type string `json:"type,omitempty"` Tag string `json:"tag,omitempty"` Options any `json:"-"` } type DNSServerOptions _DNSServerOptions func (o *DNSServerOptions) MarshalJSONContext(ctx context.Context) ([]byte, error) { return badjson.MarshallObjectsContext(ctx, (*_DNSServerOptions)(o), o.Options) } func (o *DNSServerOptions) UnmarshalJSONContext(ctx context.Context, content []byte) error { err := json.UnmarshalContext(ctx, content, (*_DNSServerOptions)(o)) if err != nil { return err } registry := service.FromContext[DNSTransportOptionsRegistry](ctx) if registry == nil { return E.New("missing DNS transport options registry in context") } var options any switch o.Type { case "", C.DNSTypeLegacy: return E.New(legacyDNSServerRemovedMessage) default: var loaded bool options, loaded = registry.CreateOptions(o.Type) if !loaded { return E.New("unknown transport type: ", o.Type) } } err = badjson.UnmarshallExcludedContext(ctx, content, (*_DNSServerOptions)(o), options) if err != nil { return err } o.Options = options return nil } type DNSServerAddressOptions struct { Server string `json:"server"` ServerPort uint16 `json:"server_port,omitempty"` } func (o DNSServerAddressOptions) Build() M.Socksaddr { return M.ParseSocksaddrHostPort(o.Server, o.ServerPort) } func (o DNSServerAddressOptions) ServerIsDomain() bool { return o.Build().IsDomain() } func (o *DNSServerAddressOptions) TakeServerOptions() ServerOptions { return ServerOptions(*o) } func (o *DNSServerAddressOptions) ReplaceServerOptions(options ServerOptions) { *o = DNSServerAddressOptions(options) } type HostsDNSServerOptions struct { Path badoption.Listable[string] `json:"path,omitempty"` Predefined *badjson.TypedMap[string, badoption.Listable[netip.Addr]] `json:"predefined,omitempty"` } type RawLocalDNSServerOptions struct { DialerOptions } type LocalDNSServerOptions struct { RawLocalDNSServerOptions PreferGo bool `json:"prefer_go,omitempty"` NeighborDomain badoption.Listable[string] `json:"neighbor_domain,omitempty"` } type RemoteDNSServerOptions struct { RawLocalDNSServerOptions DNSServerAddressOptions } type RemoteTLSDNSServerOptions struct { RemoteDNSServerOptions OutboundTLSOptionsContainer } type RemoteHTTPSDNSServerOptions struct { RemoteTLSDNSServerOptions Path string `json:"path,omitempty"` Method string `json:"method,omitempty"` Headers badoption.HTTPHeader `json:"headers,omitempty"` } type FakeIPDNSServerOptions struct { Inet4Range *badoption.Prefix `json:"inet4_range,omitempty"` Inet6Range *badoption.Prefix `json:"inet6_range,omitempty"` } type DHCPDNSServerOptions struct { LocalDNSServerOptions Interface string `json:"interface,omitempty"` } type MDNSDNSServerOptions struct { LocalDNSServerOptions Interface badoption.Listable[string] `json:"interface,omitempty"` } ================================================ FILE: option/dns_record.go ================================================ package option import ( "encoding/base64" "strings" "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" M "github.com/sagernet/sing/common/metadata" "github.com/miekg/dns" ) const defaultDNSRecordTTL uint32 = 3600 type DNSRCode int func (r DNSRCode) MarshalJSON() ([]byte, error) { rCodeValue, loaded := dns.RcodeToString[int(r)] if loaded { return json.Marshal(rCodeValue) } return json.Marshal(int(r)) } func (r *DNSRCode) UnmarshalJSON(bytes []byte) error { var intValue int err := json.Unmarshal(bytes, &intValue) if err == nil { *r = DNSRCode(intValue) return nil } var stringValue string err = json.Unmarshal(bytes, &stringValue) if err != nil { return err } rCodeValue, loaded := dns.StringToRcode[stringValue] if !loaded { return E.New("unknown rcode: " + stringValue) } *r = DNSRCode(rCodeValue) return nil } func (r *DNSRCode) Build() int { if r == nil { return dns.RcodeSuccess } return int(*r) } type DNSRecordOptions struct { dns.RR fromBase64 bool } func (o DNSRecordOptions) MarshalJSON() ([]byte, error) { if o.fromBase64 { buffer := buf.Get(dns.Len(o.RR)) defer buf.Put(buffer) offset, err := dns.PackRR(o.RR, buffer, 0, nil, false) if err != nil { return nil, err } return json.Marshal(base64.StdEncoding.EncodeToString(buffer[:offset])) } return json.Marshal(o.RR.String()) } func (o *DNSRecordOptions) UnmarshalJSON(data []byte) error { var stringValue string err := json.Unmarshal(data, &stringValue) if err != nil { return err } binary, err := base64.StdEncoding.DecodeString(stringValue) if err == nil { return o.unmarshalBase64(binary) } 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() } o.RR = record 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 { return E.New("parse binary DNS record") } o.RR = record o.fromBase64 = true return nil } 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) } ================================================ FILE: option/dns_record_test.go ================================================ 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)) } ================================================ FILE: option/dns_test.go ================================================ 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) } } ================================================ FILE: option/endpoint.go ================================================ 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 EndpointOptionsRegistry interface { CreateOptions(endpointType string) (any, bool) } type _Endpoint struct { Type string `json:"type"` Tag string `json:"tag,omitempty"` Options any `json:"-"` } type Endpoint _Endpoint func (h *Endpoint) MarshalJSONContext(ctx context.Context) ([]byte, error) { return badjson.MarshallObjectsContext(ctx, (*_Endpoint)(h), h.Options) } func (h *Endpoint) UnmarshalJSONContext(ctx context.Context, content []byte) error { err := json.UnmarshalContext(ctx, content, (*_Endpoint)(h)) if err != nil { return err } registry := service.FromContext[EndpointOptionsRegistry](ctx) if registry == nil { return E.New("missing endpoint fields registry in context") } options, loaded := registry.CreateOptions(h.Type) if !loaded { return E.New("unknown endpoint type: ", h.Type) } err = badjson.UnmarshallExcludedContext(ctx, content, (*_Endpoint)(h), options) if err != nil { return err } h.Options = options return nil } ================================================ FILE: option/experimental.go ================================================ package option import "github.com/sagernet/sing/common/json/badoption" type ExperimentalOptions struct { CacheFile *CacheFileOptions `json:"cache_file,omitempty"` ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"` V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"` Debug *DebugOptions `json:"debug,omitempty"` } type CacheFileOptions struct { Enabled bool `json:"enabled,omitempty"` Path string `json:"path,omitempty"` CacheID string `json:"cache_id,omitempty"` 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 { ExternalController string `json:"external_controller,omitempty"` ExternalUI string `json:"external_ui,omitempty"` ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"` ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"` Secret string `json:"secret,omitempty"` DefaultMode string `json:"default_mode,omitempty"` ModeList []string `json:"-"` AccessControlAllowOrigin badoption.Listable[string] `json:"access_control_allow_origin,omitempty"` AccessControlAllowPrivateNetwork bool `json:"access_control_allow_private_network,omitempty"` // Deprecated: migrated to global cache file CacheFile string `json:"cache_file,omitempty"` // Deprecated: migrated to global cache file CacheID string `json:"cache_id,omitempty"` // Deprecated: migrated to global cache file StoreMode bool `json:"store_mode,omitempty"` // Deprecated: migrated to global cache file StoreSelected bool `json:"store_selected,omitempty"` // Deprecated: migrated to global cache file StoreFakeIP bool `json:"store_fakeip,omitempty"` } type V2RayAPIOptions struct { Listen string `json:"listen,omitempty"` Stats *V2RayStatsServiceOptions `json:"stats,omitempty"` } type V2RayStatsServiceOptions struct { Enabled bool `json:"enabled,omitempty"` Inbounds []string `json:"inbounds,omitempty"` Outbounds []string `json:"outbounds,omitempty"` Users []string `json:"users,omitempty"` } ================================================ FILE: option/group.go ================================================ package option import "github.com/sagernet/sing/common/json/badoption" type SelectorOutboundOptions struct { Outbounds []string `json:"outbounds"` Default string `json:"default,omitempty"` InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` } type URLTestOutboundOptions struct { Outbounds []string `json:"outbounds"` URL string `json:"url,omitempty"` Interval badoption.Duration `json:"interval,omitempty"` Tolerance uint16 `json:"tolerance,omitempty"` IdleTimeout badoption.Duration `json:"idle_timeout,omitempty"` InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` } ================================================ FILE: option/http.go ================================================ 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:"-"` DisableEmptyDirectCheck 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 } } ================================================ FILE: option/hysteria.go ================================================ package option import ( "github.com/sagernet/sing/common/byteformats" "github.com/sagernet/sing/common/json/badoption" ) 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"` // 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 { Name string `json:"name,omitempty"` Auth []byte `json:"auth,omitempty"` AuthString string `json:"auth_str,omitempty"` } 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"` // 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 } ================================================ FILE: option/hysteria2.go ================================================ package option import ( "net/url" 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 Hysteria2InboundOptions struct { ListenOptions UpMbps int `json:"up_mbps,omitempty"` DownMbps int `json:"down_mbps,omitempty"` Obfs *Hysteria2Obfs `json:"obfs,omitempty"` 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"` Realm *Hysteria2InboundRealm `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 Hysteria2InboundRealm struct { Hysteria2Realm STUNDomainResolver *DomainResolveOptions `json:"stun_domain_resolver,omitempty"` } type Hysteria2Obfs struct { Type string `json:"type,omitempty"` Password string `json:"password,omitempty"` } type Hysteria2User struct { Name string `json:"name,omitempty"` Password string `json:"password,omitempty"` } type _Hysteria2Masquerade struct { Type string `json:"type,omitempty"` FileOptions Hysteria2MasqueradeFile `json:"-"` ProxyOptions Hysteria2MasqueradeProxy `json:"-"` StringOptions Hysteria2MasqueradeString `json:"-"` } type Hysteria2Masquerade _Hysteria2Masquerade func (m Hysteria2Masquerade) MarshalJSON() ([]byte, error) { var v any switch m.Type { case C.Hysterai2MasqueradeTypeFile: v = m.FileOptions case C.Hysterai2MasqueradeTypeProxy: v = m.ProxyOptions case C.Hysterai2MasqueradeTypeString: v = m.StringOptions default: return nil, E.New("unknown masquerade type: ", m.Type) } return badjson.MarshallObjects((_Hysteria2Masquerade)(m), v) } func (m *Hysteria2Masquerade) UnmarshalJSON(bytes []byte) error { var urlString string err := json.Unmarshal(bytes, &urlString) if err == nil { masqueradeURL, err := url.Parse(urlString) if err != nil { return E.Cause(err, "invalid masquerade URL") } switch masqueradeURL.Scheme { case "file": m.Type = C.Hysterai2MasqueradeTypeFile m.FileOptions.Directory = masqueradeURL.Path case "http", "https": m.Type = C.Hysterai2MasqueradeTypeProxy m.ProxyOptions.URL = urlString default: return E.New("unknown masquerade URL scheme: ", masqueradeURL.Scheme) } return nil } err = json.Unmarshal(bytes, (*_Hysteria2Masquerade)(m)) if err != nil { return err } var v any switch m.Type { case C.Hysterai2MasqueradeTypeFile: v = &m.FileOptions case C.Hysterai2MasqueradeTypeProxy: v = &m.ProxyOptions case C.Hysterai2MasqueradeTypeString: v = &m.StringOptions default: return E.New("unknown masquerade type: ", m.Type) } return badjson.UnmarshallExcluded(bytes, (*_Hysteria2Masquerade)(m), v) } type Hysteria2MasqueradeFile struct { Directory string `json:"directory"` } type Hysteria2MasqueradeProxy struct { URL string `json:"url"` RewriteHost bool `json:"rewrite_host,omitempty"` } type Hysteria2MasqueradeString struct { StatusCode int `json:"status_code,omitempty"` Headers badoption.HTTPHeader `json:"headers,omitempty"` Content string `json:"content"` } type Hysteria2OutboundOptions struct { DialerOptions ServerOptions 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 QUICOptions 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 HTTP2Options Users []HysteriaRealmUser `json:"users"` } ================================================ FILE: option/inbound.go ================================================ package option import ( "context" "time" 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" "github.com/sagernet/sing/service" ) type InboundOptionsRegistry interface { CreateOptions(outboundType string) (any, bool) } type _Inbound struct { Type string `json:"type"` Tag string `json:"tag,omitempty"` Options any `json:"-"` } type Inbound _Inbound func (h *Inbound) MarshalJSONContext(ctx context.Context) ([]byte, error) { return badjson.MarshallObjectsContext(ctx, (*_Inbound)(h), h.Options) } func (h *Inbound) UnmarshalJSONContext(ctx context.Context, content []byte) error { err := json.UnmarshalContext(ctx, content, (*_Inbound)(h)) if err != nil { return err } registry := service.FromContext[InboundOptionsRegistry](ctx) if registry == nil { return E.New("missing inbound fields registry in context") } options, loaded := registry.CreateOptions(h.Type) if !loaded { return E.New("unknown inbound type: ", h.Type) } err = badjson.UnmarshallExcludedContext(ctx, content, (*_Inbound)(h), options) if err != nil { return err } if listenWrapper, isListen := options.(ListenOptionsWrapper); isListen { //nolint:staticcheck if listenWrapper.TakeListenOptions().InboundOptions != (InboundOptions{}) { return E.New("legacy inbound fields are deprecated in sing-box 1.11.0 and removed in sing-box 1.13.0, checkout migration: https://sing-box.sagernet.org/migration/#migrate-legacy-inbound-fields-to-rule-actions") } } h.Options = options return nil } // Deprecated: Use rule action instead type InboundOptions struct { SniffEnabled bool `json:"sniff,omitempty"` SniffOverrideDestination bool `json:"sniff_override_destination,omitempty"` SniffTimeout badoption.Duration `json:"sniff_timeout,omitempty"` DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"` UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty"` } type ListenOptions struct { Listen *badoption.Addr `json:"listen,omitempty"` ListenPort uint16 `json:"listen_port,omitempty"` BindInterface string `json:"bind_interface,omitempty"` RoutingMark FwMark `json:"routing_mark,omitempty"` ReuseAddr bool `json:"reuse_addr,omitempty"` NetNs string `json:"netns,omitempty"` DisableTCPKeepAlive bool `json:"disable_tcp_keep_alive,omitempty"` TCPKeepAlive badoption.Duration `json:"tcp_keep_alive,omitempty"` TCPKeepAliveInterval badoption.Duration `json:"tcp_keep_alive_interval,omitempty"` TCPFastOpen bool `json:"tcp_fast_open,omitempty"` TCPMultiPath bool `json:"tcp_multi_path,omitempty"` UDPFragment *bool `json:"udp_fragment,omitempty"` UDPFragmentDefault bool `json:"-"` UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"` Detour string `json:"detour,omitempty"` // Deprecated: removed ProxyProtocol bool `json:"proxy_protocol,omitempty"` // Deprecated: removed ProxyProtocolAcceptNoHeader bool `json:"proxy_protocol_accept_no_header,omitempty"` InboundOptions } type UDPTimeoutCompat badoption.Duration func (c UDPTimeoutCompat) MarshalJSON() ([]byte, error) { return json.Marshal((time.Duration)(c).String()) } func (c *UDPTimeoutCompat) UnmarshalJSON(data []byte) error { var valueNumber int64 err := json.Unmarshal(data, &valueNumber) if err == nil { *c = UDPTimeoutCompat(time.Second * time.Duration(valueNumber)) return nil } return json.Unmarshal(data, (*badoption.Duration)(c)) } type ListenOptionsWrapper interface { TakeListenOptions() ListenOptions ReplaceListenOptions(options ListenOptions) } func (o *ListenOptions) TakeListenOptions() ListenOptions { return *o } func (o *ListenOptions) ReplaceListenOptions(options ListenOptions) { *o = options } ================================================ FILE: option/multiplex.go ================================================ package option type InboundMultiplexOptions struct { Enabled bool `json:"enabled,omitempty"` Padding bool `json:"padding,omitempty"` Brutal *BrutalOptions `json:"brutal,omitempty"` } type OutboundMultiplexOptions struct { Enabled bool `json:"enabled,omitempty"` Protocol string `json:"protocol,omitempty"` MaxConnections int `json:"max_connections,omitempty"` MinStreams int `json:"min_streams,omitempty"` MaxStreams int `json:"max_streams,omitempty"` Padding bool `json:"padding,omitempty"` Brutal *BrutalOptions `json:"brutal,omitempty"` } type BrutalOptions struct { Enabled bool `json:"enabled,omitempty"` UpMbps int `json:"up_mbps,omitempty"` DownMbps int `json:"down_mbps,omitempty"` } ================================================ FILE: option/naive.go ================================================ package option import ( "github.com/sagernet/sing/common/auth" "github.com/sagernet/sing/common/byteformats" "github.com/sagernet/sing/common/json/badoption" ) type QuicheCongestionControl string const ( QuicheCongestionControlDefault QuicheCongestionControl = "" QuicheCongestionControlBBR QuicheCongestionControl = "TBBR" QuicheCongestionControlBBRv2 QuicheCongestionControl = "B2ON" QuicheCongestionControlCubic QuicheCongestionControl = "QBIC" QuicheCongestionControlReno QuicheCongestionControl = "RENO" ) type NaiveInboundOptions struct { ListenOptions Users []auth.User `json:"users,omitempty"` Network NetworkList `json:"network,omitempty"` QUICCongestionControl string `json:"quic_congestion_control,omitempty"` InboundTLSOptionsContainer } type NaiveOutboundOptions struct { DialerOptions ServerOptions Username string `json:"username,omitempty"` Password string `json:"password,omitempty"` InsecureConcurrency int `json:"insecure_concurrency,omitempty"` ExtraHeaders badoption.HTTPHeader `json:"extra_headers,omitempty"` ReceiveWindow *byteformats.MemoryBytes `json:"stream_receive_window,omitempty"` UDPOverTCP *UDPOverTCPOptions `json:"udp_over_tcp,omitempty"` QUIC bool `json:"quic,omitempty"` QUICCongestionControl string `json:"quic_congestion_control,omitempty"` QUICSessionReceiveWindow *byteformats.MemoryBytes `json:"quic_session_receive_window,omitempty"` OutboundTLSOptionsContainer } ================================================ FILE: option/ntp.go ================================================ package option import "github.com/sagernet/sing/common/json/badoption" type NTPOptions struct { Enabled bool `json:"enabled,omitempty"` Interval badoption.Duration `json:"interval,omitempty"` WriteToSystem bool `json:"write_to_system,omitempty"` ServerOptions DialerOptions } ================================================ FILE: option/ocm.go ================================================ package option import ( "github.com/sagernet/sing/common/json/badoption" ) type OCMServiceOptions struct { ListenOptions InboundTLSOptionsContainer CredentialPath string `json:"credential_path,omitempty"` Users []OCMUser `json:"users,omitempty"` Headers badoption.HTTPHeader `json:"headers,omitempty"` Detour string `json:"detour,omitempty"` UsagesPath string `json:"usages_path,omitempty"` } type OCMUser struct { Name string `json:"name,omitempty"` Token string `json:"token,omitempty"` } ================================================ FILE: option/oom_killer.go ================================================ package option import ( "github.com/sagernet/sing/common/byteformats" "github.com/sagernet/sing/common/json/badoption" ) 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"` KillerDisabled bool `json:"-"` MemoryLimitOverride uint64 `json:"-"` } ================================================ FILE: option/options.go ================================================ package option import ( "bytes" "context" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json" ) 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"` 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"` Route *RouteOptions `json:"route,omitempty"` Services []Service `json:"services,omitempty"` Experimental *ExperimentalOptions `json:"experimental,omitempty"` } 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() err := decoder.Decode((*_Options)(o)) if err != nil { return err } o.RawMessage = content 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"` Output string `json:"output,omitempty"` Timestamp bool `json:"timestamp,omitempty"` DisableColor bool `json:"-"` } type StubOptions struct{} func checkOptions(options *Options) error { err := checkInbounds(options.Inbounds) if err != nil { return err } err = checkOutbounds(options.Outbounds, options.Endpoints) if err != nil { return err } err = checkCertificateProviders(options.CertificateProviders) if err != nil { return err } err = checkHTTPClients(options.HTTPClients) 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 } 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 { tag := inbound.Tag if tag == "" { tag = F.ToString(i) } if seen[tag] { return E.New("duplicate inbound tag: ", tag) } seen[tag] = true } return nil } func checkOutbounds(outbounds []Outbound, endpoints []Endpoint) error { seen := make(map[string]bool) for i, outbound := range outbounds { tag := outbound.Tag if tag == "" { tag = F.ToString(i) } if seen[tag] { return E.New("duplicate outbound/endpoint tag: ", tag) } seen[tag] = true } for i, endpoint := range endpoints { tag := endpoint.Tag if tag == "" { tag = F.ToString(i) } if seen[tag] { return E.New("duplicate outbound/endpoint tag: ", tag) } seen[tag] = true } return nil } ================================================ FILE: option/origin_ca.go ================================================ 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"` HTTPClient *HTTPClientOptions `json:"http_client,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 } ================================================ FILE: option/outbound.go ================================================ package option import ( "context" 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" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/service" ) type OutboundOptionsRegistry interface { CreateOptions(outboundType string) (any, bool) } type _Outbound struct { Type string `json:"type"` Tag string `json:"tag,omitempty"` Options any `json:"-"` } type Outbound _Outbound func (h *Outbound) MarshalJSONContext(ctx context.Context) ([]byte, error) { return badjson.MarshallObjectsContext(ctx, (*_Outbound)(h), h.Options) } func (h *Outbound) UnmarshalJSONContext(ctx context.Context, content []byte) error { err := json.UnmarshalContext(ctx, content, (*_Outbound)(h)) if err != nil { return err } registry := service.FromContext[OutboundOptionsRegistry](ctx) if registry == nil { return E.New("missing outbound options registry in context") } switch h.Type { case C.TypeDNS: return E.New("dns outbound is deprecated in sing-box 1.11.0 and removed in sing-box 1.13.0, use rule actions instead") } options, loaded := registry.CreateOptions(h.Type) if !loaded { return E.New("unknown outbound type: ", h.Type) } err = badjson.UnmarshallExcludedContext(ctx, content, (*_Outbound)(h), options) if err != nil { return err } if listenWrapper, isListen := options.(ListenOptionsWrapper); isListen { //nolint:staticcheck if listenWrapper.TakeListenOptions().InboundOptions != (InboundOptions{}) { return E.New("legacy inbound fields are deprecated in sing-box 1.11.0 and removed in sing-box 1.13.0, use rule actions instead") } } h.Options = options return nil } type DialerOptionsWrapper interface { TakeDialerOptions() DialerOptions ReplaceDialerOptions(options DialerOptions) } type DialerOptions struct { Detour string `json:"detour,omitempty"` BindInterface string `json:"bind_interface,omitempty"` Inet4BindAddress *badoption.Addr `json:"inet4_bind_address,omitempty"` Inet6BindAddress *badoption.Addr `json:"inet6_bind_address,omitempty"` BindAddressNoPort bool `json:"bind_address_no_port,omitempty"` ProtectPath string `json:"protect_path,omitempty"` RoutingMark FwMark `json:"routing_mark,omitempty"` ReuseAddr bool `json:"reuse_addr,omitempty"` NetNs string `json:"netns,omitempty"` ConnectTimeout badoption.Duration `json:"connect_timeout,omitempty"` TCPFastOpen bool `json:"tcp_fast_open,omitempty"` TCPMultiPath bool `json:"tcp_multi_path,omitempty"` DisableTCPKeepAlive bool `json:"disable_tcp_keep_alive,omitempty"` TCPKeepAlive badoption.Duration `json:"tcp_keep_alive,omitempty"` TCPKeepAliveInterval badoption.Duration `json:"tcp_keep_alive_interval,omitempty"` UDPFragment *bool `json:"udp_fragment,omitempty"` UDPFragmentDefault bool `json:"-"` DomainResolver *DomainResolveOptions `json:"domain_resolver,omitempty"` NetworkStrategy *NetworkStrategy `json:"network_strategy,omitempty"` NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"` FallbackNetworkType badoption.Listable[InterfaceType] `json:"fallback_network_type,omitempty"` FallbackDelay badoption.Duration `json:"fallback_delay,omitempty"` // Deprecated: migrated to domain resolver DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"` } 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"` RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` } type DomainResolveOptions _DomainResolveOptions 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 && o.ClientSubnet == nil { return json.Marshal(o.Server) } else { return json.Marshal((_DomainResolveOptions)(o)) } } func (o *DomainResolveOptions) UnmarshalJSON(bytes []byte) error { var stringValue string err := json.Unmarshal(bytes, &stringValue) if err == nil { o.Server = stringValue return nil } err = json.Unmarshal(bytes, (*_DomainResolveOptions)(o)) if err != nil { return err } if o.Server == "" { return E.New("empty domain_resolver.server") } return nil } func (o *DialerOptions) TakeDialerOptions() DialerOptions { return *o } func (o *DialerOptions) ReplaceDialerOptions(options DialerOptions) { *o = options } type ServerOptionsWrapper interface { TakeServerOptions() ServerOptions ReplaceServerOptions(options ServerOptions) } type ServerOptions struct { Server string `json:"server"` ServerPort uint16 `json:"server_port"` } func (o ServerOptions) Build() M.Socksaddr { return M.ParseSocksaddrHostPort(o.Server, o.ServerPort) } func (o ServerOptions) ServerIsDomain() bool { return o.Build().IsDomain() } func (o *ServerOptions) TakeServerOptions() ServerOptions { return *o } func (o *ServerOptions) ReplaceServerOptions(options ServerOptions) { *o = options } ================================================ FILE: option/platform.go ================================================ package option import ( E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json/badoption" ) type OnDemandOptions struct { Enabled bool `json:"enabled,omitempty"` Rules []OnDemandRule `json:"rules,omitempty"` } type OnDemandRule struct { Action *OnDemandRuleAction `json:"action,omitempty"` DNSSearchDomainMatch badoption.Listable[string] `json:"dns_search_domain_match,omitempty"` DNSServerAddressMatch badoption.Listable[string] `json:"dns_server_address_match,omitempty"` InterfaceTypeMatch *OnDemandRuleInterfaceType `json:"interface_type_match,omitempty"` SSIDMatch badoption.Listable[string] `json:"ssid_match,omitempty"` ProbeURL string `json:"probe_url,omitempty"` } type OnDemandRuleAction int func (r *OnDemandRuleAction) MarshalJSON() ([]byte, error) { if r == nil { return nil, nil } value := *r var actionName string switch value { case 1: actionName = "connect" case 2: actionName = "disconnect" case 3: actionName = "evaluate_connection" default: return nil, E.New("unknown action: ", value) } return json.Marshal(actionName) } func (r *OnDemandRuleAction) UnmarshalJSON(bytes []byte) error { var actionName string if err := json.Unmarshal(bytes, &actionName); err != nil { return err } var actionValue int switch actionName { case "connect": actionValue = 1 case "disconnect": actionValue = 2 case "evaluate_connection": actionValue = 3 case "ignore": actionValue = 4 default: return E.New("unknown action name: ", actionName) } *r = OnDemandRuleAction(actionValue) return nil } type OnDemandRuleInterfaceType int func (r *OnDemandRuleInterfaceType) MarshalJSON() ([]byte, error) { if r == nil { return nil, nil } value := *r var interfaceTypeName string switch value { case 1: interfaceTypeName = "any" case 2: interfaceTypeName = "wifi" case 3: interfaceTypeName = "cellular" default: return nil, E.New("unknown interface type: ", value) } return json.Marshal(interfaceTypeName) } func (r *OnDemandRuleInterfaceType) UnmarshalJSON(bytes []byte) error { var interfaceTypeName string if err := json.Unmarshal(bytes, &interfaceTypeName); err != nil { return err } var interfaceTypeValue int switch interfaceTypeName { case "any": interfaceTypeValue = 1 case "wifi": interfaceTypeValue = 2 case "cellular": interfaceTypeValue = 3 default: return E.New("unknown interface type name: ", interfaceTypeName) } *r = OnDemandRuleInterfaceType(interfaceTypeValue) return nil } ================================================ FILE: option/redir.go ================================================ package option type RedirectInboundOptions struct { ListenOptions } type TProxyInboundOptions struct { ListenOptions Network NetworkList `json:"network,omitempty"` } ================================================ FILE: option/resolved.go ================================================ package option import ( "context" "net/netip" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json/badoption" ) type _ResolvedServiceOptions struct { ListenOptions } type ResolvedServiceOptions _ResolvedServiceOptions func (r ResolvedServiceOptions) MarshalJSONContext(ctx context.Context) ([]byte, error) { if r.Listen != nil && netip.Addr(*r.Listen) == (netip.AddrFrom4([4]byte{127, 0, 0, 53})) { r.Listen = nil } if r.ListenPort == 53 { r.ListenPort = 0 } return json.MarshalContext(ctx, (*_ResolvedServiceOptions)(&r)) } func (r *ResolvedServiceOptions) UnmarshalJSONContext(ctx context.Context, bytes []byte) error { err := json.UnmarshalContextDisallowUnknownFields(ctx, bytes, (*_ResolvedServiceOptions)(r)) if err != nil { return err } if r.Listen == nil { r.Listen = (*badoption.Addr)(common.Ptr(netip.AddrFrom4([4]byte{127, 0, 0, 53}))) } if r.ListenPort == 0 { r.ListenPort = 53 } return nil } type ResolvedDNSServerOptions struct { Service string `json:"service"` AcceptDefaultResolvers bool `json:"accept_default_resolvers,omitempty"` // NDots int `json:"ndots,omitempty"` // Timeout badoption.Duration `json:"timeout,omitempty"` // Attempts int `json:"attempts,omitempty"` // Rotate bool `json:"rotate,omitempty"` } ================================================ FILE: option/route.go ================================================ package option import "github.com/sagernet/sing/common/json/badoption" type RouteOptions struct { GeoIP *GeoIPOptions `json:"geoip,omitempty"` Geosite *GeositeOptions `json:"geosite,omitempty"` Rules []Rule `json:"rules,omitempty"` 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"` DefaultMark FwMark `json:"default_mark,omitempty"` DefaultDomainResolver *DomainResolveOptions `json:"default_domain_resolver,omitempty"` DefaultNetworkStrategy *NetworkStrategy `json:"default_network_strategy,omitempty"` 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 { Path string `json:"path,omitempty"` DownloadURL string `json:"download_url,omitempty"` DownloadDetour string `json:"download_detour,omitempty"` } type GeositeOptions struct { Path string `json:"path,omitempty"` DownloadURL string `json:"download_url,omitempty"` DownloadDetour string `json:"download_detour,omitempty"` } ================================================ FILE: option/rule.go ================================================ package option import ( "context" "reflect" C "github.com/sagernet/sing-box/constant" "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" ) type _Rule struct { Type string `json:"type,omitempty"` DefaultOptions DefaultRule `json:"-"` LogicalOptions LogicalRule `json:"-"` } type Rule _Rule func (r Rule) MarshalJSON() ([]byte, error) { var v any switch r.Type { case C.RuleTypeDefault: r.Type = "" v = r.DefaultOptions case C.RuleTypeLogical: v = r.LogicalOptions default: return nil, E.New("unknown rule type: " + r.Type) } return badjson.MarshallObjects((_Rule)(r), v) } 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 } switch r.Type { case "", C.RuleTypeDefault: r.Type = C.RuleTypeDefault return unmarshalDefaultRuleContext(ctx, payload, &r.DefaultOptions) case C.RuleTypeLogical: return unmarshalLogicalRuleContext(ctx, payload, &r.LogicalOptions) default: return E.New("unknown rule type: " + r.Type) } } func (r Rule) IsValid() bool { switch r.Type { case C.RuleTypeDefault: return r.DefaultOptions.IsValid() case C.RuleTypeLogical: return r.LogicalOptions.IsValid() default: panic("unknown rule type: " + r.Type) } } type RawDefaultRule struct { Inbound badoption.Listable[string] `json:"inbound,omitempty"` IPVersion int `json:"ip_version,omitempty"` Network badoption.Listable[string] `json:"network,omitempty"` AuthUser badoption.Listable[string] `json:"auth_user,omitempty"` Protocol badoption.Listable[string] `json:"protocol,omitempty"` Client badoption.Listable[string] `json:"client,omitempty"` Domain badoption.Listable[string] `json:"domain,omitempty"` 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"` SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"` SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` IPIsPrivate bool `json:"ip_is_private,omitempty"` SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"` SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"` Port badoption.Listable[uint16] `json:"port,omitempty"` PortRange badoption.Listable[string] `json:"port_range,omitempty"` ProcessName badoption.Listable[string] `json:"process_name,omitempty"` 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"` NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"` NetworkIsExpensive bool `json:"network_is_expensive,omitempty"` NetworkIsConstrained bool `json:"network_is_constrained,omitempty"` WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"` WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"` 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"` Invert bool `json:"invert,omitempty"` // Deprecated: renamed to rule_set_ip_cidr_match_source Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` } type DefaultRule struct { RawDefaultRule RuleAction } func (r DefaultRule) MarshalJSON() ([]byte, error) { return badjson.MarshallObjects(r.RawDefaultRule, r.RuleAction) } func (r *DefaultRule) UnmarshalJSON(data []byte) error { err := json.Unmarshal(data, &r.RawDefaultRule) if err != nil { return err } return badjson.UnmarshallExcluded(data, &r.RawDefaultRule, &r.RuleAction) } func (r DefaultRule) IsValid() bool { var defaultValue DefaultRule defaultValue.Invert = r.Invert return !reflect.DeepEqual(r, defaultValue) } type RawLogicalRule struct { Mode string `json:"mode"` Rules []Rule `json:"rules,omitempty"` Invert bool `json:"invert,omitempty"` } type LogicalRule struct { RawLogicalRule RuleAction } func (r LogicalRule) MarshalJSON() ([]byte, error) { return badjson.MarshallObjects(r.RawLogicalRule, r.RuleAction) } func (r *LogicalRule) UnmarshalJSON(data []byte) error { err := json.Unmarshal(data, &r.RawLogicalRule) if err != nil { return err } 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) } ================================================ FILE: option/rule_action.go ================================================ package option import ( "context" "fmt" "net/netip" "time" 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 _RuleAction struct { Action string `json:"action,omitempty"` RouteOptions RouteActionOptions `json:"-"` RouteOptionsOptions RouteOptionsActionOptions `json:"-"` DirectOptions DirectActionOptions `json:"-"` BypassOptions RouteActionOptions `json:"-"` RejectOptions RejectActionOptions `json:"-"` SniffOptions RouteActionSniff `json:"-"` ResolveOptions RouteActionResolve `json:"-"` } type RuleAction _RuleAction func (r RuleAction) MarshalJSON() ([]byte, error) { if r.Action == "" { return json.Marshal(struct{}{}) } var v any switch r.Action { case C.RuleActionTypeRoute: r.Action = "" v = r.RouteOptions case C.RuleActionTypeRouteOptions: v = r.RouteOptionsOptions case C.RuleActionTypeDirect: v = r.DirectOptions case C.RuleActionTypeBypass: v = r.BypassOptions case C.RuleActionTypeReject: v = r.RejectOptions case C.RuleActionTypeHijackDNS: v = nil case C.RuleActionTypeSniff: v = r.SniffOptions case C.RuleActionTypeResolve: v = r.ResolveOptions default: return nil, E.New("unknown rule action: " + r.Action) } if v == nil { return badjson.MarshallObjects((_RuleAction)(r)) } return badjson.MarshallObjects((_RuleAction)(r), v) } func (r *RuleAction) UnmarshalJSON(data []byte) error { err := json.Unmarshal(data, (*_RuleAction)(r)) if err != nil { return err } var v any switch r.Action { case "", C.RuleActionTypeRoute: r.Action = C.RuleActionTypeRoute v = &r.RouteOptions case C.RuleActionTypeRouteOptions: v = &r.RouteOptionsOptions case C.RuleActionTypeDirect: v = &r.DirectOptions case C.RuleActionTypeBypass: v = &r.BypassOptions case C.RuleActionTypeReject: v = &r.RejectOptions case C.RuleActionTypeHijackDNS: v = nil case C.RuleActionTypeSniff: v = &r.SniffOptions case C.RuleActionTypeResolve: v = &r.ResolveOptions default: return E.New("unknown rule action: " + r.Action) } if v == nil { // check unknown fields return json.UnmarshalDisallowUnknownFields(data, &_RuleAction{}) } err = badjson.UnmarshallExcluded(data, (*_RuleAction)(r), v) if err != nil { return err } return nil } type _DNSRuleAction struct { Action string `json:"action,omitempty"` RouteOptions DNSRouteActionOptions `json:"-"` RouteOptionsOptions DNSRouteOptionsActionOptions `json:"-"` RejectOptions RejectActionOptions `json:"-"` PredefinedOptions DNSRouteActionPredefined `json:"-"` } type DNSRuleAction _DNSRuleAction func (r DNSRuleAction) MarshalJSON() ([]byte, error) { if r.Action == "" { return json.Marshal(struct{}{}) } var v any switch r.Action { 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: v = r.RejectOptions case C.RuleActionTypePredefined: v = r.PredefinedOptions 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) } func (r *DNSRuleAction) UnmarshalJSONContext(ctx context.Context, data []byte) error { err := json.Unmarshal(data, (*_DNSRuleAction)(r)) if err != nil { return err } var v any switch r.Action { 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: v = &r.RejectOptions case C.RuleActionTypePredefined: v = &r.PredefinedOptions 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) } type RouteActionOptions struct { Outbound string `json:"outbound,omitempty"` RawRouteOptionsActionOptions } type RawRouteOptionsActionOptions struct { OverrideAddress string `json:"override_address,omitempty"` OverridePort uint16 `json:"override_port,omitempty"` NetworkStrategy *NetworkStrategy `json:"network_strategy,omitempty"` FallbackDelay uint32 `json:"fallback_delay,omitempty"` UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty"` UDPConnect bool `json:"udp_connect,omitempty"` UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"` 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 func (r *RouteOptionsActionOptions) UnmarshalJSON(data []byte) error { err := json.Unmarshal(data, (*RawRouteOptionsActionOptions)(r)) if err != nil { return err } if *r == (RouteOptionsActionOptions{}) { return E.New("empty route option action") } if r.TLSFragment && r.TLSRecordFragment { return E.New("`tls_fragment` and `tls_record_fragment` are mutually exclusive") } return nil } 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"` RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` } 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"` ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` } type DNSRouteOptionsActionOptions _DNSRouteOptionsActionOptions func (r *DNSRouteOptionsActionOptions) UnmarshalJSON(data []byte) error { err := json.Unmarshal(data, (*_DNSRouteOptionsActionOptions)(r)) if err != nil { return err } if *r == (DNSRouteOptionsActionOptions{}) { return E.New("empty DNS route option action") } return nil } type _DirectActionOptions DialerOptions type DirectActionOptions _DirectActionOptions func (d DirectActionOptions) Descriptions() []string { var descriptions []string if d.BindInterface != "" { descriptions = append(descriptions, "bind_interface="+d.BindInterface) } if d.Inet4BindAddress != nil { descriptions = append(descriptions, "inet4_bind_address="+d.Inet4BindAddress.Build(netip.IPv4Unspecified()).String()) } if d.Inet6BindAddress != nil { descriptions = append(descriptions, "inet6_bind_address="+d.Inet6BindAddress.Build(netip.IPv6Unspecified()).String()) } if d.RoutingMark != 0 { descriptions = append(descriptions, "routing_mark="+fmt.Sprintf("0x%x", d.RoutingMark)) } if d.ReuseAddr { descriptions = append(descriptions, "reuse_addr") } if d.ConnectTimeout != 0 { descriptions = append(descriptions, "connect_timeout="+time.Duration(d.ConnectTimeout).String()) } if d.TCPFastOpen { descriptions = append(descriptions, "tcp_fast_open") } if d.TCPMultiPath { descriptions = append(descriptions, "tcp_multi_path") } if d.UDPFragment != nil { descriptions = append(descriptions, "udp_fragment="+fmt.Sprint(*d.UDPFragment)) } if d.DomainStrategy != DomainStrategy(C.DomainStrategyAsIS) { descriptions = append(descriptions, "domain_strategy="+d.DomainStrategy.String()) } if d.FallbackDelay != 0 { descriptions = append(descriptions, "fallback_delay="+time.Duration(d.FallbackDelay).String()) } return descriptions } func (d *DirectActionOptions) UnmarshalJSON(data []byte) error { err := json.Unmarshal(data, (*_DirectActionOptions)(d)) if err != nil { return err } if d.Detour != "" { return E.New("detour is not available in the current context") } return nil } type _RejectActionOptions struct { Method string `json:"method,omitempty"` NoDrop bool `json:"no_drop,omitempty"` } type RejectActionOptions _RejectActionOptions func (r RejectActionOptions) MarshalJSON() ([]byte, error) { switch r.Method { case C.RuleActionRejectMethodDefault: r.Method = "" } return json.Marshal((_RejectActionOptions)(r)) } func (r *RejectActionOptions) UnmarshalJSON(bytes []byte) error { err := json.Unmarshal(bytes, (*_RejectActionOptions)(r)) if err != nil { return err } switch r.Method { case "", C.RuleActionRejectMethodDefault: r.Method = C.RuleActionRejectMethodDefault case C.RuleActionRejectMethodDrop: case C.RuleActionRejectMethodReply: default: return E.New("unknown reject method: " + r.Method) } if r.Method == C.RuleActionRejectMethodDrop && r.NoDrop { return E.New("no_drop is not available in current context") } return nil } type RouteActionSniff struct { Sniffer badoption.Listable[string] `json:"sniffer,omitempty"` Timeout badoption.Duration `json:"timeout,omitempty"` } 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"` RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` } type DNSRouteActionPredefined struct { Rcode *DNSRCode `json:"rcode,omitempty"` Answer badoption.Listable[DNSRecordOptions] `json:"answer,omitempty"` Ns badoption.Listable[DNSRecordOptions] `json:"ns,omitempty"` Extra badoption.Listable[DNSRecordOptions] `json:"extra,omitempty"` } ================================================ FILE: option/rule_action_test.go ================================================ 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") } ================================================ FILE: option/rule_dns.go ================================================ package option import ( "context" "reflect" C "github.com/sagernet/sing-box/constant" "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" ) type _DNSRule struct { Type string `json:"type,omitempty"` DefaultOptions DefaultDNSRule `json:"-"` LogicalOptions LogicalDNSRule `json:"-"` } type DNSRule _DNSRule func (r DNSRule) MarshalJSON() ([]byte, error) { var v any switch r.Type { case C.RuleTypeDefault: r.Type = "" v = r.DefaultOptions case C.RuleTypeLogical: v = r.LogicalOptions default: return nil, E.New("unknown rule type: " + r.Type) } return badjson.MarshallObjects((_DNSRule)(r), v) } func (r *DNSRule) UnmarshalJSONContext(ctx context.Context, bytes []byte) error { err := json.UnmarshalContext(ctx, bytes, (*_DNSRule)(r)) if err != nil { return err } var v any switch r.Type { case "", C.RuleTypeDefault: r.Type = C.RuleTypeDefault v = &r.DefaultOptions case C.RuleTypeLogical: v = &r.LogicalOptions default: return E.New("unknown rule type: " + r.Type) } err = badjson.UnmarshallExcludedContext(ctx, bytes, (*_DNSRule)(r), v) if err != nil { return err } return nil } func (r DNSRule) IsValid() bool { switch r.Type { case C.RuleTypeDefault: return r.DefaultOptions.IsValid() case C.RuleTypeLogical: return r.LogicalOptions.IsValid() default: panic("unknown DNS rule type: " + r.Type) } } type RawDefaultDNSRule struct { Inbound badoption.Listable[string] `json:"inbound,omitempty"` IPVersion int `json:"ip_version,omitempty"` QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"` Network badoption.Listable[string] `json:"network,omitempty"` AuthUser badoption.Listable[string] `json:"auth_user,omitempty"` Protocol badoption.Listable[string] `json:"protocol,omitempty"` Domain badoption.Listable[string] `json:"domain,omitempty"` DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"` DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"` DomainRegex badoption.Listable[string] `json:"domain_regex,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"` SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"` Port badoption.Listable[uint16] `json:"port,omitempty"` PortRange badoption.Listable[string] `json:"port_range,omitempty"` ProcessName badoption.Listable[string] `json:"process_name,omitempty"` 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"` ClashMode string `json:"clash_mode,omitempty"` NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"` NetworkIsExpensive bool `json:"network_is_expensive,omitempty"` NetworkIsConstrained bool `json:"network_is_constrained,omitempty"` WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"` WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"` 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"` 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"` 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: 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"` } type DefaultDNSRule struct { RawDefaultDNSRule DNSRuleAction } func (r DefaultDNSRule) MarshalJSON() ([]byte, error) { return badjson.MarshallObjects(r.RawDefaultDNSRule, r.DNSRuleAction) } func (r *DefaultDNSRule) UnmarshalJSONContext(ctx context.Context, data []byte) error { 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 } if depth > 0 && rawAction == "" && routeOptions == (DNSRouteActionOptions{}) { r.DNSRuleAction = DNSRuleAction{} } return nil } func (r DefaultDNSRule) IsValid() bool { var defaultValue DefaultDNSRule defaultValue.Invert = r.Invert return !reflect.DeepEqual(r, defaultValue) } type RawLogicalDNSRule struct { Mode string `json:"mode"` Rules []DNSRule `json:"rules,omitempty"` Invert bool `json:"invert,omitempty"` } type LogicalDNSRule struct { RawLogicalDNSRule DNSRuleAction } func (r LogicalDNSRule) MarshalJSON() ([]byte, error) { return badjson.MarshallObjects(r.RawLogicalDNSRule, r.DNSRuleAction) } func (r *LogicalDNSRule) UnmarshalJSONContext(ctx context.Context, data []byte) error { 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(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 { return len(r.Rules) > 0 && common.All(r.Rules, DNSRule.IsValid) } ================================================ FILE: option/rule_nested.go ================================================ package option import ( "context" "reflect" "slices" "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 } return slices.ContainsFunc(keys, object.ContainsKey), 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{}{} } } ================================================ FILE: option/rule_nested_test.go ================================================ 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) } ================================================ FILE: option/rule_set.go ================================================ package option import ( "net/url" "path/filepath" "reflect" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/domain" 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/json/badjson" "github.com/sagernet/sing/common/json/badoption" "go4.org/netipx" ) type _RuleSet struct { Type string `json:"type,omitempty"` Tag string `json:"tag"` Format string `json:"format,omitempty"` InlineOptions PlainRuleSet `json:"-"` LocalOptions LocalRuleSet `json:"-"` RemoteOptions RemoteRuleSet `json:"-"` } type RuleSet _RuleSet func (r RuleSet) MarshalJSON() ([]byte, error) { if r.Type != C.RuleSetTypeInline { var defaultFormat string switch r.Type { case C.RuleSetTypeLocal: defaultFormat = ruleSetDefaultFormat(r.LocalOptions.Path) case C.RuleSetTypeRemote: defaultFormat = ruleSetDefaultFormat(r.RemoteOptions.URL) } if r.Format == defaultFormat { r.Format = "" } } var v any switch r.Type { case "", C.RuleSetTypeInline: r.Type = "" v = r.InlineOptions case C.RuleSetTypeLocal: v = r.LocalOptions case C.RuleSetTypeRemote: v = r.RemoteOptions default: return nil, E.New("unknown rule-set type: " + r.Type) } return badjson.MarshallObjects((_RuleSet)(r), v) } func (r *RuleSet) UnmarshalJSON(bytes []byte) error { err := json.Unmarshal(bytes, (*_RuleSet)(r)) if err != nil { return err } if r.Tag == "" { return E.New("missing tag") } var v any switch r.Type { case "", C.RuleSetTypeInline: r.Type = C.RuleSetTypeInline v = &r.InlineOptions case C.RuleSetTypeLocal: v = &r.LocalOptions case C.RuleSetTypeRemote: v = &r.RemoteOptions default: return E.New("unknown rule-set type: " + r.Type) } err = badjson.UnmarshallExcluded(bytes, (*_RuleSet)(r), v) if err != nil { return err } if r.Type != C.RuleSetTypeInline { if r.Format == "" { switch r.Type { case C.RuleSetTypeLocal: r.Format = ruleSetDefaultFormat(r.LocalOptions.Path) case C.RuleSetTypeRemote: r.Format = ruleSetDefaultFormat(r.RemoteOptions.URL) } } switch r.Format { case "": return E.New("missing format") case C.RuleSetFormatSource, C.RuleSetFormatBinary: default: return E.New("unknown rule-set format: " + r.Format) } } else { r.Format = "" } return nil } func ruleSetDefaultFormat(path string) string { if pathURL, err := url.Parse(path); err == nil { path = pathURL.Path } switch filepath.Ext(path) { case ".json": return C.RuleSetFormatSource case ".srs": return C.RuleSetFormatBinary default: return "" } } type LocalRuleSet struct { Path string `json:"path,omitempty"` } type RemoteRuleSet struct { URL string `json:"url"` 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 { Type string `json:"type,omitempty"` DefaultOptions DefaultHeadlessRule `json:"-"` LogicalOptions LogicalHeadlessRule `json:"-"` } type HeadlessRule _HeadlessRule func (r HeadlessRule) MarshalJSON() ([]byte, error) { var v any switch r.Type { case C.RuleTypeDefault: r.Type = "" v = r.DefaultOptions case C.RuleTypeLogical: v = r.LogicalOptions default: return nil, E.New("unknown rule type: " + r.Type) } return badjson.MarshallObjects((_HeadlessRule)(r), v) } func (r *HeadlessRule) UnmarshalJSON(bytes []byte) error { err := json.Unmarshal(bytes, (*_HeadlessRule)(r)) if err != nil { return err } var v any switch r.Type { case "", C.RuleTypeDefault: r.Type = C.RuleTypeDefault v = &r.DefaultOptions case C.RuleTypeLogical: v = &r.LogicalOptions default: return E.New("unknown rule type: " + r.Type) } err = badjson.UnmarshallExcluded(bytes, (*_HeadlessRule)(r), v) if err != nil { return err } return nil } func (r HeadlessRule) IsValid() bool { switch r.Type { case C.RuleTypeDefault, "": return r.DefaultOptions.IsValid() case C.RuleTypeLogical: return r.LogicalOptions.IsValid() default: panic("unknown rule type: " + r.Type) } } type DefaultHeadlessRule struct { QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"` Network badoption.Listable[string] `json:"network,omitempty"` Domain badoption.Listable[string] `json:"domain,omitempty"` DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"` DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"` DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"` SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"` IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"` SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"` Port badoption.Listable[uint16] `json:"port,omitempty"` PortRange badoption.Listable[string] `json:"port_range,omitempty"` ProcessName badoption.Listable[string] `json:"process_name,omitempty"` 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"` WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"` WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"` NetworkInterfaceAddress *badjson.TypedMap[InterfaceType, badoption.Listable[*badoption.Prefixable]] `json:"network_interface_address,omitempty"` DefaultInterfaceAddress badoption.Listable[*badoption.Prefixable] `json:"default_interface_address,omitempty"` Invert bool `json:"invert,omitempty"` DomainMatcher *domain.Matcher `json:"-"` SourceIPSet *netipx.IPSet `json:"-"` IPSet *netipx.IPSet `json:"-"` AdGuardDomain badoption.Listable[string] `json:"-"` AdGuardDomainMatcher *domain.AdGuardMatcher `json:"-"` } func (r DefaultHeadlessRule) IsValid() bool { var defaultValue DefaultHeadlessRule defaultValue.Invert = r.Invert return !reflect.DeepEqual(r, defaultValue) } type LogicalHeadlessRule struct { Mode string `json:"mode"` Rules []HeadlessRule `json:"rules,omitempty"` Invert bool `json:"invert,omitempty"` } func (r LogicalHeadlessRule) IsValid() bool { return len(r.Rules) > 0 && common.All(r.Rules, HeadlessRule.IsValid) } type _PlainRuleSetCompat struct { Version uint8 `json:"version"` Options PlainRuleSet `json:"-"` RawMessage json.RawMessage `json:"-"` } type PlainRuleSetCompat _PlainRuleSetCompat func (r PlainRuleSetCompat) MarshalJSON() ([]byte, error) { var v any switch r.Version { 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) } return badjson.MarshallObjects((_PlainRuleSetCompat)(r), v) } func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error { err := json.Unmarshal(bytes, (*_PlainRuleSetCompat)(r)) if err != nil { return err } var v any switch r.Version { case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3, C.RuleSetVersion4, C.RuleSetVersion5: v = &r.Options case 0: return E.New("missing rule-set version") default: return E.New("unknown rule-set version: ", r.Version) } err = badjson.UnmarshallExcluded(bytes, (*_PlainRuleSetCompat)(r), v) if err != nil { return err } r.RawMessage = bytes return nil } func (r PlainRuleSetCompat) Upgrade() (PlainRuleSet, error) { switch r.Version { case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3, C.RuleSetVersion4, C.RuleSetVersion5: default: return PlainRuleSet{}, E.New("unknown rule-set version: " + F.ToString(r.Version)) } return r.Options, nil } type PlainRuleSet struct { Rules []HeadlessRule `json:"rules,omitempty"` } ================================================ FILE: option/service.go ================================================ 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 ServiceOptionsRegistry interface { CreateOptions(serviceType string) (any, bool) } type _Service struct { Type string `json:"type"` Tag string `json:"tag,omitempty"` Options any `json:"-"` } type Service _Service func (h *Service) MarshalJSONContext(ctx context.Context) ([]byte, error) { return badjson.MarshallObjectsContext(ctx, (*_Service)(h), h.Options) } func (h *Service) UnmarshalJSONContext(ctx context.Context, content []byte) error { err := json.UnmarshalContext(ctx, content, (*_Service)(h)) if err != nil { return err } registry := service.FromContext[ServiceOptionsRegistry](ctx) if registry == nil { return E.New("missing service fields registry in context") } options, loaded := registry.CreateOptions(h.Type) if !loaded { return E.New("unknown inbound type: ", h.Type) } err = badjson.UnmarshallExcludedContext(ctx, content, (*_Service)(h), options) if err != nil { return err } h.Options = options return nil } ================================================ FILE: option/shadowsocks.go ================================================ package option type ShadowsocksInboundOptions struct { ListenOptions Network NetworkList `json:"network,omitempty"` Method string `json:"method"` Password string `json:"password,omitempty"` Users []ShadowsocksUser `json:"users,omitempty"` Destinations []ShadowsocksDestination `json:"destinations,omitempty"` Multiplex *InboundMultiplexOptions `json:"multiplex,omitempty"` Managed bool `json:"managed,omitempty"` } type ShadowsocksUser struct { Name string `json:"name"` Password string `json:"password"` } type ShadowsocksDestination struct { Name string `json:"name"` Password string `json:"password"` ServerOptions } type ShadowsocksOutboundOptions struct { DialerOptions ServerOptions Method string `json:"method"` Password string `json:"password"` Plugin string `json:"plugin,omitempty"` PluginOptions string `json:"plugin_opts,omitempty"` Network NetworkList `json:"network,omitempty"` UDPOverTCP *UDPOverTCPOptions `json:"udp_over_tcp,omitempty"` Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` } ================================================ FILE: option/shadowsocksr.go ================================================ package option type ShadowsocksROutboundOptions struct { DialerOptions ServerOptions Method string `json:"method"` Password string `json:"password"` Obfs string `json:"obfs,omitempty"` ObfsParam string `json:"obfs_param,omitempty"` Protocol string `json:"protocol,omitempty"` ProtocolParam string `json:"protocol_param,omitempty"` Network NetworkList `json:"network,omitempty"` } ================================================ FILE: option/shadowtls.go ================================================ package option import ( "encoding/json" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json/badjson" ) type ShadowTLSInboundOptions struct { ListenOptions Version int `json:"version,omitempty"` Password string `json:"password,omitempty"` Users []ShadowTLSUser `json:"users,omitempty"` Handshake ShadowTLSHandshakeOptions `json:"handshake,omitempty"` HandshakeForServerName *badjson.TypedMap[string, ShadowTLSHandshakeOptions] `json:"handshake_for_server_name,omitempty"` StrictMode bool `json:"strict_mode,omitempty"` WildcardSNI WildcardSNI `json:"wildcard_sni,omitempty"` } type WildcardSNI int const ( ShadowTLSWildcardSNIOff WildcardSNI = iota ShadowTLSWildcardSNIAuthed ShadowTLSWildcardSNIAll ) func (w WildcardSNI) MarshalJSON() ([]byte, error) { return json.Marshal(w.String()) } func (w WildcardSNI) String() string { switch w { case ShadowTLSWildcardSNIOff: return "off" case ShadowTLSWildcardSNIAuthed: return "authed" case ShadowTLSWildcardSNIAll: return "all" default: panic("unknown wildcard SNI value") } } func (w *WildcardSNI) UnmarshalJSON(bytes []byte) error { var valueString string err := json.Unmarshal(bytes, &valueString) if err != nil { return err } switch valueString { case "off", "": *w = ShadowTLSWildcardSNIOff case "authed": *w = ShadowTLSWildcardSNIAuthed case "all": *w = ShadowTLSWildcardSNIAll default: return E.New("unknown wildcard SNI value: ", valueString) } return nil } type ShadowTLSUser struct { Name string `json:"name,omitempty"` Password string `json:"password,omitempty"` } type ShadowTLSHandshakeOptions struct { ServerOptions DialerOptions } type ShadowTLSOutboundOptions struct { DialerOptions ServerOptions Version int `json:"version,omitempty"` Password string `json:"password,omitempty"` OutboundTLSOptionsContainer } ================================================ FILE: option/simple.go ================================================ package option import ( "github.com/sagernet/sing/common/auth" "github.com/sagernet/sing/common/json/badoption" ) type SocksInboundOptions struct { ListenOptions Users []auth.User `json:"users,omitempty"` DomainResolver *DomainResolveOptions `json:"domain_resolver,omitempty"` } type HTTPMixedInboundOptions struct { ListenOptions Users []auth.User `json:"users,omitempty"` DomainResolver *DomainResolveOptions `json:"domain_resolver,omitempty"` SetSystemProxy bool `json:"set_system_proxy,omitempty"` InboundTLSOptionsContainer } type SOCKSOutboundOptions struct { DialerOptions ServerOptions Version string `json:"version,omitempty"` Username string `json:"username,omitempty"` Password string `json:"password,omitempty"` Network NetworkList `json:"network,omitempty"` UDPOverTCP *UDPOverTCPOptions `json:"udp_over_tcp,omitempty"` } type HTTPOutboundOptions struct { DialerOptions ServerOptions Username string `json:"username,omitempty"` Password string `json:"password,omitempty"` OutboundTLSOptionsContainer Path string `json:"path,omitempty"` Headers badoption.HTTPHeader `json:"headers,omitempty"` } ================================================ FILE: option/ssh.go ================================================ package option import "github.com/sagernet/sing/common/json/badoption" type SSHOutboundOptions struct { DialerOptions ServerOptions User string `json:"user,omitempty"` Password string `json:"password,omitempty"` PrivateKey badoption.Listable[string] `json:"private_key,omitempty"` PrivateKeyPath string `json:"private_key_path,omitempty"` PrivateKeyPassphrase string `json:"private_key_passphrase,omitempty"` 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"` } ================================================ FILE: option/ssmapi.go ================================================ package option import ( "github.com/sagernet/sing/common/json/badjson" ) type SSMAPIServiceOptions struct { ListenOptions Servers *badjson.TypedMap[string, string] `json:"servers"` CachePath string `json:"cache_path,omitempty"` InboundTLSOptionsContainer } ================================================ FILE: option/tailscale.go ================================================ package option import ( "net/netip" "net/url" "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"` ExitNode string `json:"exit_node,omitempty"` ExitNodeAllowLANAccess bool `json:"exit_node_allow_lan_access,omitempty"` AdvertiseRoutes []netip.Prefix `json:"advertise_routes,omitempty"` AdvertiseExitNode bool `json:"advertise_exit_node,omitempty"` AdvertiseTags badoption.Listable[string] `json:"advertise_tags,omitempty"` RelayServerPort *uint16 `json:"relay_server_port,omitempty"` RelayServerStaticEndpoints []netip.AddrPort `json:"relay_server_static_endpoints,omitempty"` SystemInterface bool `json:"system_interface,omitempty"` SystemInterfaceName string `json:"system_interface_name,omitempty"` SystemInterfaceMTU uint32 `json:"system_interface_mtu,omitempty"` UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"` } 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 { Endpoint string `json:"endpoint,omitempty"` } type DERPServiceOptions struct { ListenOptions InboundTLSOptionsContainer ConfigPath string `json:"config_path,omitempty"` VerifyClientEndpoint badoption.Listable[string] `json:"verify_client_endpoint,omitempty"` VerifyClientURL badoption.Listable[*DERPVerifyClientURLOptions] `json:"verify_client_url,omitempty"` Home string `json:"home,omitempty"` MeshWith badoption.Listable[*DERPMeshOptions] `json:"mesh_with,omitempty"` MeshPSK string `json:"mesh_psk,omitempty"` MeshPSKFile string `json:"mesh_psk_file,omitempty"` STUN *DERPSTUNListenOptions `json:"stun,omitempty"` } type _DERPVerifyClientURLBase struct { URL string `json:"url,omitempty"` } type _DERPVerifyClientURLOptions struct { _DERPVerifyClientURLBase HTTPClientOptions } type DERPVerifyClientURLOptions _DERPVerifyClientURLOptions func (d DERPVerifyClientURLOptions) ServerIsDomain() bool { verifyURL, err := url.Parse(d.URL) if err != nil { return false } return M.ParseSocksaddr(verifyURL.Hostname()).IsDomain() } func (d DERPVerifyClientURLOptions) MarshalJSON() ([]byte, error) { if d.URL != "" && d.HTTPClientOptions.IsEmpty() { return json.Marshal(d.URL) } 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 = DERPVerifyClientURLOptions{ _DERPVerifyClientURLBase: _DERPVerifyClientURLBase{URL: stringValue}, } return nil } 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 { ServerOptions Host string `json:"host,omitempty"` OutboundTLSOptionsContainer DialerOptions } type _DERPSTUNListenOptions struct { Enabled bool ListenOptions } type DERPSTUNListenOptions _DERPSTUNListenOptions func (d DERPSTUNListenOptions) MarshalJSON() ([]byte, error) { portOptions := _DERPSTUNListenOptions{ Enabled: d.Enabled, ListenOptions: ListenOptions{ ListenPort: d.ListenPort, }, } if _DERPSTUNListenOptions(d) == portOptions { return json.Marshal(d.Enabled) } else { return json.Marshal(_DERPSTUNListenOptions(d)) } } func (d *DERPSTUNListenOptions) UnmarshalJSON(bytes []byte) error { var portValue uint16 err := json.Unmarshal(bytes, &portValue) if err == nil { d.Enabled = true d.ListenPort = portValue return nil } return json.Unmarshal(bytes, (*_DERPSTUNListenOptions)(d)) } ================================================ FILE: option/tls.go ================================================ package option import ( "crypto/tls" "encoding/json" "strings" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json/badoption" ) type InboundTLSOptions struct { Enabled bool `json:"enabled,omitempty"` ServerName string `json:"server_name,omitempty"` Insecure bool `json:"insecure,omitempty"` ALPN badoption.Listable[string] `json:"alpn,omitempty"` MinVersion string `json:"min_version,omitempty"` MaxVersion string `json:"max_version,omitempty"` CipherSuites badoption.Listable[string] `json:"cipher_suites,omitempty"` CurvePreferences badoption.Listable[CurvePreference] `json:"curve_preferences,omitempty"` Certificate badoption.Listable[string] `json:"certificate,omitempty"` CertificatePath string `json:"certificate_path,omitempty"` ClientAuthentication ClientAuthType `json:"client_authentication,omitempty"` ClientCertificate badoption.Listable[string] `json:"client_certificate,omitempty"` ClientCertificatePath badoption.Listable[string] `json:"client_certificate_path,omitempty"` ClientCertificatePublicKeySHA256 badoption.Listable[[]byte] `json:"client_certificate_public_key_sha256,omitempty"` Key badoption.Listable[string] `json:"key,omitempty"` 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 ACME *InboundACMEOptions `json:"acme,omitempty"` ECH *InboundECHOptions `json:"ech,omitempty"` Reality *InboundRealityOptions `json:"reality,omitempty"` } type ClientAuthType tls.ClientAuthType func (t ClientAuthType) MarshalJSON() ([]byte, error) { var stringValue string switch t { case ClientAuthType(tls.NoClientCert): stringValue = "no" case ClientAuthType(tls.RequestClientCert): stringValue = "request" case ClientAuthType(tls.RequireAnyClientCert): stringValue = "require-any" case ClientAuthType(tls.VerifyClientCertIfGiven): stringValue = "verify-if-given" case ClientAuthType(tls.RequireAndVerifyClientCert): stringValue = "require-and-verify" default: return nil, E.New("unknown client authentication type: ", int(t)) } return json.Marshal(stringValue) } func (t *ClientAuthType) UnmarshalJSON(data []byte) error { var stringValue string err := json.Unmarshal(data, &stringValue) if err != nil { return err } switch stringValue { case "no": *t = ClientAuthType(tls.NoClientCert) case "request": *t = ClientAuthType(tls.RequestClientCert) case "require-any": *t = ClientAuthType(tls.RequireAnyClientCert) case "verify-if-given": *t = ClientAuthType(tls.VerifyClientCertIfGiven) case "require-and-verify": *t = ClientAuthType(tls.RequireAndVerifyClientCert) default: return E.New("unknown client authentication type: ", stringValue) } return nil } type InboundTLSOptionsContainer struct { TLS *InboundTLSOptions `json:"tls,omitempty"` } type InboundTLSOptionsWrapper interface { TakeInboundTLSOptions() *InboundTLSOptions ReplaceInboundTLSOptions(options *InboundTLSOptions) } func (o *InboundTLSOptionsContainer) TakeInboundTLSOptions() *InboundTLSOptions { return o.TLS } func (o *InboundTLSOptionsContainer) ReplaceInboundTLSOptions(options *InboundTLSOptions) { o.TLS = options } 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"` ALPN badoption.Listable[string] `json:"alpn,omitempty"` MinVersion string `json:"min_version,omitempty"` MaxVersion string `json:"max_version,omitempty"` CipherSuites badoption.Listable[string] `json:"cipher_suites,omitempty"` CurvePreferences badoption.Listable[CurvePreference] `json:"curve_preferences,omitempty"` Certificate badoption.Listable[string] `json:"certificate,omitempty"` CertificatePath string `json:"certificate_path,omitempty"` CertificatePublicKeySHA256 badoption.Listable[[]byte] `json:"certificate_public_key_sha256,omitempty"` ClientCertificate badoption.Listable[string] `json:"client_certificate,omitempty"` ClientCertificatePath string `json:"client_certificate_path,omitempty"` ClientKey badoption.Listable[string] `json:"client_key,omitempty"` ClientKeyPath string `json:"client_key_path,omitempty"` Fragment bool `json:"fragment,omitempty"` FragmentFallbackDelay badoption.Duration `json:"fragment_fallback_delay,omitempty"` RecordFragment bool `json:"record_fragment,omitempty"` Spoof string `json:"spoof,omitempty"` SpoofMethod string `json:"spoof_method,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"` } type OutboundTLSOptionsContainer struct { TLS *OutboundTLSOptions `json:"tls,omitempty"` } type OutboundTLSOptionsWrapper interface { TakeOutboundTLSOptions() *OutboundTLSOptions ReplaceOutboundTLSOptions(options *OutboundTLSOptions) } func (o *OutboundTLSOptionsContainer) TakeOutboundTLSOptions() *OutboundTLSOptions { return o.TLS } func (o *OutboundTLSOptionsContainer) ReplaceOutboundTLSOptions(options *OutboundTLSOptions) { o.TLS = options } type CurvePreference tls.CurveID const ( CurveP256 = 23 CurveP384 = 24 CurveP521 = 25 X25519 = 29 X25519MLKEM768 = 4588 ) func (c CurvePreference) MarshalJSON() ([]byte, error) { var stringValue string switch c { case CurvePreference(CurveP256): stringValue = "P256" case CurvePreference(CurveP384): stringValue = "P384" case CurvePreference(CurveP521): stringValue = "P521" case CurvePreference(X25519): stringValue = "X25519" case CurvePreference(X25519MLKEM768): stringValue = "X25519MLKEM768" default: return nil, E.New("unknown curve id: ", int(c)) } return json.Marshal(stringValue) } func (c *CurvePreference) UnmarshalJSON(data []byte) error { var stringValue string err := json.Unmarshal(data, &stringValue) if err != nil { return err } switch strings.ToUpper(stringValue) { case "P256": *c = CurvePreference(CurveP256) case "P384": *c = CurvePreference(CurveP384) case "P521": *c = CurvePreference(CurveP521) case "X25519": *c = CurvePreference(X25519) case "X25519MLKEM768": *c = CurvePreference(X25519MLKEM768) default: return E.New("unknown curve name: ", stringValue) } return nil } type InboundRealityOptions struct { Enabled bool `json:"enabled,omitempty"` Handshake InboundRealityHandshakeOptions `json:"handshake,omitempty"` PrivateKey string `json:"private_key,omitempty"` ShortID badoption.Listable[string] `json:"short_id,omitempty"` MaxTimeDifference badoption.Duration `json:"max_time_difference,omitempty"` } type InboundRealityHandshakeOptions struct { ServerOptions DialerOptions } type InboundECHOptions struct { Enabled bool `json:"enabled,omitempty"` Key badoption.Listable[string] `json:"key,omitempty"` KeyPath string `json:"key_path,omitempty"` // Deprecated: not supported by stdlib PQSignatureSchemesEnabled bool `json:"pq_signature_schemes_enabled,omitempty"` // Deprecated: added by fault DynamicRecordSizingDisabled bool `json:"dynamic_record_sizing_disabled,omitempty"` } type OutboundECHOptions struct { Enabled bool `json:"enabled,omitempty"` Config badoption.Listable[string] `json:"config,omitempty"` ConfigPath string `json:"config_path,omitempty"` QueryServerName string `json:"query_server_name,omitempty"` // Deprecated: not supported by stdlib PQSignatureSchemesEnabled bool `json:"pq_signature_schemes_enabled,omitempty"` // Deprecated: added by fault DynamicRecordSizingDisabled bool `json:"dynamic_record_sizing_disabled,omitempty"` } type OutboundUTLSOptions struct { Enabled bool `json:"enabled,omitempty"` Fingerprint string `json:"fingerprint,omitempty"` } type OutboundRealityOptions struct { Enabled bool `json:"enabled,omitempty"` PublicKey string `json:"public_key,omitempty"` ShortID string `json:"short_id,omitempty"` } ================================================ FILE: option/tls_acme.go ================================================ package option import ( 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 InboundACMEOptions 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"` 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 *ACMEDNS01ChallengeOptions `json:"dns01_challenge,omitempty"` Profile string `json:"profile,omitempty"` } type ACMEExternalAccountOptions struct { KeyID string `json:"key_id,omitempty"` MACKey string `json:"mac_key,omitempty"` } type _ACMEDNS01ChallengeOptions struct { Provider string `json:"provider,omitempty"` AliDNSOptions ACMEDNS01AliDNSOptions `json:"-"` CloudflareOptions ACMEDNS01CloudflareOptions `json:"-"` ACMEDNSOptions ACMEDNS01ACMEDNSOptions `json:"-"` } type ACMEDNS01ChallengeOptions _ACMEDNS01ChallengeOptions func (o ACMEDNS01ChallengeOptions) 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((_ACMEDNS01ChallengeOptions)(o), v) } func (o *ACMEDNS01ChallengeOptions) UnmarshalJSON(bytes []byte) error { err := json.Unmarshal(bytes, (*_ACMEDNS01ChallengeOptions)(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 default: return E.New("unknown provider type: " + o.Provider) } err = badjson.UnmarshallExcluded(bytes, (*_ACMEDNS01ChallengeOptions)(o), v) if err != nil { return err } return nil } type ACMEDNS01AliDNSOptions struct { AccessKeyID string `json:"access_key_id,omitempty"` AccessKeySecret string `json:"access_key_secret,omitempty"` RegionID string `json:"region_id,omitempty"` SecurityToken string `json:"security_token,omitempty"` } type ACMEDNS01CloudflareOptions struct { APIToken string `json:"api_token,omitempty"` ZoneToken string `json:"zone_token,omitempty"` } type ACMEDNS01ACMEDNSOptions struct { Username string `json:"username,omitempty"` Password string `json:"password,omitempty"` Subdomain string `json:"subdomain,omitempty"` ServerURL string `json:"server_url,omitempty"` } ================================================ FILE: option/tor.go ================================================ package option type TorOutboundOptions struct { DialerOptions ExecutablePath string `json:"executable_path,omitempty"` ExtraArgs []string `json:"extra_args,omitempty"` DataDirectory string `json:"data_directory,omitempty"` Options map[string]string `json:"torrc,omitempty"` } ================================================ FILE: option/trojan.go ================================================ package option type TrojanInboundOptions struct { ListenOptions Users []TrojanUser `json:"users,omitempty"` InboundTLSOptionsContainer Fallback *ServerOptions `json:"fallback,omitempty"` FallbackForALPN map[string]*ServerOptions `json:"fallback_for_alpn,omitempty"` Multiplex *InboundMultiplexOptions `json:"multiplex,omitempty"` Transport *V2RayTransportOptions `json:"transport,omitempty"` } type TrojanUser struct { Name string `json:"name"` Password string `json:"password"` } type TrojanOutboundOptions struct { DialerOptions ServerOptions Password string `json:"password"` Network NetworkList `json:"network,omitempty"` OutboundTLSOptionsContainer Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` Transport *V2RayTransportOptions `json:"transport,omitempty"` } ================================================ FILE: option/tuic.go ================================================ package option import "github.com/sagernet/sing/common/json/badoption" type TUICInboundOptions struct { ListenOptions Users []TUICUser `json:"users,omitempty"` CongestionControl string `json:"congestion_control,omitempty"` AuthTimeout badoption.Duration `json:"auth_timeout,omitempty"` ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"` Heartbeat badoption.Duration `json:"heartbeat,omitempty"` InboundTLSOptionsContainer QUICOptions } type TUICUser struct { Name string `json:"name,omitempty"` UUID string `json:"uuid,omitempty"` Password string `json:"password,omitempty"` } type TUICOutboundOptions struct { DialerOptions ServerOptions UUID string `json:"uuid,omitempty"` Password string `json:"password,omitempty"` CongestionControl string `json:"congestion_control,omitempty"` UDPRelayMode string `json:"udp_relay_mode,omitempty"` UDPOverStream bool `json:"udp_over_stream,omitempty"` ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"` Heartbeat badoption.Duration `json:"heartbeat,omitempty"` Network NetworkList `json:"network,omitempty"` OutboundTLSOptionsContainer QUICOptions } ================================================ FILE: option/tun.go ================================================ package option import ( "net/netip" "strconv" 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/json/badoption" ) 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"` AutoRedirect bool `json:"auto_redirect,omitempty"` AutoRedirectInputMark FwMark `json:"auto_redirect_input_mark,omitempty"` AutoRedirectOutputMark FwMark `json:"auto_redirect_output_mark,omitempty"` AutoRedirectResetMark FwMark `json:"auto_redirect_reset_mark,omitempty"` AutoRedirectNFQueue uint16 `json:"auto_redirect_nfqueue,omitempty"` AutoRedirectFallbackRuleIndex int `json:"auto_redirect_iproute2_fallback_rule_index,omitempty"` ExcludeMPTCP bool `json:"exclude_mptcp,omitempty"` LoopbackAddress badoption.Listable[netip.Addr] `json:"loopback_address,omitempty"` StrictRoute bool `json:"strict_route,omitempty"` RouteAddress badoption.Listable[netip.Prefix] `json:"route_address,omitempty"` RouteAddressSet badoption.Listable[string] `json:"route_address_set,omitempty"` RouteExcludeAddress badoption.Listable[netip.Prefix] `json:"route_exclude_address,omitempty"` RouteExcludeAddressSet badoption.Listable[string] `json:"route_exclude_address_set,omitempty"` IncludeInterface badoption.Listable[string] `json:"include_interface,omitempty"` ExcludeInterface badoption.Listable[string] `json:"exclude_interface,omitempty"` IncludeUID badoption.Listable[uint32] `json:"include_uid,omitempty"` IncludeUIDRange badoption.Listable[string] `json:"include_uid_range,omitempty"` ExcludeUID badoption.Listable[uint32] `json:"exclude_uid,omitempty"` ExcludeUIDRange badoption.Listable[string] `json:"exclude_uid_range,omitempty"` 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"` InboundOptions // Deprecated: removed GSO bool `json:"gso,omitempty"` // Deprecated: merged to Address Inet4Address badoption.Listable[netip.Prefix] `json:"inet4_address,omitempty"` // Deprecated: merged to Address Inet6Address badoption.Listable[netip.Prefix] `json:"inet6_address,omitempty"` // Deprecated: merged to RouteAddress Inet4RouteAddress badoption.Listable[netip.Prefix] `json:"inet4_route_address,omitempty"` // Deprecated: merged to RouteAddress Inet6RouteAddress badoption.Listable[netip.Prefix] `json:"inet6_route_address,omitempty"` // Deprecated: merged to RouteExcludeAddress Inet4RouteExcludeAddress badoption.Listable[netip.Prefix] `json:"inet4_route_exclude_address,omitempty"` // Deprecated: merged to RouteExcludeAddress Inet6RouteExcludeAddress badoption.Listable[netip.Prefix] `json:"inet6_route_exclude_address,omitempty"` // Deprecated: removed EndpointIndependentNat bool `json:"endpoint_independent_nat,omitempty"` } type FwMark uint32 func (f FwMark) MarshalJSON() ([]byte, error) { return json.Marshal(F.ToString("0x", strconv.FormatUint(uint64(f), 16))) } func (f *FwMark) UnmarshalJSON(bytes []byte) error { var stringValue string err := json.Unmarshal(bytes, &stringValue) if err != nil { if rawErr := json.Unmarshal(bytes, (*uint32)(f)); rawErr == nil { return nil } return E.Cause(err, "invalid number or string mark") } intValue, err := strconv.ParseUint(stringValue, 0, 32) if err != nil { return err } *f = FwMark(intValue) return nil } ================================================ FILE: option/tun_platform.go ================================================ package option import "github.com/sagernet/sing/common/json/badoption" type TunPlatformOptions struct { HTTPProxy *HTTPProxyOptions `json:"http_proxy,omitempty"` } type HTTPProxyOptions struct { Enabled bool `json:"enabled,omitempty"` ServerOptions BypassDomain badoption.Listable[string] `json:"bypass_domain,omitempty"` MatchDomain badoption.Listable[string] `json:"match_domain,omitempty"` } ================================================ FILE: option/types.go ================================================ package option import ( "strings" C "github.com/sagernet/sing-box/constant" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json" N "github.com/sagernet/sing/common/network" mDNS "github.com/miekg/dns" ) type NetworkList string func (v *NetworkList) UnmarshalJSON(content []byte) error { var networkList []string err := json.Unmarshal(content, &networkList) if err != nil { var networkItem string err = json.Unmarshal(content, &networkItem) if err != nil { return err } networkList = []string{networkItem} } for _, networkName := range networkList { switch networkName { case N.NetworkTCP, N.NetworkUDP: default: return E.New("unknown network: " + networkName) } } *v = NetworkList(strings.Join(networkList, "\n")) return nil } func (v NetworkList) Build() []string { if v == "" { return []string{N.NetworkTCP, N.NetworkUDP} } return strings.Split(string(v), "\n") } type DomainStrategy C.DomainStrategy func (s DomainStrategy) String() string { switch C.DomainStrategy(s) { case C.DomainStrategyAsIS: return "" case C.DomainStrategyPreferIPv4: return "prefer_ipv4" case C.DomainStrategyPreferIPv6: return "prefer_ipv6" case C.DomainStrategyIPv4Only: return "ipv4_only" case C.DomainStrategyIPv6Only: return "ipv6_only" default: panic(E.New("unknown domain strategy: ", s)) } } func (s DomainStrategy) MarshalJSON() ([]byte, error) { var value string switch C.DomainStrategy(s) { case C.DomainStrategyAsIS: value = "" // value = "as_is" case C.DomainStrategyPreferIPv4: value = "prefer_ipv4" case C.DomainStrategyPreferIPv6: value = "prefer_ipv6" case C.DomainStrategyIPv4Only: value = "ipv4_only" case C.DomainStrategyIPv6Only: value = "ipv6_only" default: return nil, E.New("unknown domain strategy: ", s) } return json.Marshal(value) } func (s *DomainStrategy) UnmarshalJSON(bytes []byte) error { var value string err := json.Unmarshal(bytes, &value) if err != nil { return err } switch value { case "", "as_is": *s = DomainStrategy(C.DomainStrategyAsIS) case "prefer_ipv4": *s = DomainStrategy(C.DomainStrategyPreferIPv4) case "prefer_ipv6": *s = DomainStrategy(C.DomainStrategyPreferIPv6) case "ipv4_only": *s = DomainStrategy(C.DomainStrategyIPv4Only) case "ipv6_only": *s = DomainStrategy(C.DomainStrategyIPv6Only) default: return E.New("unknown domain strategy: ", value) } return nil } type DNSQueryType uint16 func (t DNSQueryType) String() string { typeName, loaded := mDNS.TypeToString[uint16(t)] if loaded { return typeName } return F.ToString(uint16(t)) } func (t DNSQueryType) MarshalJSON() ([]byte, error) { typeName, loaded := mDNS.TypeToString[uint16(t)] if loaded { return json.Marshal(typeName) } return json.Marshal(uint16(t)) } func (t *DNSQueryType) UnmarshalJSON(bytes []byte) error { var valueNumber uint16 err := json.Unmarshal(bytes, &valueNumber) if err == nil { *t = DNSQueryType(valueNumber) return nil } var valueString string err = json.Unmarshal(bytes, &valueString) if err == nil { queryType, loaded := mDNS.StringToType[valueString] if loaded { *t = DNSQueryType(queryType) return nil } } return E.New("unknown DNS query type: ", string(bytes)) } func DNSQueryTypeToString(queryType uint16) string { typeName, loaded := mDNS.TypeToString[queryType] if loaded { return typeName } return F.ToString(queryType) } type NetworkStrategy C.NetworkStrategy func (n NetworkStrategy) MarshalJSON() ([]byte, error) { return json.Marshal(C.NetworkStrategy(n).String()) } func (n *NetworkStrategy) UnmarshalJSON(content []byte) error { var value string err := json.Unmarshal(content, &value) if err != nil { return err } strategy, loaded := C.StringToNetworkStrategy[value] if !loaded { return E.New("unknown network strategy: ", value) } *n = NetworkStrategy(strategy) return nil } type InterfaceType C.InterfaceType func (t InterfaceType) Build() C.InterfaceType { return C.InterfaceType(t) } func (t InterfaceType) MarshalJSON() ([]byte, error) { return json.Marshal(C.InterfaceType(t).String()) } func (t *InterfaceType) UnmarshalJSON(content []byte) error { var value string err := json.Unmarshal(content, &value) if err != nil { return err } interfaceType, loaded := C.StringToInterfaceType[value] if !loaded { return E.New("unknown interface type: ", value) } *t = InterfaceType(interfaceType) return nil } ================================================ FILE: option/udp_over_tcp.go ================================================ package option import ( "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/uot" ) type _UDPOverTCPOptions struct { Enabled bool `json:"enabled,omitempty"` Version uint8 `json:"version,omitempty"` } type UDPOverTCPOptions _UDPOverTCPOptions func (o UDPOverTCPOptions) MarshalJSON() ([]byte, error) { switch o.Version { case 0, uot.Version: return json.Marshal(o.Enabled) default: return json.Marshal(_UDPOverTCPOptions(o)) } } func (o *UDPOverTCPOptions) UnmarshalJSON(bytes []byte) error { err := json.Unmarshal(bytes, &o.Enabled) if err == nil { return nil } return json.UnmarshalDisallowUnknownFields(bytes, (*_UDPOverTCPOptions)(o)) } ================================================ FILE: option/v2ray.go ================================================ package option ================================================ FILE: option/v2ray_transport.go ================================================ package option import ( 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 _V2RayTransportOptions struct { Type string `json:"type"` HTTPOptions V2RayHTTPOptions `json:"-"` WebsocketOptions V2RayWebsocketOptions `json:"-"` QUICOptions V2RayQUICOptions `json:"-"` GRPCOptions V2RayGRPCOptions `json:"-"` HTTPUpgradeOptions V2RayHTTPUpgradeOptions `json:"-"` } type V2RayTransportOptions _V2RayTransportOptions func (o V2RayTransportOptions) MarshalJSON() ([]byte, error) { var v any switch o.Type { case C.V2RayTransportTypeHTTP: v = o.HTTPOptions case C.V2RayTransportTypeWebsocket: v = o.WebsocketOptions case C.V2RayTransportTypeQUIC: v = o.QUICOptions case C.V2RayTransportTypeGRPC: v = o.GRPCOptions case C.V2RayTransportTypeHTTPUpgrade: v = o.HTTPUpgradeOptions case "": return nil, E.New("missing transport type") default: return nil, E.New("unknown transport type: " + o.Type) } return badjson.MarshallObjects((_V2RayTransportOptions)(o), v) } func (o *V2RayTransportOptions) UnmarshalJSON(bytes []byte) error { err := json.Unmarshal(bytes, (*_V2RayTransportOptions)(o)) if err != nil { return err } var v any switch o.Type { case C.V2RayTransportTypeHTTP: v = &o.HTTPOptions case C.V2RayTransportTypeWebsocket: v = &o.WebsocketOptions case C.V2RayTransportTypeQUIC: v = &o.QUICOptions case C.V2RayTransportTypeGRPC: v = &o.GRPCOptions case C.V2RayTransportTypeHTTPUpgrade: v = &o.HTTPUpgradeOptions default: return E.New("unknown transport type: " + o.Type) } err = badjson.UnmarshallExcluded(bytes, (*_V2RayTransportOptions)(o), v) if err != nil { return err } return nil } type V2RayHTTPOptions struct { Host badoption.Listable[string] `json:"host,omitempty"` Path string `json:"path,omitempty"` Method string `json:"method,omitempty"` Headers badoption.HTTPHeader `json:"headers,omitempty"` IdleTimeout badoption.Duration `json:"idle_timeout,omitempty"` PingTimeout badoption.Duration `json:"ping_timeout,omitempty"` } type V2RayWebsocketOptions struct { Path string `json:"path,omitempty"` Headers badoption.HTTPHeader `json:"headers,omitempty"` MaxEarlyData uint32 `json:"max_early_data,omitempty"` EarlyDataHeaderName string `json:"early_data_header_name,omitempty"` } type V2RayQUICOptions struct{} type V2RayGRPCOptions struct { ServiceName string `json:"service_name,omitempty"` IdleTimeout badoption.Duration `json:"idle_timeout,omitempty"` PingTimeout badoption.Duration `json:"ping_timeout,omitempty"` PermitWithoutStream bool `json:"permit_without_stream,omitempty"` ForceLite bool `json:"-"` // for test } type V2RayHTTPUpgradeOptions struct { Host string `json:"host,omitempty"` Path string `json:"path,omitempty"` Headers badoption.HTTPHeader `json:"headers,omitempty"` } ================================================ FILE: option/vless.go ================================================ package option type VLESSInboundOptions struct { ListenOptions Users []VLESSUser `json:"users,omitempty"` InboundTLSOptionsContainer Multiplex *InboundMultiplexOptions `json:"multiplex,omitempty"` Transport *V2RayTransportOptions `json:"transport,omitempty"` } type VLESSUser struct { Name string `json:"name"` UUID string `json:"uuid"` Flow string `json:"flow,omitempty"` } type VLESSOutboundOptions struct { DialerOptions ServerOptions UUID string `json:"uuid"` Flow string `json:"flow,omitempty"` Network NetworkList `json:"network,omitempty"` OutboundTLSOptionsContainer Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` Transport *V2RayTransportOptions `json:"transport,omitempty"` PacketEncoding *string `json:"packet_encoding,omitempty"` } ================================================ FILE: option/vmess.go ================================================ package option type VMessInboundOptions struct { ListenOptions Users []VMessUser `json:"users,omitempty"` InboundTLSOptionsContainer Multiplex *InboundMultiplexOptions `json:"multiplex,omitempty"` Transport *V2RayTransportOptions `json:"transport,omitempty"` } type VMessUser struct { Name string `json:"name"` UUID string `json:"uuid"` AlterId int `json:"alterId,omitempty"` } type VMessOutboundOptions struct { DialerOptions ServerOptions UUID string `json:"uuid"` Security string `json:"security"` AlterId int `json:"alter_id,omitempty"` GlobalPadding bool `json:"global_padding,omitempty"` AuthenticatedLength bool `json:"authenticated_length,omitempty"` Network NetworkList `json:"network,omitempty"` OutboundTLSOptionsContainer PacketEncoding string `json:"packet_encoding,omitempty"` Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` Transport *V2RayTransportOptions `json:"transport,omitempty"` } ================================================ FILE: option/wireguard.go ================================================ package option import ( "net/netip" "github.com/sagernet/sing/common/json/badoption" ) type WireGuardEndpointOptions struct { System bool `json:"system,omitempty"` Name string `json:"name,omitempty"` MTU uint32 `json:"mtu,omitempty"` Address badoption.Listable[netip.Prefix] `json:"address"` PrivateKey string `json:"private_key"` ListenPort uint16 `json:"listen_port,omitempty"` Peers []WireGuardPeer `json:"peers,omitempty"` UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"` Workers int `json:"workers,omitempty"` DialerOptions } type WireGuardPeer struct { Address string `json:"address,omitempty"` Port uint16 `json:"port,omitempty"` PublicKey string `json:"public_key,omitempty"` PreSharedKey string `json:"pre_shared_key,omitempty"` AllowedIPs badoption.Listable[netip.Prefix] `json:"allowed_ips,omitempty"` PersistentKeepaliveInterval uint16 `json:"persistent_keepalive_interval,omitempty"` Reserved []uint8 `json:"reserved,omitempty"` } ================================================ FILE: protocol/anytls/inbound.go ================================================ package anytls import ( "context" "net" "strings" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/common/listener" "github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/common/uot" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "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" anytls "github.com/anytls/sing-anytls" "github.com/anytls/sing-anytls/padding" ) func RegisterInbound(registry *inbound.Registry) { inbound.Register[option.AnyTLSInboundOptions](registry, C.TypeAnyTLS, NewInbound) } type Inbound struct { inbound.Adapter tlsConfig tls.ServerConfig router adapter.ConnectionRouterEx logger logger.ContextLogger listener *listener.Listener service *anytls.Service } func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.AnyTLSInboundOptions) (adapter.Inbound, error) { inbound := &Inbound{ Adapter: inbound.NewAdapter(C.TypeAnyTLS, tag), router: uot.NewRouter(router, logger), logger: logger, } if options.TLS != nil && options.TLS.Enabled { tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS)) if err != nil { return nil, err } inbound.tlsConfig = tlsConfig } paddingScheme := padding.DefaultPaddingScheme if len(options.PaddingScheme) > 0 { paddingScheme = []byte(strings.Join(options.PaddingScheme, "\n")) } service, err := anytls.NewService(anytls.ServiceConfig{ Users: common.Map(options.Users, func(it option.AnyTLSUser) anytls.User { return (anytls.User)(it) }), PaddingScheme: paddingScheme, Handler: (*inboundHandler)(inbound), Logger: logger, }) if err != nil { return nil, err } inbound.service = service inbound.listener = listener.New(listener.Options{ Context: ctx, Logger: logger, Network: []string{N.NetworkTCP}, Listen: options.ListenOptions, ConnectionHandler: inbound, }) return inbound, nil } func (h *Inbound) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } if h.tlsConfig != nil { err := h.tlsConfig.Start() if err != nil { return err } } return h.listener.Start() } func (h *Inbound) Close() error { return common.Close(h.listener, h.tlsConfig) } 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 { N.CloseOnHandshakeFailure(conn, onClose, err) h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source, ": TLS handshake")) return } conn = tlsConn } err := h.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, metadata.Source, onClose) if err != nil { N.CloseOnHandshakeFailure(conn, onClose, err) h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) } } type inboundHandler Inbound func (h *inboundHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { var metadata adapter.InboundContext metadata.Inbound = h.Tag() metadata.InboundType = h.Type() //nolint:staticcheck metadata.InboundDetour = h.listener.ListenOptions().Detour //nolint:staticcheck metadata.Source = source metadata.Destination = destination.Unwrap() if userName, _ := auth.UserFromContext[string](ctx); userName != "" { metadata.User = userName h.logger.InfoContext(ctx, "[", userName, "] inbound connection to ", metadata.Destination) } else { h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) } h.router.RouteConnectionEx(ctx, conn, metadata, onClose) } ================================================ FILE: protocol/anytls/outbound.go ================================================ package anytls import ( "context" "net" "os" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" 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/uot" anytls "github.com/anytls/sing-anytls" ) func RegisterOutbound(registry *outbound.Registry) { outbound.Register[option.AnyTLSOutboundOptions](registry, C.TypeAnyTLS, NewOutbound) } type Outbound struct { outbound.Adapter dialer tls.Dialer server M.Socksaddr tlsConfig tls.Config client *anytls.Client uotClient *uot.Client logger log.ContextLogger } func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.AnyTLSOutboundOptions) (adapter.Outbound, error) { outbound := &Outbound{ Adapter: outbound.NewAdapterWithDialerOptions(C.TypeAnyTLS, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.DialerOptions), server: options.ServerOptions.Build(), logger: logger, } if options.TLS == nil || !options.TLS.Enabled { return nil, C.ErrTLSRequired } // TCP Fast Open is incompatible with anytls because TFO creates a lazy connection // that only establishes on first write. The lazy connection returns an empty address // before establishment, but anytls SOCKS wrapper tries to access the remote address // during handshake, causing a null pointer dereference crash. if options.DialerOptions.TCPFastOpen { return nil, E.New("tcp_fast_open is not supported with anytls outbound") } tlsConfig, err := tls.NewClient(ctx, logger, options.Server, common.PtrValueOrDefault(options.TLS)) if err != nil { return nil, err } outbound.tlsConfig = tlsConfig outboundDialer, err := dialer.NewWithOptions(dialer.Options{ Context: ctx, Options: options.DialerOptions, RemoteIsDomain: options.ServerIsDomain(), }) if err != nil { return nil, err } outbound.dialer = tls.NewDialer(outboundDialer, tlsConfig) client, err := anytls.NewClient(ctx, anytls.ClientConfig{ Password: options.Password, IdleSessionCheckInterval: options.IdleSessionCheckInterval.Build(), IdleSessionTimeout: options.IdleSessionTimeout.Build(), MinIdleSession: options.MinIdleSession, DialOut: outbound.dialOut, Logger: logger, }) if err != nil { return nil, err } outbound.client = client outbound.uotClient = &uot.Client{ Dialer: (anytlsDialer)(client.CreateProxy), Version: uot.Version, } return outbound, nil } type anytlsDialer func(ctx context.Context, destination M.Socksaddr) (net.Conn, error) func (d anytlsDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { return d(ctx, destination) } func (d anytlsDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { return nil, os.ErrInvalid } func (h *Outbound) dialOut(ctx context.Context) (net.Conn, error) { return h.dialer.DialTLSContext(ctx, h.server) } func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { ctx, metadata := adapter.ExtendContext(ctx) metadata.Outbound = h.Tag() metadata.Destination = destination switch N.NetworkName(network) { case N.NetworkTCP: h.logger.InfoContext(ctx, "outbound connection to ", destination) return h.client.CreateProxy(ctx, destination) case N.NetworkUDP: h.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination) return h.uotClient.DialContext(ctx, network, destination) } return nil, os.ErrInvalid } func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { ctx, metadata := adapter.ExtendContext(ctx) metadata.Outbound = h.Tag() metadata.Destination = destination h.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination) return h.uotClient.ListenPacket(ctx, destination) } func (h *Outbound) Close() error { return common.Close(h.client) } ================================================ FILE: protocol/block/outbound.go ================================================ package block import ( "context" "net" "syscall" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/outbound" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) func RegisterOutbound(registry *outbound.Registry) { outbound.Register[option.StubOptions](registry, C.TypeBlock, New) } type Outbound struct { outbound.Adapter logger logger.ContextLogger } func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, _ option.StubOptions) (adapter.Outbound, error) { return &Outbound{ Adapter: outbound.NewAdapter(C.TypeBlock, tag, []string{N.NetworkTCP, N.NetworkUDP}, nil), logger: logger, }, nil } func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { h.logger.InfoContext(ctx, "blocked connection to ", destination) return nil, syscall.EPERM } func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { h.logger.InfoContext(ctx, "blocked packet connection to ", destination) return nil, syscall.EPERM } ================================================ FILE: protocol/cloudflare/inbound.go ================================================ //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 } ================================================ FILE: protocol/direct/inbound.go ================================================ package direct import ( "context" "net" "os" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/common/listener" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/udpnat2" ) func RegisterInbound(registry *inbound.Registry) { inbound.Register[option.DirectInboundOptions](registry, C.TypeDirect, NewInbound) } type Inbound struct { inbound.Adapter ctx context.Context router adapter.ConnectionRouterEx logger log.ContextLogger listener *listener.Listener udpNat *udpnat.Service overrideOption int overrideDestination M.Socksaddr } func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.DirectInboundOptions) (adapter.Inbound, error) { options.UDPFragmentDefault = true inbound := &Inbound{ Adapter: inbound.NewAdapter(C.TypeDirect, tag), ctx: ctx, router: router, logger: logger, } if options.OverrideAddress != "" && options.OverridePort != 0 { inbound.overrideOption = 1 inbound.overrideDestination = M.ParseSocksaddrHostPort(options.OverrideAddress, options.OverridePort) } else if options.OverrideAddress != "" { inbound.overrideOption = 2 inbound.overrideDestination = M.ParseSocksaddrHostPort(options.OverrideAddress, options.OverridePort) } else if options.OverridePort != 0 { inbound.overrideOption = 3 inbound.overrideDestination = M.Socksaddr{Port: options.OverridePort} } var udpTimeout time.Duration if options.UDPTimeout != 0 { udpTimeout = time.Duration(options.UDPTimeout) } else { udpTimeout = C.UDPTimeout } inbound.udpNat = udpnat.New(inbound, inbound.preparePacketConnection, udpTimeout, false) inbound.listener = listener.New(listener.Options{ Context: ctx, Logger: logger, Network: options.Network.Build(), Listen: options.ListenOptions, ConnectionHandler: inbound, PacketHandler: inbound, }) return inbound, nil } func (i *Inbound) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } return i.listener.Start() } func (i *Inbound) Close() error { return i.listener.Close() } func (i *Inbound) NewPacket(buffer *buf.Buffer, source M.Socksaddr) { i.udpNat.NewPacket([][]byte{buffer.Bytes()}, source, i.listener.UDPAddr(), nil) } 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 switch i.overrideOption { case 1: destination = i.overrideDestination case 2: destination.Addr = i.overrideDestination.Addr case 3: destination.Port = i.overrideDestination.Port } metadata.Destination = destination if i.overrideOption != 0 { i.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) } i.router.RouteConnectionEx(ctx, conn, metadata, onClose) } func (i *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { i.logger.InfoContext(ctx, "inbound packet connection from ", source) var metadata adapter.InboundContext metadata.Inbound = i.Tag() metadata.InboundType = i.Type() //nolint:staticcheck metadata.InboundDetour = i.listener.ListenOptions().Detour //nolint:staticcheck metadata.Source = source destination = i.listener.UDPAddr() switch i.overrideOption { case 1: destination = i.overrideDestination case 2: destination.Addr = i.overrideDestination.Addr case 3: destination.Port = i.overrideDestination.Port default: } i.logger.InfoContext(ctx, "inbound packet connection to ", destination) metadata.Destination = destination if i.overrideOption != 0 { conn = bufio.NewDestinationNATPacketConn(bufio.NewNetPacketConn(conn), i.listener.UDPAddr(), destination) } i.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } func (i *Inbound) preparePacketConnection(source M.Socksaddr, destination M.Socksaddr, userData any) (bool, context.Context, N.PacketWriter, N.CloseHandlerFunc) { return true, log.ContextWithNewID(i.ctx), &directPacketWriter{i.listener.PacketWriter(), source}, nil } type directPacketWriter struct { writer N.PacketWriter source M.Socksaddr } 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) } ================================================ FILE: protocol/direct/outbound.go ================================================ package direct import ( "context" "net" "net/netip" "reflect" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/outbound" "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-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" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) func RegisterOutbound(registry *outbound.Registry) { outbound.Register[option.DirectOutboundOptions](registry, C.TypeDirect, NewOutbound) } var ( _ N.ParallelDialer = (*Outbound)(nil) _ dialer.ParallelNetworkDialer = (*Outbound)(nil) _ dialer.DirectDialer = (*Outbound)(nil) _ adapter.DirectRouteOutbound = (*Outbound)(nil) ) type Outbound struct { outbound.Adapter ctx context.Context logger logger.ContextLogger dialer dialer.ParallelInterfaceDialer domainStrategy C.DomainStrategy fallbackDelay time.Duration isEmpty bool } func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.DirectOutboundOptions) (adapter.Outbound, error) { options.UDPFragmentDefault = true if options.Detour != "" { return nil, E.New("`detour` is not supported in direct context") } outboundDialer, err := dialer.NewWithOptions(dialer.Options{ Context: ctx, Options: options.DialerOptions, RemoteIsDomain: true, DirectOutbound: true, }) if err != nil { return nil, err } outbound := &Outbound{ Adapter: outbound.NewAdapterWithDialerOptions(C.TypeDirect, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, options.DialerOptions), ctx: ctx, logger: logger, //nolint:staticcheck domainStrategy: C.DomainStrategy(options.DomainStrategy), fallbackDelay: time.Duration(options.FallbackDelay), dialer: outboundDialer.(dialer.ParallelInterfaceDialer), isEmpty: reflect.DeepEqual(options.DialerOptions, option.DialerOptions{UDPFragmentDefault: true}), } //nolint:staticcheck if options.ProxyProtocol != 0 { return nil, E.New("Proxy Protocol is deprecated and removed in sing-box 1.6.0") } return outbound, nil } func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { ctx, metadata := adapter.ExtendContext(ctx) metadata.Outbound = h.Tag() metadata.Destination = destination network = N.NetworkName(network) switch network { case N.NetworkTCP: h.logger.InfoContext(ctx, "outbound connection to ", destination) case N.NetworkUDP: h.logger.InfoContext(ctx, "outbound packet connection to ", destination) } return h.dialer.DialContext(ctx, network, destination) } func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { ctx, metadata := adapter.ExtendContext(ctx) metadata.Outbound = h.Tag() metadata.Destination = destination h.logger.InfoContext(ctx, "outbound packet connection") conn, err := h.dialer.ListenPacket(ctx, destination) if err != nil { return nil, err } return conn, nil } func (h *Outbound) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { ctx := log.ContextWithNewID(h.ctx) destination, err := ping.ConnectDestination(ctx, h.logger, common.MustCast[*dialer.DefaultDialer](h.dialer).DialerForICMPDestination(metadata.Destination.Addr).Control, metadata.Destination.Addr, routeContext, timeout) if err != nil { return nil, err } h.logger.InfoContext(ctx, "linked ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString()) return destination, nil } func (h *Outbound) DialParallel(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr) (net.Conn, error) { ctx, metadata := adapter.ExtendContext(ctx) metadata.Outbound = h.Tag() metadata.Destination = destination network = N.NetworkName(network) switch network { case N.NetworkTCP: h.logger.InfoContext(ctx, "outbound connection to ", destination) case N.NetworkUDP: h.logger.InfoContext(ctx, "outbound packet connection to ", destination) } return dialer.DialParallelNetwork(ctx, h.dialer, network, destination, destinationAddresses, len(destinationAddresses) > 0 && destinationAddresses[0].Is6(), nil, nil, nil, h.fallbackDelay) } func (h *Outbound) DialParallelNetwork(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, networkStrategy *C.NetworkStrategy, networkType []C.InterfaceType, fallbackNetworkType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) { ctx, metadata := adapter.ExtendContext(ctx) metadata.Outbound = h.Tag() metadata.Destination = destination network = N.NetworkName(network) switch network { case N.NetworkTCP: h.logger.InfoContext(ctx, "outbound connection to ", destination) case N.NetworkUDP: h.logger.InfoContext(ctx, "outbound packet connection to ", destination) } return dialer.DialParallelNetwork(ctx, h.dialer, network, destination, destinationAddresses, len(destinationAddresses) > 0 && destinationAddresses[0].Is6(), networkStrategy, networkType, fallbackNetworkType, fallbackDelay) } func (h *Outbound) ListenSerialNetworkPacket(ctx context.Context, destination M.Socksaddr, destinationAddresses []netip.Addr, networkStrategy *C.NetworkStrategy, networkType []C.InterfaceType, fallbackNetworkType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) { ctx, metadata := adapter.ExtendContext(ctx) metadata.Outbound = h.Tag() metadata.Destination = destination h.logger.InfoContext(ctx, "outbound packet connection") conn, newDestination, err := dialer.ListenSerialNetworkPacket(ctx, h.dialer, destination, destinationAddresses, networkStrategy, networkType, fallbackNetworkType, fallbackDelay) if err != nil { return nil, netip.Addr{}, err } return conn, newDestination, nil } func (h *Outbound) IsEmpty() bool { return h.isEmpty } ================================================ FILE: protocol/dns/handle.go ================================================ package dns import ( "context" "encoding/binary" "net" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/dns" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/canceler" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/task" mDNS "github.com/miekg/dns" ) func HandleStreamDNSRequest(ctx context.Context, router adapter.DNSRouter, conn net.Conn, metadata adapter.InboundContext) error { var queryLength uint16 err := binary.Read(conn, binary.BigEndian, &queryLength) if err != nil { return err } if queryLength == 0 { return dns.RcodeFormatError } buffer := buf.NewSize(int(queryLength)) defer buffer.Release() _, err = buffer.ReadFullFrom(conn, int(queryLength)) if err != nil { return err } var message mDNS.Msg err = message.Unpack(buffer.Bytes()) if err != nil { return err } metadataInQuery := metadata go func() error { response, err := router.Exchange(adapter.WithContext(ctx, &metadataInQuery), &message, adapter.DNSQueryOptions{}) if err != nil { conn.Close() return err } responseLength := response.Len() responseBuffer := buf.NewSize(3 + responseLength) defer responseBuffer.Release() responseBuffer.Resize(2, 0) n, err := response.PackBuffer(responseBuffer.FreeBytes()) if err != nil { return err } responseBuffer.Truncate(len(n)) binary.BigEndian.PutUint16(responseBuffer.ExtendHeader(2), uint16(len(n))) _, err = conn.Write(responseBuffer.Bytes()) return err }() return nil } func NewDNSPacketConnection(ctx context.Context, router adapter.DNSRouter, conn N.PacketConn, cachedPackets []*N.PacketBuffer, metadata adapter.InboundContext) error { metadata.Destination = M.Socksaddr{} var reader N.PacketReader = conn var counters []N.CountFunc cachedPackets = common.Reverse(cachedPackets) for { reader, counters = N.UnwrapCountPacketReader(reader, counters) if cachedReader, isCached := reader.(N.CachedPacketReader); isCached { packet := cachedReader.ReadCachedPacket() if packet != nil { cachedPackets = append(cachedPackets, packet) continue } } if readWaiter, created := bufio.CreatePacketReadWaiter(reader); created { readWaiter.InitializeReadWaiter(N.ReadWaitOptions{}) return newDNSPacketConnection(ctx, router, conn, readWaiter, counters, cachedPackets, metadata) } break } fastClose, cancel := context.WithCancelCause(ctx) timeout := canceler.New(fastClose, cancel, C.DNSTimeout) var group task.Group group.Append0(func(_ context.Context) error { for { var message mDNS.Msg var destination M.Socksaddr var err error if len(cachedPackets) > 0 { packet := cachedPackets[0] cachedPackets = cachedPackets[1:] for _, counter := range counters { counter(int64(packet.Buffer.Len())) } err = message.Unpack(packet.Buffer.Bytes()) packet.Buffer.Release() if err != nil { cancel(err) return err } destination = packet.Destination } else { buffer := buf.NewPacket() destination, err = conn.ReadPacket(buffer) if err != nil { buffer.Release() cancel(err) return err } for _, counter := range counters { counter(int64(buffer.Len())) } err = message.Unpack(buffer.Bytes()) buffer.Release() if err != nil { cancel(err) return err } timeout.Update() } metadataInQuery := metadata go func() error { response, err := router.Exchange(adapter.WithContext(ctx, &metadataInQuery), &message, adapter.DNSQueryOptions{}) if err != nil { cancel(err) return err } timeout.Update() responseBuffer, err := dns.TruncateDNSMessage(&message, response, 1024) if err != nil { cancel(err) return err } err = conn.WritePacket(responseBuffer, destination) if err != nil { cancel(err) } return err }() } }) group.Cleanup(func() { conn.Close() }) return group.Run(fastClose) } 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 := context.WithCancelCause(ctx) timeout := canceler.New(fastClose, cancel, C.DNSTimeout) var group task.Group group.Append0(func(_ context.Context) error { for { var ( message mDNS.Msg destination M.Socksaddr err error buffer *buf.Buffer ) if len(cached) > 0 { packet := cached[0] cached = cached[1:] for _, counter := range readCounters { counter(int64(packet.Buffer.Len())) } err = message.Unpack(packet.Buffer.Bytes()) packet.Buffer.Release() destination = packet.Destination N.PutPacketBuffer(packet) if err != nil { cancel(err) return err } } else { buffer, destination, err = readWaiter.WaitReadPacket() if err != nil { cancel(err) return err } for _, counter := range readCounters { counter(int64(buffer.Len())) } err = message.Unpack(buffer.Bytes()) buffer.Release() if err != nil { cancel(err) return err } timeout.Update() } metadataInQuery := metadata go func() error { response, err := router.Exchange(adapter.WithContext(ctx, &metadataInQuery), &message, adapter.DNSQueryOptions{}) if err != nil { cancel(err) return err } timeout.Update() responseBuffer, err := dns.TruncateDNSMessage(&message, response, 1024) if err != nil { cancel(err) return err } err = conn.WritePacket(responseBuffer, destination) if err != nil { cancel(err) } return err }() } }) group.Cleanup(func() { conn.Close() }) return group.Run(fastClose) } ================================================ FILE: protocol/dns/outbound.go ================================================ package dns import ( "context" "net" "os" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/outbound" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "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) { outbound.Register[option.StubOptions](registry, C.TypeDNS, NewOutbound) } type Outbound struct { outbound.Adapter router adapter.DNSRouter logger logger.ContextLogger } func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.StubOptions) (adapter.Outbound, error) { return &Outbound{ Adapter: outbound.NewAdapter(C.TypeDNS, tag, []string{N.NetworkTCP, N.NetworkUDP}, nil), router: service.FromContext[adapter.DNSRouter](ctx), logger: logger, }, nil } func (d *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { return nil, os.ErrInvalid } func (d *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { return nil, os.ErrInvalid } 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)) err := HandleStreamDNSRequest(ctx, d.router, conn, metadata) if err != nil { conn.Close() if onClose != nil { onClose(err) } return } } } func (d *Outbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { NewDNSPacketConnection(ctx, d.router, conn, nil, metadata) } ================================================ FILE: protocol/group/selector.go ================================================ package group import ( "context" "net" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/interrupt" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" tun "github.com/sagernet/sing-tun" "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" ) func RegisterSelector(registry *outbound.Registry) { outbound.Register[option.SelectorOutboundOptions](registry, C.TypeSelector, NewSelector) } var ( _ adapter.OutboundGroup = (*Selector)(nil) _ adapter.ConnectionHandler = (*Selector)(nil) _ adapter.PacketConnectionHandler = (*Selector)(nil) ) type Selector struct { outbound.Adapter ctx context.Context outbound adapter.OutboundManager connection adapter.ConnectionManager logger logger.ContextLogger tags []string defaultTag string outbounds map[string]adapter.Outbound selected common.TypedValue[adapter.Outbound] interruptGroup *interrupt.Group interruptExternalConnections bool } func NewSelector(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SelectorOutboundOptions) (adapter.Outbound, error) { outbound := &Selector{ Adapter: outbound.NewAdapter(C.TypeSelector, tag, nil, options.Outbounds), ctx: ctx, outbound: service.FromContext[adapter.OutboundManager](ctx), connection: service.FromContext[adapter.ConnectionManager](ctx), logger: logger, tags: options.Outbounds, defaultTag: options.Default, outbounds: make(map[string]adapter.Outbound), interruptGroup: interrupt.NewGroup(), interruptExternalConnections: options.InterruptExistConnections, } if len(outbound.tags) == 0 { return nil, E.New("missing tags") } return outbound, nil } func (s *Selector) Network() []string { selected := s.selected.Load() if selected == nil { return []string{N.NetworkTCP, N.NetworkUDP} } return selected.Network() } func (s *Selector) Start() error { for i, tag := range s.tags { detour, loaded := s.outbound.Outbound(tag) if !loaded { return E.New("outbound ", i, " not found: ", tag) } s.outbounds[tag] = detour } if s.Tag() != "" { cacheFile := service.FromContext[adapter.CacheFile](s.ctx) if cacheFile != nil { selected := cacheFile.LoadSelected(s.Tag()) if selected != "" { detour, loaded := s.outbounds[selected] if loaded { s.selected.Store(detour) return nil } } } } if s.defaultTag != "" { detour, loaded := s.outbounds[s.defaultTag] if !loaded { return E.New("default outbound not found: ", s.defaultTag) } s.selected.Store(detour) return nil } s.selected.Store(s.outbounds[s.tags[0]]) return nil } func (s *Selector) Now() string { selected := s.selected.Load() if selected == nil { return s.tags[0] } return selected.Tag() } func (s *Selector) All() []string { return s.tags } func (s *Selector) SelectOutbound(tag string) bool { detour, loaded := s.outbounds[tag] if !loaded { return false } if s.selected.Swap(detour) == detour { return true } if s.Tag() != "" { cacheFile := service.FromContext[adapter.CacheFile](s.ctx) if cacheFile != nil { err := cacheFile.StoreSelected(s.Tag(), tag) if err != nil { s.logger.Error("store selected: ", err) } } } s.interruptGroup.Interrupt(s.interruptExternalConnections) return true } func (s *Selector) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { conn, err := s.selected.Load().DialContext(ctx, network, destination) if err != nil { return nil, err } return s.interruptGroup.NewConn(conn, interrupt.IsExternalConnectionFromContext(ctx)), nil } func (s *Selector) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { conn, err := s.selected.Load().ListenPacket(ctx, destination) if err != nil { return nil, err } return s.interruptGroup.NewPacketConn(conn, interrupt.IsExternalConnectionFromContext(ctx)), nil } 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.ConnectionHandler); isHandler { outboundHandler.NewConnection(ctx, conn, metadata, onClose) } else { s.connection.NewConnection(ctx, selected, conn, metadata, onClose) } } 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.PacketConnectionHandler); isHandler { outboundHandler.NewPacketConnection(ctx, conn, metadata, onClose) } else { s.connection.NewPacketConnection(ctx, selected, conn, metadata, onClose) } } func (s *Selector) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { selected := s.selected.Load() if !common.Contains(selected.Network(), metadata.Network) { return nil, E.New(metadata.Network, " is not supported by outbound: ", selected.Tag()) } return selected.(adapter.DirectRouteOutbound).NewDirectRouteConnection(metadata, routeContext, timeout) } func RealTag(detour adapter.Outbound) string { if group, isGroup := detour.(adapter.OutboundGroup); isGroup { return group.Now() } return detour.Tag() } ================================================ FILE: protocol/group/urltest.go ================================================ package group import ( "context" "net" "sync" "sync/atomic" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/interrupt" "github.com/sagernet/sing-box/common/urltest" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/batch" 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/x/list" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/pause" ) func RegisterURLTest(registry *outbound.Registry) { outbound.Register[option.URLTestOutboundOptions](registry, C.TypeURLTest, NewURLTest) } var _ adapter.OutboundGroup = (*URLTest)(nil) type URLTest struct { outbound.Adapter ctx context.Context outbound adapter.OutboundManager connection adapter.ConnectionManager logger log.ContextLogger tags []string link string interval time.Duration tolerance uint16 idleTimeout time.Duration group *URLTestGroup interruptExternalConnections bool } func NewURLTest(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.URLTestOutboundOptions) (adapter.Outbound, error) { outbound := &URLTest{ Adapter: outbound.NewAdapter(C.TypeURLTest, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.Outbounds), ctx: ctx, outbound: service.FromContext[adapter.OutboundManager](ctx), connection: service.FromContext[adapter.ConnectionManager](ctx), logger: logger, tags: options.Outbounds, link: options.URL, interval: time.Duration(options.Interval), tolerance: options.Tolerance, idleTimeout: time.Duration(options.IdleTimeout), interruptExternalConnections: options.InterruptExistConnections, } if len(outbound.tags) == 0 { return nil, E.New("missing tags") } return outbound, nil } func (s *URLTest) Start() error { outbounds := make([]adapter.Outbound, 0, len(s.tags)) for i, tag := range s.tags { detour, loaded := s.outbound.Outbound(tag) if !loaded { return E.New("outbound ", i, " not found: ", tag) } outbounds = append(outbounds, detour) } group, err := NewURLTestGroup(s.ctx, s.outbound, s.logger, outbounds, s.link, s.interval, s.tolerance, s.idleTimeout, s.interruptExternalConnections) if err != nil { return err } s.group = group return nil } func (s *URLTest) PostStart() error { s.group.PostStart() return nil } func (s *URLTest) Close() error { return common.Close( common.PtrOrNil(s.group), ) } func (s *URLTest) Now() string { if s.group.selectedOutboundTCP != nil { return s.group.selectedOutboundTCP.Tag() } else if s.group.selectedOutboundUDP != nil { return s.group.selectedOutboundUDP.Tag() } return "" } func (s *URLTest) All() []string { return s.tags } func (s *URLTest) URLTest(ctx context.Context) (map[string]uint16, error) { return s.group.URLTest(ctx) } func (s *URLTest) CheckOutbounds() { s.group.CheckOutbounds(true) } func (s *URLTest) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { s.group.Touch() var outbound adapter.Outbound switch N.NetworkName(network) { case N.NetworkTCP: outbound = s.group.selectedOutboundTCP case N.NetworkUDP: outbound = s.group.selectedOutboundUDP default: return nil, E.Extend(N.ErrUnknownNetwork, network) } if outbound == nil { outbound, _ = s.group.Select(network) } if outbound == nil { return nil, E.New("missing supported outbound") } conn, err := outbound.DialContext(ctx, network, destination) if err == nil { return s.group.interruptGroup.NewConn(conn, interrupt.IsExternalConnectionFromContext(ctx)), nil } s.logger.ErrorContext(ctx, err) s.group.history.DeleteURLTestHistory(outbound.Tag()) return nil, err } func (s *URLTest) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { s.group.Touch() outbound := s.group.selectedOutboundUDP if outbound == nil { outbound, _ = s.group.Select(N.NetworkUDP) } if outbound == nil { return nil, E.New("missing supported outbound") } conn, err := outbound.ListenPacket(ctx, destination) if err == nil { return s.group.interruptGroup.NewPacketConn(conn, interrupt.IsExternalConnectionFromContext(ctx)), nil } s.logger.ErrorContext(ctx, err) s.group.history.DeleteURLTestHistory(outbound.Tag()) return nil, err } 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) 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) } func (s *URLTest) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { s.group.Touch() selected := s.group.selectedOutboundTCP if selected == nil { selected, _ = s.group.Select(N.NetworkTCP) } if selected == nil { return nil, E.New("missing supported outbound") } if !common.Contains(selected.Network(), metadata.Network) { return nil, E.New(metadata.Network, " is not supported by outbound: ", selected.Tag()) } return selected.(adapter.DirectRouteOutbound).NewDirectRouteConnection(metadata, routeContext, timeout) } type URLTestGroup struct { ctx context.Context outbound adapter.OutboundManager pause pause.Manager pauseCallback *list.Element[pause.Callback] logger log.Logger outbounds []adapter.Outbound link string interval time.Duration tolerance uint16 idleTimeout time.Duration history adapter.URLTestHistoryStorage checking atomic.Bool selectedOutboundTCP adapter.Outbound selectedOutboundUDP adapter.Outbound interruptGroup *interrupt.Group interruptExternalConnections bool access sync.Mutex ticker *time.Ticker close chan struct{} started bool lastActive common.TypedValue[time.Time] } func NewURLTestGroup(ctx context.Context, outboundManager adapter.OutboundManager, logger log.Logger, outbounds []adapter.Outbound, link string, interval time.Duration, tolerance uint16, idleTimeout time.Duration, interruptExternalConnections bool) (*URLTestGroup, error) { if interval == 0 { interval = C.DefaultURLTestInterval } if tolerance == 0 { tolerance = 50 } if idleTimeout == 0 { idleTimeout = C.DefaultURLTestIdleTimeout } if interval > idleTimeout { return nil, E.New("interval must be less or equal than idle_timeout") } var history adapter.URLTestHistoryStorage if historyFromCtx := service.PtrFromContext[urltest.HistoryStorage](ctx); historyFromCtx != nil { history = historyFromCtx } else if clashServer := service.FromContext[adapter.ClashServer](ctx); clashServer != nil { history = clashServer.HistoryStorage() } else { history = urltest.NewHistoryStorage() } return &URLTestGroup{ ctx: ctx, outbound: outboundManager, logger: logger, outbounds: outbounds, link: link, interval: interval, tolerance: tolerance, idleTimeout: idleTimeout, history: history, close: make(chan struct{}), pause: service.FromContext[pause.Manager](ctx), interruptGroup: interrupt.NewGroup(), interruptExternalConnections: interruptExternalConnections, }, nil } func (g *URLTestGroup) PostStart() { g.access.Lock() defer g.access.Unlock() g.started = true g.lastActive.Store(time.Now()) go g.CheckOutbounds(false) } func (g *URLTestGroup) Touch() { if !g.started { return } g.access.Lock() defer g.access.Unlock() if g.ticker != nil { g.lastActive.Store(time.Now()) return } ticker := time.NewTicker(g.interval) g.ticker = ticker g.pauseCallback = pause.RegisterTicker(g.pause, ticker, g.interval, nil) go g.loopCheck(ticker, g.close) } func (g *URLTestGroup) Close() error { g.access.Lock() defer g.access.Unlock() if g.ticker == nil { return nil } g.ticker.Stop() g.ticker = nil g.pause.UnregisterCallback(g.pauseCallback) g.pauseCallback = nil close(g.close) return nil } func (g *URLTestGroup) Select(network string) (adapter.Outbound, bool) { var minDelay uint16 var minOutbound adapter.Outbound switch network { case N.NetworkTCP: if g.selectedOutboundTCP != nil { if history := g.history.LoadURLTestHistory(RealTag(g.selectedOutboundTCP)); history != nil { minOutbound = g.selectedOutboundTCP minDelay = history.Delay } } case N.NetworkUDP: if g.selectedOutboundUDP != nil { if history := g.history.LoadURLTestHistory(RealTag(g.selectedOutboundUDP)); history != nil { minOutbound = g.selectedOutboundUDP minDelay = history.Delay } } } for _, detour := range g.outbounds { if !common.Contains(detour.Network(), network) { continue } history := g.history.LoadURLTestHistory(RealTag(detour)) if history == nil { continue } if minDelay == 0 || minDelay > history.Delay+g.tolerance { minDelay = history.Delay minOutbound = detour } } if minOutbound == nil { for _, detour := range g.outbounds { if !common.Contains(detour.Network(), network) { continue } return detour, false } return nil, false } return minOutbound, true } func (g *URLTestGroup) loopCheck(ticker *time.Ticker, closeChan <-chan struct{}) { if time.Since(g.lastActive.Load()) > g.interval { g.lastActive.Store(time.Now()) g.CheckOutbounds(false) } for { select { case <-closeChan: return case <-ticker.C: } if time.Since(g.lastActive.Load()) > g.idleTimeout { g.access.Lock() if g.ticker == ticker { g.ticker.Stop() g.ticker = nil g.pause.UnregisterCallback(g.pauseCallback) g.pauseCallback = nil } g.access.Unlock() return } g.CheckOutbounds(false) } } func (g *URLTestGroup) CheckOutbounds(force bool) { _, _ = g.urlTest(g.ctx, force) } func (g *URLTestGroup) URLTest(ctx context.Context) (map[string]uint16, error) { return g.urlTest(ctx, false) } func (g *URLTestGroup) urlTest(ctx context.Context, force bool) (map[string]uint16, error) { result := make(map[string]uint16) if g.checking.Swap(true) { return result, nil } defer g.checking.Store(false) b, _ := batch.New(ctx, batch.WithConcurrencyNum[any](10)) checked := make(map[string]bool) var resultAccess sync.Mutex for _, detour := range g.outbounds { tag := detour.Tag() realTag := RealTag(detour) if checked[realTag] { continue } history := g.history.LoadURLTestHistory(realTag) if !force && history != nil && time.Since(history.Time) < g.interval { continue } checked[realTag] = true p, loaded := g.outbound.Outbound(realTag) if !loaded { continue } b.Go(realTag, func() (any, error) { testCtx, cancel := context.WithTimeout(g.ctx, C.TCPTimeout) defer cancel() t, err := urltest.URLTest(testCtx, g.link, p) if err != nil { g.logger.Debug("outbound ", tag, " unavailable: ", err) g.history.DeleteURLTestHistory(realTag) } else { g.logger.Debug("outbound ", tag, " available: ", t, "ms") g.history.StoreURLTestHistory(realTag, &adapter.URLTestHistory{ Time: time.Now(), Delay: t, }) resultAccess.Lock() result[tag] = t resultAccess.Unlock() } return nil, nil }) } b.Wait() g.performUpdateCheck() return result, nil } func (g *URLTestGroup) performUpdateCheck() { var updated bool if outbound, exists := g.Select(N.NetworkTCP); outbound != nil && (g.selectedOutboundTCP == nil || (exists && outbound != g.selectedOutboundTCP)) { if g.selectedOutboundTCP != nil { updated = true } g.selectedOutboundTCP = outbound } if outbound, exists := g.Select(N.NetworkUDP); outbound != nil && (g.selectedOutboundUDP == nil || (exists && outbound != g.selectedOutboundUDP)) { if g.selectedOutboundUDP != nil { updated = true } g.selectedOutboundUDP = outbound } if updated { g.interruptGroup.Interrupt(g.interruptExternalConnections) } } ================================================ FILE: protocol/http/inbound.go ================================================ package http import ( std_bufio "bufio" "context" "net" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/common/listener" "github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/common/uot" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/auth" E "github.com/sagernet/sing/common/exceptions" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/protocol/http" ) func RegisterInbound(registry *inbound.Registry) { inbound.Register[option.HTTPMixedInboundOptions](registry, C.TypeHTTP, NewInbound) } var _ adapter.TCPInjectableInbound = (*Inbound)(nil) type Inbound struct { inbound.Adapter router adapter.ConnectionRouterEx logger log.ContextLogger listener *listener.Listener authenticator *auth.Authenticator tlsConfig tls.ServerConfig } func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPMixedInboundOptions) (adapter.Inbound, error) { inbound := &Inbound{ Adapter: inbound.NewAdapter(C.TypeHTTP, tag), router: uot.NewRouter(router, logger), logger: logger, authenticator: auth.NewAuthenticator(options.Users), } if options.TLS != nil { tlsConfig, err := tls.NewServerWithOptions(tls.ServerOptions{ Context: ctx, Logger: logger, Options: common.PtrValueOrDefault(options.TLS), KTLSCompatible: true, }) if err != nil { return nil, err } inbound.tlsConfig = tlsConfig } inbound.listener = listener.New(listener.Options{ Context: ctx, Logger: logger, Network: []string{N.NetworkTCP}, Listen: options.ListenOptions, ConnectionHandler: inbound, SetSystemProxy: options.SetSystemProxy, SystemProxySOCKS: false, }) return inbound, nil } func (h *Inbound) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } if h.tlsConfig != nil { err := h.tlsConfig.Start() if err != nil { return E.Cause(err, "create TLS config") } } return h.listener.Start() } func (h *Inbound) Close() error { return common.Close( h.listener, h.tlsConfig, ) } 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 { N.CloseOnHandshakeFailure(conn, onClose, err) h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source, ": TLS handshake")) return } conn = tlsConn } 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)) } } func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = h.Tag() metadata.InboundType = h.Type() user, loaded := auth.UserFromContext[string](ctx) if !loaded { h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) h.router.RouteConnectionEx(ctx, conn, metadata, onClose) return } metadata.User = user h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) h.router.RouteConnectionEx(ctx, conn, metadata, onClose) } func (h *Inbound) streamUserPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = h.Tag() metadata.InboundType = h.Type() user, loaded := auth.UserFromContext[string](ctx) if !loaded { h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) return } metadata.User = user h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } ================================================ FILE: protocol/http/outbound.go ================================================ package http import ( "context" "net" "os" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "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" sHTTP "github.com/sagernet/sing/protocol/http" ) func RegisterOutbound(registry *outbound.Registry) { outbound.Register[option.HTTPOutboundOptions](registry, C.TypeHTTP, NewOutbound) } type Outbound struct { outbound.Adapter logger logger.ContextLogger client *sHTTP.Client } func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPOutboundOptions) (adapter.Outbound, error) { outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain()) if err != nil { return nil, err } detour, err := tls.NewDialerFromOptions(ctx, logger, outboundDialer, options.Server, common.PtrValueOrDefault(options.TLS)) if err != nil { return nil, err } return &Outbound{ Adapter: outbound.NewAdapterWithDialerOptions(C.TypeHTTP, tag, []string{N.NetworkTCP}, options.DialerOptions), logger: logger, client: sHTTP.NewClient(sHTTP.Options{ Dialer: detour, Server: options.ServerOptions.Build(), Username: options.Username, Password: options.Password, Path: options.Path, Headers: options.Headers.Build(), }), }, nil } func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { ctx, metadata := adapter.ExtendContext(ctx) metadata.Outbound = h.Tag() metadata.Destination = destination h.logger.InfoContext(ctx, "outbound connection to ", destination) return h.client.DialContext(ctx, network, destination) } func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { return nil, os.ErrInvalid } ================================================ FILE: protocol/hysteria/inbound.go ================================================ package hysteria import ( "context" "net" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/inbound" "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/hysteria" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/auth" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) func RegisterInbound(registry *inbound.Registry) { inbound.Register[option.HysteriaInboundOptions](registry, C.TypeHysteria, NewInbound) } type Inbound struct { inbound.Adapter router adapter.Router logger log.ContextLogger listener *listener.Listener tlsConfig tls.ServerConfig service *hysteria.Service[int] userNameList []string } func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaInboundOptions) (adapter.Inbound, error) { options.UDPFragmentDefault = true if options.TLS == nil || !options.TLS.Enabled { return nil, C.ErrTLSRequired } tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS)) if err != nil { return nil, err } inbound := &Inbound{ Adapter: inbound.NewAdapter(C.TypeHysteria, tag), router: router, logger: logger, listener: listener.New(listener.Options{ Context: ctx, Logger: logger, Listen: options.ListenOptions, }), tlsConfig: tlsConfig, } var sendBps, receiveBps uint64 if options.Up.Value() > 0 { sendBps = options.Up.Value() } else { sendBps = uint64(options.UpMbps) * hysteria.MbpsToBps } if options.Down.Value() > 0 { receiveBps = options.Down.Value() } else { receiveBps = uint64(options.DownMbps) * hysteria.MbpsToBps } var udpTimeout time.Duration if options.UDPTimeout != 0 { udpTimeout = time.Duration(options.UDPTimeout) } else { udpTimeout = C.UDPTimeout } service, err := hysteria.NewService[int](hysteria.ServiceOptions{ Context: ctx, Logger: logger, SendBPS: sendBps, ReceiveBPS: receiveBps, XPlusPassword: options.Obfs, TLSConfig: tlsConfig, QUICOptions: buildInboundQUICOptions(options), UDPTimeout: udpTimeout, Handler: inbound, }) if err != nil { return nil, err } userList := make([]int, 0, len(options.Users)) userNameList := make([]string, 0, len(options.Users)) userPasswordList := make([]string, 0, len(options.Users)) for index, user := range options.Users { userList = append(userList, index) userNameList = append(userNameList, user.Name) var password string if user.AuthString != "" { password = user.AuthString } else { password = string(user.Auth) } userPasswordList = append(userPasswordList, password) } service.UpdateUsers(userList, userPasswordList) inbound.service = service inbound.userNameList = userNameList return inbound, nil } func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { ctx = log.ContextWithNewID(ctx) var metadata adapter.InboundContext metadata.Inbound = h.Tag() metadata.InboundType = h.Type() //nolint:staticcheck metadata.InboundDetour = h.listener.ListenOptions().Detour //nolint:staticcheck metadata.OriginDestination = h.listener.UDPAddr() metadata.Source = source metadata.Destination = destination h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) userID, _ := auth.UserFromContext[int](ctx) if userName := h.userNameList[userID]; userName != "" { metadata.User = userName h.logger.InfoContext(ctx, "[", userName, "] inbound connection to ", metadata.Destination) } else { h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) } h.router.RouteConnectionEx(ctx, conn, metadata, onClose) } func (h *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { ctx = log.ContextWithNewID(ctx) var metadata adapter.InboundContext metadata.Inbound = h.Tag() metadata.InboundType = h.Type() //nolint:staticcheck metadata.InboundDetour = h.listener.ListenOptions().Detour //nolint:staticcheck metadata.OriginDestination = h.listener.UDPAddr() metadata.Source = source metadata.Destination = destination h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) userID, _ := auth.UserFromContext[int](ctx) if userName := h.userNameList[userID]; userName != "" { metadata.User = userName h.logger.InfoContext(ctx, "[", userName, "] inbound packet connection to ", metadata.Destination) } else { h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) } h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } func (h *Inbound) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } if h.tlsConfig != nil { err := h.tlsConfig.Start() if err != nil { return err } } packetConn, err := h.listener.ListenUDP() if err != nil { return err } return h.service.Start(packetConn) } func (h *Inbound) Close() error { return common.Close( h.listener, h.tlsConfig, common.PtrOrNil(h.service), ) } ================================================ FILE: protocol/hysteria/outbound.go ================================================ package hysteria import ( "context" "net" "os" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/protocol/tuic" "github.com/sagernet/sing-quic/hysteria" "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" ) func RegisterOutbound(registry *outbound.Registry) { outbound.Register[option.HysteriaOutboundOptions](registry, C.TypeHysteria, NewOutbound) } var ( _ adapter.Outbound = (*tuic.Outbound)(nil) _ adapter.InterfaceUpdateListener = (*tuic.Outbound)(nil) ) type Outbound struct { outbound.Adapter logger logger.ContextLogger client *hysteria.Client } func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaOutboundOptions) (adapter.Outbound, error) { options.UDPFragmentDefault = true if options.TLS == nil || !options.TLS.Enabled { return nil, C.ErrTLSRequired } tlsConfig, err := tls.NewClient(ctx, logger, options.Server, common.PtrValueOrDefault(options.TLS)) if err != nil { return nil, err } outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain()) if err != nil { return nil, err } networkList := options.Network.Build() var password string if options.AuthString != "" { password = options.AuthString } else { password = string(options.Auth) } var sendBps, receiveBps uint64 if options.Up.Value() > 0 { sendBps = options.Up.Value() } else { sendBps = uint64(options.UpMbps) * hysteria.MbpsToBps } if options.Down.Value() > 0 { receiveBps = options.Down.Value() } else { 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, QUICOptions: buildOutboundQUICOptions(options), UDPDisabled: !common.Contains(networkList, N.NetworkUDP), }) if err != nil { return nil, err } return &Outbound{ Adapter: outbound.NewAdapterWithDialerOptions(C.TypeHysteria, tag, networkList, options.DialerOptions), logger: logger, client: client, }, nil } func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { switch N.NetworkName(network) { case N.NetworkTCP: h.logger.InfoContext(ctx, "outbound connection to ", destination) return h.client.DialConn(ctx, destination) case N.NetworkUDP: conn, err := h.ListenPacket(ctx, destination) if err != nil { return nil, err } return bufio.NewBindPacketConn(conn, destination), nil default: return nil, E.New("unsupported network: ", network) } } func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { h.logger.InfoContext(ctx, "outbound packet connection to ", destination) return h.client.ListenPacket(ctx, destination) } func (h *Outbound) InterfaceUpdated() { h.client.CloseWithError(E.New("network changed")) } func (h *Outbound) Close() error { return h.client.CloseWithError(os.ErrClosed) } ================================================ FILE: protocol/hysteria/quic.go ================================================ 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 } ================================================ FILE: protocol/hysteria2/inbound.go ================================================ package hysteria2 import ( "context" "net" "net/http" "net/http/httputil" "net/netip" "net/url" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/inbound" "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" 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) { inbound.Register[option.Hysteria2InboundOptions](registry, C.TypeHysteria2, NewInbound) } type Inbound struct { inbound.Adapter router adapter.Router logger log.ContextLogger listener *listener.Listener tlsConfig tls.ServerConfig service *hysteria2.Service[int] userNameList []string } func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2InboundOptions) (adapter.Inbound, error) { options.UDPFragmentDefault = true if options.TLS == nil || !options.TLS.Enabled { return nil, C.ErrTLSRequired } tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS)) if err != nil { return nil, err } var salamanderPassword string if options.Obfs != nil { if options.Obfs.Password == "" { return nil, E.New("missing obfs password") } switch options.Obfs.Type { case hysteria2.ObfsTypeSalamander: salamanderPassword = options.Obfs.Password default: return nil, E.New("unknown obfs type: ", options.Obfs.Type) } } var masqueradeHandler http.Handler if options.Masquerade != nil && options.Masquerade.Type != "" { switch options.Masquerade.Type { case C.Hysterai2MasqueradeTypeFile: masqueradeHandler = http.FileServer(http.Dir(options.Masquerade.FileOptions.Directory)) case C.Hysterai2MasqueradeTypeProxy: masqueradeURL, err := url.Parse(options.Masquerade.ProxyOptions.URL) if err != nil { return nil, E.Cause(err, "parse masquerade URL") } masqueradeHandler = &httputil.ReverseProxy{ Rewrite: func(r *httputil.ProxyRequest) { r.SetURL(masqueradeURL) if !options.Masquerade.ProxyOptions.RewriteHost { r.Out.Host = r.In.Host } }, ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { w.WriteHeader(http.StatusBadGateway) }, } case C.Hysterai2MasqueradeTypeString: masqueradeHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if options.Masquerade.StringOptions.StatusCode != 0 { w.WriteHeader(options.Masquerade.StringOptions.StatusCode) } for key, values := range options.Masquerade.StringOptions.Headers { for _, value := range values { w.Header().Add(key, value) } } w.Write([]byte(options.Masquerade.StringOptions.Content)) }) default: return nil, E.New("unknown masquerade type: ", options.Masquerade.Type) } } inbound := &Inbound{ Adapter: inbound.NewAdapter(C.TypeHysteria2, tag), router: router, logger: logger, listener: listener.New(listener.Options{ Context: ctx, Logger: logger, Listen: options.ListenOptions, }), tlsConfig: tlsConfig, } var udpTimeout time.Duration if options.UDPTimeout != 0 { udpTimeout = time.Duration(options.UDPTimeout) } else { udpTimeout = C.UDPTimeout } 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, 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, MasqueradeHandler: masqueradeHandler, BBRProfile: options.BBRProfile, RealmOptions: realmOptions, }) if err != nil { return nil, err } userList := make([]int, 0, len(options.Users)) userNameList := make([]string, 0, len(options.Users)) userPasswordList := make([]string, 0, len(options.Users)) for index, user := range options.Users { userList = append(userList, index) userNameList = append(userNameList, user.Name) userPasswordList = append(userPasswordList, user.Password) } hysteriaService.UpdateUsers(userList, userPasswordList) inbound.service = hysteriaService inbound.userNameList = userNameList return inbound, nil } func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { ctx = log.ContextWithNewID(ctx) var metadata adapter.InboundContext metadata.Inbound = h.Tag() metadata.InboundType = h.Type() //nolint:staticcheck metadata.InboundDetour = h.listener.ListenOptions().Detour //nolint:staticcheck metadata.OriginDestination = h.listener.UDPAddr() metadata.Source = source metadata.Destination = destination h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) userID, _ := auth.UserFromContext[int](ctx) if userName := h.userNameList[userID]; userName != "" { metadata.User = userName h.logger.InfoContext(ctx, "[", userName, "] inbound connection to ", metadata.Destination) } else { h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) } h.router.RouteConnectionEx(ctx, conn, metadata, onClose) } func (h *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { ctx = log.ContextWithNewID(ctx) var metadata adapter.InboundContext metadata.Inbound = h.Tag() metadata.InboundType = h.Type() //nolint:staticcheck metadata.InboundDetour = h.listener.ListenOptions().Detour //nolint:staticcheck metadata.OriginDestination = h.listener.UDPAddr() metadata.Source = source metadata.Destination = destination h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) userID, _ := auth.UserFromContext[int](ctx) if userName := h.userNameList[userID]; userName != "" { metadata.User = userName h.logger.InfoContext(ctx, "[", userName, "] inbound packet connection to ", metadata.Destination) } else { h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) } h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } func (h *Inbound) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } if h.tlsConfig != nil { err := h.tlsConfig.Start() if err != nil { return err } } packetConn, err := h.listener.ListenUDP() if err != nil { return err } return h.service.Start(packetConn) } func (h *Inbound) InterfaceUpdated() { h.service.Reset() } func (h *Inbound) Close() error { return common.Close( h.listener, h.tlsConfig, common.PtrOrNil(h.service), ) } ================================================ FILE: protocol/hysteria2/outbound.go ================================================ package hysteria2 import ( "context" "net" "net/http" "net/netip" "net/url" "os" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "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-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) { outbound.Register[option.Hysteria2OutboundOptions](registry, C.TypeHysteria2, NewOutbound) } var ( _ adapter.Outbound = (*tuic.Outbound)(nil) _ adapter.InterfaceUpdateListener = (*tuic.Outbound)(nil) ) type Outbound struct { outbound.Adapter logger logger.ContextLogger client *hysteria2.Client } func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2OutboundOptions) (adapter.Outbound, error) { options.UDPFragmentDefault = true if options.TLS == nil || !options.TLS.Enabled { return nil, C.ErrTLSRequired } 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 } var salamanderPassword string if options.Obfs != nil { if options.Obfs.Password == "" { return nil, E.New("missing obfs password") } switch options.Obfs.Type { case hysteria2.ObfsTypeSalamander: salamanderPassword = options.Obfs.Password default: return nil, E.New("unknown obfs type: ", options.Obfs.Type) } } outboundDialer, err := dialer.NewWithOptions(dialer.Options{ Context: ctx, Options: options.DialerOptions, RemoteIsDomain: options.ServerIsDomain(), }) 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{ Context: ctx, Dialer: outboundDialer, Logger: logger, BrutalDebug: options.BrutalDebug, 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, 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, RealmOptions: realmOptions, }) if err != nil { return nil, err } return &Outbound{ Adapter: outbound.NewAdapterWithDialerOptions(C.TypeHysteria2, tag, networkList, options.DialerOptions), logger: logger, client: client, }, 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: h.logger.InfoContext(ctx, "outbound connection to ", destination) return h.client.DialConn(ctx, destination) case N.NetworkUDP: conn, err := h.ListenPacket(ctx, destination) if err != nil { return nil, err } return bufio.NewBindPacketConn(conn, destination), nil default: return nil, E.New("unsupported network: ", network) } } func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { h.logger.InfoContext(ctx, "outbound packet connection to ", destination) return h.client.ListenPacket(ctx) } func (h *Outbound) InterfaceUpdated() { h.client.CloseWithError(E.New("network changed")) } func (h *Outbound) Close() error { return h.client.CloseWithError(os.ErrClosed) } ================================================ FILE: protocol/hysteria2/realm.go ================================================ package hysteria2 import ( "context" "errors" "net" "net/http" "time" "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/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/go-chi/chi/v5" "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) { boxService.Register[option.HysteriaRealmServiceOptions](registry, C.TypeHysteriaRealm, NewRealmService) } 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: 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) }, }, 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, )) } ================================================ FILE: protocol/hysteria2/realm_server.go ================================================ 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 } ================================================ FILE: protocol/mixed/inbound.go ================================================ package mixed import ( std_bufio "bufio" "context" "net" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/common/listener" "github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/common/uot" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/auth" E "github.com/sagernet/sing/common/exceptions" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/protocol/http" "github.com/sagernet/sing/protocol/socks" "github.com/sagernet/sing/protocol/socks/socks4" "github.com/sagernet/sing/protocol/socks/socks5" ) func RegisterInbound(registry *inbound.Registry) { inbound.Register[option.HTTPMixedInboundOptions](registry, C.TypeMixed, NewInbound) } var _ adapter.TCPInjectableInbound = (*Inbound)(nil) type Inbound struct { inbound.Adapter router adapter.ConnectionRouterEx logger log.ContextLogger listener *listener.Listener authenticator *auth.Authenticator tlsConfig tls.ServerConfig udpTimeout time.Duration } func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPMixedInboundOptions) (adapter.Inbound, error) { var udpTimeout time.Duration if options.UDPTimeout != 0 { udpTimeout = time.Duration(options.UDPTimeout) } else { udpTimeout = C.UDPTimeout } inbound := &Inbound{ Adapter: inbound.NewAdapter(C.TypeMixed, tag), router: uot.NewRouter(router, logger), logger: logger, authenticator: auth.NewAuthenticator(options.Users), udpTimeout: udpTimeout, } if options.TLS != nil { tlsConfig, err := tls.NewServerWithOptions(tls.ServerOptions{ Context: ctx, Logger: logger, Options: common.PtrValueOrDefault(options.TLS), KTLSCompatible: true, }) if err != nil { return nil, err } inbound.tlsConfig = tlsConfig } inbound.listener = listener.New(listener.Options{ Context: ctx, Logger: logger, Network: []string{N.NetworkTCP}, Listen: options.ListenOptions, ConnectionHandler: inbound, SetSystemProxy: options.SetSystemProxy, SystemProxySOCKS: true, }) return inbound, nil } func (h *Inbound) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } if h.tlsConfig != nil { err := h.tlsConfig.Start() if err != nil { return E.Cause(err, "create TLS config") } } return h.listener.Start() } func (h *Inbound) Close() error { return common.Close( h.listener, h.tlsConfig, ) } 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 { if E.IsClosedOrCanceled(err) { h.logger.DebugContext(ctx, "connection closed: ", err) } else { h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) } } } func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error { if h.tlsConfig != nil { tlsConn, err := tls.ServerHandshake(ctx, conn, h.tlsConfig) if err != nil { return E.Cause(err, "TLS handshake") } conn = tlsConn } reader := std_bufio.NewReader(conn) headerBytes, err := reader.Peek(1) if err != nil { return E.Cause(err, "peek first byte") } switch headerBytes[0] { case socks4.Version, socks5.Version: 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.NewUpstreamHandler(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, onClose) } } func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = h.Tag() metadata.InboundType = h.Type() user, loaded := auth.UserFromContext[string](ctx) if !loaded { h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) h.router.RouteConnectionEx(ctx, conn, metadata, onClose) return } metadata.User = user h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) h.router.RouteConnectionEx(ctx, conn, metadata, onClose) } func (h *Inbound) streamUserPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = h.Tag() metadata.InboundType = h.Type() user, loaded := auth.UserFromContext[string](ctx) if !loaded { if !metadata.Destination.IsValid() { h.logger.InfoContext(ctx, "inbound packet connection") } else { h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) } h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) return } metadata.User = user if !metadata.Destination.IsValid() { h.logger.InfoContext(ctx, "[", user, "] inbound packet connection") } else { h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) } h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } ================================================ FILE: protocol/naive/inbound.go ================================================ package naive import ( "context" "errors" "io" "net" "net/http" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/common/listener" "github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/common/uot" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/transport/v2rayhttp" "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" aTLS "github.com/sagernet/sing/common/tls" sHttp "github.com/sagernet/sing/protocol/http" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" ) var ( ConfigureHTTP3ListenerFunc func(ctx context.Context, logger logger.Logger, listener *listener.Listener, handler http.Handler, tlsConfig tls.ServerConfig, options option.NaiveInboundOptions) (io.Closer, error) WrapError func(error) error ) func RegisterInbound(registry *inbound.Registry) { inbound.Register[option.NaiveInboundOptions](registry, C.TypeNaive, NewInbound) } type Inbound struct { inbound.Adapter ctx context.Context router adapter.ConnectionRouterEx logger logger.ContextLogger options option.NaiveInboundOptions listener *listener.Listener network []string networkIsDefault bool authenticator *auth.Authenticator tlsConfig tls.ServerConfig httpServer *http.Server h3Server io.Closer } func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.NaiveInboundOptions) (adapter.Inbound, error) { inbound := &Inbound{ Adapter: inbound.NewAdapter(C.TypeNaive, tag), ctx: ctx, router: uot.NewRouter(router, logger), logger: logger, listener: listener.New(listener.Options{ Context: ctx, Logger: logger, Listen: options.ListenOptions, }), networkIsDefault: options.Network == "", network: options.Network.Build(), authenticator: auth.NewAuthenticator(options.Users), } if common.Contains(inbound.network, N.NetworkUDP) { if options.TLS == nil || !options.TLS.Enabled { return nil, E.New("TLS is required for QUIC server") } } if len(options.Users) == 0 { return nil, E.New("missing users") } if options.TLS != nil { tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS)) if err != nil { return nil, err } inbound.tlsConfig = tlsConfig } return inbound, nil } func (n *Inbound) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } if n.tlsConfig != nil { err := n.tlsConfig.Start() if err != nil { return E.Cause(err, "create TLS config") } } if common.Contains(n.network, N.NetworkTCP) { tcpListener, err := n.listener.ListenTCP() if err != nil { return err } n.httpServer = &http.Server{ Handler: h2c.NewHandler(n, &http2.Server{}), BaseContext: func(listener net.Listener) context.Context { return n.ctx }, } go func() { listener := net.Listener(tcpListener) if n.tlsConfig != nil { if len(n.tlsConfig.NextProtos()) == 0 { n.tlsConfig.SetNextProtos([]string{http2.NextProtoTLS, "http/1.1"}) } else if !common.Contains(n.tlsConfig.NextProtos(), http2.NextProtoTLS) { n.tlsConfig.SetNextProtos(append([]string{http2.NextProtoTLS}, n.tlsConfig.NextProtos()...)) } listener = aTLS.NewListener(tcpListener, n.tlsConfig) } sErr := n.httpServer.Serve(listener) if sErr != nil && !errors.Is(sErr, http.ErrServerClosed) { n.logger.Error("http server serve error: ", sErr) } }() } if common.Contains(n.network, N.NetworkUDP) { http3Server, err := ConfigureHTTP3ListenerFunc(n.ctx, n.logger, n.listener, n, n.tlsConfig, n.options) if err == nil { n.h3Server = http3Server } else if len(n.network) > 1 { n.logger.Warn(E.Cause(err, "naive http3 disabled")) } else { return err } } return nil } func (n *Inbound) Close() error { return common.Close( n.listener, common.PtrOrNil(n.httpServer), n.h3Server, n.tlsConfig, ) } func (n *Inbound) ServeHTTP(writer http.ResponseWriter, request *http.Request) { ctx := log.ContextWithNewID(request.Context()) if request.Method != "CONNECT" { rejectHTTP(writer, http.StatusBadRequest) n.badRequest(ctx, request, E.New("not CONNECT request")) return } else if request.Header.Get("Padding") == "" { rejectHTTP(writer, http.StatusBadRequest) n.badRequest(ctx, request, E.New("missing naive padding")) return } userName, password, authOk := sHttp.ParseBasicAuth(request.Header.Get("Proxy-Authorization")) if authOk { authOk = n.authenticator.Verify(userName, password) } if !authOk { rejectHTTP(writer, http.StatusProxyAuthRequired) n.badRequest(ctx, request, E.New("authorization failed")) return } writer.Header().Set("Padding", generatePaddingHeader()) writer.WriteHeader(http.StatusOK) writer.(http.Flusher).Flush() hostPort := request.Header.Get("-connect-authority") if hostPort == "" { hostPort = request.URL.Host if hostPort == "" { hostPort = request.Host } } source := sHttp.SourceAddress(request) destination := M.ParseSocksaddr(hostPort).Unwrap() if hijacker, isHijacker := writer.(http.Hijacker); isHijacker { conn, _, err := hijacker.Hijack() if err != nil { n.badRequest(ctx, request, E.New("hijack failed")) return } n.newConnection(ctx, false, &naiveConn{Conn: conn}, userName, source, destination) } else { n.newConnection(ctx, true, &naiveH2Conn{ reader: request.Body, writer: writer, flusher: writer.(http.Flusher), remoteAddress: source, }, userName, source, destination) } } func (n *Inbound) newConnection(ctx context.Context, waitForClose bool, conn net.Conn, userName string, source M.Socksaddr, destination M.Socksaddr) { if userName != "" { n.logger.InfoContext(ctx, "[", userName, "] inbound connection from ", source) n.logger.InfoContext(ctx, "[", userName, "] inbound connection to ", destination) } else { n.logger.InfoContext(ctx, "inbound connection from ", source) n.logger.InfoContext(ctx, "inbound connection to ", destination) } var metadata adapter.InboundContext metadata.Inbound = n.Tag() metadata.InboundType = n.Type() //nolint:staticcheck metadata.InboundDetour = n.listener.ListenOptions().Detour //nolint:staticcheck metadata.Source = source metadata.Destination = destination metadata.OriginDestination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap() metadata.User = userName if !waitForClose { n.router.RouteConnectionEx(ctx, conn, metadata, nil) } else { done := make(chan struct{}) wrapper := v2rayhttp.NewHTTP2Wrapper(conn) n.router.RouteConnectionEx(ctx, conn, metadata, N.OnceClose(func(it error) { close(done) })) <-done wrapper.CloseWrapper() } } func (n *Inbound) badRequest(ctx context.Context, request *http.Request, err error) { n.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", request.RemoteAddr)) } func rejectHTTP(writer http.ResponseWriter, statusCode int) { hijacker, ok := writer.(http.Hijacker) if !ok { writer.WriteHeader(statusCode) return } conn, _, err := hijacker.Hijack() if err != nil { writer.WriteHeader(statusCode) return } if tcpConn, isTCP := common.Cast[*net.TCPConn](conn); isTCP { tcpConn.SetLinger(0) } conn.Close() } ================================================ FILE: protocol/naive/inbound_conn.go ================================================ package naive import ( "encoding/binary" "io" "math/rand" "net" "net/http" "os" "time" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/baderror" "github.com/sagernet/sing/common/buf" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/rw" ) const paddingCount = 8 func generatePaddingHeader() string { paddingLen := rand.Intn(32) + 30 padding := make([]byte, paddingLen) bits := rand.Uint64() for i := range 16 { padding[i] = "!#$()+<>?@[]^`{}"[bits&15] bits >>= 4 } for i := 16; i < paddingLen; i++ { padding[i] = '~' } return string(padding) } type paddingConn struct { readPadding int writePadding int readRemaining int paddingRemaining int } func (p *paddingConn) readWithPadding(reader io.Reader, buffer []byte) (n int, err error) { if p.readRemaining > 0 { if len(buffer) > p.readRemaining { buffer = buffer[:p.readRemaining] } n, err = reader.Read(buffer) if err != nil { return } p.readRemaining -= n return } if p.paddingRemaining > 0 { err = rw.SkipN(reader, p.paddingRemaining) if err != nil { return } p.paddingRemaining = 0 } if p.readPadding < paddingCount { var paddingHeader []byte if len(buffer) >= 3 { paddingHeader = buffer[:3] } else { paddingHeader = make([]byte, 3) } _, err = io.ReadFull(reader, paddingHeader) if err != nil { return } originalDataSize := int(binary.BigEndian.Uint16(paddingHeader[:2])) paddingSize := int(paddingHeader[2]) if len(buffer) > originalDataSize { buffer = buffer[:originalDataSize] } n, err = reader.Read(buffer) if err != nil { return } p.readPadding++ p.readRemaining = originalDataSize - n p.paddingRemaining = paddingSize return } return reader.Read(buffer) } func (p *paddingConn) writeWithPadding(writer io.Writer, data []byte) (n int, err error) { if p.writePadding < paddingCount { paddingSize := rand.Intn(256) buffer := buf.NewSize(3 + len(data) + paddingSize) defer buffer.Release() header := buffer.Extend(3) binary.BigEndian.PutUint16(header, uint16(len(data))) header[2] = byte(paddingSize) common.Must1(buffer.Write(data)) common.Must(buffer.WriteZeroN(paddingSize)) _, err = writer.Write(buffer.Bytes()) if err == nil { n = len(data) } p.writePadding++ return } return writer.Write(data) } func (p *paddingConn) writeBufferWithPadding(writer io.Writer, buffer *buf.Buffer) error { if p.writePadding < paddingCount { bufferLen := buffer.Len() if bufferLen > 65535 { _, err := p.writeChunked(writer, buffer.Bytes()) return err } paddingSize := rand.Intn(256) header := buffer.ExtendHeader(3) binary.BigEndian.PutUint16(header, uint16(bufferLen)) header[2] = byte(paddingSize) common.Must(buffer.WriteZeroN(paddingSize)) p.writePadding++ } return common.Error(writer.Write(buffer.Bytes())) } func (p *paddingConn) writeChunked(writer io.Writer, data []byte) (n int, err error) { for len(data) > 0 { var chunk []byte if len(data) > 65535 { chunk = data[:65535] data = data[65535:] } else { chunk = data data = nil } var written int written, err = p.writeWithPadding(writer, chunk) n += written if err != nil { return } } return } func (p *paddingConn) frontHeadroom() int { if p.writePadding < paddingCount { return 3 } return 0 } func (p *paddingConn) rearHeadroom() int { if p.writePadding < paddingCount { return 255 } return 0 } func (p *paddingConn) writerMTU() int { if p.writePadding < paddingCount { return 65535 } return 0 } func (p *paddingConn) readerReplaceable() bool { return p.readPadding == paddingCount } func (p *paddingConn) writerReplaceable() bool { return p.writePadding == paddingCount } type naiveConn struct { net.Conn paddingConn } func (c *naiveConn) Read(p []byte) (n int, err error) { n, err = c.readWithPadding(c.Conn, p) return n, wrapError(err) } func (c *naiveConn) Write(p []byte) (n int, err error) { n, err = c.writeChunked(c.Conn, p) return n, wrapError(err) } func (c *naiveConn) WriteBuffer(buffer *buf.Buffer) error { defer buffer.Release() err := c.writeBufferWithPadding(c.Conn, buffer) return wrapError(err) } func (c *naiveConn) FrontHeadroom() int { return c.frontHeadroom() } func (c *naiveConn) RearHeadroom() int { return c.rearHeadroom() } func (c *naiveConn) WriterMTU() int { return c.writerMTU() } func (c *naiveConn) Upstream() any { return c.Conn } func (c *naiveConn) ReaderReplaceable() bool { return c.readerReplaceable() } func (c *naiveConn) WriterReplaceable() bool { return c.writerReplaceable() } type naiveH2Conn struct { reader io.Reader writer io.Writer flusher http.Flusher remoteAddress net.Addr paddingConn } func (c *naiveH2Conn) Read(p []byte) (n int, err error) { n, err = c.readWithPadding(c.reader, p) return n, wrapError(err) } func (c *naiveH2Conn) Write(p []byte) (n int, err error) { n, err = c.writeChunked(c.writer, p) if err == nil { c.flusher.Flush() } return n, wrapError(err) } func (c *naiveH2Conn) WriteBuffer(buffer *buf.Buffer) error { defer buffer.Release() err := c.writeBufferWithPadding(c.writer, buffer) if err == nil { c.flusher.Flush() } return wrapError(err) } func wrapError(err error) error { err = baderror.WrapH2(err) if WrapError != nil { err = WrapError(err) } return err } func (c *naiveH2Conn) Close() error { return common.Close(c.reader, c.writer) } func (c *naiveH2Conn) LocalAddr() net.Addr { return M.Socksaddr{} } func (c *naiveH2Conn) RemoteAddr() net.Addr { return c.remoteAddress } func (c *naiveH2Conn) SetDeadline(t time.Time) error { return os.ErrInvalid } func (c *naiveH2Conn) SetReadDeadline(t time.Time) error { return os.ErrInvalid } func (c *naiveH2Conn) SetWriteDeadline(t time.Time) error { return os.ErrInvalid } func (c *naiveH2Conn) NeedAdditionalReadDeadline() bool { return true } func (c *naiveH2Conn) UpstreamReader() any { return c.reader } func (c *naiveH2Conn) UpstreamWriter() any { return c.writer } func (c *naiveH2Conn) FrontHeadroom() int { return c.frontHeadroom() } func (c *naiveH2Conn) RearHeadroom() int { return c.rearHeadroom() } func (c *naiveH2Conn) WriterMTU() int { return c.writerMTU() } func (c *naiveH2Conn) ReaderReplaceable() bool { return c.readerReplaceable() } func (c *naiveH2Conn) WriterReplaceable() bool { return c.writerReplaceable() } ================================================ FILE: protocol/naive/outbound.go ================================================ //go:build with_naive_outbound package naive import ( "context" "encoding/pem" "net" "os" "strings" "github.com/sagernet/cronet-go" _ "github.com/sagernet/cronet-go/all" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/dialer" 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" 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/common/uot" "github.com/sagernet/sing/service" mDNS "github.com/miekg/dns" ) func RegisterOutbound(registry *outbound.Registry) { outbound.Register[option.NaiveOutboundOptions](registry, C.TypeNaive, NewOutbound) } type Outbound struct { outbound.Adapter ctx context.Context logger logger.ContextLogger client *cronet.NaiveClient uotClient *uot.Client } func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.NaiveOutboundOptions) (adapter.Outbound, error) { if options.TLS == nil || !options.TLS.Enabled { return nil, C.ErrTLSRequired } if options.TLS.DisableSNI { return nil, E.New("disable_sni is not supported on naive outbound") } if options.TLS.Insecure { return nil, E.New("insecure is not supported on naive outbound") } if len(options.TLS.ALPN) > 0 { return nil, E.New("alpn is not supported on naive outbound") } if options.TLS.MinVersion != "" { return nil, E.New("min_version is not supported on naive outbound") } if options.TLS.MaxVersion != "" { return nil, E.New("max_version is not supported on naive outbound") } if len(options.TLS.CipherSuites) > 0 { return nil, E.New("cipher_suites is not supported on naive outbound") } if len(options.TLS.CurvePreferences) > 0 { return nil, E.New("curve_preferences is not supported on naive outbound") } if len(options.TLS.ClientCertificate) > 0 || options.TLS.ClientCertificatePath != "" { return nil, E.New("client_certificate is not supported on naive outbound") } if len(options.TLS.ClientKey) > 0 || options.TLS.ClientKeyPath != "" { return nil, E.New("client_key is not supported on naive outbound") } if options.TLS.Fragment || options.TLS.RecordFragment { return nil, E.New("fragment is not supported on naive outbound") } if options.TLS.KernelTx || options.TLS.KernelRx { return nil, E.New("kernel TLS is not supported on naive outbound") } if options.TLS.UTLS != nil && options.TLS.UTLS.Enabled { return nil, E.New("uTLS is not supported on naive outbound") } if options.TLS.Reality != nil && options.TLS.Reality.Enabled { return nil, E.New("reality is not supported on naive outbound") } serverAddress := options.ServerOptions.Build() var serverName string if options.TLS.ServerName != "" { serverName = options.TLS.ServerName } else { serverName = serverAddress.AddrString() } outboundDialer, err := dialer.NewWithOptions(dialer.Options{ Context: ctx, Options: options.DialerOptions, RemoteIsDomain: true, ResolverOnDetour: true, NewDialer: true, }) if err != nil { return nil, err } var trustedRootCertificates string if len(options.TLS.Certificate) > 0 { trustedRootCertificates = strings.Join(options.TLS.Certificate, "\n") } else if options.TLS.CertificatePath != "" { content, err := os.ReadFile(options.TLS.CertificatePath) if err != nil { return nil, E.Cause(err, "read certificate") } trustedRootCertificates = string(content) } extraHeaders := make(map[string]string) for key, values := range options.ExtraHeaders.Build() { if len(values) > 0 { extraHeaders[key] = values[0] } } dnsRouter := service.FromContext[adapter.DNSRouter](ctx) var dnsResolver cronet.DNSResolverFunc if dnsRouter != nil { dnsResolver = func(dnsContext context.Context, request *mDNS.Msg) *mDNS.Msg { response, err := dnsRouter.Exchange(dnsContext, request, outboundDialer.(dialer.ResolveDialer).QueryOptions()) if err != nil { logger.Error("DNS exchange failed: ", err) return dns.FixedResponseStatus(request, mDNS.RcodeServerFailure) } return response } } var echEnabled bool var echConfigList []byte var echQueryServerName string if options.TLS.ECH != nil && options.TLS.ECH.Enabled { echEnabled = true echQueryServerName = options.TLS.ECH.QueryServerName var echConfig []byte if len(options.TLS.ECH.Config) > 0 { echConfig = []byte(strings.Join(options.TLS.ECH.Config, "\n")) } else if options.TLS.ECH.ConfigPath != "" { content, err := os.ReadFile(options.TLS.ECH.ConfigPath) if err != nil { return nil, E.Cause(err, "read ECH config") } echConfig = content } if len(echConfig) > 0 { block, rest := pem.Decode(echConfig) if block == nil || block.Type != "ECH CONFIGS" || len(rest) > 0 { return nil, E.New("invalid ECH configs pem") } echConfigList = block.Bytes } } var quicCongestionControl cronet.QUICCongestionControl switch options.QUICCongestionControl { case "": quicCongestionControl = cronet.QUICCongestionControlDefault case "bbr": quicCongestionControl = cronet.QUICCongestionControlBBR case "bbr2": quicCongestionControl = cronet.QUICCongestionControlBBRv2 case "cubic": quicCongestionControl = cronet.QUICCongestionControlCubic case "reno": quicCongestionControl = cronet.QUICCongestionControlReno default: return nil, E.New("unknown quic congestion control: ", options.QUICCongestionControl) } client, err := cronet.NewNaiveClient(cronet.NaiveClientOptions{ Context: ctx, Logger: logger, ServerAddress: serverAddress, ServerName: serverName, Username: options.Username, Password: options.Password, InsecureConcurrency: options.InsecureConcurrency, ExtraHeaders: extraHeaders, TrustedRootCertificates: trustedRootCertificates, Dialer: outboundDialer, DNSResolver: dnsResolver, ECHEnabled: echEnabled, ECHConfigList: echConfigList, ECHQueryServerName: echQueryServerName, QUIC: options.QUIC, QUICCongestionControl: quicCongestionControl, }) if err != nil { return nil, err } var uotClient *uot.Client uotOptions := common.PtrValueOrDefault(options.UDPOverTCP) if uotOptions.Enabled { uotClient = &uot.Client{ Dialer: &naiveDialer{client}, Version: uotOptions.Version, } } var networks []string if uotClient != nil { networks = []string{N.NetworkTCP, N.NetworkUDP} } else { networks = []string{N.NetworkTCP} } return &Outbound{ Adapter: outbound.NewAdapterWithDialerOptions(C.TypeNaive, tag, networks, options.DialerOptions), ctx: ctx, logger: logger, client: client, uotClient: uotClient, }, nil } func (h *Outbound) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } err := h.client.Start() if err != nil { return err } h.logger.Info("NaiveProxy started, version: ", h.client.Engine().Version()) return nil } func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { switch N.NetworkName(network) { case N.NetworkTCP: h.logger.InfoContext(ctx, "outbound connection to ", destination) return h.client.DialEarly(ctx, destination) case N.NetworkUDP: if h.uotClient == nil { return nil, E.New("UDP is not supported unless UDP over TCP is enabled") } h.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination) return h.uotClient.DialContext(ctx, network, destination) default: return nil, E.Extend(N.ErrUnknownNetwork, network) } } func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { if h.uotClient == nil { return nil, E.New("UDP is not supported unless UDP over TCP is enabled") } return h.uotClient.ListenPacket(ctx, destination) } func (h *Outbound) InterfaceUpdated() { h.client.Engine().CloseAllConnections() } func (h *Outbound) Close() error { return h.client.Close() } func (h *Outbound) Client() *cronet.NaiveClient { return h.client } type naiveDialer struct { *cronet.NaiveClient } func (d *naiveDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { return d.NaiveClient.DialEarly(ctx, destination) } ================================================ FILE: protocol/naive/quic/inbound_init.go ================================================ package quic import ( "context" "io" "net/http" "time" "github.com/sagernet/quic-go" "github.com/sagernet/quic-go/congestion" "github.com/sagernet/quic-go/http3" "github.com/sagernet/sing-box/common/listener" "github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/protocol/naive" "github.com/sagernet/sing-quic" "github.com/sagernet/sing-quic/congestion_bbr1" "github.com/sagernet/sing-quic/congestion_bbr2" congestion_meta1 "github.com/sagernet/sing-quic/congestion_meta1" congestion_meta2 "github.com/sagernet/sing-quic/congestion_meta2" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/ntp" ) func init() { naive.ConfigureHTTP3ListenerFunc = func(ctx context.Context, logger logger.Logger, listener *listener.Listener, handler http.Handler, tlsConfig tls.ServerConfig, options option.NaiveInboundOptions) (io.Closer, error) { err := qtls.ConfigureHTTP3(tlsConfig) if err != nil { return nil, err } udpConn, err := listener.ListenUDP() if err != nil { return nil, err } var congestionControl func(conn *quic.Conn) congestion.CongestionControl timeFunc := ntp.TimeFuncFromContext(ctx) if timeFunc == nil { timeFunc = time.Now } switch options.QUICCongestionControl { case "", "bbr": congestionControl = func(conn *quic.Conn) congestion.CongestionControl { return congestion_meta2.NewBbrSender( congestion_meta2.DefaultClock{TimeFunc: timeFunc}, congestion.ByteCount(conn.Config().InitialPacketSize), congestion.ByteCount(congestion_meta1.InitialCongestionWindow), ) } case "bbr_standard": congestionControl = func(conn *quic.Conn) congestion.CongestionControl { return congestion_bbr1.NewBbrSender( congestion_bbr1.DefaultClock{TimeFunc: timeFunc}, congestion.ByteCount(conn.Config().InitialPacketSize), congestion_bbr1.InitialCongestionWindowPackets, congestion_bbr1.MaxCongestionWindowPackets, ) } case "bbr2": congestionControl = func(conn *quic.Conn) congestion.CongestionControl { return congestion_bbr2.NewBBR2Sender( congestion_bbr2.DefaultClock{TimeFunc: timeFunc}, congestion.ByteCount(conn.Config().InitialPacketSize), 0, false, ) } case "bbr2_variant": congestionControl = func(conn *quic.Conn) congestion.CongestionControl { return congestion_bbr2.NewBBR2Sender( congestion_bbr2.DefaultClock{TimeFunc: timeFunc}, congestion.ByteCount(conn.Config().InitialPacketSize), 32*congestion.ByteCount(conn.Config().InitialPacketSize), true, ) } case "cubic": congestionControl = func(conn *quic.Conn) congestion.CongestionControl { return congestion_meta1.NewCubicSender( congestion_meta1.DefaultClock{TimeFunc: timeFunc}, congestion.ByteCount(conn.Config().InitialPacketSize), false, ) } case "reno": congestionControl = func(conn *quic.Conn) congestion.CongestionControl { return congestion_meta1.NewCubicSender( congestion_meta1.DefaultClock{TimeFunc: timeFunc}, congestion.ByteCount(conn.Config().InitialPacketSize), true, ) } default: return nil, E.New("unknown quic congestion control: ", options.QUICCongestionControl) } quicListener, err := qtls.ListenEarly(udpConn, tlsConfig, &quic.Config{ MaxIncomingStreams: 1 << 60, Allow0RTT: true, }) if err != nil { udpConn.Close() return nil, err } h3Server := &http3.Server{ Handler: handler, ConnContext: func(ctx context.Context, conn *quic.Conn) context.Context { conn.SetCongestionControl(congestionControl(conn)) return log.ContextWithNewID(ctx) }, } go func() { sErr := h3Server.ServeListener(quicListener) udpConn.Close() if sErr != nil && !E.IsClosedOrCanceled(sErr) { logger.Error("http3 server closed: ", sErr) } }() return quicListener, nil } naive.WrapError = qtls.WrapError } ================================================ FILE: protocol/redirect/redirect.go ================================================ package redirect import ( "context" "net" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/common/listener" "github.com/sagernet/sing-box/common/redir" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) func RegisterRedirect(registry *inbound.Registry) { inbound.Register[option.RedirectInboundOptions](registry, C.TypeRedirect, NewRedirect) } type Redirect struct { inbound.Adapter router adapter.Router logger log.ContextLogger listener *listener.Listener } func NewRedirect(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.RedirectInboundOptions) (adapter.Inbound, error) { redirect := &Redirect{ Adapter: inbound.NewAdapter(C.TypeRedirect, tag), router: router, logger: logger, } redirect.listener = listener.New(listener.Options{ Context: ctx, Logger: logger, Network: []string{N.NetworkTCP}, Listen: options.ListenOptions, ConnectionHandler: redirect, }) return redirect, nil } func (h *Redirect) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } return h.listener.Start() } func (h *Redirect) Close() error { return h.listener.Close() } 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() h.logger.ErrorContext(ctx, "process connection from ", conn.RemoteAddr(), ": get redirect destination: ", err) return } metadata.Inbound = h.Tag() metadata.InboundType = h.Type() metadata.Destination = M.SocksaddrFromNetIP(destination) h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) h.router.RouteConnectionEx(ctx, conn, metadata, onClose) } ================================================ FILE: protocol/redirect/tproxy.go ================================================ package redirect import ( "context" "net" "net/netip" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/common/listener" "github.com/sagernet/sing-box/common/redir" C "github.com/sagernet/sing-box/constant" "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" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/udpnat2" ) func RegisterTProxy(registry *inbound.Registry) { inbound.Register[option.TProxyInboundOptions](registry, C.TypeTProxy, NewTProxy) } type TProxy struct { inbound.Adapter ctx context.Context router adapter.Router logger log.ContextLogger listener *listener.Listener udpNat *udpnat.Service } func NewTProxy(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TProxyInboundOptions) (adapter.Inbound, error) { tproxy := &TProxy{ Adapter: inbound.NewAdapter(C.TypeTProxy, tag), ctx: ctx, router: router, logger: logger, } var udpTimeout time.Duration if options.UDPTimeout != 0 { udpTimeout = time.Duration(options.UDPTimeout) } else { udpTimeout = C.UDPTimeout } tproxy.udpNat = udpnat.New(tproxy, tproxy.preparePacketConnection, udpTimeout, false) tproxy.listener = listener.New(listener.Options{ Context: ctx, Logger: logger, Network: options.Network.Build(), Listen: options.ListenOptions, ConnectionHandler: tproxy, OOBPacketHandler: tproxy, TProxy: true, }) return tproxy, nil } func (t *TProxy) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } return t.listener.Start() } func (t *TProxy) Close() error { return t.listener.Close() } 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() t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) t.router.RouteConnectionEx(ctx, conn, metadata, onClose) } func (t *TProxy) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { t.logger.InfoContext(ctx, "inbound packet connection from ", source) t.logger.InfoContext(ctx, "inbound packet connection to ", destination) var metadata adapter.InboundContext metadata.Inbound = t.Tag() metadata.InboundType = t.Type() metadata.Source = source metadata.Destination = destination metadata.OriginDestination = t.listener.UDPAddr() t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } 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) return } t.udpNat.NewPacket([][]byte{buffer.Bytes()}, source, M.SocksaddrFromNetIP(destination), nil) } func (t *TProxy) preparePacketConnection(source M.Socksaddr, destination M.Socksaddr, userData any) (bool, context.Context, N.PacketWriter, N.CloseHandlerFunc) { ctx := log.ContextWithNewID(t.ctx) writer := &tproxyPacketWriter{ ctx: ctx, listener: t.listener, source: source.AddrPort(), destination: destination, } return true, ctx, writer, func(it error) { common.Close(common.PtrOrNil(writer.conn)) } } type tproxyPacketWriter struct { ctx context.Context listener *listener.Listener source netip.AddrPort destination M.Socksaddr conn *net.UDPConn } func (w *tproxyPacketWriter) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { defer buffer.Release() if w.listener.ListenOptions().NetNs == "" { conn := w.conn if w.destination == destination && conn != nil { _, err := conn.WriteToUDPAddrPort(buffer.Bytes(), w.source) if err != nil { w.conn = nil } return err } } var listenConfig net.ListenConfig listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr()) listenConfig.Control = control.Append(listenConfig.Control, redir.TProxyWriteBack()) packetConn, err := w.listener.ListenPacket(listenConfig, w.ctx, "udp", destination.String()) if err != nil { return err } udpConn := packetConn.(*net.UDPConn) if w.listener.ListenOptions().NetNs == "" && w.destination == destination { w.conn = udpConn } else { defer udpConn.Close() } return common.Error(udpConn.WriteToUDPAddrPort(buffer.Bytes(), w.source)) } ================================================ FILE: protocol/shadowsocks/inbound.go ================================================ package shadowsocks import ( "context" "net" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/common/listener" "github.com/sagernet/sing-box/common/mux" "github.com/sagernet/sing-box/common/uot" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-shadowsocks" "github.com/sagernet/sing-shadowsocks/shadowaead" "github.com/sagernet/sing-shadowsocks/shadowaead_2022" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" 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/common/ntp" ) func RegisterInbound(registry *inbound.Registry) { inbound.Register[option.ShadowsocksInboundOptions](registry, C.TypeShadowsocks, NewInbound) } func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (adapter.Inbound, error) { if len(options.Users) > 0 && len(options.Destinations) > 0 { return nil, E.New("users and destinations options must not be combined") } else if options.Managed && (len(options.Users) > 0 || len(options.Destinations) > 0) { return nil, E.New("users and destinations options are not supported in managed servers") } if len(options.Users) > 0 || options.Managed { return newMultiInbound(ctx, router, logger, tag, options) } else if len(options.Destinations) > 0 { return newRelayInbound(ctx, router, logger, tag, options) } else { return newInbound(ctx, router, logger, tag, options) } } var _ adapter.TCPInjectableInbound = (*Inbound)(nil) type Inbound struct { inbound.Adapter ctx context.Context router adapter.ConnectionRouterEx logger logger.ContextLogger listener *listener.Listener service shadowsocks.Service } func newInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (*Inbound, error) { inbound := &Inbound{ Adapter: inbound.NewAdapter(C.TypeShadowsocks, tag), ctx: ctx, router: uot.NewRouter(router, logger), logger: logger, } var err error inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex)) if err != nil { return nil, err } var udpTimeout time.Duration if options.UDPTimeout != 0 { udpTimeout = time.Duration(options.UDPTimeout) } else { udpTimeout = C.UDPTimeout } switch { case options.Method == shadowsocks.MethodNone: 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.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.NewLegacyUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound), ntp.TimeFuncFromContext(ctx)) default: err = E.New("unsupported method: ", options.Method) } inbound.listener = listener.New(listener.Options{ Context: ctx, Logger: logger, Network: options.Network.Build(), Listen: options.ListenOptions, ConnectionHandler: inbound, PacketHandler: inbound, ThreadUnsafePacketWriter: true, }) return inbound, err } func (h *Inbound) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } return h.listener.Start() } func (h *Inbound) Close() error { return h.listener.Close() } //nolint:staticcheck 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 { if E.IsClosedOrCanceled(err) { h.logger.DebugContext(ctx, "connection closed: ", err) } else { h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) } } } //nolint:staticcheck 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)) } } func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) metadata.Inbound = h.Tag() metadata.InboundType = h.Type() return h.router.RouteConnection(ctx, conn, metadata) } func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { ctx = log.ContextWithNewID(ctx) h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) metadata.Inbound = h.Tag() metadata.InboundType = h.Type() return h.router.RoutePacketConnection(ctx, conn, metadata) } var _ N.PacketConn = (*stubPacketConn)(nil) type stubPacketConn struct { N.PacketWriter } func (c *stubPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) { panic("stub!") } func (c *stubPacketConn) Close() error { return nil } func (c *stubPacketConn) LocalAddr() net.Addr { panic("stub!") } func (c *stubPacketConn) SetDeadline(t time.Time) error { panic("stub!") } func (c *stubPacketConn) SetReadDeadline(t time.Time) error { panic("stub!") } func (c *stubPacketConn) SetWriteDeadline(t time.Time) error { panic("stub!") } func (h *Inbound) NewError(ctx context.Context, err error) { NewError(h.logger, ctx, err) } // Deprecated: remove func NewError(logger logger.ContextLogger, ctx context.Context, err error) { common.Close(err) if E.IsClosedOrCanceled(err) { logger.DebugContext(ctx, "connection closed: ", err) return } logger.ErrorContext(ctx, err) } ================================================ FILE: protocol/shadowsocks/inbound_multi.go ================================================ package shadowsocks import ( "context" "net" "os" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/common/listener" "github.com/sagernet/sing-box/common/mux" "github.com/sagernet/sing-box/common/uot" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-shadowsocks" "github.com/sagernet/sing-shadowsocks/shadowaead" "github.com/sagernet/sing-shadowsocks/shadowaead_2022" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/auth" "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "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" ) var ( _ adapter.TCPInjectableInbound = (*MultiInbound)(nil) _ adapter.ManagedSSMServer = (*MultiInbound)(nil) ) type MultiInbound struct { inbound.Adapter ctx context.Context router adapter.ConnectionRouterEx logger logger.ContextLogger listener *listener.Listener service shadowsocks.MultiService[int] users []option.ShadowsocksUser tracker adapter.SSMTracker } func newMultiInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (*MultiInbound, error) { inbound := &MultiInbound{ Adapter: inbound.NewAdapter(C.TypeShadowsocks, tag), ctx: ctx, router: uot.NewRouter(router, logger), logger: logger, } var err error inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex)) if err != nil { return nil, err } var udpTimeout time.Duration if options.UDPTimeout != 0 { udpTimeout = time.Duration(options.UDPTimeout) } else { udpTimeout = C.UDPTimeout } var service shadowsocks.MultiService[int] if common.Contains(shadowaead_2022.List, options.Method) { service, err = shadowaead_2022.NewMultiServiceWithPassword[int]( options.Method, options.Password, int64(udpTimeout.Seconds()), 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.NewLegacyUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound), ) } else { return nil, E.New("unsupported method: " + options.Method) } if err != nil { return nil, err } if len(options.Users) > 0 { err = service.UpdateUsersWithPasswords(common.MapIndexed(options.Users, func(index int, user option.ShadowsocksUser) int { return index }), common.Map(options.Users, func(user option.ShadowsocksUser) string { return user.Password })) if err != nil { return nil, err } } inbound.service = service inbound.users = options.Users inbound.listener = listener.New(listener.Options{ Context: ctx, Logger: logger, Network: options.Network.Build(), Listen: options.ListenOptions, ConnectionHandler: inbound, PacketHandler: inbound, ThreadUnsafePacketWriter: true, }) return inbound, err } func (h *MultiInbound) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } return h.listener.Start() } func (h *MultiInbound) Close() error { return h.listener.Close() } func (h *MultiInbound) SetTracker(tracker adapter.SSMTracker) { h.tracker = tracker } func (h *MultiInbound) UpdateUsers(users []string, uPSKs []string) error { err := h.service.UpdateUsersWithPasswords(common.MapIndexed(users, func(index int, user string) int { return index }), uPSKs) if err != nil { return err } h.users = common.Map(users, func(user string) option.ShadowsocksUser { return option.ShadowsocksUser{ Name: user, } }) return nil } //nolint:staticcheck 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 { if E.IsClosedOrCanceled(err) { h.logger.DebugContext(ctx, "connection closed: ", err) } else { h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) } } } //nolint:staticcheck 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)) } } func (h *MultiInbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { userIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { return os.ErrInvalid } user := h.users[userIndex].Name if user == "" { user = F.ToString(userIndex) } else { metadata.User = user } h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) metadata.Inbound = h.Tag() metadata.InboundType = h.Type() //nolint:staticcheck metadata.InboundDetour = h.listener.ListenOptions().Detour //nolint:staticcheck if h.tracker != nil { conn = h.tracker.TrackConnection(conn, metadata) } return h.router.RouteConnection(ctx, conn, metadata) } func (h *MultiInbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { userIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { return os.ErrInvalid } user := h.users[userIndex].Name if user == "" { user = F.ToString(userIndex) } else { metadata.User = user } ctx = log.ContextWithNewID(ctx) h.logger.InfoContext(ctx, "[", user, "] inbound packet connection from ", metadata.Source) h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) metadata.Inbound = h.Tag() metadata.InboundType = h.Type() //nolint:staticcheck metadata.InboundDetour = h.listener.ListenOptions().Detour //nolint:staticcheck if h.tracker != nil { conn = h.tracker.TrackPacketConnection(conn, metadata) } return h.router.RoutePacketConnection(ctx, conn, metadata) } //nolint:staticcheck func (h *MultiInbound) NewError(ctx context.Context, err error) { NewError(h.logger, ctx, err) } ================================================ FILE: protocol/shadowsocks/inbound_relay.go ================================================ package shadowsocks import ( "context" "net" "os" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/common/listener" "github.com/sagernet/sing-box/common/mux" "github.com/sagernet/sing-box/common/uot" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-shadowsocks/shadowaead_2022" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/auth" "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) var _ adapter.TCPInjectableInbound = (*RelayInbound)(nil) type RelayInbound struct { inbound.Adapter ctx context.Context router adapter.ConnectionRouterEx logger logger.ContextLogger listener *listener.Listener service *shadowaead_2022.RelayService[int] destinations []option.ShadowsocksDestination } func newRelayInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (*RelayInbound, error) { inbound := &RelayInbound{ Adapter: inbound.NewAdapter(C.TypeShadowsocks, tag), ctx: ctx, router: uot.NewRouter(router, logger), logger: logger, destinations: options.Destinations, } var err error inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex)) if err != nil { return nil, err } var udpTimeout time.Duration if options.UDPTimeout != 0 { udpTimeout = time.Duration(options.UDPTimeout) } else { udpTimeout = C.UDPTimeout } service, err := shadowaead_2022.NewRelayServiceWithPassword[int]( options.Method, options.Password, int64(udpTimeout.Seconds()), adapter.NewLegacyUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound), ) if err != nil { return nil, err } err = service.UpdateUsersWithPasswords(common.MapIndexed(options.Destinations, func(index int, user option.ShadowsocksDestination) int { return index }), common.Map(options.Destinations, func(user option.ShadowsocksDestination) string { return user.Password }), common.Map(options.Destinations, option.ShadowsocksDestination.Build)) if err != nil { return nil, err } inbound.service = service inbound.listener = listener.New(listener.Options{ Context: ctx, Logger: logger, Network: options.Network.Build(), Listen: options.ListenOptions, ConnectionHandler: inbound, PacketHandler: inbound, ThreadUnsafePacketWriter: true, }) return inbound, err } func (h *RelayInbound) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } return h.listener.Start() } func (h *RelayInbound) Close() error { return h.listener.Close() } //nolint:staticcheck 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 { if E.IsClosedOrCanceled(err) { h.logger.DebugContext(ctx, "connection closed: ", err) } else { h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) } } } //nolint:staticcheck 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)) } } func (h *RelayInbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { destinationIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { return os.ErrInvalid } destination := h.destinations[destinationIndex].Name if destination == "" { destination = F.ToString(destinationIndex) } else { metadata.User = destination } h.logger.InfoContext(ctx, "[", destination, "] inbound connection to ", metadata.Destination) metadata.Inbound = h.Tag() metadata.InboundType = h.Type() //nolint:staticcheck metadata.InboundDetour = h.listener.ListenOptions().Detour //nolint:staticcheck return h.router.RouteConnection(ctx, conn, metadata) } func (h *RelayInbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { destinationIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { return os.ErrInvalid } destination := h.destinations[destinationIndex].Name if destination == "" { destination = F.ToString(destinationIndex) } else { metadata.User = destination } ctx = log.ContextWithNewID(ctx) h.logger.InfoContext(ctx, "[", destination, "] inbound packet connection from ", metadata.Source) h.logger.InfoContext(ctx, "[", destination, "] inbound packet connection to ", metadata.Destination) metadata.Inbound = h.Tag() metadata.InboundType = h.Type() //nolint:staticcheck metadata.InboundDetour = h.listener.ListenOptions().Detour //nolint:staticcheck return h.router.RoutePacketConnection(ctx, conn, metadata) } //nolint:staticcheck func (h *RelayInbound) NewError(ctx context.Context, err error) { NewError(h.logger, ctx, err) } ================================================ FILE: protocol/shadowsocks/outbound.go ================================================ package shadowsocks import ( "context" "net" "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/mux" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/transport/sip003" "github.com/sagernet/sing-shadowsocks2" "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/common/uot" ) func RegisterOutbound(registry *outbound.Registry) { outbound.Register[option.ShadowsocksOutboundOptions](registry, C.TypeShadowsocks, NewOutbound) } type Outbound struct { outbound.Adapter logger logger.ContextLogger dialer N.Dialer method shadowsocks.Method serverAddr M.Socksaddr plugin sip003.Plugin uotClient *uot.Client multiplexDialer *mux.Client } func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksOutboundOptions) (adapter.Outbound, error) { method, err := shadowsocks.CreateMethod(ctx, options.Method, shadowsocks.MethodOptions{ Password: options.Password, }) if err != nil { return nil, err } outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain()) if err != nil { return nil, err } outbound := &Outbound{ Adapter: outbound.NewAdapterWithDialerOptions(C.TypeShadowsocks, tag, options.Network.Build(), options.DialerOptions), logger: logger, dialer: outboundDialer, method: method, serverAddr: options.ServerOptions.Build(), } if options.Plugin != "" { outbound.plugin, err = sip003.CreatePlugin(ctx, options.Plugin, options.PluginOptions, router, outbound.dialer, outbound.serverAddr) if err != nil { return nil, err } } uotOptions := common.PtrValueOrDefault(options.UDPOverTCP) if !uotOptions.Enabled { outbound.multiplexDialer, err = mux.NewClientWithOptions((*shadowsocksDialer)(outbound), logger, common.PtrValueOrDefault(options.Multiplex)) if err != nil { return nil, err } } if uotOptions.Enabled { outbound.uotClient = &uot.Client{ Dialer: (*shadowsocksDialer)(outbound), Version: uotOptions.Version, } } return outbound, nil } func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { ctx, metadata := adapter.ExtendContext(ctx) metadata.Outbound = h.Tag() metadata.Destination = destination if h.multiplexDialer == nil { switch N.NetworkName(network) { case N.NetworkTCP: h.logger.InfoContext(ctx, "outbound connection to ", destination) case N.NetworkUDP: if h.uotClient != nil { h.logger.InfoContext(ctx, "outbound UoT connect packet connection to ", destination) return h.uotClient.DialContext(ctx, network, destination) } else { h.logger.InfoContext(ctx, "outbound packet connection to ", destination) } } return (*shadowsocksDialer)(h).DialContext(ctx, network, destination) } else { switch N.NetworkName(network) { case N.NetworkTCP: h.logger.InfoContext(ctx, "outbound multiplex connection to ", destination) case N.NetworkUDP: h.logger.InfoContext(ctx, "outbound multiplex packet connection to ", destination) } return h.multiplexDialer.DialContext(ctx, network, destination) } } func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { ctx, metadata := adapter.ExtendContext(ctx) metadata.Outbound = h.Tag() metadata.Destination = destination if h.multiplexDialer == nil { if h.uotClient != nil { h.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination) return h.uotClient.ListenPacket(ctx, destination) } else { h.logger.InfoContext(ctx, "outbound packet connection to ", destination) } h.logger.InfoContext(ctx, "outbound packet connection to ", destination) return (*shadowsocksDialer)(h).ListenPacket(ctx, destination) } else { h.logger.InfoContext(ctx, "outbound multiplex packet connection to ", destination) return h.multiplexDialer.ListenPacket(ctx, destination) } } func (h *Outbound) InterfaceUpdated() { if h.multiplexDialer != nil { h.multiplexDialer.Reset() } } func (h *Outbound) Close() error { return common.Close(common.PtrOrNil(h.multiplexDialer)) } var _ N.Dialer = (*shadowsocksDialer)(nil) type shadowsocksDialer Outbound func (h *shadowsocksDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { ctx, metadata := adapter.ExtendContext(ctx) metadata.Outbound = h.Tag() metadata.Destination = destination switch N.NetworkName(network) { case N.NetworkTCP: var outConn net.Conn var err error if h.plugin != nil { outConn, err = h.plugin.DialContext(ctx) } else { outConn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr) } if err != nil { return nil, err } return h.method.DialEarlyConn(outConn, destination), nil case N.NetworkUDP: outConn, err := h.dialer.DialContext(ctx, N.NetworkUDP, h.serverAddr) if err != nil { return nil, err } return bufio.NewBindPacketConn(h.method.DialPacketConn(outConn), destination), nil default: return nil, E.Extend(N.ErrUnknownNetwork, network) } } func (h *shadowsocksDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { ctx, metadata := adapter.ExtendContext(ctx) metadata.Outbound = h.Tag() metadata.Destination = destination outConn, err := h.dialer.DialContext(ctx, N.NetworkUDP, h.serverAddr) if err != nil { return nil, err } return h.method.DialPacketConn(outConn), nil } ================================================ FILE: protocol/shadowtls/inbound.go ================================================ package shadowtls import ( "context" "net" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/listener" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-shadowtls" "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" ) func RegisterInbound(registry *inbound.Registry) { inbound.Register[option.ShadowTLSInboundOptions](registry, C.TypeShadowTLS, NewInbound) } type Inbound struct { inbound.Adapter router adapter.Router logger logger.ContextLogger listener *listener.Listener service *shadowtls.Service } func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowTLSInboundOptions) (adapter.Inbound, error) { inbound := &Inbound{ Adapter: inbound.NewAdapter(C.TypeShadowTLS, tag), router: router, logger: logger, } if options.Version == 0 { options.Version = 1 } var handshakeForServerName map[string]shadowtls.HandshakeConfig if options.Version > 1 { handshakeForServerName = make(map[string]shadowtls.HandshakeConfig) if options.HandshakeForServerName != nil { for _, entry := range options.HandshakeForServerName.Entries() { handshakeDialer, err := dialer.New(ctx, entry.Value.DialerOptions, entry.Value.ServerIsDomain()) if err != nil { return nil, err } handshakeForServerName[entry.Key] = shadowtls.HandshakeConfig{ Server: entry.Value.ServerOptions.Build(), Dialer: handshakeDialer, } } } } serverIsDomain := options.Handshake.ServerIsDomain() if options.WildcardSNI != option.ShadowTLSWildcardSNIOff { serverIsDomain = true } handshakeDialer, err := dialer.New(ctx, options.Handshake.DialerOptions, serverIsDomain) if err != nil { return nil, err } service, err := shadowtls.NewService(shadowtls.ServiceConfig{ Version: options.Version, Password: options.Password, Users: common.Map(options.Users, func(it option.ShadowTLSUser) shadowtls.User { return (shadowtls.User)(it) }), Handshake: shadowtls.HandshakeConfig{ Server: options.Handshake.ServerOptions.Build(), Dialer: handshakeDialer, }, HandshakeForServerName: handshakeForServerName, StrictMode: options.StrictMode, WildcardSNI: shadowtls.WildcardSNI(options.WildcardSNI), Handler: (*inboundHandler)(inbound), Logger: logger, }) if err != nil { return nil, err } inbound.service = service inbound.listener = listener.New(listener.Options{ Context: ctx, Logger: logger, Network: []string{N.NetworkTCP}, Listen: options.ListenOptions, ConnectionHandler: inbound, }) return inbound, nil } func (h *Inbound) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } return h.listener.Start() } func (h *Inbound) Close() error { return h.listener.Close() } 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 { if E.IsClosedOrCanceled(err) { h.logger.DebugContext(ctx, "connection closed: ", err) } else { h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) } } } type inboundHandler Inbound func (h *inboundHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { var metadata adapter.InboundContext metadata.Inbound = h.Tag() metadata.InboundType = h.Type() //nolint:staticcheck metadata.InboundDetour = h.listener.ListenOptions().Detour //nolint:staticcheck metadata.Source = source metadata.Destination = destination if userName, _ := auth.UserFromContext[string](ctx); userName != "" { metadata.User = userName h.logger.InfoContext(ctx, "[", userName, "] inbound connection to ", metadata.Destination) } else { h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) } h.router.RouteConnectionEx(ctx, conn, metadata, onClose) } ================================================ FILE: protocol/shadowtls/outbound.go ================================================ package shadowtls import ( "context" "net" "os" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-shadowtls" "github.com/sagernet/sing/common" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) func RegisterOutbound(registry *outbound.Registry) { outbound.Register[option.ShadowTLSOutboundOptions](registry, C.TypeShadowTLS, NewOutbound) } type Outbound struct { outbound.Adapter client *shadowtls.Client } func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowTLSOutboundOptions) (adapter.Outbound, error) { outbound := &Outbound{ Adapter: outbound.NewAdapterWithDialerOptions(C.TypeShadowTLS, tag, []string{N.NetworkTCP}, options.DialerOptions), } if options.TLS == nil || !options.TLS.Enabled { return nil, C.ErrTLSRequired } if options.Version == 0 { options.Version = 1 } if options.Version == 1 { options.TLS.MinVersion = "1.2" options.TLS.MaxVersion = "1.2" } tlsConfig, err := tls.NewClient(ctx, logger, options.Server, common.PtrValueOrDefault(options.TLS)) if err != nil { return nil, err } var tlsHandshakeFunc shadowtls.TLSHandshakeFunc switch options.Version { case 1, 2: tlsHandshakeFunc = func(ctx context.Context, conn net.Conn, _ shadowtls.TLSSessionIDGeneratorFunc) error { return common.Error(tls.ClientHandshake(ctx, conn, tlsConfig)) } case 3: if idConfig, loaded := tlsConfig.(tls.WithSessionIDGenerator); loaded { tlsHandshakeFunc = func(ctx context.Context, conn net.Conn, sessionIDGenerator shadowtls.TLSSessionIDGeneratorFunc) error { idConfig.SetSessionIDGenerator(sessionIDGenerator) return common.Error(tls.ClientHandshake(ctx, conn, tlsConfig)) } } else { stdTLSConfig, err := tlsConfig.STDConfig() if err != nil { return nil, err } tlsHandshakeFunc = shadowtls.DefaultTLSHandshakeFunc(options.Password, stdTLSConfig) } } outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain()) if err != nil { return nil, err } client, err := shadowtls.NewClient(shadowtls.ClientConfig{ Version: options.Version, Password: options.Password, Server: options.ServerOptions.Build(), Dialer: outboundDialer, TLSHandshake: tlsHandshakeFunc, Logger: logger, }) if err != nil { return nil, err } outbound.client = client return outbound, nil } func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { ctx, metadata := adapter.ExtendContext(ctx) metadata.Outbound = h.Tag() metadata.Destination = destination switch N.NetworkName(network) { case N.NetworkTCP: return h.client.DialContext(ctx) default: return nil, os.ErrInvalid } } func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { return nil, os.ErrInvalid } ================================================ FILE: protocol/socks/inbound.go ================================================ package socks import ( std_bufio "bufio" "context" "net" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/common/listener" "github.com/sagernet/sing-box/common/uot" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common/auth" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/protocol/socks" ) func RegisterInbound(registry *inbound.Registry) { inbound.Register[option.SocksInboundOptions](registry, C.TypeSOCKS, NewInbound) } var _ adapter.TCPInjectableInbound = (*Inbound)(nil) type Inbound struct { inbound.Adapter router adapter.ConnectionRouterEx logger logger.ContextLogger listener *listener.Listener authenticator *auth.Authenticator udpTimeout time.Duration } func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SocksInboundOptions) (adapter.Inbound, error) { var udpTimeout time.Duration if options.UDPTimeout != 0 { udpTimeout = time.Duration(options.UDPTimeout) } else { udpTimeout = C.UDPTimeout } inbound := &Inbound{ Adapter: inbound.NewAdapter(C.TypeSOCKS, tag), router: uot.NewRouter(router, logger), logger: logger, authenticator: auth.NewAuthenticator(options.Users), udpTimeout: udpTimeout, } inbound.listener = listener.New(listener.Options{ Context: ctx, Logger: logger, Network: []string{N.NetworkTCP}, Listen: options.ListenOptions, ConnectionHandler: inbound, }) return inbound, nil } func (h *Inbound) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } return h.listener.Start() } func (h *Inbound) Close() error { return h.listener.Close() } 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) { h.logger.DebugContext(ctx, "connection closed: ", err) } else { h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) } } } func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = h.Tag() metadata.InboundType = h.Type() user, loaded := auth.UserFromContext[string](ctx) if !loaded { h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) h.router.RouteConnectionEx(ctx, conn, metadata, onClose) return } metadata.User = user h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) h.router.RouteConnectionEx(ctx, conn, metadata, onClose) } func (h *Inbound) streamUserPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = h.Tag() metadata.InboundType = h.Type() user, loaded := auth.UserFromContext[string](ctx) if !loaded { if !metadata.Destination.IsValid() { h.logger.InfoContext(ctx, "inbound packet connection") } else { h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) } h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) return } metadata.User = user if !metadata.Destination.IsValid() { h.logger.InfoContext(ctx, "[", user, "] inbound packet connection") } else { h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) } h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } ================================================ FILE: protocol/socks/outbound.go ================================================ package socks import ( "context" "net" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/outbound" "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" "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/uot" "github.com/sagernet/sing/protocol/socks" "github.com/sagernet/sing/service" ) func RegisterOutbound(registry *outbound.Registry) { outbound.Register[option.SOCKSOutboundOptions](registry, C.TypeSOCKS, NewOutbound) } var _ adapter.Outbound = (*Outbound)(nil) type Outbound struct { outbound.Adapter dnsRouter adapter.DNSRouter logger logger.ContextLogger client *socks.Client resolve bool uotClient *uot.Client } func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SOCKSOutboundOptions) (adapter.Outbound, error) { var version socks.Version var err error if options.Version != "" { version, err = socks.ParseVersion(options.Version) } else { version = socks.Version5 } if err != nil { return nil, err } outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain()) if err != nil { return nil, err } outbound := &Outbound{ Adapter: outbound.NewAdapterWithDialerOptions(C.TypeSOCKS, tag, options.Network.Build(), options.DialerOptions), dnsRouter: service.FromContext[adapter.DNSRouter](ctx), logger: logger, client: socks.NewClient(outboundDialer, options.ServerOptions.Build(), version, options.Username, options.Password), resolve: version == socks.Version4, } uotOptions := common.PtrValueOrDefault(options.UDPOverTCP) if uotOptions.Enabled { outbound.uotClient = &uot.Client{ Dialer: outbound.client, Version: uotOptions.Version, } } return outbound, nil } func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { ctx, metadata := adapter.ExtendContext(ctx) metadata.Outbound = h.Tag() metadata.Destination = destination switch N.NetworkName(network) { case N.NetworkTCP: h.logger.InfoContext(ctx, "outbound connection to ", destination) case N.NetworkUDP: if h.uotClient != nil { h.logger.InfoContext(ctx, "outbound UoT connect packet connection to ", destination) return h.uotClient.DialContext(ctx, network, destination) } h.logger.InfoContext(ctx, "outbound packet connection to ", destination) default: return nil, E.Extend(N.ErrUnknownNetwork, network) } if h.resolve && destination.IsDomain() { destinationAddresses, err := h.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{}) if err != nil { return nil, err } return N.DialSerial(ctx, h.client, network, destination, destinationAddresses) } return h.client.DialContext(ctx, network, destination) } func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { ctx, metadata := adapter.ExtendContext(ctx) metadata.Outbound = h.Tag() metadata.Destination = destination if h.uotClient != nil { h.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination) return h.uotClient.ListenPacket(ctx, destination) } if h.resolve && destination.IsDomain() { destinationAddresses, err := h.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{}) if err != nil { return nil, err } packetConn, _, err := N.ListenSerial(ctx, h.client, destination, destinationAddresses) if err != nil { return nil, err } return packetConn, nil } h.logger.InfoContext(ctx, "outbound packet connection to ", destination) return h.client.ListenPacket(ctx, destination) } ================================================ FILE: protocol/ssh/outbound.go ================================================ package ssh import ( "bytes" "context" "encoding/base64" "math/rand" "net" "os" "strconv" "strings" "sync" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/outbound" "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" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "golang.org/x/crypto/ssh" ) func RegisterOutbound(registry *outbound.Registry) { outbound.Register[option.SSHOutboundOptions](registry, C.TypeSSH, NewOutbound) } var _ adapter.InterfaceUpdateListener = (*Outbound)(nil) type Outbound struct { outbound.Adapter ctx context.Context logger logger.ContextLogger dialer N.Dialer serverAddr M.Socksaddr user string hostKey []ssh.PublicKey hostKeyAlgorithms []string cipher []string mac []string kexAlgorithm []string clientVersion string authMethod []ssh.AuthMethod clientAccess sync.Mutex clientConn net.Conn client *ssh.Client } func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SSHOutboundOptions) (adapter.Outbound, error) { outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain()) if err != nil { return nil, err } outbound := &Outbound{ Adapter: outbound.NewAdapterWithDialerOptions(C.TypeSSH, tag, []string{N.NetworkTCP}, options.DialerOptions), ctx: ctx, logger: logger, dialer: outboundDialer, 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 { outbound.serverAddr.Port = 22 } if outbound.user == "" { outbound.user = "root" } if outbound.clientVersion == "" { outbound.clientVersion = randomVersion() } if options.Password != "" { outbound.authMethod = append(outbound.authMethod, ssh.Password(options.Password)) } if len(options.PrivateKey) > 0 || options.PrivateKeyPath != "" { var privateKey []byte if len(options.PrivateKey) > 0 { privateKey = []byte(strings.Join(options.PrivateKey, "\n")) } else { var err error privateKey, err = os.ReadFile(os.ExpandEnv(options.PrivateKeyPath)) if err != nil { return nil, E.Cause(err, "read private key") } } var signer ssh.Signer var err error if options.PrivateKeyPassphrase == "" { signer, err = ssh.ParsePrivateKey(privateKey) } else { signer, err = ssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(options.PrivateKeyPassphrase)) } if err != nil { return nil, E.Cause(err, "parse private key") } outbound.authMethod = append(outbound.authMethod, ssh.PublicKeys(signer)) } if len(options.HostKey) > 0 { for _, hostKey := range options.HostKey { key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(hostKey)) if err != nil { return nil, E.New("parse host key ", key) } outbound.hostKey = append(outbound.hostKey, key) } } return outbound, nil } func randomVersion() string { version := "SSH-2.0-OpenSSH_" if rand.Intn(2) == 0 { version += "7." + strconv.Itoa(rand.Intn(10)) } else { version += "8." + strconv.Itoa(rand.Intn(9)) } return version } func (s *Outbound) connect() (*ssh.Client, error) { if s.client != nil { return s.client, nil } s.clientAccess.Lock() defer s.clientAccess.Unlock() if s.client != nil { return s.client, nil } conn, err := s.dialer.DialContext(s.ctx, N.NetworkTCP, s.serverAddr) if err != nil { return nil, err } config := &ssh.ClientConfig{ User: s.user, Auth: s.authMethod, ClientVersion: s.clientVersion, HostKeyAlgorithms: s.hostKeyAlgorithms, HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { if len(s.hostKey) == 0 { return nil } serverKey := key.Marshal() for _, hostKey := range s.hostKey { if bytes.Equal(serverKey, hostKey.Marshal()) { return nil } } 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() return nil, E.Cause(err, "connect to ssh server") } client := ssh.NewClient(clientConn, chans, reqs) s.clientConn = conn s.client = client go func() { client.Wait() conn.Close() s.clientAccess.Lock() s.client = nil s.clientConn = nil s.clientAccess.Unlock() }() return client, nil } func (s *Outbound) InterfaceUpdated() { common.Close(s.clientConn) } func (s *Outbound) Close() error { return common.Close(s.clientConn) } func (s *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { client, err := s.connect() if err != nil { return nil, err } conn, err := client.Dial(network, destination.String()) if err != nil { return nil, err } return &chanConnWrapper{Conn: conn}, nil } func (s *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { return nil, os.ErrInvalid } type chanConnWrapper struct { net.Conn } func (c *chanConnWrapper) SetDeadline(t time.Time) error { return os.ErrInvalid } func (c *chanConnWrapper) SetReadDeadline(t time.Time) error { return os.ErrInvalid } func (c *chanConnWrapper) SetWriteDeadline(t time.Time) error { return os.ErrInvalid } ================================================ FILE: protocol/tailscale/certificate_provider.go ================================================ //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) 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) } ================================================ FILE: protocol/tailscale/dns_transport.go ================================================ //go:build with_gvisor package tailscale import ( "context" "errors" "net" "net/http" "net/netip" "net/url" "os" "strings" "sync" "github.com/sagernet/sing-box/adapter" "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/dns" "github.com/sagernet/sing-box/dns/transport" "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" 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" mDNS "github.com/miekg/dns" "go4.org/netipx" "golang.org/x/net/http2" ) func RegistryTransport(registry *dns.TransportRegistry) { dns.RegisterTransport[option.TailscaleDNSServerOptions](registry, C.DNSTypeTailscale, NewDNSTransport) } type DNSTransport struct { dns.TransportAdapter ctx context.Context logger logger.ContextLogger endpointTag string acceptDefaultResolvers bool acceptSearchDomain bool dnsRouter adapter.DNSRouter endpointManager adapter.EndpointManager endpoint *Endpoint access sync.RWMutex routePrefixes []netip.Prefix routes map[string][]adapter.DNSTransport hosts map[string][]netip.Addr searchDomains []string defaultResolvers []adapter.DNSTransport } func NewDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.TailscaleDNSServerOptions) (adapter.DNSTransport, error) { if options.Endpoint == "" { return nil, E.New("missing tailscale endpoint tag") } return &DNSTransport{ TransportAdapter: dns.NewTransportAdapter(C.DNSTypeTailscale, tag, nil), ctx: ctx, logger: logger, endpointTag: options.Endpoint, acceptDefaultResolvers: options.AcceptDefaultResolvers, acceptSearchDomain: options.AcceptSearchDomain, dnsRouter: service.FromContext[adapter.DNSRouter](ctx), endpointManager: service.FromContext[adapter.EndpointManager](ctx), }, nil } func (t *DNSTransport) Start(stage adapter.StartStage) error { if stage != adapter.StartStateInitialize { return nil } rawOutbound, loaded := t.endpointManager.Get(t.endpointTag) if !loaded { return E.New("endpoint not found: ", t.endpointTag) } ep, isTailscale := rawOutbound.(*Endpoint) if !isTailscale { return E.New("endpoint is not Tailscale: ", t.endpointTag) } if ep.onReconfigHook != nil { return E.New("only one Tailscale DNS server is allowed for single endpoint") } ep.onReconfigHook = t.onReconfig t.endpoint = ep return nil } func (t *DNSTransport) Reset() { t.access.RLock() transports := t.collectResolversLocked() t.access.RUnlock() for _, transport := range transports { transport.Reset() } } func (t *DNSTransport) onReconfig(cfg *wgcfg.Config, routerCfg *router.Config, dnsCfg *nDNS.Config) { err := t.updateDNSServers(routerCfg, dnsCfg) if err != nil { t.logger.Error(E.Cause(err, "update DNS servers")) } } func (t *DNSTransport) updateDNSServers(routeConfig *router.Config, dnsConfig *nDNS.Config) error { routePrefixes := buildRoutePrefixes(routeConfig) directDialerOnce := sync.OnceValue(func() N.Dialer { directDialer := common.Must1(dialer.NewDefault(t.ctx, option.DialerOptions{})) return &DNSDialer{transport: t, fallbackDialer: directDialer} }) routes := make(map[string][]adapter.DNSTransport) for domain, resolvers := range dnsConfig.Routes { var myResolvers []adapter.DNSTransport for _, resolver := range resolvers { myResolver, err := t.createResolver(directDialerOnce, resolver) if err != nil { return err } myResolvers = append(myResolvers, myResolver) } routes[domain.WithTrailingDot()] = myResolvers } hosts := make(map[string][]netip.Addr) 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) if err != nil { return err } defaultResolvers = append(defaultResolvers, myResolver) } t.access.Lock() oldResolvers := t.collectResolversLocked() t.routePrefixes = routePrefixes t.routes = routes t.hosts = hosts t.searchDomains = searchDomains t.defaultResolvers = defaultResolvers t.access.Unlock() for _, transport := range oldResolvers { transport.Close() } if len(defaultResolvers) > 0 { 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, ", len(searchDomains), " search domains") } return nil } func (t *DNSTransport) createResolver(directDialer func() N.Dialer, resolver *dnstype.Resolver) (adapter.DNSTransport, error) { serverURL, parseURLErr := url.Parse(resolver.Addr) var myDialer N.Dialer if parseURLErr == nil && serverURL.Scheme == "http" { myDialer = t.endpoint } else { myDialer = directDialer() } if len(resolver.BootstrapResolution) > 0 { bootstrapTransport := transport.NewUDPRaw(t.logger, t.TransportAdapter, myDialer, M.SocksaddrFrom(resolver.BootstrapResolution[0], 53)) myDialer = dialer.NewResolveDialer(t.ctx, myDialer, false, "", adapter.DNSQueryOptions{Transport: bootstrapTransport}, 0) } if serverAddr := M.ParseSocksaddr(resolver.Addr); serverAddr.IsValid() { if serverAddr.Port == 0 { serverAddr.Port = 53 } return transport.NewUDPRaw(t.logger, t.TransportAdapter, myDialer, serverAddr), nil } else if parseURLErr != nil { return nil, E.Cause(parseURLErr, "parse resolver address") } else { switch serverURL.Scheme { case "https": serverAddr = M.ParseSocksaddrHostPortStr(serverURL.Hostname(), serverURL.Port()) if serverAddr.Port == 0 { serverAddr.Port = 443 } tlsConfig := common.Must1(tls.NewClient(t.ctx, t.logger, serverAddr.AddrString(), option.OutboundTLSOptions{ ALPN: []string{http2.NextProtoTLS, "http/1.1"}, })) return transport.NewHTTPSRaw(t.TransportAdapter, t.logger, myDialer, serverURL, http.Header{}, serverAddr, tlsConfig), nil case "http": serverAddr = M.ParseSocksaddrHostPortStr(serverURL.Hostname(), serverURL.Port()) if serverAddr.Port == 0 { serverAddr.Port = 80 } return transport.NewHTTPSRaw(t.TransportAdapter, t.logger, myDialer, serverURL, http.Header{}, serverAddr, nil), nil // case "tls": default: return nil, E.New("unknown resolver scheme: ", serverURL.Scheme) } } } func buildRoutePrefixes(routeConfig *router.Config) []netip.Prefix { var builder netipx.IPSetBuilder for _, localAddr := range routeConfig.LocalAddrs { builder.AddPrefix(localAddr) } for _, route := range routeConfig.Routes { builder.AddPrefix(route) } for _, route := range routeConfig.LocalRoutes { builder.AddPrefix(route) } for _, route := range routeConfig.SubnetRoutes { builder.AddPrefix(route) } ipSet, err := builder.IPSet() if err != nil { return nil } return ipSet.Prefixes() } func (t *DNSTransport) Close() error { t.access.Lock() transports := t.collectResolversLocked() t.routePrefixes = nil t.routes = nil t.hosts = nil t.defaultResolvers = nil t.access.Unlock() var err error for _, transport := range transports { name := "resolver/" + transport.Type() + "[" + transport.Tag() + "]" err = E.Append(err, transport.Close(), func(err error) error { return E.Cause(err, "close ", name) }) } return err } 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 } 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() originalQuestion := message.Question[0] singleLabel := strings.TrimSuffix(originalQuestion.Name, ".") var lastErr error for _, searchDomain := range searchDomains { expandedName := singleLabel + "." + searchDomain question := originalQuestion question.Name = expandedName rewritten := *message rewritten.Question = []mDNS.Question{question} response, err := t.exchangeOnce(ctx, &rewritten, false) if err == nil { if response.Rcode == mDNS.RcodeNameError { continue } restoreOriginalQuestion(response, expandedName, originalQuestion) return response, nil } if errors.Is(err, dns.RcodeNameError) { continue } lastErr = err } if lastErr != nil { return nil, lastErr } 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] t.access.RLock() hosts := t.hosts routes := t.routes defaultResolvers := t.defaultResolvers t.access.RUnlock() addresses, hostsLoaded := hosts[question.Name] if hostsLoaded { switch question.Qtype { case mDNS.TypeA: addresses4 := common.Filter(addresses, func(addr netip.Addr) bool { return addr.Is4() }) if len(addresses4) > 0 { return dns.FixedResponse(message.Id, question, addresses4, C.DefaultDNSTTL), nil } case mDNS.TypeAAAA: addresses6 := common.Filter(addresses, func(addr netip.Addr) bool { return addr.Is6() }) if len(addresses6) > 0 { return dns.FixedResponse(message.Id, question, addresses6, C.DefaultDNSTTL), nil } } } for domainSuffix, transports := range routes { if strings.HasSuffix(question.Name, domainSuffix) { if len(transports) == 0 { return &mDNS.Msg{ MsgHdr: mDNS.MsgHdr{ Id: message.Id, Rcode: mDNS.RcodeNameError, Response: true, }, Question: []mDNS.Question{question}, }, nil } var lastErr error for _, dnsTransport := range transports { response, err := dnsTransport.Exchange(ctx, message) if err != nil { lastErr = err continue } return response, nil } return nil, lastErr } } if allowDefaultResolvers { if len(defaultResolvers) > 0 { var lastErr error for _, resolver := range defaultResolvers { response, err := resolver.Exchange(ctx, message) if err != nil { lastErr = err continue } return response, nil } return nil, lastErr } else { return nil, E.New("missing default resolvers") } } return nil, dns.RcodeNameError } func (t *DNSTransport) collectResolversLocked() []adapter.DNSTransport { var transports []adapter.DNSTransport for _, resolvers := range t.routes { transports = append(transports, resolvers...) } transports = append(transports, t.defaultResolvers...) return transports } type DNSDialer struct { transport *DNSTransport fallbackDialer N.Dialer } func (d *DNSDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { if destination.IsDomain() { panic("invalid request here") } routePrefixes := d.transport.routePrefixesSnapshot() for _, prefix := range routePrefixes { if prefix.Contains(destination.Addr) { return d.transport.endpoint.DialContext(ctx, network, destination) } } return d.fallbackDialer.DialContext(ctx, network, destination) } func (d *DNSDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { if destination.IsDomain() { panic("invalid request here") } routePrefixes := d.transport.routePrefixesSnapshot() for _, prefix := range routePrefixes { if prefix.Contains(destination.Addr) { return d.transport.endpoint.ListenPacket(ctx, destination) } } return d.fallbackDialer.ListenPacket(ctx, destination) } func (t *DNSTransport) routePrefixesSnapshot() []netip.Prefix { t.access.RLock() defer t.access.RUnlock() return append([]netip.Prefix(nil), t.routePrefixes...) } ================================================ FILE: protocol/tailscale/endpoint.go ================================================ //go:build with_gvisor package tailscale import ( "context" "fmt" "net" "net/http" "net/netip" "net/url" "os" "path/filepath" "reflect" "runtime" "strings" "sync/atomic" "syscall" "time" "github.com/sagernet/gvisor/pkg/tcpip" "github.com/sagernet/gvisor/pkg/tcpip/adapters/gonet" "github.com/sagernet/gvisor/pkg/tcpip/header" "github.com/sagernet/gvisor/pkg/tcpip/stack" "github.com/sagernet/gvisor/pkg/tcpip/transport/icmp" "github.com/sagernet/sing-box/adapter" "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" "github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun/ping" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "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" "github.com/sagernet/sing/service/filemanager" _ "github.com/sagernet/tailscale/feature/relayserver" "github.com/sagernet/tailscale/ipn" tsDNS "github.com/sagernet/tailscale/net/dns" "github.com/sagernet/tailscale/net/netmon" "github.com/sagernet/tailscale/net/netns" "github.com/sagernet/tailscale/net/tsaddr" tsTUN "github.com/sagernet/tailscale/net/tstun" "github.com/sagernet/tailscale/tsnet" "github.com/sagernet/tailscale/types/ipproto" "github.com/sagernet/tailscale/types/nettype" "github.com/sagernet/tailscale/version" "github.com/sagernet/tailscale/wgengine" "github.com/sagernet/tailscale/wgengine/filter" "github.com/sagernet/tailscale/wgengine/router" "github.com/sagernet/tailscale/wgengine/wgcfg" "go4.org/netipx" ) var ( _ adapter.OutboundWithPreferredRoutes = (*Endpoint)(nil) _ adapter.DirectRouteOutbound = (*Endpoint)(nil) _ dialer.PacketDialerWithDestination = (*Endpoint)(nil) ) func init() { version.SetVersion("sing-box " + C.Version) } func RegisterEndpoint(registry *endpoint.Registry) { endpoint.Register[option.TailscaleEndpointOptions](registry, C.TypeTailscale, NewEndpoint) } type Endpoint struct { endpoint.Adapter ctx context.Context router adapter.Router logger logger.ContextLogger dnsRouter adapter.DNSRouter network adapter.NetworkManager platformInterface adapter.PlatformInterface server *tsnet.Server stack *stack.Stack icmpForwarder *tun.ICMPForwarder filter *atomic.Pointer[filter.Filter] onReconfigHook wgengine.ReconfigListener cfg *wgcfg.Config dnsCfg *tsDNS.Config routeDomains common.TypedValue[map[string]bool] routePrefixes atomic.Pointer[netipx.IPSet] acceptRoutes bool exitNode string exitNodeAllowLANAccess bool advertiseRoutes []netip.Prefix advertiseExitNode bool advertiseTags []string relayServerPort *uint16 relayServerStaticEndpoints []netip.AddrPort udpTimeout time.Duration systemInterface bool systemInterfaceName string systemInterfaceMTU uint32 serverStarted bool started atomic.Bool systemTun tun.Tun systemDialer *dialer.DefaultDialer fallbackTCPCloser func() } func (t *Endpoint) registerNetstackHandlers() { netstack := t.server.ExportNetstack() if netstack == nil { return } previousTCP := netstack.GetTCPHandlerForFlow netstack.GetTCPHandlerForFlow = func(src, dst netip.AddrPort) (handler func(net.Conn), intercept bool) { if previousTCP != nil { handler, intercept = previousTCP(src, dst) if handler != nil || !intercept { return handler, intercept } } return func(conn net.Conn) { ctx := log.ContextWithNewID(t.ctx) source := M.SocksaddrFrom(src.Addr(), src.Port()) destination := M.SocksaddrFrom(dst.Addr(), dst.Port()) t.NewConnectionEx(ctx, conn, source, destination, nil) }, true } previousUDP := netstack.GetUDPHandlerForFlow netstack.GetUDPHandlerForFlow = func(src, dst netip.AddrPort) (handler func(nettype.ConnPacketConn), intercept bool) { if previousUDP != nil { handler, intercept = previousUDP(src, dst) if handler != nil || !intercept { return handler, intercept } } return func(conn nettype.ConnPacketConn) { ctx := log.ContextWithNewID(t.ctx) source := M.SocksaddrFrom(src.Addr(), src.Port()) destination := M.SocksaddrFrom(dst.Addr(), dst.Port()) packetConn := bufio.NewUnbindPacketConnWithAddr(conn, destination) t.NewPacketConnectionEx(ctx, packetConn, source, destination, nil) }, true } } func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TailscaleEndpointOptions) (adapter.Endpoint, error) { stateDirectory := options.StateDirectory if stateDirectory == "" { stateDirectory = "tailscale" } hostname := options.Hostname if hostname == "" { osHostname, _ := os.Hostname() osHostname = strings.TrimSpace(osHostname) hostname = osHostname } if hostname == "" { hostname = "sing-box" } stateDirectory = filemanager.BasePath(ctx, os.ExpandEnv(stateDirectory)) stateDirectory, _ = filepath.Abs(stateDirectory) for _, advertiseRoute := range options.AdvertiseRoutes { if advertiseRoute.Addr().IsUnspecified() && advertiseRoute.Bits() == 0 { return nil, E.New("`advertise_routes` cannot be default, use `advertise_exit_node` instead.") } } if options.AdvertiseExitNode && options.ExitNode != "" { return nil, E.New("cannot advertise an exit node and use an exit node at the same time.") } var udpTimeout time.Duration if options.UDPTimeout != 0 { udpTimeout = time.Duration(options.UDPTimeout) } else { udpTimeout = C.UDPTimeout } var remoteIsDomain bool if options.ControlURL != "" { controlURL, err := url.Parse(options.ControlURL) if err != nil { return nil, E.Cause(err, "parse control URL") } remoteIsDomain = M.ParseSocksaddr(controlURL.Hostname()).IsDomain() } else { // 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, RemoteIsDomain: remoteIsDomain, ResolverOnDetour: true, NewDialer: true, }) if err != nil { 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, Logf: func(format string, args ...any) { logger.Trace(fmt.Sprintf(format, args...)) }, UserLogf: func(format string, args ...any) { logger.Debug(fmt.Sprintf(format, args...)) }, Ephemeral: options.Ephemeral, AuthKey: options.AuthKey, ControlURL: options.ControlURL, AdvertiseTags: options.AdvertiseTags, Dialer: &endpointDialer{Dialer: outboundDialer, logger: logger}, LookupHook: func(ctx context.Context, host string) ([]netip.Addr, error) { return dnsRouter.Lookup(ctx, host, outboundDialer.(dialer.ResolveDialer).QueryOptions()) }, DNS: &dnsConfigurtor{}, HTTPClient: controlHTTPClient, } return &Endpoint{ Adapter: endpoint.NewAdapterWithDialerOptions(C.TypeTailscale, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, controlHTTPClientOptions.DialerOptions), ctx: ctx, router: router, logger: logger, dnsRouter: dnsRouter, network: service.FromContext[adapter.NetworkManager](ctx), platformInterface: service.FromContext[adapter.PlatformInterface](ctx), server: server, acceptRoutes: options.AcceptRoutes, exitNode: options.ExitNode, exitNodeAllowLANAccess: options.ExitNodeAllowLANAccess, advertiseRoutes: options.AdvertiseRoutes, advertiseExitNode: options.AdvertiseExitNode, advertiseTags: options.AdvertiseTags, relayServerPort: options.RelayServerPort, relayServerStaticEndpoints: options.RelayServerStaticEndpoints, udpTimeout: udpTimeout, systemInterface: options.SystemInterface, systemInterfaceName: options.SystemInterfaceName, systemInterfaceMTU: options.SystemInterfaceMTU, }, nil } func (t *Endpoint) Start(stage adapter.StartStage) error { switch stage { case adapter.StartStateStart: return t.start() case adapter.StartStatePostStart: return t.postStart() } return nil } func (t *Endpoint) start() error { if t.platformInterface != nil { err := t.network.UpdateInterfaces() if err != nil { return err } netmon.RegisterInterfaceGetter(func() ([]netmon.Interface, error) { return common.Map(t.network.InterfaceFinder().Interfaces(), func(it control.Interface) netmon.Interface { return netmon.Interface{ Interface: &net.Interface{ Index: it.Index, MTU: it.MTU, Name: it.Name, HardwareAddr: it.HardwareAddr, Flags: it.Flags, }, AltAddrs: common.Map(it.Addresses, func(it netip.Prefix) net.Addr { return &net.IPNet{ IP: it.Addr().AsSlice(), Mask: net.CIDRMask(it.Bits(), it.Addr().BitLen()), } }), } }), nil }) } if t.systemInterface { mtu := t.systemInterfaceMTU if mtu == 0 { mtu = uint32(tsTUN.DefaultTUNMTU()) } tunName := t.systemInterfaceName if tunName == "" { tunName = tun.CalculateInterfaceName("tailscale") } tunOptions := tun.Options{ Name: tunName, MTU: mtu, GSO: true, InterfaceScope: true, InterfaceMonitor: t.network.InterfaceMonitor(), InterfaceFinder: t.network.InterfaceFinder(), Logger: t.logger, EXP_ExternalConfiguration: true, } systemTun, err := tun.New(tunOptions) if err != nil { return err } err = systemTun.Start() if err != nil { _ = systemTun.Close() return err } wgTunDevice, err := newTunDeviceAdapter(systemTun, int(mtu), t.logger) if err != nil { _ = systemTun.Close() return err } systemDialer, err := dialer.NewDefault(t.ctx, option.DialerOptions{ BindInterface: tunName, }) if err != nil { _ = systemTun.Close() return err } t.systemTun = systemTun t.systemDialer = systemDialer t.server.TunDevice = wgTunDevice } if mark := t.network.AutoRedirectOutputMark(); mark > 0 { controlFunc := t.network.AutoRedirectOutputMarkFunc() if bindFunc := t.network.AutoDetectInterfaceFunc(); bindFunc != nil { controlFunc = control.Append(controlFunc, bindFunc) } netns.SetControlFunc(controlFunc) } else if runtime.GOOS == "android" && t.platformInterface != nil { netns.SetControlFunc(func(network, address string, c syscall.RawConn) error { return control.Raw(c, func(fd uintptr) error { return t.platformInterface.AutoDetectInterfaceControl(int(fd)) }) }) } return nil } func (t *Endpoint) postStart() error { err := t.server.Start() if err != nil { if t.systemTun != nil { _ = t.systemTun.Close() } return err } t.serverStarted = true if t.fallbackTCPCloser == nil { t.fallbackTCPCloser = t.server.RegisterFallbackTCPHandler(func(src, dst netip.AddrPort) (handler func(net.Conn), intercept bool) { return func(conn net.Conn) { ctx := log.ContextWithNewID(t.ctx) source := M.SocksaddrFrom(src.Addr(), src.Port()) destination := M.SocksaddrFrom(dst.Addr(), dst.Port()) t.NewConnectionEx(ctx, conn, source, destination, nil) }, true }) } t.server.ExportLocalBackend().ExportEngine().(wgengine.ExportedUserspaceEngine).SetOnReconfigListener(t.onReconfig) ipStack := t.server.ExportNetstack().ExportIPStack() gErr := ipStack.SetSpoofing(tun.DefaultNIC, true) if gErr != nil { return gonet.TranslateNetstackError(gErr) } gErr = ipStack.SetPromiscuousMode(tun.DefaultNIC, true) if gErr != nil { return gonet.TranslateNetstackError(gErr) } icmpForwarder := tun.NewICMPForwarder(t.ctx, ipStack, t, t.udpTimeout) ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket) ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket) t.stack = ipStack t.icmpForwarder = icmpForwarder t.registerNetstackHandlers() localBackend := t.server.ExportLocalBackend() perfs := &ipn.MaskedPrefs{ Prefs: ipn.Prefs{ RouteAll: t.acceptRoutes, AdvertiseRoutes: t.advertiseRoutes, }, RouteAllSet: true, ExitNodeIPSet: true, AdvertiseRoutesSet: true, RelayServerPortSet: true, RelayServerStaticEndpointsSet: true, } if t.advertiseExitNode { perfs.AdvertiseRoutes = append(perfs.AdvertiseRoutes, tsaddr.ExitRoutes()...) } if t.relayServerPort != nil { perfs.RelayServerPort = t.relayServerPort } if len(t.relayServerStaticEndpoints) > 0 { perfs.RelayServerStaticEndpoints = t.relayServerStaticEndpoints } _, err = localBackend.EditPrefs(perfs) if err != nil { return E.Cause(err, "update prefs") } t.filter = localBackend.ExportFilter() go t.watchState() t.started.Store(true) return nil } func (t *Endpoint) watchState() { localBackend := t.server.ExportLocalBackend() localBackend.WatchNotifications(t.ctx, ipn.NotifyInitialState, nil, func(roNotify *ipn.Notify) (keepGoing bool) { if roNotify.State != nil && *roNotify.State != ipn.NeedsLogin && *roNotify.State != ipn.NoState { return false } authURL := localBackend.StatusWithoutPeers().AuthURL if authURL != "" { t.logger.Info("Waiting for authentication: ", authURL) if t.platformInterface != nil { err := t.platformInterface.SendNotification(&adapter.Notification{ Identifier: "tailscale-authentication", TypeName: "Tailscale Authentication Notifications", TypeID: 10, Title: "Tailscale Authentication", Body: F.ToString("Tailscale outbound[", t.Tag(), "] is waiting for authentication."), OpenURL: authURL, }) if err != nil { t.logger.Error("send authentication notification: ", err) } } return false } return true }) if t.exitNode != "" { localBackend.WatchNotifications(t.ctx, ipn.NotifyInitialState, nil, func(roNotify *ipn.Notify) (keepGoing bool) { if roNotify.State == nil || *roNotify.State != ipn.Running { return true } status, err := common.Must1(t.server.LocalClient()).Status(t.ctx) if err != nil { t.logger.Error("set exit node: ", err) return } perfs := &ipn.MaskedPrefs{ Prefs: ipn.Prefs{ ExitNodeAllowLANAccess: t.exitNodeAllowLANAccess, }, ExitNodeIPSet: true, ExitNodeAllowLANAccessSet: true, } err = perfs.SetExitNodeIP(t.exitNode, status) if err != nil { t.logger.Error("set exit node: ", err) return true } _, err = localBackend.EditPrefs(perfs) if err != nil { t.logger.Error("set exit node: ", err) return true } return false }) } } 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 } netmon.RegisterInterfaceGetter(nil) netns.SetControlFunc(nil) if t.fallbackTCPCloser != nil { t.fallbackTCPCloser() t.fallbackTCPCloser = nil } if t.systemTun != nil { t.systemTun.Close() t.systemTun = nil } return err } func (t *Endpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { switch network { case N.NetworkTCP: t.logger.InfoContext(ctx, "outbound connection to ", 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 { return nil, err } return N.DialSerial(ctx, t, network, destination, destinationAddresses) } if t.systemDialer != nil { return t.systemDialer.DialContext(ctx, network, destination) } addr4, addr6 := t.server.TailscaleIPs() remoteAddr := tcpip.FullAddress{ NIC: 1, Port: destination.Port, Addr: addressFromAddr(destination.Addr), } var localAddr tcpip.FullAddress var networkProtocol tcpip.NetworkProtocolNumber if destination.IsIPv4() { if !addr4.IsValid() { return nil, E.New("missing Tailscale IPv4 address") } networkProtocol = header.IPv4ProtocolNumber localAddr = tcpip.FullAddress{ NIC: 1, Addr: addressFromAddr(addr4), } } else { if !addr6.IsValid() { return nil, E.New("missing Tailscale IPv6 address") } networkProtocol = header.IPv6ProtocolNumber localAddr = tcpip.FullAddress{ NIC: 1, Addr: addressFromAddr(addr6), } } switch N.NetworkName(network) { case N.NetworkTCP: tcpConn, err := gonet.DialTCPWithBind(ctx, t.stack, localAddr, remoteAddr, networkProtocol) if err != nil { return nil, err } return tcpConn, nil case N.NetworkUDP: udpConn, err := gonet.DialUDP(t.stack, &localAddr, &remoteAddr, networkProtocol) if err != nil { return nil, err } return udpConn, nil default: return nil, E.Extend(N.ErrUnknownNetwork, network) } } 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) } addr4, addr6 := t.server.TailscaleIPs() bind := tcpip.FullAddress{ NIC: 1, } var networkProtocol tcpip.NetworkProtocolNumber if destination.IsIPv4() { if !addr4.IsValid() { return nil, E.New("missing Tailscale IPv4 address") } networkProtocol = header.IPv4ProtocolNumber bind.Addr = addressFromAddr(addr4) } else { if !addr6.IsValid() { return nil, E.New("missing Tailscale IPv6 address") } networkProtocol = header.IPv6ProtocolNumber bind.Addr = addressFromAddr(addr6) } udpConn, err := gonet.DialUDP(t.stack, &bind, nil, networkProtocol) if err != nil { return nil, err } return udpConn, nil } func (t *Endpoint) ListenPacketWithDestination(ctx context.Context, destination M.Socksaddr) (net.PacketConn, netip.Addr, error) { t.logger.InfoContext(ctx, "outbound packet connection to ", destination) if destination.IsDomain() { destinationAddresses, err := t.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{}) if err != nil { return nil, netip.Addr{}, err } var errors []error for _, address := range destinationAddresses { packetConn, packetErr := t.listenPacketWithAddress(ctx, M.SocksaddrFrom(address, destination.Port)) if packetErr == nil { return packetConn, address, nil } errors = append(errors, packetErr) } return nil, netip.Addr{}, E.Errors(errors...) } packetConn, err := t.listenPacketWithAddress(ctx, destination) if err != nil { return nil, netip.Addr{}, err } if destination.IsIP() { return packetConn, destination.Addr, nil } return packetConn, netip.Addr{}, nil } func (t *Endpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { packetConn, destinationAddress, err := t.ListenPacketWithDestination(ctx, destination) if err != nil { return nil, err } if destinationAddress.IsValid() && destination != M.SocksaddrFrom(destinationAddress, destination.Port) { return bufio.NewNATPacketConn(bufio.NewPacketConn(packetConn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil } return packetConn, nil } func (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 switch N.NetworkName(network) { case N.NetworkTCP: ipProto = ipproto.TCP case N.NetworkUDP: ipProto = ipproto.UDP case N.NetworkICMP: if !destination.IsIPv6() { ipProto = ipproto.ICMPv4 } else { ipProto = ipproto.ICMPv6 } } response := tsFilter.Check(source.Addr, destination.Addr, destination.Port, ipProto) switch response { case filter.Drop: return nil, syscall.ECONNREFUSED case filter.DropSilently: return nil, tun.ErrDrop } } var ipVersion uint8 if !destination.IsIPv6() { ipVersion = 4 } else { ipVersion = 6 } routeDestination, err := t.router.PreMatch(adapter.InboundContext{ Inbound: t.Tag(), InboundType: t.Type(), IPVersion: ipVersion, Network: network, Source: source, Destination: destination, }, routeContext, timeout, false) if err != nil { switch { case rule.IsBypassed(err): err = nil case rule.IsRejected(err): t.logger.Trace("reject ", network, " connection from ", source.AddrString(), " to ", destination.AddrString()) default: if network == N.NetworkICMP { t.logger.Warn(E.Cause(err, "link ", network, " connection from ", source.AddrString(), " to ", destination.AddrString())) } } } return routeDestination, err } func (t *Endpoint) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { var metadata adapter.InboundContext metadata.Inbound = t.Tag() metadata.InboundType = t.Type() metadata.Source = source addr4, addr6 := t.server.TailscaleIPs() switch destination.Addr { case addr4: destination.Addr = netip.AddrFrom4([4]uint8{127, 0, 0, 1}) case addr6: destination.Addr = netip.IPv6Loopback() } metadata.Destination = destination t.logger.InfoContext(ctx, "inbound connection from ", source) t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) t.router.RouteConnectionEx(ctx, conn, metadata, onClose) } func (t *Endpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { var metadata adapter.InboundContext metadata.Inbound = t.Tag() metadata.InboundType = t.Type() metadata.Source = source addr4, addr6 := t.server.TailscaleIPs() switch destination.Addr { case addr4: metadata.OriginDestination = destination destination.Addr = netip.AddrFrom4([4]uint8{127, 0, 0, 1}) conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, destination) case addr6: metadata.OriginDestination = destination destination.Addr = netip.IPv6Loopback() conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, destination) } metadata.Destination = destination t.logger.InfoContext(ctx, "inbound packet connection from ", source) t.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } 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 if t.systemDialer != nil { destination, err = ping.ConnectDestination( ctx, t.logger, t.systemDialer.DialerForICMPDestination(metadata.Destination.Addr).Control, metadata.Destination.Addr, routeContext, timeout, ) } else { inet4Address, inet6Address := t.server.TailscaleIPs() if metadata.Destination.Addr.Is4() && !inet4Address.IsValid() || metadata.Destination.Addr.Is6() && !inet6Address.IsValid() { return nil, E.New("Tailscale is not ready yet") } destination, err = ping.ConnectGVisor( ctx, t.logger, metadata.Source.Addr, metadata.Destination.Addr, routeContext, t.stack, inet4Address, inet6Address, timeout, ) } if err != nil { return nil, err } t.logger.InfoContext(ctx, "linked ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString()) return destination, nil } func (t *Endpoint) PreferredDomain(domain string) bool { routeDomains := t.routeDomains.Load() if routeDomains == nil { return false } return routeDomains[strings.ToLower(domain)] } func (t *Endpoint) PreferredAddress(address netip.Addr) bool { routePrefixes := t.routePrefixes.Load() if routePrefixes == nil { return false } return routePrefixes.Contains(address) } func (t *Endpoint) Server() *tsnet.Server { return t.server } func (t *Endpoint) onReconfig(cfg *wgcfg.Config, routerCfg *router.Config, dnsCfg *tsDNS.Config) { if cfg == nil || dnsCfg == nil { return } if (t.cfg != nil && reflect.DeepEqual(t.cfg, cfg)) && (t.dnsCfg != nil && reflect.DeepEqual(t.dnsCfg, dnsCfg)) { return } var inet4Address, inet6Address netip.Addr for _, address := range cfg.Addresses { if address.Addr().Is4() { inet4Address = address.Addr() } else if address.Addr().Is6() { inet6Address = address.Addr() } } t.icmpForwarder.SetLocalAddresses(inet4Address, inet6Address) t.cfg = cfg t.dnsCfg = dnsCfg routeDomains := make(map[string]bool) for fqdn := range dnsCfg.Routes { routeDomains[fqdn.WithoutTrailingDot()] = true } for _, fqdn := range dnsCfg.SearchDomains { routeDomains[fqdn.WithoutTrailingDot()] = true } t.routeDomains.Store(routeDomains) var builder netipx.IPSetBuilder for _, peer := range cfg.Peers { for _, allowedIP := range peer.AllowedIPs { builder.AddPrefix(allowedIP) } } t.routePrefixes.Store(common.Must1(builder.IPSet())) if t.onReconfigHook != nil { t.onReconfigHook(cfg, routerCfg, dnsCfg) } } func addressFromAddr(destination netip.Addr) tcpip.Address { if destination.Is6() { return tcpip.AddrFrom16(destination.As16()) } else { return tcpip.AddrFrom4(destination.As4()) } } type endpointDialer struct { N.Dialer logger logger.ContextLogger } func (d *endpointDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { switch N.NetworkName(network) { case N.NetworkTCP: d.logger.InfoContext(ctx, "output connection to ", destination) case N.NetworkUDP: d.logger.InfoContext(ctx, "output packet connection to ", destination) } return d.Dialer.DialContext(ctx, network, destination) } func (d *endpointDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { d.logger.InfoContext(ctx, "output packet connection") return d.Dialer.ListenPacket(ctx, destination) } type dnsConfigurtor struct { baseConfig tsDNS.OSConfig } func (c *dnsConfigurtor) SetDNS(cfg tsDNS.OSConfig) error { c.baseConfig = cfg return nil } func (c *dnsConfigurtor) SupportsSplitDNS() bool { return true } func (c *dnsConfigurtor) GetBaseConfig() (tsDNS.OSConfig, error) { return c.baseConfig, nil } func (c *dnsConfigurtor) Close() error { return nil } ================================================ FILE: protocol/tailscale/hostinfo_tvos.go ================================================ //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) } ================================================ FILE: protocol/tailscale/ping.go ================================================ //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, } } ================================================ FILE: protocol/tailscale/status.go ================================================ //go:build with_gvisor package tailscale import ( "context" "slices" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/tailscale/ipn" "github.com/sagernet/tailscale/ipn/ipnstate" ) var _ adapter.TailscaleEndpoint = (*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) } 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)) } 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 } 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, } } ================================================ FILE: protocol/tailscale/tun_device_unix.go ================================================ //go:build with_gvisor && !windows package tailscale import ( "encoding/hex" "errors" "io" "os" "sync" "sync/atomic" singTun "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/logger" wgTun "github.com/sagernet/wireguard-go/tun" ) type tunDeviceAdapter struct { tun singTun.Tun linuxTUN singTun.LinuxTUN events chan wgTun.Event mtu int logger logger.ContextLogger debugTun bool readCount atomic.Uint32 writeCount atomic.Uint32 closeOnce sync.Once } func newTunDeviceAdapter(tun singTun.Tun, mtu int, logger logger.ContextLogger) (wgTun.Device, error) { if tun == nil { return nil, os.ErrInvalid } if mtu == 0 { mtu = 1500 } adapter := &tunDeviceAdapter{ tun: tun, events: make(chan wgTun.Event, 1), mtu: mtu, logger: logger, debugTun: os.Getenv("SINGBOX_TS_TUN_DEBUG") != "", } if linuxTUN, ok := tun.(singTun.LinuxTUN); ok { adapter.linuxTUN = linuxTUN } adapter.events <- wgTun.EventUp return adapter, nil } func (a *tunDeviceAdapter) File() *os.File { return nil } func (a *tunDeviceAdapter) Read(bufs [][]byte, sizes []int, offset int) (count int, err error) { if a.linuxTUN != nil { n, err := a.linuxTUN.BatchRead(bufs, offset-singTun.PacketOffset, sizes) if err == nil { for i := range n { a.debugPacket("read", bufs[i][offset:offset+sizes[i]]) } } return n, err } if offset < singTun.PacketOffset { return 0, io.ErrShortBuffer } readBuf := bufs[0][offset-singTun.PacketOffset:] n, err := a.tun.Read(readBuf) if err == nil { if n < singTun.PacketOffset { return 0, io.ErrUnexpectedEOF } sizes[0] = n - singTun.PacketOffset a.debugPacket("read", readBuf[singTun.PacketOffset:n]) return 1, nil } if errors.Is(err, singTun.ErrTooManySegments) { err = wgTun.ErrTooManySegments } return 0, err } func (a *tunDeviceAdapter) Write(bufs [][]byte, offset int) (count int, err error) { if a.linuxTUN != nil { for i := range bufs { a.debugPacket("write", bufs[i][offset:]) } return a.linuxTUN.BatchWrite(bufs, offset) } for _, packet := range bufs { a.debugPacket("write", packet[offset:]) if singTun.PacketOffset > 0 { clear(packet[offset-singTun.PacketOffset : offset]) singTun.PacketFillHeader(packet[offset-singTun.PacketOffset:], singTun.PacketIPVersion(packet[offset:])) } _, err = a.tun.Write(packet[offset-singTun.PacketOffset:]) if err != nil { return 0, err } } // WireGuard will not read count. return 0, nil } func (a *tunDeviceAdapter) MTU() (int, error) { return a.mtu, nil } func (a *tunDeviceAdapter) Name() (string, error) { return a.tun.Name() } func (a *tunDeviceAdapter) Events() <-chan wgTun.Event { return a.events } func (a *tunDeviceAdapter) Close() error { var err error a.closeOnce.Do(func() { close(a.events) err = a.tun.Close() }) return err } func (a *tunDeviceAdapter) BatchSize() int { if a.linuxTUN != nil { return a.linuxTUN.BatchSize() } return 1 } func (a *tunDeviceAdapter) debugPacket(direction string, packet []byte) { if !a.debugTun || a.logger == nil { return } var counter *atomic.Uint32 switch direction { case "read": counter = &a.readCount case "write": counter = &a.writeCount default: return } if counter.Add(1) > 8 { return } sample := packet if len(sample) > 64 { sample = sample[:64] } a.logger.Trace("tailscale tun ", direction, " len=", len(packet), " head=", hex.EncodeToString(sample)) } ================================================ FILE: protocol/tailscale/tun_device_windows.go ================================================ //go:build with_gvisor && windows package tailscale import ( "errors" "os" "sync" "sync/atomic" singTun "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/logger" wgTun "github.com/sagernet/wireguard-go/tun" ) type tunDeviceAdapter struct { tun singTun.WinTun nativeTun *singTun.NativeTun events chan wgTun.Event mtu atomic.Int64 closeOnce sync.Once } func newTunDeviceAdapter(tun singTun.Tun, mtu int, _ logger.ContextLogger) (wgTun.Device, error) { winTun, ok := tun.(singTun.WinTun) if !ok { return nil, errors.New("not a windows tun device") } nativeTun, ok := winTun.(*singTun.NativeTun) if !ok { return nil, errors.New("unsupported windows tun device") } if mtu == 0 { mtu = 1500 } adapter := &tunDeviceAdapter{ tun: winTun, nativeTun: nativeTun, events: make(chan wgTun.Event, 1), } adapter.mtu.Store(int64(mtu)) adapter.events <- wgTun.EventUp return adapter, nil } func (a *tunDeviceAdapter) File() *os.File { return nil } func (a *tunDeviceAdapter) Read(bufs [][]byte, sizes []int, offset int) (count int, err error) { packet, release, err := a.tun.ReadPacket() if err != nil { return 0, err } defer release() sizes[0] = copy(bufs[0][offset-singTun.PacketOffset:], packet) return 1, nil } func (a *tunDeviceAdapter) Write(bufs [][]byte, offset int) (count int, err error) { for _, packet := range bufs { if singTun.PacketOffset > 0 { singTun.PacketFillHeader(packet[offset-singTun.PacketOffset:], singTun.PacketIPVersion(packet[offset:])) } _, err = a.tun.Write(packet[offset-singTun.PacketOffset:]) if err != nil { return 0, err } } return 0, nil } func (a *tunDeviceAdapter) MTU() (int, error) { return int(a.mtu.Load()), nil } func (a *tunDeviceAdapter) ForceMTU(mtu int) { if mtu <= 0 { return } update := int(a.mtu.Load()) != mtu a.mtu.Store(int64(mtu)) if update { select { case a.events <- wgTun.EventMTUUpdate: default: } } } func (a *tunDeviceAdapter) LUID() uint64 { if a.nativeTun == nil { return 0 } return a.nativeTun.LUID() } func (a *tunDeviceAdapter) Name() (string, error) { return a.tun.Name() } func (a *tunDeviceAdapter) Events() <-chan wgTun.Event { return a.events } func (a *tunDeviceAdapter) Close() error { var err error a.closeOnce.Do(func() { close(a.events) err = a.tun.Close() }) return err } func (a *tunDeviceAdapter) BatchSize() int { return 1 } ================================================ FILE: protocol/tor/outbound.go ================================================ package tor import ( "context" "net" "os" "path/filepath" "strings" "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" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "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/rw" "github.com/sagernet/sing/protocol/socks" "github.com/cretz/bine/control" "github.com/cretz/bine/tor" ) func RegisterOutbound(registry *outbound.Registry) { outbound.Register[option.TorOutboundOptions](registry, C.TypeTor, NewOutbound) } type Outbound struct { outbound.Adapter ctx context.Context logger logger.ContextLogger proxy *proxybridge.Bridge startConf *tor.StartConf options map[string]string events chan control.Event instance *tor.Tor socksClient *socks.Client } func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TorOutboundOptions) (adapter.Outbound, error) { var startConf tor.StartConf startConf.DataDir = os.ExpandEnv(options.DataDirectory) startConf.TempDataDirBase = os.TempDir() startConf.ExtraArgs = options.ExtraArgs if options.DataDirectory != "" { dataDirAbs, _ := filepath.Abs(startConf.DataDir) if geoIPPath := filepath.Join(dataDirAbs, "geoip"); rw.IsFile(geoIPPath) && !common.Contains(options.ExtraArgs, "--GeoIPFile") { options.ExtraArgs = append(options.ExtraArgs, "--GeoIPFile", geoIPPath) } if geoIP6Path := filepath.Join(dataDirAbs, "geoip6"); rw.IsFile(geoIP6Path) && !common.Contains(options.ExtraArgs, "--GeoIPv6File") { options.ExtraArgs = append(options.ExtraArgs, "--GeoIPv6File", geoIP6Path) } } if options.ExecutablePath != "" { startConf.ExePath = options.ExecutablePath startConf.ProcessCreator = nil startConf.UseEmbeddedControlConn = false } if startConf.DataDir != "" { torrcFile := filepath.Join(startConf.DataDir, "torrc") err := rw.MkdirParent(torrcFile) if err != nil { return nil, err } if !rw.IsFile(torrcFile) { err := os.WriteFile(torrcFile, []byte(""), 0o600) if err != nil { return nil, err } } startConf.TorrcFile = torrcFile } outboundDialer, err := dialer.New(ctx, options.DialerOptions, false) 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: proxy, startConf: &startConf, options: options.Options, }, nil } func (t *Outbound) Start() error { err := t.start() if err != nil { t.Close() } return err } var torLogEvents = []control.EventCode{ control.EventCodeLogDebug, control.EventCodeLogErr, control.EventCodeLogInfo, control.EventCodeLogNotice, control.EventCodeLogWarn, } func (t *Outbound) start() error { torInstance, err := tor.Start(t.ctx, t.startConf) if err != nil { return E.New(strings.ToLower(err.Error())) } t.instance = torInstance t.events = make(chan control.Event, 8) err = torInstance.Control.AddEventListener(t.events, torLogEvents...) if err != nil { return err } go t.recvLoop() proxyPort := "127.0.0.1:" + F.ToString(t.proxy.Port()) proxyUsername := t.proxy.Username() proxyPassword := t.proxy.Password() t.logger.Trace("created upstream proxy at ", proxyPort) t.logger.Trace("upstream proxy username ", proxyUsername) t.logger.Trace("upstream proxy password ", proxyPassword) confOptions := []*control.KeyVal{ control.NewKeyVal("Socks5Proxy", proxyPort), control.NewKeyVal("Socks5ProxyUsername", proxyUsername), control.NewKeyVal("Socks5ProxyPassword", proxyPassword), } err = torInstance.Control.ResetConf(confOptions...) if err != nil { return err } if len(t.options) > 0 { for key, value := range t.options { switch key { case "Socks5Proxy", "Socks5ProxyUsername", "Socks5ProxyPassword": continue } err = torInstance.Control.SetConf(control.NewKeyVal(key, value)) if err != nil { return E.Cause(err, "set ", key, "=", value) } } } err = torInstance.EnableNetwork(t.ctx, true) if err != nil { return err } info, err := torInstance.Control.GetInfo("net/listeners/socks") if err != nil { return err } if len(info) != 1 || info[0].Key != "net/listeners/socks" { return E.New("get socks proxy address") } t.logger.Trace("obtained tor socks5 address ", info[0].Val) // TODO: set password for tor socks5 server if supported t.socksClient = socks.NewClient(N.SystemDialer, M.ParseSocksaddr(info[0].Val), socks.Version5, "", "") return nil } func (t *Outbound) recvLoop() { for rawEvent := range t.events { switch event := rawEvent.(type) { case *control.LogEvent: event.Raw = strings.ToLower(event.Raw) switch event.Severity { case control.EventCodeLogDebug, control.EventCodeLogInfo: t.logger.Trace(event.Raw) case control.EventCodeLogNotice: if strings.Contains(event.Raw, "disablenetwork") || strings.Contains(event.Raw, "socks listener") { t.logger.Trace(event.Raw) continue } t.logger.Info(event.Raw) case control.EventCodeLogWarn: t.logger.Warn(event.Raw) case control.EventCodeLogErr: t.logger.Error(event.Raw) } } } } func (t *Outbound) Close() error { err := common.Close( common.PtrOrNil(t.proxy), common.PtrOrNil(t.instance), ) if t.events != nil { close(t.events) t.events = nil } return err } func (t *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { t.logger.InfoContext(ctx, "outbound connection to ", destination) return t.socksClient.DialContext(ctx, network, destination) } func (t *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { return nil, os.ErrInvalid } ================================================ FILE: protocol/trojan/inbound.go ================================================ package trojan import ( "context" "net" "os" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/common/listener" "github.com/sagernet/sing-box/common/mux" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/transport/trojan" "github.com/sagernet/sing-box/transport/v2ray" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/auth" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) func RegisterInbound(registry *inbound.Registry) { inbound.Register[option.TrojanInboundOptions](registry, C.TypeTrojan, NewInbound) } var _ adapter.TCPInjectableInbound = (*Inbound)(nil) type Inbound struct { inbound.Adapter router adapter.ConnectionRouterEx logger log.ContextLogger listener *listener.Listener service *trojan.Service[int] users []option.TrojanUser tlsConfig tls.ServerConfig fallbackAddr M.Socksaddr fallbackAddrTLSNextProto map[string]M.Socksaddr transport adapter.V2RayServerTransport } func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TrojanInboundOptions) (adapter.Inbound, error) { inbound := &Inbound{ Adapter: inbound.NewAdapter(C.TypeTrojan, tag), router: router, logger: logger, users: options.Users, } if options.TLS != nil { tlsConfig, err := tls.NewServerWithOptions(tls.ServerOptions{ Context: ctx, Logger: logger, Options: common.PtrValueOrDefault(options.TLS), KTLSCompatible: common.PtrValueOrDefault(options.Transport).Type == "" && !common.PtrValueOrDefault(options.Multiplex).Enabled, }) if err != nil { return nil, err } inbound.tlsConfig = tlsConfig } var fallbackHandler N.TCPConnectionHandlerEx if options.Fallback != nil && options.Fallback.Server != "" || len(options.FallbackForALPN) > 0 { if options.Fallback != nil && options.Fallback.Server != "" { inbound.fallbackAddr = options.Fallback.Build() if !inbound.fallbackAddr.IsValid() { return nil, E.New("invalid fallback address: ", inbound.fallbackAddr) } } if len(options.FallbackForALPN) > 0 { if inbound.tlsConfig == nil { return nil, E.New("fallback for ALPN is not supported without TLS") } fallbackAddrNextProto := make(map[string]M.Socksaddr) for nextProto, destination := range options.FallbackForALPN { fallbackAddr := destination.Build() if !fallbackAddr.IsValid() { return nil, E.New("invalid fallback address for ALPN ", nextProto, ": ", fallbackAddr) } fallbackAddrNextProto[nextProto] = fallbackAddr } inbound.fallbackAddrTLSNextProto = fallbackAddrNextProto } fallbackHandler = adapter.NewUpstreamContextHandler(inbound.fallbackConnection, nil) } 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 { return it.Password })) if err != nil { return nil, err } if options.Transport != nil { inbound.transport, err = v2ray.NewServerTransport(ctx, logger, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*inboundTransportHandler)(inbound)) if err != nil { return nil, E.Cause(err, "create server transport: ", options.Transport.Type) } } inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex)) if err != nil { return nil, err } inbound.service = service inbound.listener = listener.New(listener.Options{ Context: ctx, Logger: logger, Network: []string{N.NetworkTCP}, Listen: options.ListenOptions, ConnectionHandler: inbound, }) return inbound, nil } func (h *Inbound) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } if h.tlsConfig != nil { err := h.tlsConfig.Start() if err != nil { return E.Cause(err, "create TLS config") } } if h.transport == nil { return h.listener.Start() } if common.Contains(h.transport.Network(), N.NetworkTCP) { tcpListener, err := h.listener.ListenTCP() if err != nil { return err } go func() { sErr := h.transport.Serve(tcpListener) if sErr != nil && !E.IsClosed(sErr) { h.logger.Error("transport serve error: ", sErr) } }() } if common.Contains(h.transport.Network(), N.NetworkUDP) { udpConn, err := h.listener.ListenUDP() if err != nil { return err } go func() { sErr := h.transport.ServePacket(udpConn) if sErr != nil && !E.IsClosed(sErr) { h.logger.Error("transport serve error: ", sErr) } }() } return nil } func (h *Inbound) Close() error { return common.Close( h.listener, h.tlsConfig, h.transport, ) } 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 { N.CloseOnHandshakeFailure(conn, onClose, err) h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source, ": TLS handshake")) return } conn = tlsConn } err := h.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, metadata.Source, onClose) if err != nil { N.CloseOnHandshakeFailure(conn, onClose, err) h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) } } func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = h.Tag() metadata.InboundType = h.Type() userIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid) return } user := h.users[userIndex].Name if user == "" { user = F.ToString(userIndex) } else { metadata.User = user } h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) h.router.RouteConnectionEx(ctx, conn, metadata, onClose) } func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = h.Tag() metadata.InboundType = h.Type() userIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid) return } user := h.users[userIndex].Name if user == "" { user = F.ToString(userIndex) } else { metadata.User = user } h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } func (h *Inbound) fallbackConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { var fallbackAddr M.Socksaddr if len(h.fallbackAddrTLSNextProto) > 0 { if tlsConn, loaded := common.Cast[tls.Conn](conn); loaded { connectionState := tlsConn.ConnectionState() if connectionState.NegotiatedProtocol != "" { if fallbackAddr, loaded = h.fallbackAddrTLSNextProto[connectionState.NegotiatedProtocol]; !loaded { h.logger.DebugContext(ctx, "process connection from ", metadata.Source, ": fallback disabled for ALPN: ", connectionState.NegotiatedProtocol) N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid) return } } } } if !fallbackAddr.IsValid() { if !h.fallbackAddr.IsValid() { h.logger.DebugContext(ctx, "process connection from ", metadata.Source, ": fallback disabled by default") N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid) return } fallbackAddr = h.fallbackAddr } metadata.Inbound = h.Tag() metadata.InboundType = h.Type() metadata.Destination = fallbackAddr h.logger.InfoContext(ctx, "fallback connection to ", fallbackAddr) h.router.RouteConnectionEx(ctx, conn, metadata, onClose) } var _ adapter.V2RayServerTransportHandler = (*inboundTransportHandler)(nil) type inboundTransportHandler Inbound func (h *inboundTransportHandler) 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 //nolint:staticcheck metadata.InboundDetour = h.listener.ListenOptions().Detour //nolint:staticcheck h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) (*Inbound)(h).NewConnection(ctx, conn, metadata, onClose) } ================================================ FILE: protocol/trojan/outbound.go ================================================ package trojan import ( "context" "net" "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/mux" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/transport/trojan" "github.com/sagernet/sing-box/transport/v2ray" "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" ) func RegisterOutbound(registry *outbound.Registry) { outbound.Register[option.TrojanOutboundOptions](registry, C.TypeTrojan, NewOutbound) } type Outbound struct { outbound.Adapter logger logger.ContextLogger dialer N.Dialer serverAddr M.Socksaddr key [56]byte multiplexDialer *mux.Client tlsConfig tls.Config tlsDialer tls.Dialer transport adapter.V2RayClientTransport } func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TrojanOutboundOptions) (adapter.Outbound, error) { outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain()) if err != nil { return nil, err } outbound := &Outbound{ Adapter: outbound.NewAdapterWithDialerOptions(C.TypeTrojan, tag, options.Network.Build(), options.DialerOptions), logger: logger, dialer: outboundDialer, serverAddr: options.ServerOptions.Build(), key: trojan.Key(options.Password), } if options.TLS != nil { outbound.tlsConfig, err = tls.NewClientWithOptions(tls.ClientOptions{ Context: ctx, Logger: logger, ServerAddress: options.Server, Options: common.PtrValueOrDefault(options.TLS), KTLSCompatible: common.PtrValueOrDefault(options.Transport).Type == "" && !common.PtrValueOrDefault(options.Multiplex).Enabled, }) if err != nil { return nil, err } outbound.tlsDialer = tls.NewDialer(outboundDialer, outbound.tlsConfig) } if options.Transport != nil { outbound.transport, err = v2ray.NewClientTransport(ctx, outbound.dialer, outbound.serverAddr, common.PtrValueOrDefault(options.Transport), outbound.tlsConfig) if err != nil { return nil, E.Cause(err, "create client transport: ", options.Transport.Type) } } outbound.multiplexDialer, err = mux.NewClientWithOptions((*trojanDialer)(outbound), logger, common.PtrValueOrDefault(options.Multiplex)) if err != nil { return nil, err } return outbound, nil } func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { if h.multiplexDialer == nil { switch N.NetworkName(network) { case N.NetworkTCP: h.logger.InfoContext(ctx, "outbound connection to ", destination) case N.NetworkUDP: h.logger.InfoContext(ctx, "outbound packet connection to ", destination) } return (*trojanDialer)(h).DialContext(ctx, network, destination) } else { switch N.NetworkName(network) { case N.NetworkTCP: h.logger.InfoContext(ctx, "outbound multiplex connection to ", destination) case N.NetworkUDP: h.logger.InfoContext(ctx, "outbound multiplex packet connection to ", destination) } return h.multiplexDialer.DialContext(ctx, network, destination) } } func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { if h.multiplexDialer == nil { h.logger.InfoContext(ctx, "outbound packet connection to ", destination) return (*trojanDialer)(h).ListenPacket(ctx, destination) } else { h.logger.InfoContext(ctx, "outbound multiplex packet connection to ", destination) return h.multiplexDialer.ListenPacket(ctx, destination) } } func (h *Outbound) InterfaceUpdated() { if h.transport != nil { h.transport.Close() } if h.multiplexDialer != nil { h.multiplexDialer.Reset() } } func (h *Outbound) Close() error { return common.Close(common.PtrOrNil(h.multiplexDialer), h.transport) } type trojanDialer Outbound func (h *trojanDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { ctx, metadata := adapter.ExtendContext(ctx) metadata.Outbound = h.Tag() metadata.Destination = destination var conn net.Conn var err error if h.transport != nil { conn, err = h.transport.DialContext(ctx) } else if h.tlsDialer != nil { conn, err = h.tlsDialer.DialTLSContext(ctx, h.serverAddr) } else { conn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr) } if err != nil { common.Close(conn) return nil, err } switch N.NetworkName(network) { case N.NetworkTCP: return trojan.NewClientConn(conn, h.key, destination), nil case N.NetworkUDP: return bufio.NewBindPacketConn(trojan.NewClientPacketConn(conn, h.key), destination), nil default: return nil, E.Extend(N.ErrUnknownNetwork, network) } } func (h *trojanDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { conn, err := h.DialContext(ctx, N.NetworkUDP, destination) if err != nil { return nil, err } return conn.(net.PacketConn), nil } ================================================ FILE: protocol/tuic/inbound.go ================================================ package tuic import ( "context" "net" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/common/listener" "github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/common/uot" 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" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/gofrs/uuid/v5" ) func RegisterInbound(registry *inbound.Registry) { inbound.Register[option.TUICInboundOptions](registry, C.TypeTUIC, NewInbound) } type Inbound struct { inbound.Adapter router adapter.ConnectionRouterEx logger log.ContextLogger listener *listener.Listener tlsConfig tls.ServerConfig server *tuic.Service[int] userNameList []string } func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICInboundOptions) (adapter.Inbound, error) { options.UDPFragmentDefault = true if options.TLS == nil || !options.TLS.Enabled { return nil, C.ErrTLSRequired } tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS)) if err != nil { return nil, err } inbound := &Inbound{ Adapter: inbound.NewAdapter(C.TypeTUIC, tag), router: uot.NewRouter(router, logger), logger: logger, listener: listener.New(listener.Options{ Context: ctx, Logger: logger, Listen: options.ListenOptions, }), tlsConfig: tlsConfig, } var udpTimeout time.Duration if options.UDPTimeout != 0 { udpTimeout = time.Duration(options.UDPTimeout) } else { udpTimeout = C.UDPTimeout } service, err := tuic.NewService[int](tuic.ServiceOptions{ 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, Heartbeat: time.Duration(options.Heartbeat), UDPTimeout: udpTimeout, Handler: inbound, }) if err != nil { return nil, err } var userList []int var userNameList []string var userUUIDList [][16]byte var userPasswordList []string for index, user := range options.Users { if user.UUID == "" { return nil, E.New("missing uuid for user ", index) } userUUID, err := uuid.FromString(user.UUID) if err != nil { return nil, E.Cause(err, "invalid uuid for user ", index) } userList = append(userList, index) userNameList = append(userNameList, user.Name) userUUIDList = append(userUUIDList, userUUID) userPasswordList = append(userPasswordList, user.Password) } service.UpdateUsers(userList, userUUIDList, userPasswordList) inbound.server = service inbound.userNameList = userNameList return inbound, nil } func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { ctx = log.ContextWithNewID(ctx) var metadata adapter.InboundContext metadata.Inbound = h.Tag() metadata.InboundType = h.Type() //nolint:staticcheck metadata.InboundDetour = h.listener.ListenOptions().Detour //nolint:staticcheck metadata.OriginDestination = h.listener.UDPAddr() metadata.Source = source metadata.Destination = destination h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) userID, _ := auth.UserFromContext[int](ctx) if userName := h.userNameList[userID]; userName != "" { metadata.User = userName h.logger.InfoContext(ctx, "[", userName, "] inbound connection to ", metadata.Destination) } else { h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) } h.router.RouteConnectionEx(ctx, conn, metadata, onClose) } func (h *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { ctx = log.ContextWithNewID(ctx) var metadata adapter.InboundContext metadata.Inbound = h.Tag() metadata.InboundType = h.Type() //nolint:staticcheck metadata.InboundDetour = h.listener.ListenOptions().Detour //nolint:staticcheck metadata.OriginDestination = h.listener.UDPAddr() metadata.Source = source metadata.Destination = destination h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) userID, _ := auth.UserFromContext[int](ctx) if userName := h.userNameList[userID]; userName != "" { metadata.User = userName h.logger.InfoContext(ctx, "[", userName, "] inbound packet connection to ", metadata.Destination) } else { h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) } h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } func (h *Inbound) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } if h.tlsConfig != nil { err := h.tlsConfig.Start() if err != nil { return err } } packetConn, err := h.listener.ListenUDP() if err != nil { return err } return h.server.Start(packetConn) } func (h *Inbound) Close() error { return common.Close( h.listener, h.tlsConfig, common.PtrOrNil(h.server), ) } ================================================ FILE: protocol/tuic/outbound.go ================================================ package tuic import ( "context" "net" "os" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" qtls "github.com/sagernet/sing-quic" "github.com/sagernet/sing-quic/tuic" "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/common/uot" "github.com/gofrs/uuid/v5" ) func RegisterOutbound(registry *outbound.Registry) { outbound.Register[option.TUICOutboundOptions](registry, C.TypeTUIC, NewOutbound) } var _ adapter.InterfaceUpdateListener = (*Outbound)(nil) type Outbound struct { outbound.Adapter logger logger.ContextLogger client *tuic.Client udpStream bool } func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICOutboundOptions) (adapter.Outbound, error) { options.UDPFragmentDefault = true if options.TLS == nil || !options.TLS.Enabled { return nil, C.ErrTLSRequired } tlsConfig, err := tls.NewClient(ctx, logger, options.Server, common.PtrValueOrDefault(options.TLS)) if err != nil { return nil, err } userUUID, err := uuid.FromString(options.UUID) if err != nil { return nil, E.Cause(err, "invalid uuid") } var tuicUDPStream bool if options.UDPOverStream && options.UDPRelayMode != "" { return nil, E.New("udp_over_stream is conflict with udp_relay_mode") } switch options.UDPRelayMode { case "native": case "quic": tuicUDPStream = true } outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain()) if err != nil { return nil, err } client, err := tuic.NewClient(tuic.ClientOptions{ 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, UDPStream: tuicUDPStream, ZeroRTTHandshake: options.ZeroRTTHandshake, Heartbeat: time.Duration(options.Heartbeat), }) if err != nil { return nil, err } return &Outbound{ Adapter: outbound.NewAdapterWithDialerOptions(C.TypeTUIC, tag, options.Network.Build(), options.DialerOptions), logger: logger, client: client, udpStream: options.UDPOverStream, }, nil } func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { switch N.NetworkName(network) { case N.NetworkTCP: h.logger.InfoContext(ctx, "outbound connection to ", destination) return h.client.DialConn(ctx, destination) case N.NetworkUDP: if h.udpStream { h.logger.InfoContext(ctx, "outbound stream packet connection to ", destination) streamConn, err := h.client.DialConn(ctx, uot.RequestDestination(uot.Version)) if err != nil { return nil, err } return uot.NewLazyConn(streamConn, uot.Request{ IsConnect: true, Destination: destination, }), nil } else { conn, err := h.ListenPacket(ctx, destination) if err != nil { return nil, err } return bufio.NewBindPacketConn(conn, destination), nil } default: return nil, E.New("unsupported network: ", network) } } func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { if h.udpStream { h.logger.InfoContext(ctx, "outbound stream packet connection to ", destination) streamConn, err := h.client.DialConn(ctx, uot.RequestDestination(uot.Version)) if err != nil { return nil, err } return uot.NewLazyConn(streamConn, uot.Request{ IsConnect: false, Destination: destination, }), nil } else { h.logger.InfoContext(ctx, "outbound packet connection to ", destination) return h.client.ListenPacket(ctx) } } func (h *Outbound) InterfaceUpdated() { _ = h.client.CloseWithError(E.New("network changed")) } func (h *Outbound) Close() error { return h.client.CloseWithError(os.ErrClosed) } ================================================ FILE: protocol/tun/inbound.go ================================================ package tun import ( "context" "net" "net/netip" "os" "runtime" "strconv" "strings" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/common/taskmonitor" 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" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json/badoption" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/ranges" "github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/service" "go4.org/netipx" ) func RegisterInbound(registry *inbound.Registry) { inbound.Register[option.TunInboundOptions](registry, C.TypeTun, NewInbound) } type Inbound struct { tag string ctx context.Context router adapter.Router networkManager adapter.NetworkManager logger log.ContextLogger tunOptions tun.Options udpTimeout time.Duration dnsHijackAddress []netip.Addr stack string tunIf tun.Tun tunStack tun.Stack platformInterface adapter.PlatformInterface platformOptions option.TunPlatformOptions autoRedirect tun.AutoRedirect routeRuleSet []adapter.RuleSet routeRuleSetCallback []*list.Element[adapter.RuleSetUpdateCallback] routeExcludeRuleSet []adapter.RuleSet routeExcludeRuleSetCallback []*list.Element[adapter.RuleSetUpdateCallback] routeAddressSet []*netipx.IPSet routeExcludeAddressSet []*netipx.IPSet } func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TunInboundOptions) (adapter.Inbound, error) { //nolint:staticcheck if len(options.Inet4Address) > 0 || len(options.Inet6Address) > 0 || len(options.Inet4RouteAddress) > 0 || len(options.Inet6RouteAddress) > 0 || len(options.Inet4RouteExcludeAddress) > 0 || len(options.Inet6RouteExcludeAddress) > 0 { return nil, E.New("legacy tun address fields are deprecated in sing-box 1.10.0 and removed in sing-box 1.12.0") } //nolint:staticcheck if options.GSO { return nil, E.New("GSO option in tun is deprecated in sing-box 1.11.0 and removed in sing-box 1.12.0") } //nolint:staticcheck if options.InboundOptions != (option.InboundOptions{}) { return nil, E.New("legacy inbound fields are deprecated in sing-box 1.11.0 and removed in sing-box 1.13.0, checkout migration: https://sing-box.sagernet.org/migration/#migrate-legacy-inbound-fields-to-rule-actions") } address := options.Address inet4Address := common.Filter(address, func(it netip.Prefix) bool { return it.Addr().Is4() }) inet6Address := common.Filter(address, func(it netip.Prefix) bool { return it.Addr().Is6() }) routeAddress := options.RouteAddress inet4RouteAddress := common.Filter(routeAddress, func(it netip.Prefix) bool { return it.Addr().Is4() }) inet6RouteAddress := common.Filter(routeAddress, func(it netip.Prefix) bool { return it.Addr().Is6() }) routeExcludeAddress := options.RouteExcludeAddress inet4RouteExcludeAddress := common.Filter(routeExcludeAddress, func(it netip.Prefix) bool { return it.Addr().Is4() }) inet6RouteExcludeAddress := common.Filter(routeExcludeAddress, func(it netip.Prefix) bool { return it.Addr().Is6() }) platformInterface := service.FromContext[adapter.PlatformInterface](ctx) tunMTU := options.MTU enableGSO := C.IsLinux && options.Stack == "gvisor" && platformInterface == nil && tunMTU > 0 && tunMTU < 49152 if tunMTU == 0 { if platformInterface != nil && platformInterface.UnderNetworkExtension() { // In Network Extension, when MTU exceeds 4064 (4096-UTUN_IF_HEADROOM_SIZE), the performance of tun will drop significantly, which may be a system bug. tunMTU = 4064 } else if C.IsAndroid { // Some Android devices report ENOBUFS when using MTU 65535 tunMTU = 9000 } else { tunMTU = 65535 } } var udpTimeout time.Duration if options.UDPTimeout != 0 { udpTimeout = time.Duration(options.UDPTimeout) } else { udpTimeout = C.UDPTimeout } var err error includeUID := uidToRange(options.IncludeUID) if len(options.IncludeUIDRange) > 0 { includeUID, err = parseRange(includeUID, options.IncludeUIDRange) if err != nil { return nil, E.Cause(err, "parse include_uid_range") } } excludeUID := uidToRange(options.ExcludeUID) if len(options.ExcludeUIDRange) > 0 { excludeUID, err = parseRange(excludeUID, options.ExcludeUIDRange) if err != nil { return nil, E.Cause(err, "parse exclude_uid_range") } } tableIndex := options.IPRoute2TableIndex if tableIndex == 0 { tableIndex = tun.DefaultIPRoute2TableIndex } ruleIndex := options.IPRoute2RuleIndex if ruleIndex == 0 { ruleIndex = tun.DefaultIPRoute2RuleIndex } autoRedirectFallbackRuleIndex := options.AutoRedirectFallbackRuleIndex if autoRedirectFallbackRuleIndex == 0 { autoRedirectFallbackRuleIndex = tun.DefaultIPRoute2AutoRedirectFallbackRuleIndex } inputMark := uint32(options.AutoRedirectInputMark) if inputMark == 0 { inputMark = tun.DefaultAutoRedirectInputMark } outputMark := uint32(options.AutoRedirectOutputMark) if outputMark == 0 { outputMark = tun.DefaultAutoRedirectOutputMark } resetMark := uint32(options.AutoRedirectResetMark) if resetMark == 0 { resetMark = tun.DefaultAutoRedirectResetMark } nfQueue := options.AutoRedirectNFQueue 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{ tag: tag, ctx: ctx, router: router, networkManager: networkManager, logger: logger, tunOptions: tun.Options{ Name: options.InterfaceName, MTU: tunMTU, GSO: enableGSO, Inet4Address: inet4Address, Inet6Address: inet6Address, DNSMode: options.DNSMode, DNSAddress: options.DNSAddress, AutoRoute: options.AutoRoute, IPRoute2TableIndex: tableIndex, IPRoute2RuleIndex: ruleIndex, IPRoute2AutoRedirectFallbackRuleIndex: autoRedirectFallbackRuleIndex, AutoRedirectInputMark: inputMark, AutoRedirectOutputMark: outputMark, AutoRedirectResetMark: resetMark, AutoRedirectNFQueue: nfQueue, ExcludeMPTCP: options.ExcludeMPTCP, Inet4LoopbackAddress: common.Filter(options.LoopbackAddress, netip.Addr.Is4), Inet6LoopbackAddress: common.Filter(options.LoopbackAddress, netip.Addr.Is6), StrictRoute: options.StrictRoute, IncludeInterface: options.IncludeInterface, ExcludeInterface: options.ExcludeInterface, Inet4RouteAddress: inet4RouteAddress, Inet6RouteAddress: inet6RouteAddress, Inet4RouteExcludeAddress: inet4RouteExcludeAddress, Inet6RouteExcludeAddress: inet6RouteExcludeAddress, IncludeUID: includeUID, ExcludeUID: excludeUID, IncludeAndroidUser: options.IncludeAndroidUser, IncludePackage: options.IncludePackage, ExcludePackage: options.ExcludePackage, IncludeMACAddress: includeMACAddress, ExcludeMACAddress: excludeMACAddress, InterfaceMonitor: networkManager.InterfaceMonitor(), EXP_MultiPendingPackets: multiPendingPackets, }, udpTimeout: udpTimeout, stack: options.Stack, platformInterface: platformInterface, platformOptions: common.PtrValueOrDefault(options.Platform), } for _, routeAddressSet := range options.RouteAddressSet { ruleSet, loaded := router.RuleSet(routeAddressSet) if !loaded { return nil, E.New("parse route_address_set: rule-set not found: ", routeAddressSet) } inbound.routeRuleSet = append(inbound.routeRuleSet, ruleSet) } for _, routeExcludeAddressSet := range options.RouteExcludeAddressSet { ruleSet, loaded := router.RuleSet(routeExcludeAddressSet) if !loaded { return nil, E.New("parse route_exclude_address_set: rule-set not found: ", routeExcludeAddressSet) } inbound.routeExcludeRuleSet = append(inbound.routeExcludeRuleSet, ruleSet) } if options.AutoRedirect { if !options.AutoRoute { return nil, E.New("`auto_route` is required by `auto_redirect`") } disableNFTables, dErr := strconv.ParseBool(os.Getenv("DISABLE_NFTABLES")) inbound.autoRedirect, err = tun.NewAutoRedirect(tun.AutoRedirectOptions{ TunOptions: &inbound.tunOptions, Context: ctx, Handler: (*autoRedirectHandler)(inbound), Logger: logger, NetworkMonitor: networkManager.NetworkMonitor(), InterfaceFinder: networkManager.InterfaceFinder(), TableName: "sing-box", DisableNFTables: dErr == nil && disableNFTables, RouteAddressSet: &inbound.routeAddressSet, RouteExcludeAddressSet: &inbound.routeExcludeAddressSet, }) if err != nil { return nil, E.Cause(err, "initialize auto-redirect") } if !C.IsAndroid { inbound.tunOptions.AutoRedirectMarkMode = true err = networkManager.RegisterAutoRedirectOutputMark(inbound.tunOptions.AutoRedirectOutputMark) if err != nil { return nil, err } } } return inbound, nil } func uidToRange(uidList badoption.Listable[uint32]) []ranges.Range[uint32] { return common.Map(uidList, func(uid uint32) ranges.Range[uint32] { return ranges.NewSingle(uid) }) } func parseRange(uidRanges []ranges.Range[uint32], rangeList []string) ([]ranges.Range[uint32], error) { for _, uidRange := range rangeList { if !strings.Contains(uidRange, ":") { return nil, E.New("missing ':' in range: ", uidRange) } subIndex := strings.Index(uidRange, ":") if subIndex == 0 { return nil, E.New("missing range start: ", uidRange) } else if subIndex == len(uidRange)-1 { return nil, E.New("missing range end: ", uidRange) } var start, end uint64 var err error start, err = strconv.ParseUint(uidRange[:subIndex], 0, 32) if err != nil { return nil, E.Cause(err, "parse range start") } end, err = strconv.ParseUint(uidRange[subIndex+1:], 0, 32) if err != nil { return nil, E.Cause(err, "parse range end") } uidRanges = append(uidRanges, ranges.New(uint32(start), uint32(end))) } return uidRanges, nil } func (t *Inbound) Type() string { return C.TypeTun } func (t *Inbound) Tag() string { return t.tag } 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()) } if t.tunOptions.Name == "" { t.tunOptions.Name = tun.CalculateInterfaceName("") } if t.platformInterface == nil { t.routeAddressSet = common.FlatMap(t.routeRuleSet, adapter.RuleSet.ExtractIPSet) for _, routeRuleSet := range t.routeRuleSet { ipSets := routeRuleSet.ExtractIPSet() if len(ipSets) == 0 { t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeRuleSet.Name()) } routeRuleSet.IncRef() t.routeAddressSet = append(t.routeAddressSet, ipSets...) if t.autoRedirect != nil { t.routeRuleSetCallback = append(t.routeRuleSetCallback, routeRuleSet.RegisterCallback(t.updateRouteAddressSet)) } } t.routeExcludeAddressSet = common.FlatMap(t.routeExcludeRuleSet, adapter.RuleSet.ExtractIPSet) for _, routeExcludeRuleSet := range t.routeExcludeRuleSet { ipSets := routeExcludeRuleSet.ExtractIPSet() if len(ipSets) == 0 { t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeExcludeRuleSet.Name()) } routeExcludeRuleSet.IncRef() t.routeExcludeAddressSet = append(t.routeExcludeAddressSet, ipSets...) if t.autoRedirect != nil { t.routeExcludeRuleSetCallback = append(t.routeExcludeRuleSetCallback, routeExcludeRuleSet.RegisterCallback(t.updateRouteAddressSet)) } } } var ( tunInterface tun.Tun err error ) monitor := taskmonitor.New(t.logger, C.StartTimeout) tunOptions := t.tunOptions if t.autoRedirect == nil && !(runtime.GOOS == "android" && t.platformInterface != nil) { for _, ipSet := range t.routeAddressSet { for _, prefix := range ipSet.Prefixes() { if prefix.Addr().Is4() { tunOptions.Inet4RouteAddress = append(tunOptions.Inet4RouteAddress, prefix) } else { tunOptions.Inet6RouteAddress = append(tunOptions.Inet6RouteAddress, prefix) } } } for _, ipSet := range t.routeExcludeAddressSet { for _, prefix := range ipSet.Prefixes() { if prefix.Addr().Is4() { tunOptions.Inet4RouteExcludeAddress = append(tunOptions.Inet4RouteExcludeAddress, prefix) } else { tunOptions.Inet6RouteExcludeAddress = append(tunOptions.Inet6RouteExcludeAddress, prefix) } } } } monitor.Start("open interface") if t.platformInterface != nil && t.platformInterface.UsePlatformInterface() { tunInterface, err = t.platformInterface.OpenInterface(&tunOptions, t.platformOptions) } else { tunInterface, err = tun.New(tunOptions) } monitor.Finish() t.tunOptions.Name = tunOptions.Name if err != nil { return E.Cause(err, "configure tun interface") } t.logger.Trace("creating stack") t.tunIf = tunInterface var ( forwarderBindInterface bool includeAllNetworks bool ) if t.platformInterface != nil { forwarderBindInterface = true includeAllNetworks = t.platformInterface.NetworkExtensionIncludeAllNetworks() } tunStack, err := tun.NewStack(t.stack, tun.StackOptions{ Context: t.ctx, Tun: tunInterface, TunOptions: t.tunOptions, UDPTimeout: t.udpTimeout, Handler: t, Logger: t.logger, ForwarderBindInterface: forwarderBindInterface, InterfaceFinder: t.networkManager.InterfaceFinder(), IncludeAllNetworks: includeAllNetworks, }) if err != nil { return err } t.tunStack = tunStack t.logger.Info("started at ", t.tunOptions.Name) case adapter.StartStatePostStart: monitor := taskmonitor.New(t.logger, C.StartTimeout) monitor.Start("starting tun stack") err := t.tunStack.Start() monitor.Finish() if err != nil { return E.Cause(err, "starting tun stack") } monitor.Start("starting tun interface") err = t.tunIf.Start() monitor.Finish() if err != nil { return E.Cause(err, "starting TUN interface") } if t.autoRedirect != nil { monitor.Start("initialize auto-redirect") err := t.autoRedirect.Start() monitor.Finish() if err != nil { return E.Cause(err, "auto-redirect") } } t.routeAddressSet = nil t.routeExcludeAddressSet = nil } return nil } func (t *Inbound) updateRouteAddressSet(it adapter.RuleSet) { t.routeAddressSet = common.FlatMap(t.routeRuleSet, adapter.RuleSet.ExtractIPSet) t.routeExcludeAddressSet = common.FlatMap(t.routeExcludeRuleSet, adapter.RuleSet.ExtractIPSet) t.autoRedirect.UpdateRouteAddressSet() t.routeAddressSet = nil t.routeExcludeAddressSet = nil } func (t *Inbound) Close() error { return common.Close( t.tunStack, t.tunIf, t.autoRedirect, ) } func (t *Inbound) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { var ipVersion uint8 if !destination.IsIPv6() { ipVersion = 4 } else { ipVersion = 6 } routeDestination, err := t.router.PreMatch(adapter.InboundContext{ Inbound: t.tag, InboundType: C.TypeTun, IPVersion: ipVersion, Network: network, Source: source, Destination: destination, }, routeContext, timeout, false) if err != nil { switch { case rule.IsBypassed(err): err = nil case rule.IsRejected(err): t.logger.Trace("reject ", network, " connection from ", source.AddrString(), " to ", destination.AddrString()) default: if network == N.NetworkICMP { t.logger.Warn(E.Cause(err, "link ", network, " connection from ", source.AddrString(), " to ", destination.AddrString())) } } } return routeDestination, err } func (t *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { ctx = log.ContextWithNewID(ctx) var metadata adapter.InboundContext metadata.Inbound = t.tag metadata.InboundType = C.TypeTun metadata.Source = source metadata.Destination = 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) } func (t *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { ctx = log.ContextWithNewID(ctx) var metadata adapter.InboundContext metadata.Inbound = t.tag metadata.InboundType = C.TypeTun metadata.Source = source metadata.Destination = 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) } type autoRedirectHandler Inbound func (t *autoRedirectHandler) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { var ipVersion uint8 if !destination.IsIPv6() { ipVersion = 4 } else { ipVersion = 6 } routeDestination, err := t.router.PreMatch(adapter.InboundContext{ Inbound: t.tag, InboundType: C.TypeTun, IPVersion: ipVersion, Network: network, Source: source, Destination: destination, }, routeContext, timeout, true) if err != nil { switch { case rule.IsBypassed(err): t.logger.Trace("bypass ", network, " connection from ", source.AddrString(), " to ", destination.AddrString()) case rule.IsRejected(err): t.logger.Trace("reject ", network, " connection from ", source.AddrString(), " to ", destination.AddrString()) default: if network == N.NetworkICMP { t.logger.Warn(E.Cause(err, "link ", network, " connection from ", source.AddrString(), " to ", destination.AddrString())) } } } return routeDestination, err } func (t *autoRedirectHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { ctx = log.ContextWithNewID(ctx) var metadata adapter.InboundContext metadata.Inbound = t.tag metadata.InboundType = C.TypeTun metadata.Source = source metadata.Destination = 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) } func (t *autoRedirectHandler) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { panic("unexcepted") } ================================================ FILE: protocol/vless/inbound.go ================================================ package vless import ( "context" "net" "os" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/common/listener" "github.com/sagernet/sing-box/common/mux" "github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/common/uot" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/transport/v2ray" "github.com/sagernet/sing-vmess/packetaddr" "github.com/sagernet/sing-vmess/vless" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/auth" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) func RegisterInbound(registry *inbound.Registry) { inbound.Register[option.VLESSInboundOptions](registry, C.TypeVLESS, NewInbound) } var _ adapter.TCPInjectableInbound = (*Inbound)(nil) type Inbound struct { inbound.Adapter ctx context.Context router adapter.ConnectionRouterEx logger logger.ContextLogger listener *listener.Listener users []option.VLESSUser service *vless.Service[int] tlsConfig tls.ServerConfig transport adapter.V2RayServerTransport } func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VLESSInboundOptions) (adapter.Inbound, error) { inbound := &Inbound{ Adapter: inbound.NewAdapter(C.TypeVLESS, tag), ctx: ctx, router: uot.NewRouter(router, logger), logger: logger, users: options.Users, } var err error inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex)) if err != nil { return nil, err } 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 { return it.UUID }), common.Map(inbound.users, func(it option.VLESSUser) string { return it.Flow })) inbound.service = service if options.TLS != nil { inbound.tlsConfig, err = tls.NewServerWithOptions(tls.ServerOptions{ Context: ctx, Logger: logger, Options: common.PtrValueOrDefault(options.TLS), KTLSCompatible: common.PtrValueOrDefault(options.Transport).Type == "" && !common.PtrValueOrDefault(options.Multiplex).Enabled && common.All(options.Users, func(it option.VLESSUser) bool { return it.Flow == "" }), }) if err != nil { return nil, err } } if options.Transport != nil { inbound.transport, err = v2ray.NewServerTransport(ctx, logger, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*inboundTransportHandler)(inbound)) if err != nil { return nil, E.Cause(err, "create server transport: ", options.Transport.Type) } } inbound.listener = listener.New(listener.Options{ Context: ctx, Logger: logger, Network: []string{N.NetworkTCP}, Listen: options.ListenOptions, ConnectionHandler: inbound, }) return inbound, nil } func (h *Inbound) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } if h.tlsConfig != nil { err := h.tlsConfig.Start() if err != nil { return err } } if h.transport == nil { return h.listener.Start() } if common.Contains(h.transport.Network(), N.NetworkTCP) { tcpListener, err := h.listener.ListenTCP() if err != nil { return err } go func() { sErr := h.transport.Serve(tcpListener) if sErr != nil && !E.IsClosed(sErr) { h.logger.Error("transport serve error: ", sErr) } }() } if common.Contains(h.transport.Network(), N.NetworkUDP) { udpConn, err := h.listener.ListenUDP() if err != nil { return err } go func() { sErr := h.transport.ServePacket(udpConn) if sErr != nil && !E.IsClosed(sErr) { h.logger.Error("transport serve error: ", sErr) } }() } return nil } func (h *Inbound) Close() error { return common.Close( h.service, h.listener, h.tlsConfig, h.transport, ) } 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 { N.CloseOnHandshakeFailure(conn, onClose, err) h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source, ": TLS handshake")) return } conn = tlsConn } err := h.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, metadata.Source, onClose) if err != nil { N.CloseOnHandshakeFailure(conn, onClose, err) h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) } } func (h *Inbound) newConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = h.Tag() metadata.InboundType = h.Type() userIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid) return } user := h.users[userIndex].Name if user == "" { user = F.ToString(userIndex) } else { metadata.User = user } h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) h.router.RouteConnectionEx(ctx, conn, metadata, onClose) } func (h *Inbound) newPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = h.Tag() metadata.InboundType = h.Type() userIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid) return } user := h.users[userIndex].Name if user == "" { user = F.ToString(userIndex) } else { metadata.User = user } if metadata.Destination.Fqdn == packetaddr.SeqPacketMagicAddress { metadata.Destination = M.Socksaddr{} conn = packetaddr.NewConn(bufio.NewNetPacketConn(conn), metadata.Destination) h.logger.InfoContext(ctx, "[", user, "] inbound packet addr connection") } else { h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) } h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } var _ adapter.V2RayServerTransportHandler = (*inboundTransportHandler)(nil) type inboundTransportHandler Inbound func (h *inboundTransportHandler) 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 //nolint:staticcheck metadata.InboundDetour = h.listener.ListenOptions().Detour //nolint:staticcheck h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) (*Inbound)(h).NewConnection(ctx, conn, metadata, onClose) } ================================================ FILE: protocol/vless/outbound.go ================================================ package vless import ( "context" "net" "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/mux" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/transport/v2ray" "github.com/sagernet/sing-vmess/packetaddr" "github.com/sagernet/sing-vmess/vless" "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" ) func RegisterOutbound(registry *outbound.Registry) { outbound.Register[option.VLESSOutboundOptions](registry, C.TypeVLESS, NewOutbound) } type Outbound struct { outbound.Adapter logger logger.ContextLogger dialer N.Dialer client *vless.Client serverAddr M.Socksaddr multiplexDialer *mux.Client tlsConfig tls.Config tlsDialer tls.Dialer transport adapter.V2RayClientTransport packetAddr bool xudp bool } func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VLESSOutboundOptions) (adapter.Outbound, error) { outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain()) if err != nil { return nil, err } outbound := &Outbound{ Adapter: outbound.NewAdapterWithDialerOptions(C.TypeVLESS, tag, options.Network.Build(), options.DialerOptions), logger: logger, dialer: outboundDialer, serverAddr: options.ServerOptions.Build(), } if options.TLS != nil { outbound.tlsConfig, err = tls.NewClientWithOptions(tls.ClientOptions{ Context: ctx, Logger: logger, ServerAddress: options.Server, Options: common.PtrValueOrDefault(options.TLS), KTLSCompatible: common.PtrValueOrDefault(options.Transport).Type == "" && !common.PtrValueOrDefault(options.Multiplex).Enabled && options.Flow == "", }) if err != nil { return nil, err } outbound.tlsDialer = tls.NewDialer(outboundDialer, outbound.tlsConfig) } if options.Transport != nil { outbound.transport, err = v2ray.NewClientTransport(ctx, outbound.dialer, outbound.serverAddr, common.PtrValueOrDefault(options.Transport), outbound.tlsConfig) if err != nil { return nil, E.Cause(err, "create client transport: ", options.Transport.Type) } } if options.PacketEncoding == nil { outbound.xudp = true } else { switch *options.PacketEncoding { case "": case "packetaddr": outbound.packetAddr = true case "xudp": outbound.xudp = true default: return nil, E.New("unknown packet encoding: ", options.PacketEncoding) } } outbound.client, err = vless.NewClient(options.UUID, options.Flow, logger) if err != nil { return nil, err } outbound.multiplexDialer, err = mux.NewClientWithOptions((*vlessDialer)(outbound), logger, common.PtrValueOrDefault(options.Multiplex)) if err != nil { return nil, err } return outbound, nil } func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { if h.multiplexDialer == nil { switch N.NetworkName(network) { case N.NetworkTCP: h.logger.InfoContext(ctx, "outbound connection to ", destination) case N.NetworkUDP: h.logger.InfoContext(ctx, "outbound packet connection to ", destination) } return (*vlessDialer)(h).DialContext(ctx, network, destination) } else { switch N.NetworkName(network) { case N.NetworkTCP: h.logger.InfoContext(ctx, "outbound multiplex connection to ", destination) case N.NetworkUDP: h.logger.InfoContext(ctx, "outbound multiplex packet connection to ", destination) } return h.multiplexDialer.DialContext(ctx, network, destination) } } func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { if h.multiplexDialer == nil { h.logger.InfoContext(ctx, "outbound packet connection to ", destination) return (*vlessDialer)(h).ListenPacket(ctx, destination) } else { h.logger.InfoContext(ctx, "outbound multiplex packet connection to ", destination) return h.multiplexDialer.ListenPacket(ctx, destination) } } func (h *Outbound) InterfaceUpdated() { if h.transport != nil { h.transport.Close() } if h.multiplexDialer != nil { h.multiplexDialer.Reset() } } func (h *Outbound) Close() error { return common.Close(common.PtrOrNil(h.multiplexDialer), h.transport) } type vlessDialer Outbound func (h *vlessDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { ctx, metadata := adapter.ExtendContext(ctx) metadata.Outbound = h.Tag() metadata.Destination = destination var conn net.Conn var err error if h.transport != nil { conn, err = h.transport.DialContext(ctx) } else if h.tlsDialer != nil { conn, err = h.tlsDialer.DialTLSContext(ctx, h.serverAddr) } else { conn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr) } if err != nil { return nil, err } switch N.NetworkName(network) { case N.NetworkTCP: h.logger.InfoContext(ctx, "outbound connection to ", destination) return h.client.DialEarlyConn(conn, destination) case N.NetworkUDP: h.logger.InfoContext(ctx, "outbound packet connection to ", destination) if h.xudp { return h.client.DialEarlyXUDPPacketConn(conn, destination) } else if h.packetAddr { if destination.IsDomain() { return nil, E.New("packetaddr: domain destination is not supported") } packetConn, err := h.client.DialEarlyPacketConn(conn, M.Socksaddr{Fqdn: packetaddr.SeqPacketMagicAddress}) if err != nil { return nil, err } return bufio.NewBindPacketConn(packetaddr.NewConn(packetConn, destination), destination), nil } else { return h.client.DialEarlyPacketConn(conn, destination) } default: return nil, E.Extend(N.ErrUnknownNetwork, network) } } func (h *vlessDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { h.logger.InfoContext(ctx, "outbound packet connection to ", destination) ctx, metadata := adapter.ExtendContext(ctx) metadata.Outbound = h.Tag() metadata.Destination = destination var conn net.Conn var err error if h.transport != nil { conn, err = h.transport.DialContext(ctx) } else if h.tlsDialer != nil { conn, err = h.tlsDialer.DialTLSContext(ctx, h.serverAddr) } else { conn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr) } if err != nil { common.Close(conn) return nil, err } if h.xudp { return h.client.DialEarlyXUDPPacketConn(conn, destination) } else if h.packetAddr { if destination.IsDomain() { return nil, E.New("packetaddr: domain destination is not supported") } conn, err := h.client.DialEarlyPacketConn(conn, M.Socksaddr{Fqdn: packetaddr.SeqPacketMagicAddress}) if err != nil { return nil, err } return packetaddr.NewConn(conn, destination), nil } else { return h.client.DialEarlyPacketConn(conn, destination) } } ================================================ FILE: protocol/vmess/inbound.go ================================================ package vmess import ( "context" "net" "os" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/common/listener" "github.com/sagernet/sing-box/common/mux" "github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/common/uot" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/transport/v2ray" "github.com/sagernet/sing-vmess" "github.com/sagernet/sing-vmess/packetaddr" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/auth" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "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" ) func RegisterInbound(registry *inbound.Registry) { inbound.Register[option.VMessInboundOptions](registry, C.TypeVMess, NewInbound) } var _ adapter.TCPInjectableInbound = (*Inbound)(nil) type Inbound struct { inbound.Adapter ctx context.Context router adapter.ConnectionRouterEx logger logger.ContextLogger listener *listener.Listener service *vmess.Service[int] users []option.VMessUser tlsConfig tls.ServerConfig transport adapter.V2RayServerTransport } func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VMessInboundOptions) (adapter.Inbound, error) { inbound := &Inbound{ Adapter: inbound.NewAdapter(C.TypeVMess, tag), ctx: ctx, router: uot.NewRouter(router, logger), logger: logger, users: options.Users, } var err error inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex)) if err != nil { return nil, err } var serviceOptions []vmess.ServiceOption if timeFunc := ntp.TimeFuncFromContext(ctx); timeFunc != nil { serviceOptions = append(serviceOptions, vmess.ServiceWithTimeFunc(timeFunc)) } if options.Transport != nil && options.Transport.Type != "" { serviceOptions = append(serviceOptions, vmess.ServiceWithDisableHeaderProtection()) } 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 }), common.Map(options.Users, func(it option.VMessUser) string { return it.UUID }), common.Map(options.Users, func(it option.VMessUser) int { return it.AlterId })) if err != nil { return nil, err } if options.TLS != nil { inbound.tlsConfig, err = tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS)) if err != nil { return nil, err } } if options.Transport != nil { inbound.transport, err = v2ray.NewServerTransport(ctx, logger, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*inboundTransportHandler)(inbound)) if err != nil { return nil, E.Cause(err, "create server transport: ", options.Transport.Type) } } inbound.listener = listener.New(listener.Options{ Context: ctx, Logger: logger, Network: []string{N.NetworkTCP}, Listen: options.ListenOptions, ConnectionHandler: inbound, }) return inbound, nil } func (h *Inbound) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } err := h.service.Start() if err != nil { return err } if h.tlsConfig != nil { err = h.tlsConfig.Start() if err != nil { return err } } if h.transport == nil { return h.listener.Start() } if common.Contains(h.transport.Network(), N.NetworkTCP) { tcpListener, err := h.listener.ListenTCP() if err != nil { return err } go func() { sErr := h.transport.Serve(tcpListener) if sErr != nil && !E.IsClosed(sErr) { h.logger.Error("transport serve error: ", sErr) } }() } if common.Contains(h.transport.Network(), N.NetworkUDP) { udpConn, err := h.listener.ListenUDP() if err != nil { return err } go func() { sErr := h.transport.ServePacket(udpConn) if sErr != nil && !E.IsClosed(sErr) { h.logger.Error("transport serve error: ", sErr) } }() } return nil } func (h *Inbound) Close() error { return common.Close( h.service, h.listener, h.tlsConfig, h.transport, ) } 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 { N.CloseOnHandshakeFailure(conn, onClose, err) h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source, ": TLS handshake")) return } conn = tlsConn } err := h.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, metadata.Source, onClose) if err != nil { N.CloseOnHandshakeFailure(conn, onClose, err) h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) } } func (h *Inbound) newConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = h.Tag() metadata.InboundType = h.Type() userIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid) return } user := h.users[userIndex].Name if user == "" { user = F.ToString(userIndex) } else { metadata.User = user } h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) h.router.RouteConnectionEx(ctx, conn, metadata, onClose) } func (h *Inbound) newPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = h.Tag() metadata.InboundType = h.Type() userIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid) return } user := h.users[userIndex].Name if user == "" { user = F.ToString(userIndex) } else { metadata.User = user } if metadata.Destination.Fqdn == packetaddr.SeqPacketMagicAddress { metadata.Destination = M.Socksaddr{} conn = packetaddr.NewConn(bufio.NewNetPacketConn(conn), metadata.Destination) h.logger.InfoContext(ctx, "[", user, "] inbound packet addr connection") } else { h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) } h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } var _ adapter.V2RayServerTransportHandler = (*inboundTransportHandler)(nil) type inboundTransportHandler Inbound func (h *inboundTransportHandler) 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 //nolint:staticcheck metadata.InboundDetour = h.listener.ListenOptions().Detour //nolint:staticcheck h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) (*Inbound)(h).NewConnection(ctx, conn, metadata, onClose) } ================================================ FILE: protocol/vmess/outbound.go ================================================ package vmess import ( "context" "net" "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/mux" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/transport/v2ray" "github.com/sagernet/sing-vmess" "github.com/sagernet/sing-vmess/packetaddr" "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/common/ntp" ) func RegisterOutbound(registry *outbound.Registry) { outbound.Register[option.VMessOutboundOptions](registry, C.TypeVMess, NewOutbound) } type Outbound struct { outbound.Adapter logger logger.ContextLogger dialer N.Dialer client *vmess.Client serverAddr M.Socksaddr multiplexDialer *mux.Client tlsConfig tls.Config tlsDialer tls.Dialer transport adapter.V2RayClientTransport packetAddr bool xudp bool } func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VMessOutboundOptions) (adapter.Outbound, error) { outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain()) if err != nil { return nil, err } outbound := &Outbound{ Adapter: outbound.NewAdapterWithDialerOptions(C.TypeVMess, tag, options.Network.Build(), options.DialerOptions), logger: logger, dialer: outboundDialer, serverAddr: options.ServerOptions.Build(), } if options.TLS != nil { outbound.tlsConfig, err = tls.NewClient(ctx, logger, options.Server, common.PtrValueOrDefault(options.TLS)) if err != nil { return nil, err } if outbound.tlsConfig != nil { outbound.tlsDialer = tls.NewDialer(outboundDialer, outbound.tlsConfig) } } if options.Transport != nil { outbound.transport, err = v2ray.NewClientTransport(ctx, outbound.dialer, outbound.serverAddr, common.PtrValueOrDefault(options.Transport), outbound.tlsConfig) if err != nil { return nil, E.Cause(err, "create client transport: ", options.Transport.Type) } } outbound.multiplexDialer, err = mux.NewClientWithOptions((*vmessDialer)(outbound), logger, common.PtrValueOrDefault(options.Multiplex)) if err != nil { return nil, err } switch options.PacketEncoding { case "": case "packetaddr": outbound.packetAddr = true case "xudp": outbound.xudp = true default: return nil, E.New("unknown packet encoding: ", options.PacketEncoding) } var clientOptions []vmess.ClientOption if timeFunc := ntp.TimeFuncFromContext(ctx); timeFunc != nil { clientOptions = append(clientOptions, vmess.ClientWithTimeFunc(timeFunc)) } if options.GlobalPadding { clientOptions = append(clientOptions, vmess.ClientWithGlobalPadding()) } if options.AuthenticatedLength { clientOptions = append(clientOptions, vmess.ClientWithAuthenticatedLength()) } security := options.Security if security == "" { security = "auto" } if security == "auto" && outbound.tlsConfig != nil { security = "zero" } client, err := vmess.NewClient(options.UUID, security, options.AlterId, clientOptions...) if err != nil { return nil, err } outbound.client = client return outbound, nil } func (h *Outbound) InterfaceUpdated() { if h.transport != nil { h.transport.Close() } if h.multiplexDialer != nil { h.multiplexDialer.Reset() } } func (h *Outbound) Close() error { return common.Close(common.PtrOrNil(h.multiplexDialer), h.transport) } func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { if h.multiplexDialer == nil { switch N.NetworkName(network) { case N.NetworkTCP: h.logger.InfoContext(ctx, "outbound connection to ", destination) case N.NetworkUDP: h.logger.InfoContext(ctx, "outbound packet connection to ", destination) } return (*vmessDialer)(h).DialContext(ctx, network, destination) } else { switch N.NetworkName(network) { case N.NetworkTCP: h.logger.InfoContext(ctx, "outbound multiplex connection to ", destination) case N.NetworkUDP: h.logger.InfoContext(ctx, "outbound multiplex packet connection to ", destination) } return h.multiplexDialer.DialContext(ctx, network, destination) } } func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { if h.multiplexDialer == nil { h.logger.InfoContext(ctx, "outbound packet connection to ", destination) return (*vmessDialer)(h).ListenPacket(ctx, destination) } else { h.logger.InfoContext(ctx, "outbound multiplex packet connection to ", destination) return h.multiplexDialer.ListenPacket(ctx, destination) } } type vmessDialer Outbound func (h *vmessDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { ctx, metadata := adapter.ExtendContext(ctx) metadata.Outbound = h.Tag() metadata.Destination = destination var conn net.Conn var err error if h.transport != nil { conn, err = h.transport.DialContext(ctx) } else if h.tlsDialer != nil { conn, err = h.tlsDialer.DialTLSContext(ctx, h.serverAddr) } else { conn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr) } if err != nil { common.Close(conn) return nil, err } switch N.NetworkName(network) { case N.NetworkTCP: return h.client.DialEarlyConn(conn, destination), nil case N.NetworkUDP: return h.client.DialEarlyPacketConn(conn, destination), nil default: return nil, E.Extend(N.ErrUnknownNetwork, network) } } func (h *vmessDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { ctx, metadata := adapter.ExtendContext(ctx) metadata.Outbound = h.Tag() metadata.Destination = destination var conn net.Conn var err error if h.transport != nil { conn, err = h.transport.DialContext(ctx) } else if h.tlsDialer != nil { conn, err = h.tlsDialer.DialTLSContext(ctx, h.serverAddr) } else { conn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr) } if err != nil { return nil, err } if h.packetAddr { if destination.IsDomain() { return nil, E.New("packetaddr: domain destination is not supported") } return packetaddr.NewConn(h.client.DialEarlyPacketConn(conn, M.Socksaddr{Fqdn: packetaddr.SeqPacketMagicAddress}), destination), nil } else if h.xudp { return h.client.DialEarlyXUDPPacketConn(conn, destination), nil } else { return h.client.DialEarlyPacketConn(conn, destination), nil } } ================================================ FILE: protocol/wireguard/endpoint.go ================================================ package wireguard import ( "context" "net" "net/netip" "sync/atomic" "time" "github.com/sagernet/sing-box/adapter" "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/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/route/rule" "github.com/sagernet/sing-box/transport/wireguard" "github.com/sagernet/sing-tun" "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" ) var ( _ adapter.OutboundWithPreferredRoutes = (*Endpoint)(nil) _ dialer.PacketDialerWithDestination = (*Endpoint)(nil) ) func RegisterEndpoint(registry *endpoint.Registry) { endpoint.Register[option.WireGuardEndpointOptions](registry, C.TypeWireGuard, NewEndpoint) } type Endpoint struct { endpoint.Adapter ctx context.Context router adapter.Router dnsRouter adapter.DNSRouter 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) { ep := &Endpoint{ Adapter: endpoint.NewAdapterWithDialerOptions(C.TypeWireGuard, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, options.DialerOptions), ctx: ctx, router: router, dnsRouter: service.FromContext[adapter.DNSRouter](ctx), logger: logger, localAddresses: options.Address, } if options.Detour != "" && options.ListenPort != 0 { return nil, E.New("`listen_port` is conflict with `detour`") } outboundDialer, err := dialer.NewWithOptions(dialer.Options{ Context: ctx, Options: options.DialerOptions, RemoteIsDomain: common.Any(options.Peers, func(it option.WireGuardPeer) bool { return !M.ParseAddr(it.Address).IsValid() }), ResolverOnDetour: true, }) if err != nil { return nil, err } var udpTimeout time.Duration if options.UDPTimeout != 0 { udpTimeout = time.Duration(options.UDPTimeout) } else { udpTimeout = C.UDPTimeout } wgEndpoint, err := wireguard.NewEndpoint(wireguard.EndpointOptions{ Context: ctx, Logger: logger, System: options.System, Handler: ep, UDPTimeout: udpTimeout, Dialer: outboundDialer, CreateDialer: func(interfaceName string) N.Dialer { return common.Must1(dialer.NewDefault(ctx, option.DialerOptions{ BindInterface: interfaceName, })) }, Name: options.Name, MTU: options.MTU, Address: options.Address, PrivateKey: options.PrivateKey, ListenPort: options.ListenPort, ResolvePeer: func(domain string) (netip.Addr, error) { endpointAddresses, lookupErr := ep.dnsRouter.Lookup(ctx, domain, outboundDialer.(dialer.ResolveDialer).QueryOptions()) if lookupErr != nil { return netip.Addr{}, lookupErr } return endpointAddresses[0], nil }, Peers: common.Map(options.Peers, func(it option.WireGuardPeer) wireguard.PeerOptions { return wireguard.PeerOptions{ Endpoint: M.ParseSocksaddrHostPort(it.Address, it.Port), PublicKey: it.PublicKey, PreSharedKey: it.PreSharedKey, AllowedIPs: it.AllowedIPs, PersistentKeepaliveInterval: it.PersistentKeepaliveInterval, Reserved: it.Reserved, } }), Workers: options.Workers, }) if err != nil { return nil, err } ep.endpoint = wgEndpoint return ep, nil } func (w *Endpoint) Start(stage adapter.StartStage) error { switch stage { case adapter.StartStateStart: return w.endpoint.Start(false) case adapter.StartStatePostStart: 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 } else { ipVersion = 6 } routeDestination, err := w.router.PreMatch(adapter.InboundContext{ Inbound: w.Tag(), InboundType: w.Type(), IPVersion: ipVersion, Network: network, Source: source, Destination: destination, }, routeContext, timeout, false) if err != nil { switch { case rule.IsBypassed(err): err = nil case rule.IsRejected(err): w.logger.Trace("reject ", network, " connection from ", source.AddrString(), " to ", destination.AddrString()) default: if network == N.NetworkICMP { w.logger.Warn(E.Cause(err, "link ", network, " connection from ", source.AddrString(), " to ", destination.AddrString())) } } } return routeDestination, err } func (w *Endpoint) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { var metadata adapter.InboundContext metadata.Inbound = w.Tag() metadata.InboundType = w.Type() metadata.Source = source for _, localPrefix := range w.localAddresses { if localPrefix.Contains(destination.Addr) { metadata.OriginDestination = destination if destination.Addr.Is4() { destination.Addr = netip.AddrFrom4([4]uint8{127, 0, 0, 1}) } else { destination.Addr = netip.IPv6Loopback() } break } } metadata.Destination = destination w.logger.InfoContext(ctx, "inbound connection from ", source) w.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) w.router.RouteConnectionEx(ctx, conn, metadata, onClose) } func (w *Endpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { var metadata adapter.InboundContext metadata.Inbound = w.Tag() metadata.InboundType = w.Type() metadata.Source = source metadata.Destination = destination for _, localPrefix := range w.localAddresses { if localPrefix.Contains(destination.Addr) { metadata.OriginDestination = destination if destination.Addr.Is4() { metadata.Destination.Addr = netip.AddrFrom4([4]uint8{127, 0, 0, 1}) } else { metadata.Destination.Addr = netip.IPv6Loopback() } conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, metadata.Destination) } } w.logger.InfoContext(ctx, "inbound packet connection from ", source) w.logger.InfoContext(ctx, "inbound packet connection to ", destination) w.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } func (w *Endpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { switch network { case N.NetworkTCP: w.logger.InfoContext(ctx, "outbound connection to ", 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 { return nil, err } return N.DialSerial(ctx, w.endpoint, network, destination, destinationAddresses) } else if !destination.Addr.IsValid() { return nil, E.New("invalid destination: ", destination) } return w.endpoint.DialContext(ctx, network, 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 { return nil, netip.Addr{}, err } return N.ListenSerial(ctx, w.endpoint, destination, destinationAddresses) } packetConn, err := w.endpoint.ListenPacket(ctx, destination) if err != nil { return nil, netip.Addr{}, err } if destination.IsIP() { return packetConn, destination.Addr, nil } return packetConn, netip.Addr{}, nil } func (w *Endpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { packetConn, destinationAddress, err := w.ListenPacketWithDestination(ctx, destination) if err != nil { return nil, err } if destinationAddress.IsValid() && destination != M.SocksaddrFrom(destinationAddress, destination.Port) { return bufio.NewNATPacketConn(bufio.NewPacketConn(packetConn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil } return packetConn, nil } func (w *Endpoint) PreferredDomain(domain string) bool { return false } 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) } ================================================ FILE: release/DEFAULT_BUILD_TAGS ================================================ 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 ================================================ FILE: release/DEFAULT_BUILD_TAGS_OTHERS ================================================ 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 ================================================ FILE: release/DEFAULT_BUILD_TAGS_WINDOWS ================================================ 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 ================================================ FILE: release/LDFLAGS ================================================ -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0 ================================================ FILE: release/completions/sing-box.bash ================================================ # bash completion for sing-box -*- shell-script -*- __sing-box_debug() { if [[ -n ${BASH_COMP_DEBUG_FILE:-} ]]; then echo "$*" >> "${BASH_COMP_DEBUG_FILE}" fi } # Homebrew on Macs have version 1.3 of bash-completion which doesn't include # _init_completion. This is a very minimal version of that function. __sing-box_init_completion() { COMPREPLY=() _get_comp_words_by_ref "$@" cur prev words cword } __sing-box_index_of_word() { local w word=$1 shift index=0 for w in "$@"; do [[ $w = "$word" ]] && return index=$((index+1)) done index=-1 } __sing-box_contains_word() { local w word=$1; shift for w in "$@"; do [[ $w = "$word" ]] && return done return 1 } __sing-box_handle_go_custom_completion() { __sing-box_debug "${FUNCNAME[0]}: cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}" local shellCompDirectiveError=1 local shellCompDirectiveNoSpace=2 local shellCompDirectiveNoFileComp=4 local shellCompDirectiveFilterFileExt=8 local shellCompDirectiveFilterDirs=16 local out requestComp lastParam lastChar comp directive args # Prepare the command to request completions for the program. # Calling ${words[0]} instead of directly sing-box allows handling aliases args=("${words[@]:1}") # Disable ActiveHelp which is not supported for bash completion v1 requestComp="SING_BOX_ACTIVE_HELP=0 ${words[0]} __completeNoDesc ${args[*]}" lastParam=${words[$((${#words[@]}-1))]} lastChar=${lastParam:$((${#lastParam}-1)):1} __sing-box_debug "${FUNCNAME[0]}: lastParam ${lastParam}, lastChar ${lastChar}" if [ -z "${cur}" ] && [ "${lastChar}" != "=" ]; then # If the last parameter is complete (there is a space following it) # We add an extra empty parameter so we can indicate this to the go method. __sing-box_debug "${FUNCNAME[0]}: Adding extra empty parameter" requestComp="${requestComp} \"\"" fi __sing-box_debug "${FUNCNAME[0]}: calling ${requestComp}" # Use eval to handle any environment variables and such out=$(eval "${requestComp}" 2>/dev/null) # Extract the directive integer at the very end of the output following a colon (:) directive=${out##*:} # Remove the directive out=${out%:*} if [ "${directive}" = "${out}" ]; then # There is not directive specified directive=0 fi __sing-box_debug "${FUNCNAME[0]}: the completion directive is: ${directive}" __sing-box_debug "${FUNCNAME[0]}: the completions are: ${out}" if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then # Error code. No completion. __sing-box_debug "${FUNCNAME[0]}: received error from custom completion go code" return else if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then if [[ $(type -t compopt) = "builtin" ]]; then __sing-box_debug "${FUNCNAME[0]}: activating no space" compopt -o nospace fi fi if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then if [[ $(type -t compopt) = "builtin" ]]; then __sing-box_debug "${FUNCNAME[0]}: activating no file completion" compopt +o default fi fi fi if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then # File extension filtering local fullFilter filter filteringCmd # Do not use quotes around the $out variable or else newline # characters will be kept. for filter in ${out}; do fullFilter+="$filter|" done filteringCmd="_filedir $fullFilter" __sing-box_debug "File filtering command: $filteringCmd" $filteringCmd elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then # File completion for directories only local subdir # Use printf to strip any trailing newline subdir=$(printf "%s" "${out}") if [ -n "$subdir" ]; then __sing-box_debug "Listing directories in $subdir" __sing-box_handle_subdirs_in_dir_flag "$subdir" else __sing-box_debug "Listing directories in ." _filedir -d fi else while IFS='' read -r comp; do COMPREPLY+=("$comp") done < <(compgen -W "${out}" -- "$cur") fi } __sing-box_handle_reply() { __sing-box_debug "${FUNCNAME[0]}" local comp case $cur in -*) if [[ $(type -t compopt) = "builtin" ]]; then compopt -o nospace fi local allflags if [ ${#must_have_one_flag[@]} -ne 0 ]; then allflags=("${must_have_one_flag[@]}") else allflags=("${flags[*]} ${two_word_flags[*]}") fi while IFS='' read -r comp; do COMPREPLY+=("$comp") done < <(compgen -W "${allflags[*]}" -- "$cur") if [[ $(type -t compopt) = "builtin" ]]; then [[ "${COMPREPLY[0]}" == *= ]] || compopt +o nospace fi # complete after --flag=abc if [[ $cur == *=* ]]; then if [[ $(type -t compopt) = "builtin" ]]; then compopt +o nospace fi local index flag flag="${cur%=*}" __sing-box_index_of_word "${flag}" "${flags_with_completion[@]}" COMPREPLY=() if [[ ${index} -ge 0 ]]; then PREFIX="" cur="${cur#*=}" ${flags_completion[${index}]} if [ -n "${ZSH_VERSION:-}" ]; then # zsh completion needs --flag= prefix eval "COMPREPLY=( \"\${COMPREPLY[@]/#/${flag}=}\" )" fi fi fi if [[ -z "${flag_parsing_disabled}" ]]; then # If flag parsing is enabled, we have completed the flags and can return. # If flag parsing is disabled, we may not know all (or any) of the flags, so we fallthrough # to possibly call handle_go_custom_completion. return 0; fi ;; esac # check if we are handling a flag with special work handling local index __sing-box_index_of_word "${prev}" "${flags_with_completion[@]}" if [[ ${index} -ge 0 ]]; then ${flags_completion[${index}]} return fi # we are parsing a flag and don't have a special handler, no completion if [[ ${cur} != "${words[cword]}" ]]; then return fi local completions completions=("${commands[@]}") if [[ ${#must_have_one_noun[@]} -ne 0 ]]; then completions+=("${must_have_one_noun[@]}") elif [[ -n "${has_completion_function}" ]]; then # if a go completion function is provided, defer to that function __sing-box_handle_go_custom_completion fi if [[ ${#must_have_one_flag[@]} -ne 0 ]]; then completions+=("${must_have_one_flag[@]}") fi while IFS='' read -r comp; do COMPREPLY+=("$comp") done < <(compgen -W "${completions[*]}" -- "$cur") if [[ ${#COMPREPLY[@]} -eq 0 && ${#noun_aliases[@]} -gt 0 && ${#must_have_one_noun[@]} -ne 0 ]]; then while IFS='' read -r comp; do COMPREPLY+=("$comp") done < <(compgen -W "${noun_aliases[*]}" -- "$cur") fi if [[ ${#COMPREPLY[@]} -eq 0 ]]; then if declare -F __sing-box_custom_func >/dev/null; then # try command name qualified custom func __sing-box_custom_func else # otherwise fall back to unqualified for compatibility declare -F __custom_func >/dev/null && __custom_func fi fi # available in bash-completion >= 2, not always present on macOS if declare -F __ltrim_colon_completions >/dev/null; then __ltrim_colon_completions "$cur" fi # If there is only 1 completion and it is a flag with an = it will be completed # but we don't want a space after the = if [[ "${#COMPREPLY[@]}" -eq "1" ]] && [[ $(type -t compopt) = "builtin" ]] && [[ "${COMPREPLY[0]}" == --*= ]]; then compopt -o nospace fi } # The arguments should be in the form "ext1|ext2|extn" __sing-box_handle_filename_extension_flag() { local ext="$1" _filedir "@(${ext})" } __sing-box_handle_subdirs_in_dir_flag() { local dir="$1" pushd "${dir}" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return } __sing-box_handle_flag() { __sing-box_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" # if a command required a flag, and we found it, unset must_have_one_flag() local flagname=${words[c]} local flagvalue="" # if the word contained an = if [[ ${words[c]} == *"="* ]]; then flagvalue=${flagname#*=} # take in as flagvalue after the = flagname=${flagname%=*} # strip everything after the = flagname="${flagname}=" # but put the = back fi __sing-box_debug "${FUNCNAME[0]}: looking for ${flagname}" if __sing-box_contains_word "${flagname}" "${must_have_one_flag[@]}"; then must_have_one_flag=() fi # if you set a flag which only applies to this command, don't show subcommands if __sing-box_contains_word "${flagname}" "${local_nonpersistent_flags[@]}"; then commands=() fi # keep flag value with flagname as flaghash # flaghash variable is an associative array which is only supported in bash > 3. if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then if [ -n "${flagvalue}" ] ; then flaghash[${flagname}]=${flagvalue} elif [ -n "${words[ $((c+1)) ]}" ] ; then flaghash[${flagname}]=${words[ $((c+1)) ]} else flaghash[${flagname}]="true" # pad "true" for bool flag fi fi # skip the argument to a two word flag if [[ ${words[c]} != *"="* ]] && __sing-box_contains_word "${words[c]}" "${two_word_flags[@]}"; then __sing-box_debug "${FUNCNAME[0]}: found a flag ${words[c]}, skip the next argument" c=$((c+1)) # if we are looking for a flags value, don't show commands if [[ $c -eq $cword ]]; then commands=() fi fi c=$((c+1)) } __sing-box_handle_noun() { __sing-box_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" if __sing-box_contains_word "${words[c]}" "${must_have_one_noun[@]}"; then must_have_one_noun=() elif __sing-box_contains_word "${words[c]}" "${noun_aliases[@]}"; then must_have_one_noun=() fi nouns+=("${words[c]}") c=$((c+1)) } __sing-box_handle_command() { __sing-box_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" local next_command if [[ -n ${last_command} ]]; then next_command="_${last_command}_${words[c]//:/__}" else if [[ $c -eq 0 ]]; then next_command="_sing-box_root_command" else next_command="_${words[c]//:/__}" fi fi c=$((c+1)) __sing-box_debug "${FUNCNAME[0]}: looking for ${next_command}" declare -F "$next_command" >/dev/null && $next_command } __sing-box_handle_word() { if [[ $c -ge $cword ]]; then __sing-box_handle_reply return fi __sing-box_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" if [[ "${words[c]}" == -* ]]; then __sing-box_handle_flag elif __sing-box_contains_word "${words[c]}" "${commands[@]}"; then __sing-box_handle_command elif [[ $c -eq 0 ]]; then __sing-box_handle_command elif __sing-box_contains_word "${words[c]}" "${command_aliases[@]}"; then # aliashash variable is an associative array which is only supported in bash > 3. if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then words[c]=${aliashash[${words[c]}]} __sing-box_handle_command else __sing-box_handle_noun fi else __sing-box_handle_noun fi __sing-box_handle_word } _sing-box_check() { last_command="sing-box_check" command_aliases=() commands=() flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_format() { last_command="sing-box_format" command_aliases=() commands=() flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--write") flags+=("-w") local_nonpersistent_flags+=("--write") local_nonpersistent_flags+=("-w") flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_generate_ech-keypair() { last_command="sing-box_generate_ech-keypair" command_aliases=() commands=() flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--pq-signature-schemes-enabled") local_nonpersistent_flags+=("--pq-signature-schemes-enabled") flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_generate_rand() { last_command="sing-box_generate_rand" command_aliases=() commands=() flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--base64") local_nonpersistent_flags+=("--base64") flags+=("--hex") local_nonpersistent_flags+=("--hex") flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_generate_reality-keypair() { last_command="sing-box_generate_reality-keypair" command_aliases=() commands=() flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_generate_tls-keypair() { last_command="sing-box_generate_tls-keypair" command_aliases=() commands=() flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--months=") two_word_flags+=("--months") two_word_flags+=("-m") local_nonpersistent_flags+=("--months") local_nonpersistent_flags+=("--months=") local_nonpersistent_flags+=("-m") flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_generate_uuid() { last_command="sing-box_generate_uuid" command_aliases=() commands=() flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_generate_vapid-keypair() { last_command="sing-box_generate_vapid-keypair" command_aliases=() commands=() flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_generate_wg-keypair() { last_command="sing-box_generate_wg-keypair" command_aliases=() commands=() flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_generate() { last_command="sing-box_generate" command_aliases=() commands=() commands+=("ech-keypair") commands+=("rand") commands+=("reality-keypair") commands+=("tls-keypair") commands+=("uuid") commands+=("vapid-keypair") commands+=("wg-keypair") flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_geoip_export() { last_command="sing-box_geoip_export" command_aliases=() commands=() flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--output=") two_word_flags+=("--output") two_word_flags+=("-o") local_nonpersistent_flags+=("--output") local_nonpersistent_flags+=("--output=") local_nonpersistent_flags+=("-o") flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") flags+=("--file=") two_word_flags+=("--file") two_word_flags+=("-f") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_geoip_list() { last_command="sing-box_geoip_list" command_aliases=() commands=() flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") flags+=("--file=") two_word_flags+=("--file") two_word_flags+=("-f") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_geoip_lookup() { last_command="sing-box_geoip_lookup" command_aliases=() commands=() flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") flags+=("--file=") two_word_flags+=("--file") two_word_flags+=("-f") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_geoip() { last_command="sing-box_geoip" command_aliases=() commands=() commands+=("export") commands+=("list") commands+=("lookup") flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--file=") two_word_flags+=("--file") two_word_flags+=("-f") flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_geosite_export() { last_command="sing-box_geosite_export" command_aliases=() commands=() flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--output=") two_word_flags+=("--output") two_word_flags+=("-o") local_nonpersistent_flags+=("--output") local_nonpersistent_flags+=("--output=") local_nonpersistent_flags+=("-o") flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") flags+=("--file=") two_word_flags+=("--file") two_word_flags+=("-f") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_geosite_list() { last_command="sing-box_geosite_list" command_aliases=() commands=() flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") flags+=("--file=") two_word_flags+=("--file") two_word_flags+=("-f") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_geosite_lookup() { last_command="sing-box_geosite_lookup" command_aliases=() commands=() flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") flags+=("--file=") two_word_flags+=("--file") two_word_flags+=("-f") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_geosite() { last_command="sing-box_geosite" command_aliases=() commands=() commands+=("export") commands+=("list") commands+=("lookup") flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--file=") two_word_flags+=("--file") two_word_flags+=("-f") flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_merge() { last_command="sing-box_merge" command_aliases=() commands=() flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_rule-set_compile() { last_command="sing-box_rule-set_compile" command_aliases=() commands=() flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--output=") two_word_flags+=("--output") two_word_flags+=("-o") local_nonpersistent_flags+=("--output") local_nonpersistent_flags+=("--output=") local_nonpersistent_flags+=("-o") flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_rule-set_convert() { last_command="sing-box_rule-set_convert" command_aliases=() commands=() flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--output=") two_word_flags+=("--output") two_word_flags+=("-o") local_nonpersistent_flags+=("--output") local_nonpersistent_flags+=("--output=") local_nonpersistent_flags+=("-o") flags+=("--type=") two_word_flags+=("--type") two_word_flags+=("-t") local_nonpersistent_flags+=("--type") local_nonpersistent_flags+=("--type=") local_nonpersistent_flags+=("-t") flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_rule-set_decompile() { last_command="sing-box_rule-set_decompile" command_aliases=() commands=() flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--output=") two_word_flags+=("--output") two_word_flags+=("-o") local_nonpersistent_flags+=("--output") local_nonpersistent_flags+=("--output=") local_nonpersistent_flags+=("-o") flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_rule-set_format() { last_command="sing-box_rule-set_format" command_aliases=() commands=() flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--write") flags+=("-w") local_nonpersistent_flags+=("--write") local_nonpersistent_flags+=("-w") flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_rule-set_match() { last_command="sing-box_rule-set_match" command_aliases=() commands=() flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--format=") two_word_flags+=("--format") two_word_flags+=("-f") local_nonpersistent_flags+=("--format") local_nonpersistent_flags+=("--format=") local_nonpersistent_flags+=("-f") flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_rule-set_merge() { last_command="sing-box_rule-set_merge" command_aliases=() commands=() flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_rule-set_upgrade() { last_command="sing-box_rule-set_upgrade" command_aliases=() commands=() flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--write") flags+=("-w") local_nonpersistent_flags+=("--write") local_nonpersistent_flags+=("-w") flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_rule-set() { last_command="sing-box_rule-set" command_aliases=() commands=() commands+=("compile") commands+=("convert") commands+=("decompile") commands+=("format") commands+=("match") commands+=("merge") commands+=("upgrade") flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_run() { last_command="sing-box_run" command_aliases=() commands=() flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_tools_connect() { last_command="sing-box_tools_connect" command_aliases=() commands=() flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--network=") two_word_flags+=("--network") two_word_flags+=("-n") local_nonpersistent_flags+=("--network") local_nonpersistent_flags+=("--network=") local_nonpersistent_flags+=("-n") flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") flags+=("--outbound=") two_word_flags+=("--outbound") two_word_flags+=("-o") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_tools_fetch() { last_command="sing-box_tools_fetch" command_aliases=() commands=() flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") flags+=("--outbound=") two_word_flags+=("--outbound") two_word_flags+=("-o") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_tools_synctime() { last_command="sing-box_tools_synctime" command_aliases=() commands=() flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--format=") two_word_flags+=("--format") two_word_flags+=("-f") local_nonpersistent_flags+=("--format") local_nonpersistent_flags+=("--format=") local_nonpersistent_flags+=("-f") flags+=("--server=") two_word_flags+=("--server") two_word_flags+=("-s") local_nonpersistent_flags+=("--server") local_nonpersistent_flags+=("--server=") local_nonpersistent_flags+=("-s") flags+=("--write") flags+=("-w") local_nonpersistent_flags+=("--write") local_nonpersistent_flags+=("-w") flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") flags+=("--outbound=") two_word_flags+=("--outbound") two_word_flags+=("-o") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_tools() { last_command="sing-box_tools" command_aliases=() commands=() commands+=("connect") commands+=("fetch") commands+=("synctime") flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--outbound=") two_word_flags+=("--outbound") two_word_flags+=("-o") flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_version() { last_command="sing-box_version" command_aliases=() commands=() flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--name") flags+=("-n") local_nonpersistent_flags+=("--name") local_nonpersistent_flags+=("-n") flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } _sing-box_root_command() { last_command="sing-box" command_aliases=() commands=() commands+=("check") commands+=("format") commands+=("generate") commands+=("geoip") commands+=("geosite") commands+=("merge") commands+=("rule-set") commands+=("run") commands+=("tools") commands+=("version") flags=() two_word_flags=() local_nonpersistent_flags=() flags_with_completion=() flags_completion=() flags+=("--config=") two_word_flags+=("--config") two_word_flags+=("-c") flags+=("--config-directory=") two_word_flags+=("--config-directory") two_word_flags+=("-C") flags+=("--directory=") two_word_flags+=("--directory") two_word_flags+=("-D") flags+=("--disable-color") must_have_one_flag=() must_have_one_noun=() noun_aliases=() } __start_sing-box() { local cur prev words cword split declare -A flaghash 2>/dev/null || : declare -A aliashash 2>/dev/null || : if declare -F _init_completion >/dev/null 2>&1; then _init_completion -s || return else __sing-box_init_completion -n "=" || return fi local c=0 local flag_parsing_disabled= local flags=() local two_word_flags=() local local_nonpersistent_flags=() local flags_with_completion=() local flags_completion=() local commands=("sing-box") local command_aliases=() local must_have_one_flag=() local must_have_one_noun=() local has_completion_function="" local last_command="" local nouns=() local noun_aliases=() __sing-box_handle_word } if [[ $(type -t compopt) = "builtin" ]]; then complete -o default -F __start_sing-box sing-box else complete -o default -o nospace -F __start_sing-box sing-box fi # ex: ts=4 sw=4 et filetype=sh ================================================ FILE: release/completions/sing-box.fish ================================================ # fish completion for sing-box -*- shell-script -*- function __sing_box_debug set -l file "$BASH_COMP_DEBUG_FILE" if test -n "$file" echo "$argv" >> $file end end function __sing_box_perform_completion __sing_box_debug "Starting __sing_box_perform_completion" # Extract all args except the last one set -l args (commandline -opc) # Extract the last arg and escape it in case it is a space set -l lastArg (string escape -- (commandline -ct)) __sing_box_debug "args: $args" __sing_box_debug "last arg: $lastArg" # Disable ActiveHelp which is not supported for fish shell set -l requestComp "SING_BOX_ACTIVE_HELP=0 $args[1] __complete $args[2..-1] $lastArg" __sing_box_debug "Calling $requestComp" set -l results (eval $requestComp 2> /dev/null) # Some programs may output extra empty lines after the directive. # Let's ignore them or else it will break completion. # Ref: https://github.com/spf13/cobra/issues/1279 for line in $results[-1..1] if test (string trim -- $line) = "" # Found an empty line, remove it set results $results[1..-2] else # Found non-empty line, we have our proper output break end end set -l comps $results[1..-2] set -l directiveLine $results[-1] # For Fish, when completing a flag with an = (e.g., -n=) # completions must be prefixed with the flag set -l flagPrefix (string match -r -- '-.*=' "$lastArg") __sing_box_debug "Comps: $comps" __sing_box_debug "DirectiveLine: $directiveLine" __sing_box_debug "flagPrefix: $flagPrefix" for comp in $comps printf "%s%s\n" "$flagPrefix" "$comp" end printf "%s\n" "$directiveLine" end # this function limits calls to __sing_box_perform_completion, by caching the result behind $__sing_box_perform_completion_once_result function __sing_box_perform_completion_once __sing_box_debug "Starting __sing_box_perform_completion_once" if test -n "$__sing_box_perform_completion_once_result" __sing_box_debug "Seems like a valid result already exists, skipping __sing_box_perform_completion" return 0 end set --global __sing_box_perform_completion_once_result (__sing_box_perform_completion) if test -z "$__sing_box_perform_completion_once_result" __sing_box_debug "No completions, probably due to a failure" return 1 end __sing_box_debug "Performed completions and set __sing_box_perform_completion_once_result" return 0 end # this function is used to clear the $__sing_box_perform_completion_once_result variable after completions are run function __sing_box_clear_perform_completion_once_result __sing_box_debug "" __sing_box_debug "========= clearing previously set __sing_box_perform_completion_once_result variable ==========" set --erase __sing_box_perform_completion_once_result __sing_box_debug "Successfully erased the variable __sing_box_perform_completion_once_result" end function __sing_box_requires_order_preservation __sing_box_debug "" __sing_box_debug "========= checking if order preservation is required ==========" __sing_box_perform_completion_once if test -z "$__sing_box_perform_completion_once_result" __sing_box_debug "Error determining if order preservation is required" return 1 end set -l directive (string sub --start 2 $__sing_box_perform_completion_once_result[-1]) __sing_box_debug "Directive is: $directive" set -l shellCompDirectiveKeepOrder 32 set -l keeporder (math (math --scale 0 $directive / $shellCompDirectiveKeepOrder) % 2) __sing_box_debug "Keeporder is: $keeporder" if test $keeporder -ne 0 __sing_box_debug "This does require order preservation" return 0 end __sing_box_debug "This doesn't require order preservation" return 1 end # This function does two things: # - Obtain the completions and store them in the global __sing_box_comp_results # - Return false if file completion should be performed function __sing_box_prepare_completions __sing_box_debug "" __sing_box_debug "========= starting completion logic ==========" # Start fresh set --erase __sing_box_comp_results __sing_box_perform_completion_once __sing_box_debug "Completion results: $__sing_box_perform_completion_once_result" if test -z "$__sing_box_perform_completion_once_result" __sing_box_debug "No completion, probably due to a failure" # Might as well do file completion, in case it helps return 1 end set -l directive (string sub --start 2 $__sing_box_perform_completion_once_result[-1]) set --global __sing_box_comp_results $__sing_box_perform_completion_once_result[1..-2] __sing_box_debug "Completions are: $__sing_box_comp_results" __sing_box_debug "Directive is: $directive" set -l shellCompDirectiveError 1 set -l shellCompDirectiveNoSpace 2 set -l shellCompDirectiveNoFileComp 4 set -l shellCompDirectiveFilterFileExt 8 set -l shellCompDirectiveFilterDirs 16 if test -z "$directive" set directive 0 end set -l compErr (math (math --scale 0 $directive / $shellCompDirectiveError) % 2) if test $compErr -eq 1 __sing_box_debug "Received error directive: aborting." # Might as well do file completion, in case it helps return 1 end set -l filefilter (math (math --scale 0 $directive / $shellCompDirectiveFilterFileExt) % 2) set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) % 2) if test $filefilter -eq 1; or test $dirfilter -eq 1 __sing_box_debug "File extension filtering or directory filtering not supported" # Do full file completion instead return 1 end set -l nospace (math (math --scale 0 $directive / $shellCompDirectiveNoSpace) % 2) set -l nofiles (math (math --scale 0 $directive / $shellCompDirectiveNoFileComp) % 2) __sing_box_debug "nospace: $nospace, nofiles: $nofiles" # If we want to prevent a space, or if file completion is NOT disabled, # we need to count the number of valid completions. # To do so, we will filter on prefix as the completions we have received # may not already be filtered so as to allow fish to match on different # criteria than the prefix. if test $nospace -ne 0; or test $nofiles -eq 0 set -l prefix (commandline -t | string escape --style=regex) __sing_box_debug "prefix: $prefix" set -l completions (string match -r -- "^$prefix.*" $__sing_box_comp_results) set --global __sing_box_comp_results $completions __sing_box_debug "Filtered completions are: $__sing_box_comp_results" # Important not to quote the variable for count to work set -l numComps (count $__sing_box_comp_results) __sing_box_debug "numComps: $numComps" if test $numComps -eq 1; and test $nospace -ne 0 # We must first split on \t to get rid of the descriptions to be # able to check what the actual completion will be. # We don't need descriptions anyway since there is only a single # real completion which the shell will expand immediately. set -l split (string split --max 1 \t $__sing_box_comp_results[1]) # Fish won't add a space if the completion ends with any # of the following characters: @=/:., set -l lastChar (string sub -s -1 -- $split) if not string match -r -q "[@=/:.,]" -- "$lastChar" # In other cases, to support the "nospace" directive we trick the shell # by outputting an extra, longer completion. __sing_box_debug "Adding second completion to perform nospace directive" set --global __sing_box_comp_results $split[1] $split[1]. __sing_box_debug "Completions are now: $__sing_box_comp_results" end end if test $numComps -eq 0; and test $nofiles -eq 0 # To be consistent with bash and zsh, we only trigger file # completion when there are no other completions __sing_box_debug "Requesting file completion" return 1 end end return 0 end # Since Fish completions are only loaded once the user triggers them, we trigger them ourselves # so we can properly delete any completions provided by another script. # Only do this if the program can be found, or else fish may print some errors; besides, # the existing completions will only be loaded if the program can be found. if type -q "sing-box" # The space after the program name is essential to trigger completion for the program # and not completion of the program name itself. # Also, we use '> /dev/null 2>&1' since '&>' is not supported in older versions of fish. complete --do-complete "sing-box " > /dev/null 2>&1 end # Remove any pre-existing completions for the program since we will be handling all of them. complete -c sing-box -e # this will get called after the two calls below and clear the $__sing_box_perform_completion_once_result global complete -c sing-box -n '__sing_box_clear_perform_completion_once_result' # The call to __sing_box_prepare_completions will setup __sing_box_comp_results # which provides the program's completion choices. # If this doesn't require order preservation, we don't use the -k flag complete -c sing-box -n 'not __sing_box_requires_order_preservation && __sing_box_prepare_completions' -f -a '$__sing_box_comp_results' # otherwise we use the -k flag complete -k -c sing-box -n '__sing_box_requires_order_preservation && __sing_box_prepare_completions' -f -a '$__sing_box_comp_results' ================================================ FILE: release/completions/sing-box.zsh ================================================ #compdef sing-box compdef _sing-box sing-box # zsh completion for sing-box -*- shell-script -*- __sing-box_debug() { local file="$BASH_COMP_DEBUG_FILE" if [[ -n ${file} ]]; then echo "$*" >> "${file}" fi } _sing-box() { local shellCompDirectiveError=1 local shellCompDirectiveNoSpace=2 local shellCompDirectiveNoFileComp=4 local shellCompDirectiveFilterFileExt=8 local shellCompDirectiveFilterDirs=16 local shellCompDirectiveKeepOrder=32 local lastParam lastChar flagPrefix requestComp out directive comp lastComp noSpace keepOrder local -a completions __sing-box_debug "\n========= starting completion logic ==========" __sing-box_debug "CURRENT: ${CURRENT}, words[*]: ${words[*]}" # The user could have moved the cursor backwards on the command-line. # We need to trigger completion from the $CURRENT location, so we need # to truncate the command-line ($words) up to the $CURRENT location. # (We cannot use $CURSOR as its value does not work when a command is an alias.) words=("${=words[1,CURRENT]}") __sing-box_debug "Truncated words[*]: ${words[*]}," lastParam=${words[-1]} lastChar=${lastParam[-1]} __sing-box_debug "lastParam: ${lastParam}, lastChar: ${lastChar}" # For zsh, when completing a flag with an = (e.g., sing-box -n=) # completions must be prefixed with the flag setopt local_options BASH_REMATCH if [[ "${lastParam}" =~ '-.*=' ]]; then # We are dealing with a flag with an = flagPrefix="-P ${BASH_REMATCH}" fi # Prepare the command to obtain completions requestComp="${words[1]} __complete ${words[2,-1]}" if [ "${lastChar}" = "" ]; then # If the last parameter is complete (there is a space following it) # We add an extra empty parameter so we can indicate this to the go completion code. __sing-box_debug "Adding extra empty parameter" requestComp="${requestComp} \"\"" fi __sing-box_debug "About to call: eval ${requestComp}" # Use eval to handle any environment variables and such out=$(eval ${requestComp} 2>/dev/null) __sing-box_debug "completion output: ${out}" # Extract the directive integer following a : from the last line local lastLine while IFS='\n' read -r line; do lastLine=${line} done < <(printf "%s\n" "${out[@]}") __sing-box_debug "last line: ${lastLine}" if [ "${lastLine[1]}" = : ]; then directive=${lastLine[2,-1]} # Remove the directive including the : and the newline local suffix (( suffix=${#lastLine}+2)) out=${out[1,-$suffix]} else # There is no directive specified. Leave $out as is. __sing-box_debug "No directive found. Setting do default" directive=0 fi __sing-box_debug "directive: ${directive}" __sing-box_debug "completions: ${out}" __sing-box_debug "flagPrefix: ${flagPrefix}" if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then __sing-box_debug "Completion received error. Ignoring completions." return fi local activeHelpMarker="_activeHelp_ " local endIndex=${#activeHelpMarker} local startIndex=$((${#activeHelpMarker}+1)) local hasActiveHelp=0 while IFS='\n' read -r comp; do # Check if this is an activeHelp statement (i.e., prefixed with $activeHelpMarker) if [ "${comp[1,$endIndex]}" = "$activeHelpMarker" ];then __sing-box_debug "ActiveHelp found: $comp" comp="${comp[$startIndex,-1]}" if [ -n "$comp" ]; then compadd -x "${comp}" __sing-box_debug "ActiveHelp will need delimiter" hasActiveHelp=1 fi continue fi if [ -n "$comp" ]; then # If requested, completions are returned with a description. # The description is preceded by a TAB character. # For zsh's _describe, we need to use a : instead of a TAB. # We first need to escape any : as part of the completion itself. comp=${comp//:/\\:} local tab="$(printf '\t')" comp=${comp//$tab/:} __sing-box_debug "Adding completion: ${comp}" completions+=${comp} lastComp=$comp fi done < <(printf "%s\n" "${out[@]}") # Add a delimiter after the activeHelp statements, but only if: # - there are completions following the activeHelp statements, or # - file completion will be performed (so there will be choices after the activeHelp) if [ $hasActiveHelp -eq 1 ]; then if [ ${#completions} -ne 0 ] || [ $((directive & shellCompDirectiveNoFileComp)) -eq 0 ]; then __sing-box_debug "Adding activeHelp delimiter" compadd -x "--" hasActiveHelp=0 fi fi if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then __sing-box_debug "Activating nospace." noSpace="-S ''" fi if [ $((directive & shellCompDirectiveKeepOrder)) -ne 0 ]; then __sing-box_debug "Activating keep order." keepOrder="-V" fi if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then # File extension filtering local filteringCmd filteringCmd='_files' for filter in ${completions[@]}; do if [ ${filter[1]} != '*' ]; then # zsh requires a glob pattern to do file filtering filter="\*.$filter" fi filteringCmd+=" -g $filter" done filteringCmd+=" ${flagPrefix}" __sing-box_debug "File filtering command: $filteringCmd" _arguments '*:filename:'"$filteringCmd" elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then # File completion for directories only local subdir subdir="${completions[1]}" if [ -n "$subdir" ]; then __sing-box_debug "Listing directories in $subdir" pushd "${subdir}" >/dev/null 2>&1 else __sing-box_debug "Listing directories in ." fi local result _arguments '*:dirname:_files -/'" ${flagPrefix}" result=$? if [ -n "$subdir" ]; then popd >/dev/null 2>&1 fi return $result else __sing-box_debug "Calling _describe" if eval _describe $keepOrder "completions" completions $flagPrefix $noSpace; then __sing-box_debug "_describe found some completions" # Return the success of having called _describe return 0 else __sing-box_debug "_describe did not find completions." __sing-box_debug "Checking if we should do file completion." if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then __sing-box_debug "deactivating file completion" # We must return an error code here to let zsh know that there were no # completions found by _describe; this is what will trigger other # matching algorithms to attempt to find completions. # For example zsh can match letters in the middle of words. return 1 else # Perform file completion __sing-box_debug "Activating file completion" # We must return the result of this command, so it must be the # last command, or else we must store its result to return it. _arguments '*:filename:_files'" ${flagPrefix}" fi fi fi } # don't run the completion function when being source-ed or eval-ed if [ "$funcstack[1]" = "_sing-box" ]; then _sing-box fi ================================================ FILE: release/config/config.json ================================================ { "log": { "level": "info" }, "dns": { "servers": [ { "type": "tls", "tag": "google", "server": "8.8.8.8" } ] }, "inbounds": [ { "type": "shadowsocks", "listen": "::", "listen_port": 8080, "network": "tcp", "method": "2022-blake3-aes-128-gcm", "password": "Gn1JUS14bLUHgv1cWDDp4A==", "multiplex": { "enabled": true, "padding": true } } ], "outbounds": [ { "type": "direct" } ], "route": { "rules": [ { "port": 53, "action": "hijack-dns" } ] } } ================================================ FILE: release/config/openwrt.conf ================================================ config sing-box 'main' option enabled '1' option conffile '/etc/sing-box/config.json' option workdir '/usr/share/sing-box' option log_stderr '1' ================================================ FILE: release/config/openwrt.init ================================================ #!/bin/sh /etc/rc.common USE_PROCD=1 START=99 PROG="/usr/bin/sing-box" start_service() { config_load "sing-box" local enabled config_file working_directory local log_stderr config_get_bool enabled "main" "enabled" "0" [ "$enabled" -eq "1" ] || return 0 config_get config_file "main" "conffile" "/etc/sing-box/config.json" config_get working_directory "main" "workdir" "/usr/share/sing-box" config_get_bool log_stderr "main" "log_stderr" "1" procd_open_instance procd_set_param command "$PROG" run -c "$config_file" -D "$working_directory" procd_set_param file "$config_file" procd_set_param stderr "$log_stderr" procd_set_param limits core="unlimited" procd_set_param limits nofile="1000000 1000000" procd_set_param respawn procd_close_instance } service_triggers() { procd_add_reload_trigger "sing-box" } ================================================ FILE: release/config/openwrt.keep ================================================ /etc/sing-box/ ================================================ FILE: release/config/openwrt.prerm ================================================ #!/bin/sh [ -s ${IPKG_INSTROOT}/lib/functions.sh ] || exit 0 . ${IPKG_INSTROOT}/lib/functions.sh default_prerm $0 $@ ================================================ FILE: release/config/sing-box-split-dns.xml ================================================ ================================================ FILE: release/config/sing-box.confd ================================================ # /etc/conf.d/sing-box: config file for /etc/init.d/sing-box # sing-box configuration path, could be file or directory # SINGBOX_CONFIG=/etc/sing-box # SINGBOX_WORKDIR=/var/lib/sing-box ================================================ FILE: release/config/sing-box.initd ================================================ #!/sbin/openrc-run name=$RC_SVCNAME description="sing-box service" supervisor="supervise-daemon" command="/usr/bin/sing-box" extra_commands="checkconfig" extra_started_commands="reload" : ${SINGBOX_CONFIG:=${config:-"/etc/sing-box"}} if [ -d "$SINGBOX_CONFIG" ]; then _config_opt="-C $SINGBOX_CONFIG" elif [ -z "$SINGBOX_CONFIG" ]; then _config_opt="" else _config_opt="-c $SINGBOX_CONFIG" fi _workdir=${SINGBOX_WORKDIR:-${workdir:-"/var/lib/sing-box"}} command_args="run --disable-color -D $_workdir $_config_opt" depend() { after net dns } checkconfig() { ebegin "Checking $RC_SVCNAME configuration" sing-box check -D "$_workdir" $_config_opt eend $? } start_pre() { checkconfig } reload() { ebegin "Reloading $RC_SVCNAME" checkconfig && $supervisor "$RC_SVCNAME" --signal HUP eend $? } ================================================ FILE: release/config/sing-box.postinst ================================================ #!/bin/sh systemd-sysusers sing-box.conf ================================================ FILE: release/config/sing-box.rules ================================================ polkit.addRule(function(action, subject) { if ((action.id == "org.freedesktop.resolve1.set-domains" || action.id == "org.freedesktop.resolve1.set-default-route" || action.id == "org.freedesktop.resolve1.set-dns-servers") && subject.user == "sing-box") { return polkit.Result.YES; } }); ================================================ FILE: release/config/sing-box.service ================================================ [Unit] Description=sing-box service Documentation=https://sing-box.sagernet.org After=network.target nss-lookup.target network-online.target [Service] User=sing-box StateDirectory=sing-box CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH ExecStart=/usr/bin/sing-box -D /var/lib/sing-box -C /etc/sing-box run ExecReload=/bin/kill -HUP $MAINPID Restart=on-failure RestartSec=10s LimitNOFILE=infinity [Install] WantedBy=multi-user.target ================================================ FILE: release/config/sing-box.sysusers ================================================ u sing-box - "sing-box Service" ================================================ FILE: release/config/sing-box@.service ================================================ [Unit] Description=sing-box service Documentation=https://sing-box.sagernet.org After=network.target nss-lookup.target network-online.target [Service] User=sing-box StateDirectory=sing-box-%i CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH ExecStart=/usr/bin/sing-box -D /var/lib/sing-box-%i -c /etc/sing-box/%i.json run ExecReload=/bin/kill -HUP $MAINPID Restart=on-failure RestartSec=10s LimitNOFILE=infinity [Install] WantedBy=multi-user.target ================================================ FILE: release/local/common.sh ================================================ #!/usr/bin/env bash set -e -o pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" BINARY_NAME="sing-box" INSTALL_BIN_PATH="/usr/local/bin" INSTALL_CONFIG_PATH="/usr/local/etc/sing-box" INSTALL_DATA_PATH="/var/lib/sing-box" SYSTEMD_SERVICE_PATH="/etc/systemd/system" DEFAULT_BUILD_TAGS="$(cat "$PROJECT_DIR/release/DEFAULT_BUILD_TAGS_OTHERS")" setup_environment() { if [ -d /usr/local/go ]; then export PATH="$PATH:/usr/local/go/bin" fi if ! command -v go &> /dev/null; then echo "Error: Go is not installed or not in PATH" echo "Run install_go.sh to install Go" exit 1 fi } get_build_tags() { local extra_tags="$1" if [ -n "$extra_tags" ]; then echo "${DEFAULT_BUILD_TAGS},${extra_tags}" else echo "${DEFAULT_BUILD_TAGS}" fi } get_version() { cd "$PROJECT_DIR" GOHOSTOS=$(go env GOHOSTOS) GOHOSTARCH=$(go env GOHOSTARCH) CGO_ENABLED=0 GOOS=$GOHOSTOS GOARCH=$GOHOSTARCH go run github.com/sagernet/sing-box/cmd/internal/read_tag@latest } get_ldflags() { local version version=$(get_version) local shared_ldflags shared_ldflags=$(cat "$PROJECT_DIR/release/LDFLAGS") echo "-X 'github.com/sagernet/sing-box/constant.Version=${version}' ${shared_ldflags} -s -w -buildid=" } build_sing_box() { local tags="$1" local ldflags ldflags=$(get_ldflags) echo "Building sing-box with tags: $tags" cd "$PROJECT_DIR" export GOTOOLCHAIN=local go install -v -trimpath -ldflags "$ldflags" -tags "$tags" ./cmd/sing-box } install_binary() { local gopath gopath=$(go env GOPATH) echo "Installing binary to $INSTALL_BIN_PATH/$BINARY_NAME" sudo cp "${gopath}/bin/${BINARY_NAME}" "${INSTALL_BIN_PATH}/" } setup_config() { echo "Setting up configuration" sudo mkdir -p "$INSTALL_CONFIG_PATH" if [ ! -f "$INSTALL_CONFIG_PATH/config.json" ]; then sudo cp "$PROJECT_DIR/release/config/config.json" "$INSTALL_CONFIG_PATH/config.json" echo "Default config installed to $INSTALL_CONFIG_PATH/config.json" else echo "Config already exists at $INSTALL_CONFIG_PATH/config.json (not overwriting)" fi } setup_systemd() { echo "Setting up systemd service" sudo cp "$SCRIPT_DIR/sing-box.service" "$SYSTEMD_SERVICE_PATH/" sudo systemctl daemon-reload } stop_service() { if systemctl is-active --quiet sing-box; then echo "Stopping sing-box service" sudo systemctl stop sing-box fi } start_service() { echo "Starting sing-box service" sudo systemctl start sing-box } restart_service() { echo "Restarting sing-box service" sudo systemctl restart sing-box } ================================================ FILE: release/local/debug.sh ================================================ #!/usr/bin/env bash set -e -o pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" source "$SCRIPT_DIR/common.sh" setup_environment echo "Updating sing-box from git repository..." cd "$PROJECT_DIR" git fetch git reset FETCH_HEAD --hard git clean -fdx BUILD_TAGS=$(get_build_tags "debug") build_sing_box "$BUILD_TAGS" stop_service install_binary start_service echo "" echo "Following service logs (Ctrl+C to exit)..." sudo journalctl -u sing-box --output cat -f ================================================ FILE: release/local/enable.sh ================================================ #!/usr/bin/env bash set -e -o pipefail sudo systemctl enable sing-box sudo systemctl start sing-box sudo journalctl -u sing-box --output cat -f ================================================ FILE: release/local/install.sh ================================================ #!/usr/bin/env bash set -e -o pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" source "$SCRIPT_DIR/common.sh" setup_environment BUILD_TAGS=$(get_build_tags) build_sing_box "$BUILD_TAGS" install_binary setup_config setup_systemd echo "" echo "Installation complete!" echo "To enable and start the service, run: $SCRIPT_DIR/enable.sh" ================================================ FILE: release/local/install_go.sh ================================================ #!/usr/bin/env bash set -e -o pipefail go_version=$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g') curl -Lo go.tar.gz "https://go.dev/dl/go$go_version.linux-amd64.tar.gz" sudo rm -rf /usr/local/go sudo tar -C /usr/local -xzf go.tar.gz rm go.tar.gz ================================================ FILE: release/local/reinstall.sh ================================================ #!/usr/bin/env bash set -e -o pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" source "$SCRIPT_DIR/common.sh" setup_environment BUILD_TAGS=$(get_build_tags) build_sing_box "$BUILD_TAGS" stop_service install_binary start_service echo "" echo "Reinstallation complete!" ================================================ FILE: release/local/sing-box.service ================================================ [Unit] Description=sing-box service Documentation=https://sing-box.sagernet.org After=network.target nss-lookup.target network-online.target [Service] CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH ExecStart=/usr/local/bin/sing-box -D /var/lib/sing-box -C /usr/local/etc/sing-box run ExecReload=/bin/kill -HUP $MAINPID Restart=on-failure RestartSec=10s LimitNOFILE=infinity [Install] WantedBy=multi-user.target ================================================ FILE: release/local/uninstall.sh ================================================ #!/usr/bin/env bash set -e -o pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" source "$SCRIPT_DIR/common.sh" echo "Uninstalling sing-box..." if systemctl is-active --quiet sing-box 2>/dev/null; then echo "Stopping sing-box service..." sudo systemctl stop sing-box fi if systemctl is-enabled --quiet sing-box 2>/dev/null; then echo "Disabling sing-box service..." sudo systemctl disable sing-box fi echo "Removing files..." sudo rm -rf "$INSTALL_DATA_PATH" sudo rm -rf "$INSTALL_BIN_PATH/$BINARY_NAME" sudo rm -rf "$INSTALL_CONFIG_PATH" sudo rm -rf "$SYSTEMD_SERVICE_PATH/sing-box.service" echo "Reloading systemd..." sudo systemctl daemon-reload echo "" echo "Uninstallation complete!" ================================================ FILE: release/local/update.sh ================================================ #!/usr/bin/env bash set -e -o pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" source "$SCRIPT_DIR/common.sh" echo "Updating sing-box from git repository..." cd "$PROJECT_DIR" git fetch git reset FETCH_HEAD --hard git clean -fdx echo "" echo "Running reinstall..." exec "$SCRIPT_DIR/reinstall.sh" ================================================ FILE: route/conn.go ================================================ package route import ( "context" "io" "net" "net/netip" "os" "strings" "sync" "sync/atomic" "time" "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" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/canceler" 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/common/x/list" ) var _ adapter.ConnectionManager = (*ConnectionManager)(nil) type ConnectionManager struct { logger logger.ContextLogger access sync.Mutex connections list.List[io.Closer] } func NewConnectionManager(logger logger.ContextLogger) *ConnectionManager { return &ConnectionManager{ logger: logger, } } func (m *ConnectionManager) Start(stage adapter.StartStage) error { return nil } func (m *ConnectionManager) Count() int { return m.connections.Len() } func (m *ConnectionManager) CloseAll() { m.access.Lock() var closers []io.Closer for element := m.connections.Front(); element != nil; { nextElement := element.Next() closers = append(closers, element.Value) m.connections.Remove(element) element = nextElement } m.access.Unlock() for _, closer := range closers { common.Close(closer) } } func (m *ConnectionManager) Close() error { m.CloseAll() return nil } func (m *ConnectionManager) TrackConn(conn net.Conn) net.Conn { m.access.Lock() element := m.connections.PushBack(conn) m.access.Unlock() return &trackedConn{ Conn: conn, manager: m, element: element, } } func (m *ConnectionManager) TrackPacketConn(conn net.PacketConn) net.PacketConn { m.access.Lock() element := m.connections.PushBack(conn) m.access.Unlock() return &trackedPacketConn{ PacketConn: conn, manager: m, element: element, } } func (m *ConnectionManager) NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { ctx = adapter.WithContext(ctx, &metadata) var ( remoteConn net.Conn err error ) if len(metadata.DestinationAddresses) > 0 || metadata.Destination.IsIP() { remoteConn, err = dialer.DialSerialNetwork(ctx, this, N.NetworkTCP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay) } else { remoteConn, err = this.DialContext(ctx, N.NetworkTCP, metadata.Destination) } if err != nil { var remoteString string if len(metadata.DestinationAddresses) > 0 { remoteString = "[" + strings.Join(common.Map(metadata.DestinationAddresses, netip.Addr.String), ",") + "]" } else { remoteString = metadata.Destination.String() } var dialerString string if outbound, isOutbound := this.(adapter.Outbound); isOutbound { dialerString = " using outbound/" + outbound.Type() + "[" + outbound.Tag() + "]" } err = E.Cause(err, "open connection to ", remoteString, dialerString) N.CloseOnHandshakeFailure(conn, onClose, err) m.logger.ErrorContext(ctx, err) return } err = N.ReportConnHandshakeSuccess(conn, remoteConn) if err != nil { err = E.Cause(err, "report handshake success") remoteConn.Close() N.CloseOnHandshakeFailure(conn, onClose, err) m.logger.ErrorContext(ctx, err) return } 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 } serverFirst := sniff.Skip(&metadata) var done atomic.Bool if m.kickWriteHandshake(ctx, conn, remoteConn, serverFirst, false, &done, onClose) { return } if m.kickWriteHandshake(ctx, remoteConn, conn, serverFirst, true, &done, onClose) { return } go m.connectionCopy(ctx, conn, remoteConn, false, &done, onClose) go m.connectionCopy(ctx, remoteConn, conn, true, &done, onClose) } func (m *ConnectionManager) NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { ctx = adapter.WithContext(ctx, &metadata) var ( remotePacketConn net.PacketConn remoteConn net.Conn destinationAddress netip.Addr err error ) if metadata.UDPConnect { parallelDialer, isParallelDialer := this.(dialer.ParallelInterfaceDialer) if len(metadata.DestinationAddresses) > 0 { if isParallelDialer { remoteConn, err = dialer.DialSerialNetwork(ctx, parallelDialer, N.NetworkUDP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay) } else { remoteConn, err = N.DialSerial(ctx, this, N.NetworkUDP, metadata.Destination, metadata.DestinationAddresses) } } else if metadata.Destination.IsIP() { if isParallelDialer { remoteConn, err = dialer.DialSerialNetwork(ctx, parallelDialer, N.NetworkUDP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay) } else { remoteConn, err = this.DialContext(ctx, N.NetworkUDP, metadata.Destination) } } else { remoteConn, err = this.DialContext(ctx, N.NetworkUDP, metadata.Destination) } if err != nil { var remoteString string if len(metadata.DestinationAddresses) > 0 { remoteString = "[" + strings.Join(common.Map(metadata.DestinationAddresses, netip.Addr.String), ",") + "]" } else { remoteString = metadata.Destination.String() } var dialerString string if outbound, isOutbound := this.(adapter.Outbound); isOutbound { dialerString = " using outbound/" + outbound.Type() + "[" + outbound.Tag() + "]" } err = E.Cause(err, "open packet connection to ", remoteString, dialerString) N.CloseOnHandshakeFailure(conn, onClose, err) m.logger.ErrorContext(ctx, err) return } remotePacketConn = bufio.NewUnbindPacketConn(remoteConn) connRemoteAddr := M.AddrFromNet(remoteConn.RemoteAddr()) if connRemoteAddr != metadata.Destination.Addr { destinationAddress = connRemoteAddr } } else { if len(metadata.DestinationAddresses) > 0 { remotePacketConn, destinationAddress, err = dialer.ListenSerialNetworkPacket(ctx, this, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay) } else if packetDialer, withDestination := this.(dialer.PacketDialerWithDestination); withDestination { remotePacketConn, destinationAddress, err = packetDialer.ListenPacketWithDestination(ctx, metadata.Destination) } else { remotePacketConn, err = this.ListenPacket(ctx, metadata.Destination) } if err != nil { var dialerString string if outbound, isOutbound := this.(adapter.Outbound); isOutbound { dialerString = " using outbound/" + outbound.Type() + "[" + outbound.Tag() + "]" } err = E.Cause(err, "listen packet connection using ", dialerString) N.CloseOnHandshakeFailure(conn, onClose, err) m.logger.ErrorContext(ctx, err) return } } err = N.ReportPacketConnHandshakeSuccess(conn, remotePacketConn) if err != nil { conn.Close() remotePacketConn.Close() m.logger.ErrorContext(ctx, "report handshake success: ", err) return } if destinationAddress.IsValid() { var originDestination M.Socksaddr if metadata.RouteOriginalDestination.IsValid() { originDestination = metadata.RouteOriginalDestination } else { originDestination = metadata.Destination } if natConn, loaded := common.Cast[bufio.NATPacketConn](conn); loaded { natConn.UpdateDestination(destinationAddress) } else { destination := M.SocksaddrFrom(destinationAddress, metadata.Destination.Port) if metadata.Destination != destination { if metadata.UDPDisableDomainUnmapping { remotePacketConn = bufio.NewUnidirectionalNATPacketConn(bufio.NewPacketConn(remotePacketConn), destination, originDestination) } else { remotePacketConn = bufio.NewNATPacketConn(bufio.NewPacketConn(remotePacketConn), destination, originDestination) } } else if metadata.RouteOriginalDestination.IsValid() && metadata.RouteOriginalDestination != metadata.Destination { remotePacketConn = bufio.NewDestinationNATPacketConn(bufio.NewPacketConn(remotePacketConn), metadata.Destination, metadata.RouteOriginalDestination) } } } else if metadata.RouteOriginalDestination.IsValid() && metadata.RouteOriginalDestination != metadata.Destination { remotePacketConn = bufio.NewDestinationNATPacketConn(bufio.NewPacketConn(remotePacketConn), metadata.Destination, metadata.RouteOriginalDestination) } var udpTimeout time.Duration if metadata.UDPTimeout > 0 { udpTimeout = metadata.UDPTimeout } else { protocol := metadata.Protocol if protocol == "" { protocol = C.PortProtocols[metadata.Destination.Port] } if protocol != "" { udpTimeout = C.ProtocolTimeouts[protocol] } } if udpTimeout > 0 { ctx, conn = canceler.NewPacketConn(ctx, conn, udpTimeout) } destination := bufio.NewPacketConn(remotePacketConn) var done atomic.Bool go m.packetConnectionCopy(ctx, conn, destination, false, &done, onClose) go m.packetConnectionCopy(ctx, destination, conn, true, &done, onClose) } func (m *ConnectionManager) connectionCopy(ctx context.Context, source net.Conn, destination net.Conn, direction bool, done *atomic.Bool, onClose N.CloseHandlerFunc) { _, err := bufio.CopyWithIncreateBuffer(destination, source, bufio.DefaultIncreaseBufferAfter, bufio.DefaultBatchSize) if err != nil { common.Close(source, destination) } else if duplexDst, isDuplex := destination.(N.WriteCloser); isDuplex { err = duplexDst.CloseWrite() if err != nil { common.Close(source, destination) } } else { destination.Close() } if done.Swap(true) { if onClose != nil { onClose(err) } common.Close(source, destination) } if !direction { if err == nil { m.logger.DebugContext(ctx, "connection upload finished") } else if !E.IsClosedOrCanceled(err) { m.logger.ErrorContext(ctx, "connection upload closed: ", err) } else { m.logger.TraceContext(ctx, "connection upload closed") } } else { if err == nil { m.logger.DebugContext(ctx, "connection download finished") } else if !E.IsClosedOrCanceled(err) { m.logger.ErrorContext(ctx, "connection download closed: ", err) } else { m.logger.TraceContext(ctx, "connection download closed") } } } 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 ( err error wrotePayload bool ) if serverFirst { _ = destination.SetWriteDeadline(time.Now().Add(C.ReadPayloadTimeout)) _, 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 } if !wrotePayload && (E.IsMulti(err, os.ErrInvalid, context.DeadlineExceeded, io.EOF) || E.IsTimeout(err)) { return false } if !done.Swap(true) { if onClose != nil { onClose(err) } } common.Close(source, destination) if !direction { m.logger.ErrorContext(ctx, "connection upload handshake: ", err) } else { m.logger.ErrorContext(ctx, "connection download handshake: ", err) } return true } func (m *ConnectionManager) packetConnectionCopy(ctx context.Context, source N.PacketReader, destination N.PacketWriter, direction bool, done *atomic.Bool, onClose N.CloseHandlerFunc) { _, err := bufio.CopyPacket(destination, source) if !direction { if err == nil { m.logger.DebugContext(ctx, "packet upload finished") } else if E.IsClosedOrCanceled(err) { m.logger.TraceContext(ctx, "packet upload closed") } else { m.logger.DebugContext(ctx, "packet upload closed: ", err) } } else { if err == nil { m.logger.DebugContext(ctx, "packet download finished") } else if E.IsClosedOrCanceled(err) { m.logger.TraceContext(ctx, "packet download closed") } else { m.logger.DebugContext(ctx, "packet download closed: ", err) } } if !done.Swap(true) { if onClose != nil { onClose(err) } } common.Close(source, destination) } type trackedConn struct { net.Conn manager *ConnectionManager element *list.Element[io.Closer] } func (c *trackedConn) Close() error { c.manager.access.Lock() c.manager.connections.Remove(c.element) c.manager.access.Unlock() return c.Conn.Close() } func (c *trackedConn) Upstream() any { return c.Conn } func (c *trackedConn) ReaderReplaceable() bool { return true } func (c *trackedConn) WriterReplaceable() bool { return true } type trackedPacketConn struct { net.PacketConn manager *ConnectionManager element *list.Element[io.Closer] } func (c *trackedPacketConn) Close() error { c.manager.access.Lock() c.manager.connections.Remove(c.element) c.manager.access.Unlock() return c.PacketConn.Close() } func (c *trackedPacketConn) Upstream() any { return bufio.NewPacketConn(c.PacketConn) } func (c *trackedPacketConn) ReaderReplaceable() bool { return true } func (c *trackedPacketConn) WriterReplaceable() bool { return true } ================================================ FILE: route/dns.go ================================================ package route import ( "context" "net" "time" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/dns" dnsOutbound "github.com/sagernet/sing-box/protocol/dns" R "github.com/sagernet/sing-box/route/rule" "github.com/sagernet/sing/common/buf" 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/common/udpnat2" mDNS "github.com/miekg/dns" ) func (r *Router) hijackDNSStream(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { metadata.Destination = M.Socksaddr{} for { conn.SetReadDeadline(time.Now().Add(C.DNSTimeout)) err := dnsOutbound.HandleStreamDNSRequest(ctx, r.dns, conn, metadata) if err != nil { if !E.IsClosedOrCanceled(err) { return err } else { return nil } } } } func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetBuffers []*N.PacketBuffer, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error { if natConn, isNatConn := conn.(udpnat.Conn); isNatConn { metadata.Destination = M.Socksaddr{} for _, packet := range packetBuffers { buffer := packet.Buffer destination := packet.Destination N.PutPacketBuffer(packet) go ExchangeDNSPacket(ctx, r.dns, r.logger, natConn, buffer, metadata, destination) } natConn.SetHandler(&dnsHijacker{ router: r.dns, logger: r.logger, conn: conn, ctx: ctx, metadata: metadata, onClose: onClose, }) return nil } err := dnsOutbound.NewDNSPacketConnection(ctx, r.dns, conn, packetBuffers, metadata) N.CloseOnHandshakeFailure(conn, onClose, err) if err != nil && !E.IsClosedOrCanceled(err) { return E.Cause(err, "process DNS packet") } return nil } func ExchangeDNSPacket(ctx context.Context, router adapter.DNSRouter, logger logger.ContextLogger, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext, destination M.Socksaddr) { err := exchangeDNSPacket(ctx, router, conn, buffer, metadata, destination) if err != nil && !R.IsRejected(err) && !E.IsClosedOrCanceled(err) { logger.ErrorContext(ctx, E.Cause(err, "process DNS packet")) } } func exchangeDNSPacket(ctx context.Context, router adapter.DNSRouter, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext, destination M.Socksaddr) error { var message mDNS.Msg err := message.Unpack(buffer.Bytes()) buffer.Release() if err != nil { return E.Cause(err, "unpack request") } response, err := router.Exchange(adapter.WithContext(ctx, &metadata), &message, adapter.DNSQueryOptions{}) if err != nil { return err } responseBuffer, err := dns.TruncateDNSMessage(&message, response, 1024) if err != nil { return err } err = conn.WritePacket(responseBuffer, destination) return err } type dnsHijacker struct { router adapter.DNSRouter logger logger.ContextLogger conn N.PacketConn ctx context.Context metadata adapter.InboundContext onClose N.CloseHandlerFunc } func (h *dnsHijacker) NewPacketEx(buffer *buf.Buffer, destination M.Socksaddr) { go ExchangeDNSPacket(h.ctx, h.router, h.logger, h.conn, buffer, h.metadata, destination) } func (h *dnsHijacker) Close() error { if h.onClose != nil { h.onClose(nil) } return nil } ================================================ FILE: route/neighbor_resolver_darwin.go ================================================ //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) 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() 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 } ================================================ FILE: route/neighbor_resolver_hostname.go ================================================ 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() != "") } ================================================ FILE: route/neighbor_resolver_lease.go ================================================ 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 rest, ok := strings.CutPrefix(line, "hardware ethernet "); ok { macString := strings.TrimSuffix(rest, ";") parsed, macErr := net.ParseMAC(macString) if macErr == nil { currentMAC = parsed } } else if rest, ok := strings.CutPrefix(line, "client-hostname "); ok { hostname := strings.TrimSuffix(rest, ";") hostname = strings.Trim(hostname, "\"") if hostname != "" { currentHostname = hostname } } else if rest, ok := strings.CutPrefix(line, "binding state "); ok { state := strings.TrimSuffix(rest, ";") 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 } } } } ================================================ FILE: route/neighbor_resolver_linux.go ================================================ //go:build linux package route import ( "net" "net/netip" "os" "slices" "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.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) 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() 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 { address, mac, isDelete, ok := ParseNeighborMessage(message) 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() } ================================================ FILE: route/neighbor_resolver_parse.go ================================================ 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) } ================================================ FILE: route/neighbor_resolver_platform.go ================================================ 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) 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() 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") } ================================================ FILE: route/neighbor_resolver_stub.go ================================================ //go:build !linux && !darwin 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 } ================================================ FILE: route/neighbor_table_darwin.go ================================================ //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 } ================================================ FILE: route/neighbor_table_linux.go ================================================ //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 } ================================================ FILE: route/network.go ================================================ package route import ( "context" "errors" "net" "net/netip" "os" "runtime" "strings" "sync" "syscall" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/settings" "github.com/sagernet/sing-box/common/taskmonitor" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" 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/winpowrprof" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/pause" "golang.org/x/exp/slices" ) var _ adapter.NetworkManager = (*NetworkManager)(nil) type NetworkManager struct { ctx context.Context logger logger.ContextLogger router adapter.Router interfaceFinder *control.DefaultInterfaceFinder networkInterfaces common.TypedValue[[]adapter.NetworkInterface] autoDetectInterface bool defaultOptions adapter.NetworkOptions autoRedirectOutputMark uint32 networkMonitor tun.NetworkUpdateMonitor interfaceMonitor tun.DefaultInterfaceMonitor packageManager tun.PackageManager powerListener winpowrprof.EventListener pauseManager pause.Manager platformInterface adapter.PlatformInterface connectionManager adapter.ConnectionManager endpoint adapter.EndpointManager inbound adapter.InboundManager outbound adapter.OutboundManager needWIFIState bool wifiMonitor settings.WIFIMonitor wifiState adapter.WIFIState wifiStateMutex sync.RWMutex started bool } func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, options option.RouteOptions, dnsOptions option.DNSOptions) (*NetworkManager, error) { defaultDomainResolver := common.PtrValueOrDefault(options.DefaultDomainResolver) if options.AutoDetectInterface && !(C.IsLinux || C.IsDarwin || C.IsWindows) { return nil, E.New("`auto_detect_interface` is only supported on Linux, Windows and macOS") } else if options.OverrideAndroidVPN && !C.IsAndroid { return nil, E.New("`override_android_vpn` is only supported on Android") } else if options.DefaultInterface != "" && !(C.IsLinux || C.IsDarwin || C.IsWindows) { return nil, E.New("`default_interface` is only supported on Linux, Windows and macOS") } else if options.DefaultMark != 0 && !C.IsLinux { return nil, E.New("`default_mark` is only supported on linux") } nm := &NetworkManager{ ctx: ctx, logger: logger, interfaceFinder: control.NewDefaultInterfaceFinder(), autoDetectInterface: options.AutoDetectInterface, defaultOptions: adapter.NetworkOptions{ BindInterface: options.DefaultInterface, RoutingMark: uint32(options.DefaultMark), 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, ClientSubnet: defaultDomainResolver.ClientSubnet.Build(netip.Prefix{}), }, NetworkStrategy: (*C.NetworkStrategy)(options.DefaultNetworkStrategy), NetworkType: common.Map(options.DefaultNetworkType, option.InterfaceType.Build), FallbackNetworkType: common.Map(options.DefaultFallbackNetworkType, option.InterfaceType.Build), FallbackDelay: time.Duration(options.DefaultFallbackDelay), }, pauseManager: service.FromContext[pause.Manager](ctx), platformInterface: service.FromContext[adapter.PlatformInterface](ctx), connectionManager: service.FromContext[adapter.ConnectionManager](ctx), endpoint: service.FromContext[adapter.EndpointManager](ctx), inbound: service.FromContext[adapter.InboundManager](ctx), outbound: service.FromContext[adapter.OutboundManager](ctx), needWIFIState: hasRule(options.Rules, isWIFIRule) || hasDNSRule(dnsOptions.Rules, isWIFIDNSRule), } if options.DefaultNetworkStrategy != nil { if options.DefaultInterface != "" { return nil, E.New("`default_network_strategy` is conflict with `default_interface`") } if !options.AutoDetectInterface { return nil, E.New("`auto_detect_interface` is required by `default_network_strategy`") } } usePlatformDefaultInterfaceMonitor := nm.platformInterface != nil enforceInterfaceMonitor := options.AutoDetectInterface if !usePlatformDefaultInterfaceMonitor { networkMonitor, err := tun.NewNetworkUpdateMonitor(logger) if !((err != nil && !enforceInterfaceMonitor) || errors.Is(err, os.ErrInvalid)) { if err != nil { return nil, E.Cause(err, "create network monitor") } nm.networkMonitor = networkMonitor interfaceMonitor, err := tun.NewDefaultInterfaceMonitor(nm.networkMonitor, logger, tun.DefaultInterfaceMonitorOptions{ InterfaceFinder: nm.interfaceFinder, OverrideAndroidVPN: options.OverrideAndroidVPN, UnderNetworkExtension: nm.platformInterface != nil && nm.platformInterface.UnderNetworkExtension(), }) if err != nil { return nil, E.New("auto_detect_interface unsupported on current platform") } interfaceMonitor.RegisterCallback(nm.notifyInterfaceUpdate) nm.interfaceMonitor = interfaceMonitor } } else { interfaceMonitor := nm.platformInterface.CreateDefaultInterfaceMonitor(logger) interfaceMonitor.RegisterCallback(nm.notifyInterfaceUpdate) nm.interfaceMonitor = interfaceMonitor } return nm, nil } 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() monitor.Finish() if err != nil { return err } } if r.interfaceMonitor != nil { monitor.Start("initialize interface monitor") err := r.interfaceMonitor.Start() monitor.Finish() if err != nil { return err } } case adapter.StartStateStart: if runtime.GOOS == "windows" { powerListener, err := winpowrprof.NewEventListener(r.notifyWindowsPowerEvent) if err == nil { r.powerListener = powerListener } else { r.logger.Warn("initialize power listener: ", err) } } if r.powerListener != nil { monitor.Start("start power listener") err := r.powerListener.Start() monitor.Finish() if err != nil { return E.Cause(err, "start power listener") } } if C.IsAndroid && r.platformInterface == nil { monitor.Start("initialize package manager") packageManager, err := tun.NewPackageManager(tun.PackageManagerOptions{ Callback: r, Logger: r.logger, }) monitor.Finish() if err != nil { return E.Cause(err, "create package manager") } monitor.Start("start package manager") err = packageManager.Start() monitor.Finish() if err != nil { r.logger.Warn("initialize package manager: ", err) } else { r.packageManager = packageManager } } case adapter.StartStatePostStart: if r.needWIFIState && !(r.platformInterface != nil && r.platformInterface.UsePlatformWIFIMonitor()) { wifiMonitor, err := settings.NewWIFIMonitor(r.onWIFIStateChanged) if err != nil { if err != os.ErrInvalid { r.logger.Warn(E.Cause(err, "create WIFI monitor")) } } else { r.wifiMonitor = wifiMonitor err = r.wifiMonitor.Start() if err != nil { r.logger.Warn(E.Cause(err, "start WIFI monitor")) } } } r.started = true } return nil } func (r *NetworkManager) Initialize(ruleSets []adapter.RuleSet) { for _, ruleSet := range ruleSets { metadata := ruleSet.Metadata() if metadata.ContainsWIFIRule { r.needWIFIState = true break } } } func (r *NetworkManager) Close() error { monitor := taskmonitor.New(r.logger, C.StopTimeout) var err error if r.packageManager != nil { monitor.Start("close package manager") err = E.Append(err, r.packageManager.Close(), func(err error) error { return E.Cause(err, "close package manager") }) monitor.Finish() } if r.powerListener != nil { monitor.Start("close power listener") err = E.Append(err, r.powerListener.Close(), func(err error) error { return E.Cause(err, "close power listener") }) monitor.Finish() } if r.interfaceMonitor != nil { monitor.Start("close interface monitor") err = E.Append(err, r.interfaceMonitor.Close(), func(err error) error { return E.Cause(err, "close interface monitor") }) monitor.Finish() } if r.networkMonitor != nil { monitor.Start("close network monitor") err = E.Append(err, r.networkMonitor.Close(), func(err error) error { return E.Cause(err, "close network monitor") }) monitor.Finish() } if r.wifiMonitor != nil { monitor.Start("close WIFI monitor") err = E.Append(err, r.wifiMonitor.Close(), func(err error) error { return E.Cause(err, "close WIFI monitor") }) monitor.Finish() } return err } func (r *NetworkManager) InterfaceFinder() control.InterfaceFinder { return r.interfaceFinder } func (r *NetworkManager) UpdateInterfaces() error { if r.platformInterface == nil || !r.platformInterface.UsePlatformNetworkInterfaces() { return r.interfaceFinder.Update() } else { interfaces, err := r.platformInterface.NetworkInterfaces() if err != nil { return err } if C.IsDarwin { err = r.interfaceFinder.Update() if err != nil { return err } // NEInterface only provides name,index and type interfaces = common.Map(interfaces, func(it adapter.NetworkInterface) adapter.NetworkInterface { iif, _ := r.interfaceFinder.ByIndex(it.Index) if iif != nil { it.Interface = *iif } return it }) } else { r.interfaceFinder.UpdateInterfaces(common.Map(interfaces, func(it adapter.NetworkInterface) control.Interface { return it.Interface })) } oldInterfaces := r.networkInterfaces.Load() newInterfaces := common.Filter(interfaces, func(it adapter.NetworkInterface) bool { return it.Flags&net.FlagUp != 0 }) r.networkInterfaces.Store(newInterfaces) if len(newInterfaces) > 0 && !slices.EqualFunc(oldInterfaces, newInterfaces, func(oldInterface adapter.NetworkInterface, newInterface adapter.NetworkInterface) bool { return oldInterface.Interface.Index == newInterface.Interface.Index && oldInterface.Interface.Name == newInterface.Interface.Name && oldInterface.Interface.Flags == newInterface.Interface.Flags && oldInterface.Type == newInterface.Type && oldInterface.Expensive == newInterface.Expensive && oldInterface.Constrained == newInterface.Constrained }) { r.logger.Info("updated available networks: ", strings.Join(common.Map(newInterfaces, func(it adapter.NetworkInterface) string { var options []string options = append(options, F.ToString(it.Type)) if it.Expensive { options = append(options, "expensive") } if it.Constrained { options = append(options, "constrained") } return F.ToString(it.Name, " (", strings.Join(options, ", "), ")") }), ", ")) } return nil } } func (r *NetworkManager) DefaultNetworkInterface() *adapter.NetworkInterface { iif := r.interfaceMonitor.DefaultInterface() if iif == nil { return nil } for _, it := range r.networkInterfaces.Load() { if it.Interface.Index == iif.Index { return &it } } return &adapter.NetworkInterface{Interface: *iif} } func (r *NetworkManager) NetworkInterfaces() []adapter.NetworkInterface { return r.networkInterfaces.Load() } func (r *NetworkManager) AutoDetectInterface() bool { return r.autoDetectInterface } func (r *NetworkManager) AutoDetectInterfaceFunc() control.Func { if r.platformInterface != nil && r.platformInterface.UsePlatformAutoDetectInterfaceControl() { return func(network, address string, conn syscall.RawConn) error { return control.Raw(conn, func(fd uintptr) error { return r.platformInterface.AutoDetectInterfaceControl(int(fd)) }) } } else { if r.interfaceMonitor == nil { return nil } return control.BindToInterfaceFunc(r.interfaceFinder, func(network string, address string) (interfaceName string, interfaceIndex int, err error) { remoteAddr := M.ParseSocksaddr(address).Addr if remoteAddr.IsValid() { iif, err := r.interfaceFinder.ByAddr(remoteAddr) if err == nil { return iif.Name, iif.Index, nil } } defaultInterface := r.interfaceMonitor.DefaultInterface() if defaultInterface == nil { return "", -1, tun.ErrNoRoute } return defaultInterface.Name, defaultInterface.Index, nil }) } } func (r *NetworkManager) ProtectFunc() control.Func { if r.platformInterface != nil && r.platformInterface.UsePlatformAutoDetectInterfaceControl() { return func(network, address string, conn syscall.RawConn) error { return control.Raw(conn, func(fd uintptr) error { return r.platformInterface.AutoDetectInterfaceControl(int(fd)) }) } } return nil } func (r *NetworkManager) DefaultOptions() adapter.NetworkOptions { return r.defaultOptions } func (r *NetworkManager) RegisterAutoRedirectOutputMark(mark uint32) error { if r.autoRedirectOutputMark > 0 { return E.New("only one auto-redirect can be configured") } r.autoRedirectOutputMark = mark return nil } func (r *NetworkManager) AutoRedirectOutputMark() uint32 { return r.autoRedirectOutputMark } func (r *NetworkManager) AutoRedirectOutputMarkFunc() control.Func { return func(network, address string, conn syscall.RawConn) error { if r.autoRedirectOutputMark == 0 { return nil } return control.RoutingMark(r.autoRedirectOutputMark)(network, address, conn) } } func (r *NetworkManager) NetworkMonitor() tun.NetworkUpdateMonitor { return r.networkMonitor } func (r *NetworkManager) InterfaceMonitor() tun.DefaultInterfaceMonitor { return r.interfaceMonitor } func (r *NetworkManager) PackageManager() tun.PackageManager { return r.packageManager } func (r *NetworkManager) NeedWIFIState() bool { return r.needWIFIState } func (r *NetworkManager) WIFIState() adapter.WIFIState { r.wifiStateMutex.RLock() defer r.wifiStateMutex.RUnlock() return r.wifiState } func (r *NetworkManager) onWIFIStateChanged(state adapter.WIFIState) { state.BSSID = adapter.NormalizeWIFIBSSID(state.BSSID) r.wifiStateMutex.Lock() if state != r.wifiState { r.wifiState = state r.wifiStateMutex.Unlock() if state.SSID != "" { r.logger.Info("WIFI state changed: SSID=", state.SSID, ", BSSID=", state.BSSID) } else { r.logger.Info("WIFI disconnected") } } else { r.wifiStateMutex.Unlock() } } func (r *NetworkManager) UpdateWIFIState() { var state adapter.WIFIState if r.wifiMonitor != nil { state = r.wifiMonitor.ReadWIFIState() } else if r.platformInterface != nil && r.platformInterface.UsePlatformWIFIMonitor() { state = r.platformInterface.ReadWIFIState() } else { return } r.onWIFIStateChanged(state) } func (r *NetworkManager) ResetNetwork() { if r.connectionManager != nil { r.connectionManager.CloseAll() } for _, endpoint := range r.endpoint.Endpoints() { listener, isListener := endpoint.(adapter.InterfaceUpdateListener) if isListener { listener.InterfaceUpdated() } } for _, inbound := range r.inbound.Inbounds() { listener, isListener := inbound.(adapter.InterfaceUpdateListener) if isListener { listener.InterfaceUpdated() } } for _, outbound := range r.outbound.Outbounds() { listener, isListener := outbound.(adapter.InterfaceUpdateListener) if isListener { listener.InterfaceUpdated() } } r.router.ResetNetwork() } func (r *NetworkManager) notifyInterfaceUpdate(defaultInterface *control.Interface, flags int) { if defaultInterface == nil { r.pauseManager.NetworkPause() r.logger.Error("missing default interface") return } r.pauseManager.NetworkWake() var options []string options = append(options, F.ToString("index ", defaultInterface.Index)) if C.IsAndroid && r.platformInterface == nil { var vpnStatus string if r.interfaceMonitor.AndroidVPNEnabled() { vpnStatus = "enabled" } else { vpnStatus = "disabled" } options = append(options, "vpn "+vpnStatus) } else if r.platformInterface != nil { networkInterface := common.Find(r.networkInterfaces.Load(), func(it adapter.NetworkInterface) bool { return it.Interface.Index == defaultInterface.Index }) if networkInterface.Name == "" { // race return } options = append(options, F.ToString("type ", networkInterface.Type)) if networkInterface.Expensive { options = append(options, "expensive") } if networkInterface.Constrained { options = append(options, "constrained") } } r.logger.Info("updated default interface ", defaultInterface.Name, ", ", strings.Join(options, ", ")) r.UpdateWIFIState() if !r.started { return } r.ResetNetwork() } func (r *NetworkManager) notifyWindowsPowerEvent(event int) { switch event { case winpowrprof.EVENT_SUSPEND: r.pauseManager.DevicePause() r.ResetNetwork() case winpowrprof.EVENT_RESUME: if !r.pauseManager.IsDevicePaused() { return } fallthrough case winpowrprof.EVENT_RESUME_AUTOMATIC: r.pauseManager.DeviceWake() r.ResetNetwork() } } func (r *NetworkManager) OnPackagesUpdated(packages int, sharedUsers int) { r.logger.Info("updated packages list: ", packages, " packages, ", sharedUsers, " shared users") } ================================================ FILE: route/platform_searcher.go ================================================ package route import ( "context" "net/netip" "syscall" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/process" N "github.com/sagernet/sing/common/network" ) type platformSearcher struct { platform adapter.PlatformInterface } func newPlatformSearcher(platform adapter.PlatformInterface) process.Searcher { return &platformSearcher{platform: platform} } func (s *platformSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { if !s.platform.UsePlatformConnectionOwnerFinder() { return nil, process.ErrNotFound } var ipProtocol int32 switch N.NetworkName(network) { case N.NetworkTCP: ipProtocol = syscall.IPPROTO_TCP case N.NetworkUDP: ipProtocol = syscall.IPPROTO_UDP default: return nil, process.ErrNotFound } request := &adapter.FindConnectionOwnerRequest{ IpProtocol: ipProtocol, SourceAddress: source.Addr().String(), SourcePort: int32(source.Port()), DestinationAddress: destination.Addr().String(), DestinationPort: int32(destination.Port()), } return s.platform.FindConnectionOwner(request) } func (s *platformSearcher) Close() error { return nil } ================================================ FILE: route/process_cache.go ================================================ package route import ( "context" "net/netip" "slices" "strings" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/process" ) type processCacheKey struct { Network string Source netip.AddrPort Destination netip.AddrPort } type processCacheEntry struct { result *adapter.ConnectionOwner err error } func (r *Router) findProcessInfoCached(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { key := processCacheKey{ Network: network, Source: source, Destination: destination, } if entry, ok := r.processCache.Get(key); ok { return entry.result, entry.err } result, err := process.FindProcessInfo(r.processSearcher, ctx, network, source, destination) r.processCache.Add(key, processCacheEntry{result: result, err: err}) return result, err } func (r *Router) searchProcessInfo(ctx context.Context, metadata *adapter.InboundContext) { if r.processSearcher == nil || metadata.ProcessInfo != nil || !r.isLocalSource(metadata.Source.Addr) { return } var originDestination netip.AddrPort if metadata.OriginDestination.IsValid() { originDestination = metadata.OriginDestination.AddrPort() } else if metadata.Destination.IsIP() { originDestination = metadata.Destination.AddrPort() } processInfo, err := r.findProcessInfoCached(ctx, metadata.Network, metadata.Source.AddrPort(), originDestination) if err != nil { r.logger.InfoContext(ctx, "failed to search process: ", err) return } metadata.ProcessInfo = processInfo if processInfo.ProcessPath != "" { if processInfo.UserName != "" { r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath, ", user: ", processInfo.UserName) } else if processInfo.UserId != -1 { r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath, ", user id: ", processInfo.UserId) } else { r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath) } return } if len(processInfo.AndroidPackageNames) > 0 { r.logger.InfoContext(ctx, "found package name: ", strings.Join(processInfo.AndroidPackageNames, ", ")) return } if processInfo.UserId != -1 { if processInfo.UserName != "" { r.logger.InfoContext(ctx, "found user: ", processInfo.UserName) } else { r.logger.InfoContext(ctx, "found user id: ", processInfo.UserId) } } } func (r *Router) isLocalSource(source netip.Addr) bool { if source.IsLoopback() { return true } if r.platformInterface != nil { if slices.Contains(r.platformInterface.MyInterfaceAddress(), source) { return true } } for _, netInterface := range r.network.InterfaceFinder().Interfaces() { for _, prefix := range netInterface.Addresses { if prefix.Addr() == source { return true } } } return false } ================================================ FILE: route/route.go ================================================ package route import ( "context" "errors" "net" "net/netip" "strings" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/sniff" C "github.com/sagernet/sing-box/constant" R "github.com/sagernet/sing-box/route/rule" "github.com/sagernet/sing-mux" "github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun/ping" "github.com/sagernet/sing-vmess" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/bufio/deadline" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/uot" "golang.org/x/exp/slices" ) // Deprecated: use RouteConnectionEx instead. func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { done := make(chan any) err := r.routeConnection(ctx, conn, metadata, N.OnceClose(func(it error) { close(done) })) if err != nil { return err } select { case <-done: case <-r.ctx.Done(): } return nil } func (r *Router) RouteConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { err := r.routeConnection(ctx, conn, metadata, onClose) if err != nil { N.CloseOnHandshakeFailure(conn, onClose, err) if E.IsClosedOrCanceled(err) || R.IsRejected(err) { r.logger.DebugContext(ctx, "connection closed: ", err) } else { r.logger.ErrorContext(ctx, err) } } } func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error { //nolint:staticcheck if metadata.InboundDetour != "" { if metadata.LastInbound == metadata.InboundDetour { return E.New("routing loop on detour: ", metadata.InboundDetour) } detour, loaded := r.inbound.Get(metadata.InboundDetour) if !loaded { return E.New("inbound detour not found: ", metadata.InboundDetour) } injectable, isInjectable := detour.(adapter.TCPInjectableInbound) if !isInjectable { return E.New("inbound detour is not TCP injectable: ", metadata.InboundDetour) } metadata.LastInbound = metadata.Inbound metadata.Inbound = metadata.InboundDetour metadata.InboundDetour = "" injectable.NewConnection(ctx, conn, metadata, onClose) return nil } metadata.Network = N.NetworkTCP switch metadata.Destination.Fqdn { case mux.Destination.Fqdn: return E.New("global multiplex is deprecated since sing-box v1.7.0, enable multiplex in Inbound fields instead.") case vmess.MuxDestination.Fqdn: return E.New("global multiplex (v2ray legacy) not supported since sing-box v1.7.0.") case uot.MagicAddress: return E.New("global UoT not supported since sing-box v1.7.0.") 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) } selectedRule, _, buffers, _, err := r.matchRule(ctx, &metadata, false, false, conn, nil) if err != nil { return err } var selectedOutbound adapter.Outbound if selectedRule != nil { switch action := selectedRule.Action().(type) { case *R.RuleActionRoute: var loaded bool selectedOutbound, loaded = r.outbound.Outbound(action.Outbound) if !loaded { buf.ReleaseMulti(buffers) return E.New("outbound not found: ", action.Outbound) } if !common.Contains(selectedOutbound.Network(), N.NetworkTCP) { buf.ReleaseMulti(buffers) return E.New("TCP is not supported by outbound: ", selectedOutbound.Tag()) } case *R.RuleActionBypass: if action.Outbound == "" { break } var loaded bool selectedOutbound, loaded = r.outbound.Outbound(action.Outbound) if !loaded { buf.ReleaseMulti(buffers) return E.New("outbound not found: ", action.Outbound) } if !common.Contains(selectedOutbound.Network(), N.NetworkTCP) { buf.ReleaseMulti(buffers) return E.New("TCP is not supported by outbound: ", selectedOutbound.Tag()) } case *R.RuleActionReject: buf.ReleaseMulti(buffers) if action.Method == C.RuleActionRejectMethodReply { return E.New("reject method `reply` is not supported for TCP connections") } return action.Error(ctx) case *R.RuleActionHijackDNS: for _, buffer := range buffers { conn = bufio.NewCachedConn(conn, buffer) } N.CloseOnHandshakeFailure(conn, onClose, r.hijackDNSStream(ctx, conn, metadata)) return nil } } if selectedRule == nil { defaultOutbound := r.outbound.Default() if !common.Contains(defaultOutbound.Network(), N.NetworkTCP) { buf.ReleaseMulti(buffers) return E.New("TCP is not supported by default outbound: ", defaultOutbound.Tag()) } selectedOutbound = defaultOutbound } for _, buffer := range buffers { conn = bufio.NewCachedConn(conn, buffer) } for _, tracker := range r.trackers { conn = tracker.RoutedConnection(ctx, conn, metadata, selectedRule, selectedOutbound) } if outboundHandler, isHandler := selectedOutbound.(adapter.ConnectionHandler); isHandler { outboundHandler.NewConnection(ctx, conn, metadata, onClose) } else { r.connection.NewConnection(ctx, selectedOutbound, conn, metadata, onClose) } return nil } func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { done := make(chan any) err := r.routePacketConnection(ctx, conn, metadata, N.OnceClose(func(it error) { close(done) })) if err != nil { conn.Close() if E.IsClosedOrCanceled(err) || R.IsRejected(err) { r.logger.DebugContext(ctx, "connection closed: ", err) } else { r.logger.ErrorContext(ctx, err) } } select { case <-done: case <-r.ctx.Done(): } return nil } func (r *Router) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { err := r.routePacketConnection(ctx, conn, metadata, onClose) if err != nil { N.CloseOnHandshakeFailure(conn, onClose, err) if E.IsClosedOrCanceled(err) || R.IsRejected(err) { r.logger.DebugContext(ctx, "connection closed: ", err) } else { r.logger.ErrorContext(ctx, err) } } } func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error { //nolint:staticcheck if metadata.InboundDetour != "" { if metadata.LastInbound == metadata.InboundDetour { return E.New("routing loop on detour: ", metadata.InboundDetour) } detour, loaded := r.inbound.Get(metadata.InboundDetour) if !loaded { return E.New("inbound detour not found: ", metadata.InboundDetour) } injectable, isInjectable := detour.(adapter.UDPInjectableInbound) if !isInjectable { return E.New("inbound detour is not UDP injectable: ", metadata.InboundDetour) } metadata.LastInbound = metadata.Inbound metadata.Inbound = metadata.InboundDetour metadata.InboundDetour = "" injectable.NewPacketConnection(ctx, conn, metadata, onClose) return nil } // TODO: move to UoT metadata.Network = N.NetworkUDP // Currently we don't have deadline usages for UDP connections /*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 } var selectedOutbound adapter.Outbound var selectReturn bool if selectedRule != nil { switch action := selectedRule.Action().(type) { case *R.RuleActionRoute: var loaded bool selectedOutbound, loaded = r.outbound.Outbound(action.Outbound) if !loaded { N.ReleaseMultiPacketBuffer(packetBuffers) return E.New("outbound not found: ", action.Outbound) } if !common.Contains(selectedOutbound.Network(), N.NetworkUDP) { N.ReleaseMultiPacketBuffer(packetBuffers) return E.New("UDP is not supported by outbound: ", selectedOutbound.Tag()) } case *R.RuleActionBypass: if action.Outbound == "" { break } var loaded bool selectedOutbound, loaded = r.outbound.Outbound(action.Outbound) if !loaded { N.ReleaseMultiPacketBuffer(packetBuffers) return E.New("outbound not found: ", action.Outbound) } if !common.Contains(selectedOutbound.Network(), N.NetworkUDP) { N.ReleaseMultiPacketBuffer(packetBuffers) return E.New("UDP is not supported by outbound: ", selectedOutbound.Tag()) } case *R.RuleActionReject: N.ReleaseMultiPacketBuffer(packetBuffers) if action.Method == C.RuleActionRejectMethodReply { return E.New("reject method `reply` is not supported for UDP connections") } return action.Error(ctx) case *R.RuleActionHijackDNS: return r.hijackDNSPacket(ctx, conn, packetBuffers, metadata, onClose) } } if selectedRule == nil || selectReturn { defaultOutbound := r.outbound.Default() if !common.Contains(defaultOutbound.Network(), N.NetworkUDP) { N.ReleaseMultiPacketBuffer(packetBuffers) return E.New("UDP is not supported by outbound: ", defaultOutbound.Tag()) } selectedOutbound = defaultOutbound } for _, buffer := range packetBuffers { conn = bufio.NewCachedPacketConn(conn, buffer.Buffer, buffer.Destination) N.PutPacketBuffer(buffer) } for _, tracker := range r.trackers { conn = tracker.RoutedPacketConnection(ctx, conn, metadata, selectedRule, selectedOutbound) } if metadata.FakeIP { conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, metadata.Destination) } if outboundHandler, isHandler := selectedOutbound.(adapter.PacketConnectionHandler); isHandler { outboundHandler.NewPacketConnection(ctx, conn, metadata, onClose) } else { r.connection.NewPacketConnection(ctx, selectedOutbound, conn, metadata, onClose) } return nil } func (r *Router) PreMatch(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration, supportBypass bool) (tun.DirectRouteDestination, error) { selectedRule, _, _, _, err := r.matchRule(r.ctx, &metadata, true, supportBypass, nil, nil) if err != nil { return nil, err } var directRouteOutbound adapter.DirectRouteOutbound if selectedRule != nil { switch action := selectedRule.Action().(type) { case *R.RuleActionReject: switch metadata.Network { case N.NetworkTCP: if action.Method == C.RuleActionRejectMethodReply { return nil, E.New("reject method `reply` is not supported for TCP connections") } case N.NetworkUDP: if action.Method == C.RuleActionRejectMethodReply { return nil, E.New("reject method `reply` is not supported for UDP connections") } } return nil, action.Error(context.Background()) case *R.RuleActionBypass: if supportBypass { return nil, &R.BypassedError{Cause: tun.ErrBypass} } if routeContext == nil { return nil, nil } outbound, loaded := r.outbound.Outbound(action.Outbound) if !loaded { return nil, E.New("outbound not found: ", action.Outbound) } if !common.Contains(outbound.Network(), metadata.Network) { return nil, E.New(metadata.Network, " is not supported by outbound: ", action.Outbound) } directRouteOutbound = outbound.(adapter.DirectRouteOutbound) case *R.RuleActionRoute: if routeContext == nil { return nil, nil } outbound, loaded := r.outbound.Outbound(action.Outbound) if !loaded { return nil, E.New("outbound not found: ", action.Outbound) } if !common.Contains(outbound.Network(), metadata.Network) { return nil, E.New(metadata.Network, " is not supported by outbound: ", action.Outbound) } directRouteOutbound = outbound.(adapter.DirectRouteOutbound) } } if directRouteOutbound == nil { if selectedRule != nil || metadata.Network != N.NetworkICMP { return nil, nil } defaultOutbound := r.outbound.Default() if !common.Contains(defaultOutbound.Network(), metadata.Network) { return nil, E.New(metadata.Network, " is not supported by default outbound: ", defaultOutbound.Tag()) } directRouteOutbound = defaultOutbound.(adapter.DirectRouteOutbound) } if metadata.Destination.IsDomain() { if len(metadata.DestinationAddresses) == 0 { var strategy C.DomainStrategy if metadata.Source.IsIPv4() { strategy = C.DomainStrategyIPv4Only } else { strategy = C.DomainStrategyIPv6Only } err = r.actionResolve(r.ctx, &metadata, &R.RuleActionResolve{ Strategy: strategy, }) if err != nil { return nil, err } } var newDestination netip.Addr if metadata.Source.IsIPv4() { for _, address := range metadata.DestinationAddresses { if address.Is4() { newDestination = address break } } } else { for _, address := range metadata.DestinationAddresses { if address.Is6() { newDestination = address break } } } if !newDestination.IsValid() { if metadata.Source.IsIPv4() { return nil, E.New("no IPv4 address found for domain: ", metadata.Destination.Fqdn) } else { return nil, E.New("no IPv6 address found for domain: ", metadata.Destination.Fqdn) } } metadata.Destination = M.Socksaddr{ Addr: newDestination, } routeContext = ping.NewContextDestinationWriter(routeContext, metadata.OriginDestination.Addr) var routeDestination tun.DirectRouteDestination routeDestination, err = directRouteOutbound.NewDirectRouteConnection(metadata, routeContext, timeout) if err != nil { return nil, err } return ping.NewDestinationWriter(routeDestination, newDestination), nil } return directRouteOutbound.NewDirectRouteConnection(metadata, routeContext, timeout) } func (r *Router) matchRule( ctx context.Context, metadata *adapter.InboundContext, preMatch bool, supportBypass bool, inputConn net.Conn, inputPacketConn N.PacketConn, ) ( selectedRule adapter.Rule, selectedRuleIndex int, 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 { fatalErr = E.New("missing fakeip record, try enable `experimental.cache_file`") return } if domain != "" { metadata.OriginDestination = metadata.Destination metadata.Destination = M.Socksaddr{ Fqdn: domain, Port: metadata.Destination.Port, } metadata.FakeIP = true r.logger.DebugContext(ctx, "found fakeip domain: ", domain) } } else if metadata.Domain == "" { domain, loaded := r.dns.LookupReverseMapping(metadata.Destination.Addr) if loaded { metadata.Domain = domain r.logger.DebugContext(ctx, "found reserve mapped domain: ", metadata.Domain) } } if metadata.Destination.IsIPv4() { metadata.IPVersion = 4 } else if metadata.Destination.IsIPv6() { metadata.IPVersion = 6 } match: for currentRuleIndex, currentRule := range r.rules { metadata.ResetRuleCache() if !currentRule.Match(metadata) { continue } if !preMatch { ruleDescription := currentRule.String() if ruleDescription != "" { r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] ", currentRule, " => ", currentRule.Action()) } else { r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] => ", currentRule.Action()) } } else { switch currentRule.Action().Type() { case C.RuleActionTypeReject: ruleDescription := currentRule.String() if ruleDescription != "" { r.logger.DebugContext(ctx, "pre-match[", currentRuleIndex, "] ", currentRule, " => ", currentRule.Action()) } else { r.logger.DebugContext(ctx, "pre-match[", currentRuleIndex, "] => ", currentRule.Action()) } } } var routeOptions *R.RuleActionRouteOptions switch action := currentRule.Action().(type) { case *R.RuleActionRoute: routeOptions = &action.RuleActionRouteOptions case *R.RuleActionRouteOptions: routeOptions = action case *R.RuleActionBypass: if action.Outbound != "" { routeOptions = &action.RuleActionRouteOptions } } if routeOptions != nil { // TODO: add nat if (routeOptions.OverrideAddress.IsValid() || routeOptions.OverridePort > 0) && !metadata.RouteOriginalDestination.IsValid() { metadata.RouteOriginalDestination = metadata.Destination } if routeOptions.OverrideAddress.IsValid() { metadata.Destination = M.Socksaddr{ Addr: routeOptions.OverrideAddress.Addr, Port: metadata.Destination.Port, Fqdn: routeOptions.OverrideAddress.Fqdn, } metadata.DestinationAddresses = nil } if routeOptions.OverridePort > 0 { metadata.Destination = M.Socksaddr{ Addr: metadata.Destination.Addr, Port: routeOptions.OverridePort, Fqdn: metadata.Destination.Fqdn, } } if routeOptions.NetworkStrategy != nil { metadata.NetworkStrategy = routeOptions.NetworkStrategy } if len(routeOptions.NetworkType) > 0 { metadata.NetworkType = routeOptions.NetworkType } if len(routeOptions.FallbackNetworkType) > 0 { metadata.FallbackNetworkType = routeOptions.FallbackNetworkType } if routeOptions.FallbackDelay != 0 { metadata.FallbackDelay = routeOptions.FallbackDelay } if routeOptions.UDPDisableDomainUnmapping { metadata.UDPDisableDomainUnmapping = true } if routeOptions.UDPConnect { metadata.UDPConnect = true } if routeOptions.UDPTimeout > 0 { metadata.UDPTimeout = routeOptions.UDPTimeout } if routeOptions.TLSFragment { metadata.TLSFragment = true metadata.TLSFragmentFallbackDelay = routeOptions.TLSFragmentFallbackDelay } 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: if !preMatch { newBuffer, newPacketBuffers, newErr := r.actionSniff(ctx, metadata, action, inputConn, inputPacketConn, buffers, packetBuffers) if newBuffer != nil { buffers = append(buffers, newBuffer) } else if len(newPacketBuffers) > 0 { packetBuffers = append(packetBuffers, newPacketBuffers...) } if newErr != nil { fatalErr = newErr return } } else if metadata.Network != N.NetworkICMP { selectedRule = currentRule selectedRuleIndex = currentRuleIndex break match } case *R.RuleActionResolve: fatalErr = r.actionResolve(ctx, metadata, action) if fatalErr != nil { return } } actionType := currentRule.Action().Type() if actionType == C.RuleActionTypeRoute || actionType == C.RuleActionTypeReject || actionType == C.RuleActionTypeHijackDNS { selectedRule = currentRule selectedRuleIndex = currentRuleIndex break match } if actionType == C.RuleActionTypeBypass { bypassAction := currentRule.Action().(*R.RuleActionBypass) if !supportBypass && bypassAction.Outbound == "" { continue match } selectedRule = currentRule selectedRuleIndex = currentRuleIndex break match } } return } func (r *Router) actionSniff( ctx context.Context, metadata *adapter.InboundContext, action *R.RuleActionSniff, inputConn net.Conn, inputPacketConn N.PacketConn, inputBuffers []*buf.Buffer, inputPacketBuffers []*N.PacketBuffer, ) (buffer *buf.Buffer, packetBuffers []*N.PacketBuffer, fatalErr error) { if sniff.Skip(metadata) { r.logger.DebugContext(ctx, "sniff skipped due to port considered as server-first") return } else if metadata.Protocol != "" { r.logger.DebugContext(ctx, "duplicate sniff skipped") return } if inputConn != nil { if len(action.StreamSniffers) == 0 && len(action.PacketSniffers) > 0 { return } else if slices.Equal(metadata.SnifferNames, action.SnifferNames) && metadata.SniffError != nil && !errors.Is(metadata.SniffError, sniff.ErrNeedMoreData) { r.logger.DebugContext(ctx, "packet sniff skipped due to previous error: ", metadata.SniffError) return } var streamSniffers []sniff.StreamSniffer if len(action.StreamSniffers) > 0 { streamSniffers = action.StreamSniffers } else { streamSniffers = []sniff.StreamSniffer{ sniff.TLSClientHello, sniff.HTTPHost, sniff.StreamDomainNameQuery, sniff.BitTorrent, sniff.SSH, sniff.RDP, } } sniffBuffer := buf.NewPacket() err := sniff.PeekStream( ctx, metadata, inputConn, inputBuffers, sniffBuffer, action.Timeout, streamSniffers..., ) metadata.SnifferNames = action.SnifferNames metadata.SniffError = err if err == nil { //goland:noinspection GoDeprecation if action.OverrideDestination && M.IsDomainName(metadata.Domain) { metadata.Destination = M.Socksaddr{ Fqdn: metadata.Domain, Port: metadata.Destination.Port, } } if metadata.Domain != "" && metadata.Client != "" { r.logger.DebugContext(ctx, "sniffed protocol: ", metadata.Protocol, ", domain: ", metadata.Domain, ", client: ", metadata.Client) } else if metadata.Domain != "" { r.logger.DebugContext(ctx, "sniffed protocol: ", metadata.Protocol, ", domain: ", metadata.Domain) } else { r.logger.DebugContext(ctx, "sniffed protocol: ", metadata.Protocol) } } if !sniffBuffer.IsEmpty() { buffer = sniffBuffer } else { sniffBuffer.Release() } } else if inputPacketConn != nil { if len(action.PacketSniffers) == 0 && len(action.StreamSniffers) > 0 { return } else if slices.Equal(metadata.SnifferNames, action.SnifferNames) && metadata.SniffError != nil && !errors.Is(metadata.SniffError, sniff.ErrNeedMoreData) { r.logger.DebugContext(ctx, "packet sniff skipped due to previous error: ", metadata.SniffError) return } quicMoreData := func() bool { return slices.Equal(metadata.SnifferNames, action.SnifferNames) && errors.Is(metadata.SniffError, sniff.ErrNeedMoreData) } var packetSniffers []sniff.PacketSniffer if len(action.PacketSniffers) > 0 { packetSniffers = action.PacketSniffers } else { packetSniffers = []sniff.PacketSniffer{ sniff.DomainNameQuery, sniff.QUICClientHello, sniff.STUNMessage, sniff.UTP, sniff.UDPTracker, sniff.DTLSRecord, sniff.NTP, } } var err error for _, packetBuffer := range inputPacketBuffers { if quicMoreData() { err = sniff.PeekPacket( ctx, metadata, packetBuffer.Buffer.Bytes(), sniff.QUICClientHello, ) } else { err = sniff.PeekPacket( ctx, metadata, packetBuffer.Buffer.Bytes(), packetSniffers..., ) } metadata.SnifferNames = action.SnifferNames metadata.SniffError = err if errors.Is(err, sniff.ErrNeedMoreData) { // TODO: replace with generic message when there are more multi-packet protocols r.logger.DebugContext(ctx, "attempt to sniff fragmented QUIC client hello") continue } goto finally } packetBuffers = inputPacketBuffers for { var ( sniffBuffer = buf.NewPacket() destination M.Socksaddr done = make(chan struct{}) ) go func() { sniffTimeout := C.ReadPayloadTimeout if action.Timeout > 0 { sniffTimeout = action.Timeout } inputPacketConn.SetReadDeadline(time.Now().Add(sniffTimeout)) destination, err = inputPacketConn.ReadPacket(sniffBuffer) inputPacketConn.SetReadDeadline(time.Time{}) close(done) }() select { case <-done: case <-ctx.Done(): inputPacketConn.Close() fatalErr = ctx.Err() return } if err != nil { sniffBuffer.Release() if !errors.Is(err, context.DeadlineExceeded) { fatalErr = err return } } else { if quicMoreData() { err = sniff.PeekPacket( ctx, metadata, sniffBuffer.Bytes(), sniff.QUICClientHello, ) } else { err = sniff.PeekPacket( ctx, metadata, sniffBuffer.Bytes(), packetSniffers..., ) } packetBuffer := N.NewPacketBuffer() *packetBuffer = N.PacketBuffer{ Buffer: sniffBuffer, Destination: destination, } packetBuffers = append(packetBuffers, packetBuffer) metadata.SnifferNames = action.SnifferNames metadata.SniffError = err if errors.Is(err, sniff.ErrNeedMoreData) { // TODO: replace with generic message when there are more multi-packet protocols r.logger.DebugContext(ctx, "attempt to sniff fragmented QUIC client hello") continue } } goto finally } finally: if err == nil { //goland:noinspection GoDeprecation if action.OverrideDestination && M.IsDomainName(metadata.Domain) { metadata.Destination = M.Socksaddr{ Fqdn: metadata.Domain, Port: metadata.Destination.Port, } } if metadata.Domain != "" && metadata.Client != "" { r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", domain: ", metadata.Domain, ", client: ", metadata.Client) } else if metadata.Domain != "" { r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", domain: ", metadata.Domain) } else if metadata.Client != "" { r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", client: ", metadata.Client) } else { r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol) } } } return } func (r *Router) actionResolve(ctx context.Context, metadata *adapter.InboundContext, action *R.RuleActionResolve) error { if metadata.Destination.IsDomain() { var transport adapter.DNSTransport if action.Server != "" { var loaded bool transport, loaded = r.dnsTransport.Transport(action.Server) if !loaded { return E.New("DNS server not found: ", action.Server) } } addresses, err := r.dns.Lookup(adapter.WithContext(ctx, metadata), metadata.Destination.Fqdn, adapter.DNSQueryOptions{ Transport: transport, Strategy: action.Strategy, DisableCache: action.DisableCache, DisableOptimisticCache: action.DisableOptimisticCache, RewriteTTL: action.RewriteTTL, Timeout: action.Timeout, ClientSubnet: action.ClientSubnet, }) if err != nil { return err } metadata.DestinationAddresses = addresses r.logger.DebugContext(ctx, "resolved [", strings.Join(F.MapToString(metadata.DestinationAddresses), " "), "]") } return nil } ================================================ FILE: route/router.go ================================================ package route import ( "context" "os" "runtime" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/process" "github.com/sagernet/sing-box/common/taskmonitor" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" R "github.com/sagernet/sing-box/route/rule" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/task" "github.com/sagernet/sing/contrab/freelru" "github.com/sagernet/sing/contrab/maphash" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/pause" ) var _ adapter.Router = (*Router)(nil) type Router struct { ctx context.Context logger log.ContextLogger inbound adapter.InboundManager outbound adapter.OutboundManager dns adapter.DNSRouter dnsTransport adapter.DNSTransportManager connection adapter.ConnectionManager network adapter.NetworkManager httpClientManager adapter.HTTPClientManager 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 started bool } func NewRouter(ctx context.Context, logFactory log.Factory, options option.RouteOptions, dnsOptions option.DNSOptions) *Router { return &Router{ ctx: ctx, logger: logFactory.NewLogger("router"), inbound: service.FromContext[adapter.InboundManager](ctx), outbound: service.FromContext[adapter.OutboundManager](ctx), dns: service.FromContext[adapter.DNSRouter](ctx), 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, 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), } } 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, "]") } r.rules = append(r.rules, rule) } for i, options := range ruleSets { if _, exists := r.ruleSetMap[options.Tag]; exists { return E.New("duplicate rule-set tag: ", options.Tag) } ruleSet, err := R.NewRuleSet(r.ctx, r.logger, options) if err != nil { return E.Cause(err, "parse rule-set[", i, "]") } r.ruleSets = append(r.ruleSets, ruleSet) r.ruleSetMap[options.Tag] = ruleSet } return nil } 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 { monitor.Start("initialize rule-set") 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, startContext) if err != nil { return E.Cause(err, "initialize rule-set[", i, "]") } return nil }) } ruleSetStartGroup.Concurrency(5) ruleSetStartGroup.FastFail() err := ruleSetStartGroup.Run(r.ctx) monitor.Finish() if err != nil { return err } } if startContext != nil { startContext.Close() } r.network.Initialize(r.ruleSets) needFindProcess := r.needFindProcess for _, ruleSet := range r.ruleSets { metadata := ruleSet.Metadata() if metadata.ContainsProcessRule { needFindProcess = true } } if C.IsAndroid && r.platformInterface != nil { needFindProcess = true } r.needFindProcess = needFindProcess if needFindProcess { if r.platformInterface != nil && r.platformInterface.UsePlatformConnectionOwnerFinder() { r.processSearcher = newPlatformSearcher(r.platformInterface) } else { monitor.Start("initialize process searcher") searcher, err := process.NewSearcher(process.Config{ Logger: r.logger, PackageManager: r.network.PackageManager(), }) monitor.Finish() if err != nil { if err != os.ErrInvalid { r.logger.Warn(E.Cause(err, "create process searcher")) } } else { r.processSearcher = searcher } } } if r.processSearcher != nil { processCache := common.Must1(freelru.NewSharded[processCacheKey, processCacheEntry](256, maphash.NewHasher[processCacheKey]().Hash32)) processCache.SetLifetime(200 * time.Millisecond) r.processCache = processCache } case adapter.StartStatePostStart: for i, rule := range r.rules { monitor.Start("initialize rule[", i, "]") err := rule.Start() monitor.Finish() if err != nil { return E.Cause(err, "initialize rule[", i, "]") } } for _, ruleSet := range r.ruleSets { monitor.Start("post start rule_set[", ruleSet.Name(), "]") err := ruleSet.PostStart() monitor.Finish() if err != nil { return E.Cause(err, "post start rule_set[", ruleSet.Name(), "]") } } r.started = true return nil case adapter.StartStateStarted: for _, ruleSet := range r.ruleSets { ruleSet.Cleanup() } runtime.GC() } return nil } 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 { return E.Cause(err, "close rule[", i, "]") }) monitor.Finish() } for i, ruleSet := range r.ruleSets { monitor.Start("close rule-set[", i, "]") err = E.Append(err, ruleSet.Close(), func(err error) error { return E.Cause(err, "close rule-set[", i, "]") }) monitor.Finish() } if r.processSearcher != nil { monitor.Start("close process searcher") err = E.Append(err, r.processSearcher.Close(), func(err error) error { return E.Cause(err, "close process searcher") }) monitor.Finish() } return err } func (r *Router) RuleSet(tag string) (adapter.RuleSet, bool) { ruleSet, loaded := r.ruleSetMap[tag] return ruleSet, loaded } func (r *Router) Rules() []adapter.Rule { return r.rules } func (r *Router) AppendTracker(tracker adapter.ConnectionTracker) { r.trackers = append(r.trackers, tracker) } 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.httpClientManager.ResetNetwork() r.dns.ResetNetwork() } ================================================ FILE: route/rule/match_state.go ================================================ package rule import "github.com/sagernet/sing-box/adapter" type ruleMatchState uint8 const ( ruleMatchSourceAddress ruleMatchState = 1 << iota ruleMatchSourcePort ruleMatchDestinationAddress ruleMatchDestinationPort ) type ruleMatchStateSet uint16 func singleRuleMatchState(state ruleMatchState) ruleMatchStateSet { return 1 << state } func emptyRuleMatchState() ruleMatchStateSet { return singleRuleMatchState(0) } func (s ruleMatchStateSet) isEmpty() bool { return s == 0 } func (s ruleMatchStateSet) contains(state ruleMatchState) bool { return s&(1< 0 } func (r *abstractDefaultRule) destinationIPCIDRMatchesDestination(metadata *adapter.InboundContext) bool { return !metadata.IgnoreDestinationIPCIDRMatch && !metadata.IPCIDRMatchSource && len(r.destinationIPCIDRItems) > 0 } func (r *abstractDefaultRule) requiresSourceAddressMatch(metadata *adapter.InboundContext) bool { return len(r.sourceAddressItems) > 0 || r.destinationIPCIDRMatchesSource(metadata) } func (r *abstractDefaultRule) requiresDestinationAddressMatch(metadata *adapter.InboundContext) bool { return len(r.destinationAddressItems) > 0 || r.destinationIPCIDRMatchesDestination(metadata) } func (r *abstractDefaultRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { return r.matchStatesWithBase(metadata, 0) } func (r *abstractDefaultRule) matchStatesWithBase(metadata *adapter.InboundContext, inheritedBase ruleMatchState) ruleMatchStateSet { if len(r.allItems) == 0 { return emptyRuleMatchState().withBase(inheritedBase) } evaluationBase := inheritedBase if r.invert { evaluationBase = 0 } baseState := evaluationBase if len(r.sourceAddressItems) > 0 { metadata.DidMatch = true if matchAnyItem(r.sourceAddressItems, metadata) { baseState |= ruleMatchSourceAddress } } if r.destinationIPCIDRMatchesSource(metadata) && !baseState.has(ruleMatchSourceAddress) { metadata.DidMatch = true if matchAnyItem(r.destinationIPCIDRItems, metadata) { baseState |= ruleMatchSourceAddress } } else if r.destinationIPCIDRMatchesSource(metadata) { metadata.DidMatch = true } if len(r.sourcePortItems) > 0 { metadata.DidMatch = true if matchAnyItem(r.sourcePortItems, metadata) { baseState |= ruleMatchSourcePort } } if len(r.destinationAddressItems) > 0 { metadata.DidMatch = true if matchAnyItem(r.destinationAddressItems, metadata) { baseState |= ruleMatchDestinationAddress } } if r.destinationIPCIDRMatchesDestination(metadata) && !baseState.has(ruleMatchDestinationAddress) { metadata.DidMatch = true if matchAnyItem(r.destinationIPCIDRItems, metadata) { baseState |= ruleMatchDestinationAddress } } else if r.destinationIPCIDRMatchesDestination(metadata) { metadata.DidMatch = true } if len(r.destinationPortItems) > 0 { metadata.DidMatch = true if matchAnyItem(r.destinationPortItems, metadata) { baseState |= ruleMatchDestinationPort } } for _, item := range r.items { metadata.DidMatch = true if !item.Match(metadata) { return r.invertedFailure(inheritedBase) } } var stateSet ruleMatchStateSet if r.ruleSetItem != nil { metadata.DidMatch = true stateSet = matchRuleItemStatesWithBase(r.ruleSetItem, metadata, baseState) } else { stateSet = singleRuleMatchState(baseState) } stateSet = stateSet.filter(func(state ruleMatchState) bool { if r.requiresSourceAddressMatch(metadata) && !state.has(ruleMatchSourceAddress) { return false } if len(r.sourcePortItems) > 0 && !state.has(ruleMatchSourcePort) { return false } if r.requiresDestinationAddressMatch(metadata) && !state.has(ruleMatchDestinationAddress) { return false } if len(r.destinationPortItems) > 0 && !state.has(ruleMatchDestinationPort) { return false } return true }) if stateSet.isEmpty() { return r.invertedFailure(inheritedBase) } if r.invert { if metadata.IgnoreDestinationIPCIDRMatch && stateSet == emptyRuleMatchState() && !metadata.DidMatch && len(r.destinationIPCIDRItems) > 0 { return emptyRuleMatchState().withBase(inheritedBase) } return 0 } return stateSet } func (r *abstractDefaultRule) invertedFailure(base ruleMatchState) ruleMatchStateSet { if r.invert { return emptyRuleMatchState().withBase(base) } return 0 } func (r *abstractDefaultRule) Action() adapter.RuleAction { return r.action } func (r *abstractDefaultRule) String() string { if !r.invert { return strings.Join(F.MapToString(r.allItems), " ") } else { return "!(" + strings.Join(F.MapToString(r.allItems), " ") + ")" } } type abstractLogicalRule struct { rules []adapter.HeadlessRule mode string invert bool action adapter.RuleAction } func (r *abstractLogicalRule) Type() string { return C.RuleTypeLogical } func (r *abstractLogicalRule) Start() error { for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (interface { Start() error }, bool, ) { rule, loaded := it.(interface { Start() error }) return rule, loaded }) { err := rule.Start() if err != nil { return err } } return nil } func (r *abstractLogicalRule) Close() error { for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (io.Closer, bool) { rule, loaded := it.(io.Closer) return rule, loaded }) { err := rule.Close() if err != nil { return err } } return nil } func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool { return !r.matchStates(metadata).isEmpty() } func (r *abstractLogicalRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { return r.matchStatesWithBase(metadata, 0) } func (r *abstractLogicalRule) matchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet { evaluationBase := base if r.invert { evaluationBase = 0 } var stateSet ruleMatchStateSet if r.mode == C.LogicalTypeAnd { stateSet = emptyRuleMatchState().withBase(evaluationBase) for _, rule := range r.rules { nestedMetadata := *metadata nestedMetadata.ResetRuleCache() nestedStateSet := matchHeadlessRuleStatesWithBase(rule, &nestedMetadata, evaluationBase) if nestedStateSet.isEmpty() { if r.invert { return emptyRuleMatchState().withBase(base) } return 0 } stateSet = stateSet.combine(nestedStateSet) } } else { for _, rule := range r.rules { nestedMetadata := *metadata nestedMetadata.ResetRuleCache() stateSet = stateSet.merge(matchHeadlessRuleStatesWithBase(rule, &nestedMetadata, evaluationBase)) } if stateSet.isEmpty() { if r.invert { return emptyRuleMatchState().withBase(base) } return 0 } } if r.invert { return 0 } return stateSet } func (r *abstractLogicalRule) Action() adapter.RuleAction { return r.action } func (r *abstractLogicalRule) String() string { var op string switch r.mode { case C.LogicalTypeAnd: op = "&&" case C.LogicalTypeOr: op = "||" } if !r.invert { return strings.Join(F.MapToString(r.rules), " "+op+" ") } else { return "!(" + strings.Join(F.MapToString(r.rules), " "+op+" ") + ")" } } func matchAnyItem(items []RuleItem, metadata *adapter.InboundContext) bool { return common.Any(items, func(it RuleItem) bool { return it.Match(metadata) }) } func (s ruleMatchState) has(target ruleMatchState) bool { return s&target != 0 } ================================================ FILE: route/rule/rule_abstract_test.go ================================================ package rule import ( "context" "testing" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common/x/list" "github.com/stretchr/testify/require" "go4.org/netipx" ) type fakeRuleSet struct { matched bool } func (f *fakeRuleSet) Name() string { return "fake-rule-set" } func (f *fakeRuleSet) StartContext(context.Context, *adapter.HTTPStartContext) error { return nil } func (f *fakeRuleSet) PostStart() error { return nil } func (f *fakeRuleSet) Metadata() adapter.RuleSetMetadata { return adapter.RuleSetMetadata{} } func (f *fakeRuleSet) ExtractIPSet() []*netipx.IPSet { return nil } func (f *fakeRuleSet) IncRef() {} func (f *fakeRuleSet) DecRef() {} func (f *fakeRuleSet) Cleanup() {} func (f *fakeRuleSet) RegisterCallback(adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] { return nil } func (f *fakeRuleSet) UnregisterCallback(*list.Element[adapter.RuleSetUpdateCallback]) {} func (f *fakeRuleSet) Close() error { return nil } func (f *fakeRuleSet) Match(*adapter.InboundContext) bool { return f.matched } func (f *fakeRuleSet) String() string { return "fake-rule-set" } type fakeRuleItem struct { matched bool } func (f *fakeRuleItem) Match(*adapter.InboundContext) bool { return f.matched } func (f *fakeRuleItem) String() string { return "fake-rule-item" } func newRuleSetOnlyRule(ruleSetMatched bool, invert bool) *DefaultRule { ruleSetItem := &RuleSetItem{ setList: []adapter.RuleSet{&fakeRuleSet{matched: ruleSetMatched}}, } return &DefaultRule{ abstractDefaultRule: abstractDefaultRule{ ruleSetItem: ruleSetItem, allItems: []RuleItem{ruleSetItem}, invert: invert, }, } } func newSingleItemRule(matched bool) *DefaultRule { item := &fakeRuleItem{matched: matched} return &DefaultRule{ abstractDefaultRule: abstractDefaultRule{ items: []RuleItem{item}, allItems: []RuleItem{item}, }, } } func TestAbstractDefaultRule_RuleSetOnly_InvertFalse(t *testing.T) { t.Parallel() require.True(t, newRuleSetOnlyRule(true, false).Match(&adapter.InboundContext{})) require.False(t, newRuleSetOnlyRule(false, false).Match(&adapter.InboundContext{})) } func TestAbstractDefaultRule_RuleSetOnly_InvertTrue(t *testing.T) { t.Parallel() require.False(t, newRuleSetOnlyRule(true, true).Match(&adapter.InboundContext{})) require.True(t, newRuleSetOnlyRule(false, true).Match(&adapter.InboundContext{})) } func TestAbstractLogicalRule_And_WithRuleSetInvert(t *testing.T) { t.Parallel() testCases := []struct { name string aMatched bool ruleSetBMatch bool expected bool }{ { name: "A true B true", aMatched: true, ruleSetBMatch: true, expected: false, }, { name: "A true B false", aMatched: true, ruleSetBMatch: false, expected: true, }, { name: "A false B true", aMatched: false, ruleSetBMatch: true, expected: false, }, { name: "A false B false", aMatched: false, ruleSetBMatch: false, expected: false, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { t.Parallel() logicalRule := &abstractLogicalRule{ mode: C.LogicalTypeAnd, rules: []adapter.HeadlessRule{ newSingleItemRule(testCase.aMatched), newRuleSetOnlyRule(testCase.ruleSetBMatch, true), }, } require.Equal(t, testCase.expected, logicalRule.Match(&adapter.InboundContext{})) }) } } ================================================ FILE: route/rule/rule_action.go ================================================ package rule import ( "context" "errors" "net/netip" "strings" "sync" "time" "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" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "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: routeOptions, }, nil case C.RuleActionTypeRouteOptions: 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: routeOptions, }, nil case C.RuleActionTypeDirect: directDialer, err := dialer.New(ctx, option.DialerOptions(action.DirectOptions), false) if err != nil { return nil, err } var description string descriptions := action.DirectOptions.Descriptions() switch len(descriptions) { case 0: case 1: description = F.ToString("(", descriptions[0], ")") case 2: description = F.ToString("(", descriptions[0], ",", descriptions[1], ")") default: description = F.ToString("(", descriptions[0], ",", descriptions[1], ",...)") } return &RuleActionDirect{ Dialer: directDialer, description: description, }, nil case C.RuleActionTypeReject: return &RuleActionReject{ Method: action.RejectOptions.Method, NoDrop: action.RejectOptions.NoDrop, logger: logger, }, nil case C.RuleActionTypeHijackDNS: return &RuleActionHijackDNS{}, nil case C.RuleActionTypeSniff: sniffAction := &RuleActionSniff{ SnifferNames: action.SniffOptions.Sniffer, Timeout: time.Duration(action.SniffOptions.Timeout), } return sniffAction, sniffAction.build() 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, RewriteTTL: action.ResolveOptions.RewriteTTL, ClientSubnet: action.ResolveOptions.ClientSubnet.Build(netip.Prefix{}), }, nil default: panic(F.ToString("unknown rule action: ", action.Action)) } } func NewDNSRuleAction(logger logger.ContextLogger, action option.DNSRuleAction) adapter.RuleAction { switch action.Action { case "": return nil case C.RuleActionTypeRoute: return &RuleActionDNSRoute{ 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, 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), Timeout: time.Duration(action.RouteOptions.Timeout), 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), Timeout: time.Duration(action.RouteOptionsOptions.Timeout), 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{ Method: action.RejectOptions.Method, NoDrop: action.RejectOptions.NoDrop, logger: logger, } case C.RuleActionTypePredefined: return &RuleActionPredefined{ Rcode: action.PredefinedOptions.Rcode.Build(), Answer: common.Map(action.PredefinedOptions.Answer, option.DNSRecordOptions.Build), Ns: common.Map(action.PredefinedOptions.Ns, option.DNSRecordOptions.Build), Extra: common.Map(action.PredefinedOptions.Extra, option.DNSRecordOptions.Build), } default: panic(F.ToString("unknown rule action: ", action.Action)) } } type RuleActionRoute struct { Outbound string RuleActionRouteOptions } func (r *RuleActionRoute) Type() string { return C.RuleActionTypeRoute } func (r *RuleActionRoute) String() string { var descriptions []string descriptions = append(descriptions, r.Outbound) descriptions = append(descriptions, r.Descriptions()...) return F.ToString("route(", strings.Join(descriptions, ","), ")") } type RuleActionBypass struct { Outbound string RuleActionRouteOptions } func (r *RuleActionBypass) Type() string { return C.RuleActionTypeBypass } func (r *RuleActionBypass) String() string { if r.Outbound == "" { return "bypass()" } var descriptions []string descriptions = append(descriptions, r.Outbound) descriptions = append(descriptions, r.Descriptions()...) return F.ToString("bypass(", strings.Join(descriptions, ","), ")") } type RuleActionRouteOptions struct { OverrideAddress M.Socksaddr OverridePort uint16 NetworkStrategy *C.NetworkStrategy NetworkType []C.InterfaceType FallbackNetworkType []C.InterfaceType FallbackDelay time.Duration UDPDisableDomainUnmapping bool UDPConnect bool UDPTimeout time.Duration TLSFragment bool TLSFragmentFallbackDelay time.Duration TLSRecordFragment bool TLSSpoof string TLSSpoofMethod tlsspoof.Method } func (r *RuleActionRouteOptions) Type() string { return C.RuleActionTypeRouteOptions } func (r *RuleActionRouteOptions) String() string { return F.ToString("route-options(", strings.Join(r.Descriptions(), ","), ")") } func (r *RuleActionRouteOptions) Descriptions() []string { var descriptions []string if r.OverrideAddress.IsValid() { descriptions = append(descriptions, F.ToString("override-address=", r.OverrideAddress.AddrString())) } if r.OverridePort > 0 { descriptions = append(descriptions, F.ToString("override-port=", r.OverridePort)) } if r.NetworkStrategy != nil { descriptions = append(descriptions, F.ToString("network-strategy=", r.NetworkStrategy)) } if r.NetworkType != nil { 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.FallbackNetworkType, C.InterfaceType.String), ","))) } if r.FallbackDelay > 0 { descriptions = append(descriptions, F.ToString("fallback-delay=", r.FallbackDelay.String())) } if r.UDPDisableDomainUnmapping { descriptions = append(descriptions, "udp-disable-domain-unmapping") } if r.UDPConnect { descriptions = append(descriptions, "udp-connect") } if r.UDPTimeout > 0 { descriptions = append(descriptions, "udp-timeout") } if r.TLSFragment { descriptions = append(descriptions, "tls-fragment") } if r.TLSFragmentFallbackDelay > 0 { descriptions = append(descriptions, F.ToString("tls-fragment-fallback-delay=", r.TLSFragmentFallbackDelay.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 } type RuleActionDNSRoute struct { Server string RuleActionDNSRouteOptions } func (r *RuleActionDNSRoute) Type() string { return C.RuleActionTypeRoute } 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, server) 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)) } 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)) } return F.ToString(action, "(", strings.Join(descriptions, ","), ")") } type RuleActionDNSRouteOptions struct { Strategy C.DomainStrategy Timeout time.Duration DisableCache bool DisableOptimisticCache bool RewriteTTL *uint32 ClientSubnet netip.Prefix } func (r *RuleActionDNSRouteOptions) Type() string { return C.RuleActionTypeRouteOptions } func (r *RuleActionDNSRouteOptions) String() string { var descriptions []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)) } 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)) } return F.ToString("route-options(", strings.Join(descriptions, ","), ")") } type RuleActionDirect struct { Dialer N.Dialer description string } func (r *RuleActionDirect) Type() string { return C.RuleActionTypeDirect } func (r *RuleActionDirect) String() string { return "direct" + r.description } type RejectedError struct { Cause error } func (r *RejectedError) Error() string { return "rejected" } func (r *RejectedError) Unwrap() error { return r.Cause } func IsRejected(err error) bool { var rejected *RejectedError return errors.As(err, &rejected) } type BypassedError struct { Cause error } func (b *BypassedError) Error() string { return "bypassed" } func (b *BypassedError) Unwrap() error { return b.Cause } func IsBypassed(err error) bool { var bypassed *BypassedError return errors.As(err, &bypassed) } type RuleActionReject struct { Method string NoDrop bool logger logger.ContextLogger dropAccess sync.Mutex dropCounter []time.Time } func (r *RuleActionReject) Type() string { return C.RuleActionTypeReject } func (r *RuleActionReject) String() string { if r.Method == C.RuleActionRejectMethodDefault { return "reject" } return F.ToString("reject(", r.Method, ")") } func (r *RuleActionReject) Error(ctx context.Context) error { var returnErr error switch r.Method { case C.RuleActionRejectMethodDefault: returnErr = &RejectedError{tun.ErrReset} case C.RuleActionRejectMethodDrop: return &RejectedError{tun.ErrDrop} case C.RuleActionRejectMethodReply: return nil default: panic(F.ToString("unknown reject method: ", r.Method)) } if r.NoDrop { return returnErr } r.dropAccess.Lock() defer r.dropAccess.Unlock() timeNow := time.Now() r.dropCounter = common.Filter(r.dropCounter, func(t time.Time) bool { return timeNow.Sub(t) <= 30*time.Second }) r.dropCounter = append(r.dropCounter, timeNow) if len(r.dropCounter) > 50 { if ctx != nil { r.logger.DebugContext(ctx, "dropped due to flooding") } return &RejectedError{tun.ErrDrop} } return returnErr } type RuleActionHijackDNS struct{} func (r *RuleActionHijackDNS) Type() string { return C.RuleActionTypeHijackDNS } func (r *RuleActionHijackDNS) String() string { return "hijack-dns" } type RuleActionSniff struct { SnifferNames []string StreamSniffers []sniff.StreamSniffer PacketSniffers []sniff.PacketSniffer Timeout time.Duration // Deprecated OverrideDestination bool } func (r *RuleActionSniff) Type() string { return C.RuleActionTypeSniff } func (r *RuleActionSniff) build() error { for _, name := range r.SnifferNames { switch name { case C.ProtocolTLS: r.StreamSniffers = append(r.StreamSniffers, sniff.TLSClientHello) case C.ProtocolHTTP: r.StreamSniffers = append(r.StreamSniffers, sniff.HTTPHost) case C.ProtocolQUIC: r.PacketSniffers = append(r.PacketSniffers, sniff.QUICClientHello) case C.ProtocolDNS: r.StreamSniffers = append(r.StreamSniffers, sniff.StreamDomainNameQuery) r.PacketSniffers = append(r.PacketSniffers, sniff.DomainNameQuery) case C.ProtocolSTUN: r.PacketSniffers = append(r.PacketSniffers, sniff.STUNMessage) case C.ProtocolBitTorrent: r.StreamSniffers = append(r.StreamSniffers, sniff.BitTorrent) r.PacketSniffers = append(r.PacketSniffers, sniff.UTP) r.PacketSniffers = append(r.PacketSniffers, sniff.UDPTracker) case C.ProtocolDTLS: r.PacketSniffers = append(r.PacketSniffers, sniff.DTLSRecord) case C.ProtocolSSH: r.StreamSniffers = append(r.StreamSniffers, sniff.SSH) case C.ProtocolRDP: r.StreamSniffers = append(r.StreamSniffers, sniff.RDP) case C.ProtocolNTP: r.PacketSniffers = append(r.PacketSniffers, sniff.NTP) default: return E.New("unknown sniffer: ", name) } } return nil } func (r *RuleActionSniff) String() string { if len(r.SnifferNames) == 0 && r.Timeout == 0 { return "sniff" } else if len(r.SnifferNames) > 0 && r.Timeout == 0 { return F.ToString("sniff(", strings.Join(r.SnifferNames, ","), ")") } else if len(r.SnifferNames) == 0 && r.Timeout > 0 { return F.ToString("sniff(", r.Timeout.String(), ")") } else { return F.ToString("sniff(", strings.Join(r.SnifferNames, ","), ",", r.Timeout.String(), ")") } } type RuleActionResolve struct { Server string Timeout time.Duration Strategy C.DomainStrategy DisableCache bool DisableOptimisticCache bool RewriteTTL *uint32 ClientSubnet netip.Prefix } func (r *RuleActionResolve) Type() string { return C.RuleActionTypeResolve } func (r *RuleActionResolve) String() string { var options []string if r.Server != "" { options = append(options, r.Server) } if r.Strategy != C.DomainStrategyAsIS { options = append(options, F.ToString(option.DomainStrategy(r.Strategy))) } 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)) } 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)) } if len(options) == 0 { return "resolve" } else { return F.ToString("resolve(", strings.Join(options, ","), ")") } } type RuleActionPredefined struct { Rcode int Answer []dns.RR Ns []dns.RR Extra []dns.RR } func (r *RuleActionPredefined) Type() string { return C.RuleActionTypePredefined } func (r *RuleActionPredefined) String() string { var options []string options = append(options, dns.RcodeToString[r.Rcode]) options = append(options, common.Map(r.Answer, dns.RR.String)...) options = append(options, common.Map(r.Ns, dns.RR.String)...) options = append(options, common.Map(r.Extra, dns.RR.String)...) return F.ToString("predefined(", strings.Join(options, ","), ")") } func (r *RuleActionPredefined) Response(request *dns.Msg) *dns.Msg { return &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: request.Id, Response: true, Authoritative: true, RecursionDesired: true, RecursionAvailable: true, Rcode: r.Rcode, }, Question: request.Question, Answer: rewriteRecords(r.Answer, request.Question[0]), Ns: rewriteRecords(r.Ns, request.Question[0]), Extra: rewriteRecords(r.Extra, request.Question[0]), } } func rewriteRecords(records []dns.RR, question dns.Question) []dns.RR { return common.Map(records, func(it dns.RR) dns.RR { if strings.HasPrefix(it.Header().Name, "*") { if strings.HasSuffix(question.Name, it.Header().Name[1:]) { it = dns.Copy(it) it.Header().Name = question.Name } } return it }) } ================================================ FILE: route/rule/rule_default.go ================================================ package rule import ( "context" "github.com/sagernet/sing-box/adapter" 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" "github.com/sagernet/sing/service" ) func NewRule(ctx context.Context, logger log.ContextLogger, options option.Rule, checkOutbound bool) (adapter.Rule, error) { switch options.Type { case "", C.RuleTypeDefault: if !options.DefaultOptions.IsValid() { return nil, E.New("missing conditions") } switch options.DefaultOptions.Action { case "", C.RuleActionTypeRoute: if options.DefaultOptions.RouteOptions.Outbound == "" && checkOutbound { return nil, E.New("missing outbound field") } } return NewDefaultRule(ctx, logger, options.DefaultOptions) case C.RuleTypeLogical: if !options.LogicalOptions.IsValid() { return nil, E.New("missing conditions") } switch options.LogicalOptions.Action { case "", C.RuleActionTypeRoute: if options.LogicalOptions.RouteOptions.Outbound == "" && checkOutbound { return nil, E.New("missing outbound field") } } return NewLogicalRule(ctx, logger, options.LogicalOptions) default: return nil, E.New("unknown rule type: ", options.Type) } } var _ adapter.Rule = (*DefaultRule)(nil) type DefaultRule struct { abstractDefaultRule } func (r *DefaultRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { return r.abstractDefaultRule.matchStates(metadata) } type RuleItem interface { Match(metadata *adapter.InboundContext) bool String() string } func NewDefaultRule(ctx context.Context, logger log.ContextLogger, options option.DefaultRule) (*DefaultRule, error) { action, err := NewRuleAction(ctx, logger, options.RuleAction) if err != nil { return nil, E.Cause(err, "action") } rule := &DefaultRule{ abstractDefaultRule{ invert: options.Invert, action: action, }, } router := service.FromContext[adapter.Router](ctx) networkManager := service.FromContext[adapter.NetworkManager](ctx) if len(options.Inbound) > 0 { item := NewInboundRule(options.Inbound) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if options.IPVersion > 0 { switch options.IPVersion { case 4, 6: item := NewIPVersionItem(options.IPVersion == 6) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) default: return nil, E.New("invalid ip version: ", options.IPVersion) } } if len(options.Network) > 0 { item := NewNetworkItem(options.Network) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.AuthUser) > 0 { item := NewAuthUserItem(options.AuthUser) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.Protocol) > 0 { item := NewProtocolItem(options.Protocol) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.Client) > 0 { item := NewClientItem(options.Client) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.Domain) > 0 || len(options.DomainSuffix) > 0 { item, err := NewDomainItem(options.Domain, options.DomainSuffix) if err != nil { return nil, err } rule.destinationAddressItems = append(rule.destinationAddressItems, item) rule.allItems = append(rule.allItems, item) } if len(options.DomainKeyword) > 0 { item := NewDomainKeywordItem(options.DomainKeyword) rule.destinationAddressItems = append(rule.destinationAddressItems, item) rule.allItems = append(rule.allItems, item) } if len(options.DomainRegex) > 0 { item, err := NewDomainRegexItem(options.DomainRegex) if err != nil { return nil, err } rule.destinationAddressItems = append(rule.destinationAddressItems, item) rule.allItems = append(rule.allItems, item) } if len(options.Geosite) > 0 { 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 { return nil, E.New("geoip database is deprecated in sing-box 1.8.0 and removed in sing-box 1.12.0") } if len(options.GeoIP) > 0 { return nil, E.New("geoip database is deprecated in sing-box 1.8.0 and removed in sing-box 1.12.0") } if len(options.SourceIPCIDR) > 0 { item, err := NewIPCIDRItem(true, options.SourceIPCIDR) if err != nil { return nil, E.Cause(err, "source_ip_cidr") } rule.sourceAddressItems = append(rule.sourceAddressItems, item) rule.allItems = append(rule.allItems, item) } if options.SourceIPIsPrivate { item := NewIPIsPrivateItem(true) rule.sourceAddressItems = append(rule.sourceAddressItems, item) rule.allItems = append(rule.allItems, item) } if len(options.IPCIDR) > 0 { item, err := NewIPCIDRItem(false, options.IPCIDR) if err != nil { return nil, E.Cause(err, "ipcidr") } rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) rule.allItems = append(rule.allItems, item) } if options.IPIsPrivate { item := NewIPIsPrivateItem(false) rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) rule.allItems = append(rule.allItems, item) } if len(options.SourcePort) > 0 { item := NewPortItem(true, options.SourcePort) rule.sourcePortItems = append(rule.sourcePortItems, item) rule.allItems = append(rule.allItems, item) } if len(options.SourcePortRange) > 0 { item, err := NewPortRangeItem(true, options.SourcePortRange) if err != nil { return nil, E.Cause(err, "source_port_range") } rule.sourcePortItems = append(rule.sourcePortItems, item) rule.allItems = append(rule.allItems, item) } if len(options.Port) > 0 { item := NewPortItem(false, options.Port) rule.destinationPortItems = append(rule.destinationPortItems, item) rule.allItems = append(rule.allItems, item) } if len(options.PortRange) > 0 { item, err := NewPortRangeItem(false, options.PortRange) if err != nil { return nil, E.Cause(err, "port_range") } rule.destinationPortItems = append(rule.destinationPortItems, item) rule.allItems = append(rule.allItems, item) } if len(options.ProcessName) > 0 { item := NewProcessItem(options.ProcessName) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.ProcessPath) > 0 { item := NewProcessPathItem(options.ProcessPath) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.ProcessPathRegex) > 0 { item, err := NewProcessPathRegexItem(options.ProcessPathRegex) if err != nil { return nil, E.Cause(err, "process_path_regex") } rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.PackageName) > 0 { item := NewPackageNameItem(options.PackageName) 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) rule.allItems = append(rule.allItems, item) } if len(options.UserID) > 0 { item := NewUserIDItem(options.UserID) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if options.ClashMode != "" { item := NewClashModeItem(ctx, options.ClashMode) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.NetworkType) > 0 { item := NewNetworkTypeItem(networkManager, common.Map(options.NetworkType, option.InterfaceType.Build)) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if options.NetworkIsExpensive { item := NewNetworkIsExpensiveItem(networkManager) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if options.NetworkIsConstrained { item := NewNetworkIsConstrainedItem(networkManager) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.WIFISSID) > 0 { item := NewWIFISSIDItem(networkManager, options.WIFISSID) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.WIFIBSSID) > 0 { item := NewWIFIBSSIDItem(networkManager, options.WIFIBSSID) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if options.InterfaceAddress != nil && options.InterfaceAddress.Size() > 0 { item := NewInterfaceAddressItem(networkManager, options.InterfaceAddress) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if options.NetworkInterfaceAddress != nil && options.NetworkInterfaceAddress.Size() > 0 { item := NewNetworkInterfaceAddressItem(networkManager, options.NetworkInterfaceAddress) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.DefaultInterfaceAddress) > 0 { item := NewDefaultInterfaceAddressItem(networkManager, options.DefaultInterfaceAddress) 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) rule.allItems = append(rule.allItems, item) } if len(options.RuleSet) > 0 { //nolint:staticcheck if options.Deprecated_RulesetIPCIDRMatchSource { return nil, E.New("rule_set_ipcidr_match_source is deprecated in sing-box 1.10.0 and removed in sing-box 1.11.0") } var matchSource bool if options.RuleSetIPCIDRMatchSource { matchSource = true } item := NewRuleSetItem(router, options.RuleSet, matchSource, false) rule.ruleSetItem = item rule.allItems = append(rule.allItems, item) } return rule, nil } var _ adapter.Rule = (*LogicalRule)(nil) type LogicalRule struct { abstractLogicalRule } func (r *LogicalRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { return r.abstractLogicalRule.matchStates(metadata) } func NewLogicalRule(ctx context.Context, logger log.ContextLogger, options option.LogicalRule) (*LogicalRule, error) { action, err := NewRuleAction(ctx, logger, options.RuleAction) if err != nil { return nil, E.Cause(err, "action") } rule := &LogicalRule{ abstractLogicalRule{ rules: make([]adapter.HeadlessRule, len(options.Rules)), invert: options.Invert, action: action, }, } switch options.Mode { case C.LogicalTypeAnd: rule.mode = C.LogicalTypeAnd case C.LogicalTypeOr: rule.mode = C.LogicalTypeOr default: 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, "]") } rule.rules[i] = subRule } return rule, nil } ================================================ FILE: route/rule/rule_default_interface_address.go ================================================ package rule import ( "net/netip" "strings" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/json/badoption" ) var _ RuleItem = (*DefaultInterfaceAddressItem)(nil) type DefaultInterfaceAddressItem struct { interfaceMonitor tun.DefaultInterfaceMonitor interfaceAddresses []netip.Prefix } func NewDefaultInterfaceAddressItem(networkManager adapter.NetworkManager, interfaceAddresses badoption.Listable[*badoption.Prefixable]) *DefaultInterfaceAddressItem { item := &DefaultInterfaceAddressItem{ interfaceMonitor: networkManager.InterfaceMonitor(), interfaceAddresses: make([]netip.Prefix, 0, len(interfaceAddresses)), } for _, prefixable := range interfaceAddresses { item.interfaceAddresses = append(item.interfaceAddresses, prefixable.Build(netip.Prefix{})) } return item } func (r *DefaultInterfaceAddressItem) Match(metadata *adapter.InboundContext) bool { defaultInterface := r.interfaceMonitor.DefaultInterface() if defaultInterface == nil { return false } for _, address := range r.interfaceAddresses { if common.All(defaultInterface.Addresses, func(it netip.Prefix) bool { return !address.Overlaps(it) }) { return false } } return true } func (r *DefaultInterfaceAddressItem) String() string { addressLen := len(r.interfaceAddresses) switch { case addressLen == 1: return "default_interface_address=" + r.interfaceAddresses[0].String() case addressLen > 3: return "default_interface_address=[" + strings.Join(common.Map(r.interfaceAddresses[:3], netip.Prefix.String), " ") + "...]" default: return "default_interface_address=[" + strings.Join(common.Map(r.interfaceAddresses, netip.Prefix.String), " ") + "]" } } ================================================ FILE: route/rule/rule_dns.go ================================================ package rule import ( "context" "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, 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, C.RuleActionTypeEvaluate: if options.DefaultOptions.RouteOptions.Server == "" && checkServer { return nil, E.New("missing server field") } } 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, C.RuleActionTypeEvaluate: if options.LogicalOptions.RouteOptions.Server == "" && checkServer { return nil, E.New("missing server field") } } 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, 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) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } router := service.FromContext[adapter.Router](ctx) networkManager := service.FromContext[adapter.NetworkManager](ctx) if options.IPVersion > 0 { switch options.IPVersion { case 4, 6: item := NewIPVersionItem(options.IPVersion == 6) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) default: return nil, E.New("invalid ip version: ", options.IPVersion) } } if len(options.QueryType) > 0 { item := NewQueryTypeItem(options.QueryType) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.Network) > 0 { item := NewNetworkItem(options.Network) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.AuthUser) > 0 { item := NewAuthUserItem(options.AuthUser) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.Protocol) > 0 { item := NewProtocolItem(options.Protocol) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.Domain) > 0 || len(options.DomainSuffix) > 0 { item, err := NewDomainItem(options.Domain, options.DomainSuffix) if err != nil { return nil, err } rule.destinationAddressItems = append(rule.destinationAddressItems, item) rule.allItems = append(rule.allItems, item) } if len(options.DomainKeyword) > 0 { item := NewDomainKeywordItem(options.DomainKeyword) rule.destinationAddressItems = append(rule.destinationAddressItems, item) rule.allItems = append(rule.allItems, item) } if len(options.DomainRegex) > 0 { item, err := NewDomainRegexItem(options.DomainRegex) if err != nil { return nil, E.Cause(err, "domain_regex") } rule.destinationAddressItems = append(rule.destinationAddressItems, item) rule.allItems = append(rule.allItems, item) } 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 { return nil, E.New("geoip database is deprecated in sing-box 1.8.0 and removed in sing-box 1.12.0") } if len(options.GeoIP) > 0 { return nil, E.New("geoip database is deprecated in sing-box 1.8.0 and removed in sing-box 1.12.0") } if len(options.SourceIPCIDR) > 0 { item, err := NewIPCIDRItem(true, options.SourceIPCIDR) if err != nil { return nil, E.Cause(err, "source_ip_cidr") } rule.sourceAddressItems = append(rule.sourceAddressItems, item) rule.allItems = append(rule.allItems, item) } if len(options.IPCIDR) > 0 { item, err := NewIPCIDRItem(false, options.IPCIDR) if err != nil { return nil, E.Cause(err, "ip_cidr") } rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) rule.allItems = append(rule.allItems, item) } if options.SourceIPIsPrivate { item := NewIPIsPrivateItem(true) rule.sourceAddressItems = append(rule.sourceAddressItems, item) rule.allItems = append(rule.allItems, item) } if options.IPIsPrivate { item := NewIPIsPrivateItem(false) rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) rule.allItems = append(rule.allItems, item) } if options.IPAcceptAny { 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) rule.allItems = append(rule.allItems, item) } if len(options.SourcePortRange) > 0 { item, err := NewPortRangeItem(true, options.SourcePortRange) if err != nil { return nil, E.Cause(err, "source_port_range") } rule.sourcePortItems = append(rule.sourcePortItems, item) rule.allItems = append(rule.allItems, item) } if len(options.Port) > 0 { item := NewPortItem(false, options.Port) rule.destinationPortItems = append(rule.destinationPortItems, item) rule.allItems = append(rule.allItems, item) } if len(options.PortRange) > 0 { item, err := NewPortRangeItem(false, options.PortRange) if err != nil { return nil, E.Cause(err, "port_range") } rule.destinationPortItems = append(rule.destinationPortItems, item) rule.allItems = append(rule.allItems, item) } if len(options.ProcessName) > 0 { item := NewProcessItem(options.ProcessName) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.ProcessPath) > 0 { item := NewProcessPathItem(options.ProcessPath) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.ProcessPathRegex) > 0 { item, err := NewProcessPathRegexItem(options.ProcessPathRegex) if err != nil { return nil, E.Cause(err, "process_path_regex") } rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.PackageName) > 0 { item := NewPackageNameItem(options.PackageName) 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) rule.allItems = append(rule.allItems, item) } if len(options.UserID) > 0 { item := NewUserIDItem(options.UserID) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.Outbound) > 0 { item := NewOutboundRule(ctx, options.Outbound) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if options.ClashMode != "" { item := NewClashModeItem(ctx, options.ClashMode) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.NetworkType) > 0 { item := NewNetworkTypeItem(networkManager, common.Map(options.NetworkType, option.InterfaceType.Build)) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if options.NetworkIsExpensive { item := NewNetworkIsExpensiveItem(networkManager) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if options.NetworkIsConstrained { item := NewNetworkIsConstrainedItem(networkManager) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.WIFISSID) > 0 { item := NewWIFISSIDItem(networkManager, options.WIFISSID) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.WIFIBSSID) > 0 { item := NewWIFIBSSIDItem(networkManager, options.WIFIBSSID) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if options.InterfaceAddress != nil && options.InterfaceAddress.Size() > 0 { item := NewInterfaceAddressItem(networkManager, options.InterfaceAddress) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if options.NetworkInterfaceAddress != nil && options.NetworkInterfaceAddress.Size() > 0 { item := NewNetworkInterfaceAddressItem(networkManager, options.NetworkInterfaceAddress) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.DefaultInterfaceAddress) > 0 { item := NewDefaultInterfaceAddressItem(networkManager, options.DefaultInterfaceAddress) 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 := 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) } else { return nil, E.New(deprecated.OptionRuleSetIPCIDRAcceptEmpty.MessageWithLink()) } } if len(options.RuleSet) > 0 { //nolint:staticcheck if options.Deprecated_RulesetIPCIDRMatchSource { return nil, E.New("rule_set_ipcidr_match_source is deprecated in sing-box 1.10.0 and removed in sing-box 1.11.0") } var matchSource bool if options.RuleSetIPCIDRMatchSource { matchSource = true } item := NewRuleSetItem(router, options.RuleSet, matchSource, options.RuleSetIPCIDRAcceptEmpty) //nolint:staticcheck rule.ruleSetItem = item rule.allItems = append(rule.allItems, item) } return rule, nil } func (r *DefaultDNSRule) Action() adapter.RuleAction { return r.action } func (r *DefaultDNSRule) WithAddressLimit() bool { if len(r.destinationIPCIDRItems) > 0 { return true } if r.ruleSetItem != nil { ruleSet, isRuleSet := r.ruleSetItem.(*RuleSetItem) if isRuleSet && ruleSet.ContainsDestinationIPCIDRRule() { return true } } return false } 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.abstractDefaultRule.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) type LogicalDNSRule struct { abstractLogicalRule } func (r *LogicalDNSRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { return r.abstractLogicalRule.matchStates(metadata) } 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 matchHeadlessRuleStatesWithBase(typedRule, metadata, 0) } } 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)), invert: options.Invert, action: NewDNSRuleAction(logger, options.DNSRuleAction), }, } switch options.Mode { case C.LogicalTypeAnd: r.mode = C.LogicalTypeAnd case C.LogicalTypeOr: r.mode = C.LogicalTypeOr default: return nil, E.New("unknown logical mode: ", options.Mode) } for i, subRule := range options.Rules { 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, "]") } r.rules[i] = rule } return r, nil } func (r *LogicalDNSRule) Action() adapter.RuleAction { return r.action } func (r *LogicalDNSRule) WithAddressLimit() bool { for _, rawRule := range r.rules { switch rule := rawRule.(type) { case *DefaultDNSRule: if rule.WithAddressLimit() { return true } case *LogicalDNSRule: if rule.WithAddressLimit() { return true } } } return false } 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.abstractLogicalRule.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() } ================================================ FILE: route/rule/rule_headless.go ================================================ package rule 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" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/service" ) func NewHeadlessRule(ctx context.Context, options option.HeadlessRule) (adapter.HeadlessRule, error) { switch options.Type { case "", C.RuleTypeDefault: if !options.DefaultOptions.IsValid() { return nil, E.New("missing conditions") } return NewDefaultHeadlessRule(ctx, options.DefaultOptions) case C.RuleTypeLogical: if !options.LogicalOptions.IsValid() { return nil, E.New("missing conditions") } return NewLogicalHeadlessRule(ctx, options.LogicalOptions) default: return nil, E.New("unknown rule type: ", options.Type) } } var _ adapter.HeadlessRule = (*DefaultHeadlessRule)(nil) type DefaultHeadlessRule struct { abstractDefaultRule } func (r *DefaultHeadlessRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { return r.abstractDefaultRule.matchStates(metadata) } func NewDefaultHeadlessRule(ctx context.Context, options option.DefaultHeadlessRule) (*DefaultHeadlessRule, error) { networkManager := service.FromContext[adapter.NetworkManager](ctx) rule := &DefaultHeadlessRule{ abstractDefaultRule{ invert: options.Invert, }, } if len(options.QueryType) > 0 { item := NewQueryTypeItem(options.QueryType) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.Network) > 0 { item := NewNetworkItem(options.Network) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.Domain) > 0 || len(options.DomainSuffix) > 0 { item, err := NewDomainItem(options.Domain, options.DomainSuffix) if err != nil { return nil, err } rule.destinationAddressItems = append(rule.destinationAddressItems, item) rule.allItems = append(rule.allItems, item) } else if options.DomainMatcher != nil { item := NewRawDomainItem(options.DomainMatcher) rule.destinationAddressItems = append(rule.destinationAddressItems, item) rule.allItems = append(rule.allItems, item) } if len(options.DomainKeyword) > 0 { item := NewDomainKeywordItem(options.DomainKeyword) rule.destinationAddressItems = append(rule.destinationAddressItems, item) rule.allItems = append(rule.allItems, item) } if len(options.DomainRegex) > 0 { item, err := NewDomainRegexItem(options.DomainRegex) if err != nil { return nil, E.Cause(err, "domain_regex") } rule.destinationAddressItems = append(rule.destinationAddressItems, item) rule.allItems = append(rule.allItems, item) } if len(options.SourceIPCIDR) > 0 { item, err := NewIPCIDRItem(true, options.SourceIPCIDR) if err != nil { return nil, E.Cause(err, "source_ip_cidr") } rule.sourceAddressItems = append(rule.sourceAddressItems, item) rule.allItems = append(rule.allItems, item) } else if options.SourceIPSet != nil { item := NewRawIPCIDRItem(true, options.SourceIPSet) rule.sourceAddressItems = append(rule.sourceAddressItems, item) rule.allItems = append(rule.allItems, item) } if len(options.IPCIDR) > 0 { item, err := NewIPCIDRItem(false, options.IPCIDR) if err != nil { return nil, E.Cause(err, "ipcidr") } rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) rule.allItems = append(rule.allItems, item) } else if options.IPSet != nil { item := NewRawIPCIDRItem(false, options.IPSet) rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) rule.allItems = append(rule.allItems, item) } if len(options.SourcePort) > 0 { item := NewPortItem(true, options.SourcePort) rule.sourcePortItems = append(rule.sourcePortItems, item) rule.allItems = append(rule.allItems, item) } if len(options.SourcePortRange) > 0 { item, err := NewPortRangeItem(true, options.SourcePortRange) if err != nil { return nil, E.Cause(err, "source_port_range") } rule.sourcePortItems = append(rule.sourcePortItems, item) rule.allItems = append(rule.allItems, item) } if len(options.Port) > 0 { item := NewPortItem(false, options.Port) rule.destinationPortItems = append(rule.destinationPortItems, item) rule.allItems = append(rule.allItems, item) } if len(options.PortRange) > 0 { item, err := NewPortRangeItem(false, options.PortRange) if err != nil { return nil, E.Cause(err, "port_range") } rule.destinationPortItems = append(rule.destinationPortItems, item) rule.allItems = append(rule.allItems, item) } if len(options.ProcessName) > 0 { item := NewProcessItem(options.ProcessName) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.ProcessPath) > 0 { item := NewProcessPathItem(options.ProcessPath) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.ProcessPathRegex) > 0 { item, err := NewProcessPathRegexItem(options.ProcessPathRegex) if err != nil { return nil, E.Cause(err, "process_path_regex") } rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.PackageName) > 0 { item := NewPackageNameItem(options.PackageName) 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)) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if options.NetworkIsExpensive { item := NewNetworkIsExpensiveItem(networkManager) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if options.NetworkIsConstrained { item := NewNetworkIsConstrainedItem(networkManager) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.WIFISSID) > 0 { item := NewWIFISSIDItem(networkManager, options.WIFISSID) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.WIFIBSSID) > 0 { item := NewWIFIBSSIDItem(networkManager, options.WIFIBSSID) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if options.NetworkInterfaceAddress != nil && options.NetworkInterfaceAddress.Size() > 0 { item := NewNetworkInterfaceAddressItem(networkManager, options.NetworkInterfaceAddress) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.DefaultInterfaceAddress) > 0 { item := NewDefaultInterfaceAddressItem(networkManager, options.DefaultInterfaceAddress) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } } if len(options.AdGuardDomain) > 0 { item := NewAdGuardDomainItem(options.AdGuardDomain) rule.destinationAddressItems = append(rule.destinationAddressItems, item) rule.allItems = append(rule.allItems, item) } else if options.AdGuardDomainMatcher != nil { item := NewRawAdGuardDomainItem(options.AdGuardDomainMatcher) rule.destinationAddressItems = append(rule.destinationAddressItems, item) rule.allItems = append(rule.allItems, item) } return rule, nil } var _ adapter.HeadlessRule = (*LogicalHeadlessRule)(nil) type LogicalHeadlessRule struct { abstractLogicalRule } func (r *LogicalHeadlessRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { return r.abstractLogicalRule.matchStates(metadata) } func NewLogicalHeadlessRule(ctx context.Context, options option.LogicalHeadlessRule) (*LogicalHeadlessRule, error) { r := &LogicalHeadlessRule{ abstractLogicalRule{ rules: make([]adapter.HeadlessRule, len(options.Rules)), invert: options.Invert, }, } switch options.Mode { case C.LogicalTypeAnd: r.mode = C.LogicalTypeAnd case C.LogicalTypeOr: r.mode = C.LogicalTypeOr default: return nil, E.New("unknown logical mode: ", options.Mode) } for i, subRule := range options.Rules { rule, err := NewHeadlessRule(ctx, subRule) if err != nil { return nil, E.Cause(err, "sub rule[", i, "]") } r.rules[i] = rule } return r, nil } ================================================ FILE: route/rule/rule_interface_address.go ================================================ package rule import ( "net/netip" "strings" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/json/badjson" "github.com/sagernet/sing/common/json/badoption" ) var _ RuleItem = (*InterfaceAddressItem)(nil) type InterfaceAddressItem struct { networkManager adapter.NetworkManager interfaceAddresses map[string][]netip.Prefix description string } func NewInterfaceAddressItem(networkManager adapter.NetworkManager, interfaceAddresses *badjson.TypedMap[string, badoption.Listable[*badoption.Prefixable]]) *InterfaceAddressItem { item := &InterfaceAddressItem{ networkManager: networkManager, interfaceAddresses: make(map[string][]netip.Prefix, interfaceAddresses.Size()), } var entryDescriptions []string for _, entry := range interfaceAddresses.Entries() { prefixes := make([]netip.Prefix, 0, len(entry.Value)) for _, prefixable := range entry.Value { prefixes = append(prefixes, prefixable.Build(netip.Prefix{})) } item.interfaceAddresses[entry.Key] = prefixes entryDescriptions = append(entryDescriptions, entry.Key+"="+strings.Join(common.Map(prefixes, netip.Prefix.String), ",")) } item.description = "interface_address=[" + strings.Join(entryDescriptions, " ") + "]" return item } func (r *InterfaceAddressItem) Match(metadata *adapter.InboundContext) bool { interfaces := r.networkManager.InterfaceFinder().Interfaces() for ifName, addresses := range r.interfaceAddresses { iface := common.Find(interfaces, func(it control.Interface) bool { return it.Name == ifName }) if iface.Name == "" { return false } if common.All(addresses, func(address netip.Prefix) bool { return common.All(iface.Addresses, func(it netip.Prefix) bool { return !address.Overlaps(it) }) }) { return false } } return true } func (r *InterfaceAddressItem) String() string { return r.description } ================================================ FILE: route/rule/rule_item_adguard.go ================================================ package rule import ( "strings" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing/common/domain" ) var _ RuleItem = (*AdGuardDomainItem)(nil) type AdGuardDomainItem struct { matcher *domain.AdGuardMatcher } func NewAdGuardDomainItem(ruleLines []string) *AdGuardDomainItem { return &AdGuardDomainItem{ domain.NewAdGuardMatcher(ruleLines), } } func NewRawAdGuardDomainItem(matcher *domain.AdGuardMatcher) *AdGuardDomainItem { return &AdGuardDomainItem{ matcher, } } func (r *AdGuardDomainItem) Match(metadata *adapter.InboundContext) bool { var domainHost string if metadata.Domain != "" { domainHost = metadata.Domain } else { domainHost = metadata.Destination.Fqdn } if domainHost == "" { return false } return r.matcher.Match(strings.ToLower(domainHost)) } func (r *AdGuardDomainItem) String() string { return "!adguard_domain_rules=" } ================================================ FILE: route/rule/rule_item_auth_user.go ================================================ package rule import ( "strings" "github.com/sagernet/sing-box/adapter" F "github.com/sagernet/sing/common/format" ) var _ RuleItem = (*AuthUserItem)(nil) type AuthUserItem struct { users []string userMap map[string]bool } func NewAuthUserItem(users []string) *AuthUserItem { userMap := make(map[string]bool) for _, protocol := range users { userMap[protocol] = true } return &AuthUserItem{ users: users, userMap: userMap, } } func (r *AuthUserItem) Match(metadata *adapter.InboundContext) bool { return r.userMap[metadata.User] } func (r *AuthUserItem) String() string { if len(r.users) == 1 { return F.ToString("auth_user=", r.users[0]) } return F.ToString("auth_user=[", strings.Join(r.users, " "), "]") } ================================================ FILE: route/rule/rule_item_cidr.go ================================================ package rule import ( "net/netip" "slices" "strings" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "go4.org/netipx" ) var _ RuleItem = (*IPCIDRItem)(nil) type IPCIDRItem struct { ipSet *netipx.IPSet isSource bool description string } func NewIPCIDRItem(isSource bool, prefixStrings []string) (*IPCIDRItem, error) { var builder netipx.IPSetBuilder for i, prefixString := range prefixStrings { prefix, err := netip.ParsePrefix(prefixString) if err == nil { builder.AddPrefix(prefix) continue } addr, addrErr := netip.ParseAddr(prefixString) if addrErr == nil { builder.Add(addr) continue } return nil, E.Cause(err, "parse [", i, "]") } var description string if isSource { description = "source_ip_cidr=" } else { description = "ip_cidr=" } if dLen := len(prefixStrings); dLen == 1 { description += prefixStrings[0] } else if dLen > 3 { description += "[" + strings.Join(prefixStrings[:3], " ") + "...]" } else { description += "[" + strings.Join(prefixStrings, " ") + "]" } ipSet, err := builder.IPSet() if err != nil { return nil, err } return &IPCIDRItem{ ipSet: ipSet, isSource: isSource, description: description, }, nil } func NewRawIPCIDRItem(isSource bool, ipSet *netipx.IPSet) *IPCIDRItem { var description string if isSource { description = "source_ip_cidr=" } else { description = "ip_cidr=" } description += "" return &IPCIDRItem{ ipSet: ipSet, isSource: isSource, description: description, } } 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 } return slices.ContainsFunc(addresses, r.ipSet.Contains) } if metadata.Destination.IsIP() { return r.ipSet.Contains(metadata.Destination.Addr) } if len(metadata.DestinationAddresses) > 0 { return common.Any(metadata.DestinationAddresses, r.ipSet.Contains) } return metadata.IPCIDRAcceptEmpty } func (r *IPCIDRItem) String() string { return r.description } ================================================ FILE: route/rule/rule_item_clash_mode.go ================================================ package rule import ( "context" "strings" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing/service" ) var _ RuleItem = (*ClashModeItem)(nil) type ClashModeItem struct { ctx context.Context clashServer adapter.ClashServer mode string } func NewClashModeItem(ctx context.Context, mode string) *ClashModeItem { return &ClashModeItem{ ctx: ctx, mode: mode, } } func (r *ClashModeItem) Start() error { r.clashServer = service.FromContext[adapter.ClashServer](r.ctx) return nil } func (r *ClashModeItem) Match(metadata *adapter.InboundContext) bool { if r.clashServer == nil { return false } return strings.EqualFold(r.clashServer.Mode(), r.mode) } func (r *ClashModeItem) String() string { return "clash_mode=" + r.mode } ================================================ FILE: route/rule/rule_item_client.go ================================================ package rule import ( "strings" "github.com/sagernet/sing-box/adapter" F "github.com/sagernet/sing/common/format" ) var _ RuleItem = (*ClientItem)(nil) type ClientItem struct { clients []string clientMap map[string]bool } func NewClientItem(clients []string) *ClientItem { clientMap := make(map[string]bool) for _, client := range clients { clientMap[client] = true } return &ClientItem{ clients: clients, clientMap: clientMap, } } func (r *ClientItem) Match(metadata *adapter.InboundContext) bool { return r.clientMap[metadata.Client] } func (r *ClientItem) String() string { if len(r.clients) == 1 { return F.ToString("client=", r.clients[0]) } return F.ToString("client=[", strings.Join(r.clients, " "), "]") } ================================================ FILE: route/rule/rule_item_domain.go ================================================ package rule import ( "slices" "strings" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing/common/domain" E "github.com/sagernet/sing/common/exceptions" ) var _ RuleItem = (*DomainItem)(nil) type DomainItem struct { matcher *domain.Matcher description string } func NewDomainItem(domains []string, domainSuffixes []string) (*DomainItem, error) { if slices.Contains(domains, "") { return nil, E.New("domain: empty item is not allowed") } if slices.Contains(domainSuffixes, "") { return nil, E.New("domain_suffix: empty item is not allowed") } var description string if dLen := len(domains); dLen > 0 { if dLen == 1 { description = "domain=" + domains[0] } else if dLen > 3 { description = "domain=[" + strings.Join(domains[:3], " ") + "...]" } else { description = "domain=[" + strings.Join(domains, " ") + "]" } } if dsLen := len(domainSuffixes); dsLen > 0 { if len(description) > 0 { description += " " } if dsLen == 1 { description += "domain_suffix=" + domainSuffixes[0] } else if dsLen > 3 { description += "domain_suffix=[" + strings.Join(domainSuffixes[:3], " ") + "...]" } else { description += "domain_suffix=[" + strings.Join(domainSuffixes, " ") + "]" } } return &DomainItem{ domain.NewMatcher(domains, domainSuffixes, false), description, }, nil } func NewRawDomainItem(matcher *domain.Matcher) *DomainItem { return &DomainItem{ matcher, "domain/domain_suffix=", } } func (r *DomainItem) Match(metadata *adapter.InboundContext) bool { var domainHost string if metadata.Domain != "" { domainHost = metadata.Domain } else { domainHost = metadata.Destination.Fqdn } if domainHost == "" { return false } return r.matcher.Match(strings.ToLower(domainHost)) } func (r *DomainItem) String() string { return r.description } ================================================ FILE: route/rule/rule_item_domain_keyword.go ================================================ package rule import ( "strings" "github.com/sagernet/sing-box/adapter" ) var _ RuleItem = (*DomainKeywordItem)(nil) type DomainKeywordItem struct { keywords []string } func NewDomainKeywordItem(keywords []string) *DomainKeywordItem { return &DomainKeywordItem{keywords} } func (r *DomainKeywordItem) Match(metadata *adapter.InboundContext) bool { var domainHost string if metadata.Domain != "" { domainHost = metadata.Domain } else { domainHost = metadata.Destination.Fqdn } if domainHost == "" { return false } domainHost = strings.ToLower(domainHost) for _, keyword := range r.keywords { if strings.Contains(domainHost, keyword) { return true } } return false } func (r *DomainKeywordItem) String() string { kLen := len(r.keywords) if kLen == 1 { return "domain_keyword=" + r.keywords[0] } else if kLen > 3 { return "domain_keyword=[" + strings.Join(r.keywords[:3], " ") + "...]" } else { return "domain_keyword=[" + strings.Join(r.keywords, " ") + "]" } } ================================================ FILE: route/rule/rule_item_domain_regex.go ================================================ 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 = (*DomainRegexItem)(nil) type DomainRegexItem struct { matchers []*regexp.Regexp description string } func NewDomainRegexItem(expressions []string) (*DomainRegexItem, 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 := "domain_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 &DomainRegexItem{matchers, description}, nil } func (r *DomainRegexItem) Match(metadata *adapter.InboundContext) bool { var domainHost string if metadata.Domain != "" { domainHost = metadata.Domain } else { domainHost = metadata.Destination.Fqdn } if domainHost == "" { return false } domainHost = strings.ToLower(domainHost) for _, matcher := range r.matchers { if matcher.MatchString(domainHost) { return true } } return false } func (r *DomainRegexItem) String() string { return r.description } ================================================ FILE: route/rule/rule_item_inbound.go ================================================ package rule import ( "strings" "github.com/sagernet/sing-box/adapter" F "github.com/sagernet/sing/common/format" ) var _ RuleItem = (*InboundItem)(nil) type InboundItem struct { inbounds []string inboundMap map[string]bool } func NewInboundRule(inbounds []string) *InboundItem { rule := &InboundItem{inbounds, make(map[string]bool)} for _, inbound := range inbounds { rule.inboundMap[inbound] = true } return rule } func (r *InboundItem) Match(metadata *adapter.InboundContext) bool { return r.inboundMap[metadata.Inbound] } func (r *InboundItem) String() string { if len(r.inbounds) == 1 { return F.ToString("inbound=", r.inbounds[0]) } else { return F.ToString("inbound=[", strings.Join(r.inbounds, " "), "]") } } ================================================ FILE: route/rule/rule_item_ip_accept_any.go ================================================ package rule import ( "github.com/sagernet/sing-box/adapter" ) var _ RuleItem = (*IPAcceptAnyItem)(nil) type IPAcceptAnyItem struct{} func NewIPAcceptAnyItem() *IPAcceptAnyItem { return &IPAcceptAnyItem{} } func (r *IPAcceptAnyItem) Match(metadata *adapter.InboundContext) bool { if metadata.DestinationAddressMatchFromResponse { return len(metadata.DNSResponseAddressesForMatch()) > 0 } return len(metadata.DestinationAddresses) > 0 } func (r *IPAcceptAnyItem) String() string { return "ip_accept_any=true" } ================================================ FILE: route/rule/rule_item_ip_is_private.go ================================================ package rule import ( "github.com/sagernet/sing-box/adapter" N "github.com/sagernet/sing/common/network" ) var _ RuleItem = (*IPIsPrivateItem)(nil) type IPIsPrivateItem struct { isSource bool } func NewIPIsPrivateItem(isSource bool) *IPIsPrivateItem { return &IPIsPrivateItem{isSource} } func (r *IPIsPrivateItem) Match(metadata *adapter.InboundContext) bool { if r.isSource { return !N.IsPublicAddr(metadata.Source.Addr) } 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 } func (r *IPIsPrivateItem) String() string { if r.isSource { return "source_ip_is_private=true" } else { return "ip_is_private=true" } } ================================================ FILE: route/rule/rule_item_ipversion.go ================================================ package rule import ( "github.com/sagernet/sing-box/adapter" ) var _ RuleItem = (*IPVersionItem)(nil) type IPVersionItem struct { isIPv6 bool } func NewIPVersionItem(isIPv6 bool) *IPVersionItem { return &IPVersionItem{isIPv6} } func (r *IPVersionItem) Match(metadata *adapter.InboundContext) bool { return metadata.IPVersion != 0 && metadata.IPVersion == 6 == r.isIPv6 || metadata.Destination.IsIP() && metadata.Destination.IsIPv6() == r.isIPv6 } func (r *IPVersionItem) String() string { var versionStr string if r.isIPv6 { versionStr = "6" } else { versionStr = "4" } return "ip_version=" + versionStr } ================================================ FILE: route/rule/rule_item_network.go ================================================ package rule import ( "strings" "github.com/sagernet/sing-box/adapter" F "github.com/sagernet/sing/common/format" ) var _ RuleItem = (*NetworkItem)(nil) type NetworkItem struct { networks []string networkMap map[string]bool } func NewNetworkItem(networks []string) *NetworkItem { networkMap := make(map[string]bool) for _, network := range networks { networkMap[network] = true } return &NetworkItem{ networks: networks, networkMap: networkMap, } } func (r *NetworkItem) Match(metadata *adapter.InboundContext) bool { return r.networkMap[metadata.Network] } func (r *NetworkItem) String() string { description := "network=" pLen := len(r.networks) if pLen == 1 { description += F.ToString(r.networks[0]) } else { description += "[" + strings.Join(F.MapToString(r.networks), " ") + "]" } return description } ================================================ FILE: route/rule/rule_item_network_is_constrained.go ================================================ package rule import ( "github.com/sagernet/sing-box/adapter" ) var _ RuleItem = (*NetworkIsConstrainedItem)(nil) type NetworkIsConstrainedItem struct { networkManager adapter.NetworkManager } func NewNetworkIsConstrainedItem(networkManager adapter.NetworkManager) *NetworkIsConstrainedItem { return &NetworkIsConstrainedItem{ networkManager: networkManager, } } func (r *NetworkIsConstrainedItem) Match(metadata *adapter.InboundContext) bool { networkInterface := r.networkManager.DefaultNetworkInterface() if networkInterface == nil { return false } return networkInterface.Constrained } func (r *NetworkIsConstrainedItem) String() string { return "network_is_expensive=true" } ================================================ FILE: route/rule/rule_item_network_is_expensive.go ================================================ package rule import ( "github.com/sagernet/sing-box/adapter" ) var _ RuleItem = (*NetworkIsExpensiveItem)(nil) type NetworkIsExpensiveItem struct { networkManager adapter.NetworkManager } func NewNetworkIsExpensiveItem(networkManager adapter.NetworkManager) *NetworkIsExpensiveItem { return &NetworkIsExpensiveItem{ networkManager: networkManager, } } func (r *NetworkIsExpensiveItem) Match(metadata *adapter.InboundContext) bool { networkInterface := r.networkManager.DefaultNetworkInterface() if networkInterface == nil { return false } return networkInterface.Expensive } func (r *NetworkIsExpensiveItem) String() string { return "network_is_expensive=true" } ================================================ FILE: route/rule/rule_item_network_type.go ================================================ package rule import ( "strings" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common" F "github.com/sagernet/sing/common/format" ) var _ RuleItem = (*NetworkTypeItem)(nil) type NetworkTypeItem struct { networkManager adapter.NetworkManager networkType []C.InterfaceType } func NewNetworkTypeItem(networkManager adapter.NetworkManager, networkType []C.InterfaceType) *NetworkTypeItem { return &NetworkTypeItem{ networkManager: networkManager, networkType: networkType, } } func (r *NetworkTypeItem) Match(metadata *adapter.InboundContext) bool { networkInterface := r.networkManager.DefaultNetworkInterface() if networkInterface == nil { return false } return common.Contains(r.networkType, networkInterface.Type) } func (r *NetworkTypeItem) String() string { if len(r.networkType) == 1 { return F.ToString("network_type=", r.networkType[0]) } else { return F.ToString("network_type=", "["+strings.Join(F.MapToString(r.networkType), " ")+"]") } } ================================================ FILE: route/rule/rule_item_outbound.go ================================================ package rule import ( "context" "strings" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/experimental/deprecated" F "github.com/sagernet/sing/common/format" ) var _ RuleItem = (*OutboundItem)(nil) type OutboundItem struct { outbounds []string outboundMap map[string]bool matchAny bool } func NewOutboundRule(ctx context.Context, outbounds []string) *OutboundItem { deprecated.Report(ctx, deprecated.OptionOutboundDNSRuleItem) rule := &OutboundItem{outbounds: outbounds, outboundMap: make(map[string]bool)} for _, outbound := range outbounds { if outbound == "any" { rule.matchAny = true } else { rule.outboundMap[outbound] = true } } return rule } func (r *OutboundItem) Match(metadata *adapter.InboundContext) bool { if r.matchAny { return metadata.Outbound != "" } return r.outboundMap[metadata.Outbound] } func (r *OutboundItem) String() string { if len(r.outbounds) == 1 { return F.ToString("outbound=", r.outbounds[0]) } else { return F.ToString("outbound=[", strings.Join(r.outbounds, " "), "]") } } ================================================ FILE: route/rule/rule_item_package_name.go ================================================ package rule import ( "strings" "github.com/sagernet/sing-box/adapter" ) var _ RuleItem = (*PackageNameItem)(nil) type PackageNameItem struct { packageNames []string packageMap map[string]bool } func NewPackageNameItem(packageNameList []string) *PackageNameItem { rule := &PackageNameItem{ packageNames: packageNameList, packageMap: make(map[string]bool), } for _, packageName := range packageNameList { rule.packageMap[packageName] = true } return rule } func (r *PackageNameItem) Match(metadata *adapter.InboundContext) bool { if metadata.ProcessInfo == nil || len(metadata.ProcessInfo.AndroidPackageNames) == 0 { return false } for _, packageName := range metadata.ProcessInfo.AndroidPackageNames { if r.packageMap[packageName] { return true } } return false } func (r *PackageNameItem) String() string { var description string pLen := len(r.packageNames) if pLen == 1 { description = "package_name=" + r.packageNames[0] } else { description = "package_name=[" + strings.Join(r.packageNames, " ") + "]" } return description } ================================================ FILE: route/rule/rule_item_package_name_regex.go ================================================ package rule import ( "regexp" "slices" "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 { if slices.ContainsFunc(metadata.ProcessInfo.AndroidPackageNames, matcher.MatchString) { return true } } return false } func (r *PackageNameRegexItem) String() string { return r.description } ================================================ FILE: route/rule/rule_item_port.go ================================================ package rule import ( "strings" "github.com/sagernet/sing-box/adapter" F "github.com/sagernet/sing/common/format" ) var _ RuleItem = (*PortItem)(nil) type PortItem struct { ports []uint16 portMap map[uint16]bool isSource bool } func NewPortItem(isSource bool, ports []uint16) *PortItem { portMap := make(map[uint16]bool) for _, port := range ports { portMap[port] = true } return &PortItem{ ports: ports, portMap: portMap, isSource: isSource, } } func (r *PortItem) Match(metadata *adapter.InboundContext) bool { if r.isSource { return r.portMap[metadata.Source.Port] } else { return r.portMap[metadata.Destination.Port] } } func (r *PortItem) String() string { var description string if r.isSource { description = "source_port=" } else { description = "port=" } pLen := len(r.ports) if pLen == 1 { description += F.ToString(r.ports[0]) } else { description += "[" + strings.Join(F.MapToString(r.ports), " ") + "]" } return description } ================================================ FILE: route/rule/rule_item_port_range.go ================================================ package rule import ( "strconv" "strings" "github.com/sagernet/sing-box/adapter" E "github.com/sagernet/sing/common/exceptions" ) var ErrBadPortRange = E.New("bad port range") var _ RuleItem = (*PortRangeItem)(nil) type PortRangeItem struct { isSource bool portRanges []string portRangeList []rangeItem } type rangeItem struct { start uint16 end uint16 } func NewPortRangeItem(isSource bool, rangeList []string) (*PortRangeItem, error) { portRangeList := make([]rangeItem, 0, len(rangeList)) for _, portRange := range rangeList { if !strings.Contains(portRange, ":") { return nil, E.Extend(ErrBadPortRange, portRange) } subIndex := strings.Index(portRange, ":") var start, end uint64 var err error if subIndex > 0 { start, err = strconv.ParseUint(portRange[:subIndex], 10, 16) if err != nil { return nil, E.Cause(err, E.Extend(ErrBadPortRange, portRange)) } } if subIndex == len(portRange)-1 { end = 0xFFFF } else { end, err = strconv.ParseUint(portRange[subIndex+1:], 10, 16) if err != nil { return nil, E.Cause(err, E.Extend(ErrBadPortRange, portRange)) } } portRangeList = append(portRangeList, rangeItem{uint16(start), uint16(end)}) } return &PortRangeItem{ isSource: isSource, portRanges: rangeList, portRangeList: portRangeList, }, nil } func (r *PortRangeItem) Match(metadata *adapter.InboundContext) bool { var port uint16 if r.isSource { port = metadata.Source.Port } else { port = metadata.Destination.Port } for _, portRange := range r.portRangeList { if port >= portRange.start && port <= portRange.end { return true } } return false } func (r *PortRangeItem) String() string { var description string if r.isSource { description = "source_port_range=" } else { description = "port_range=" } pLen := len(r.portRanges) if pLen == 1 { description += r.portRanges[0] } else { description += "[" + strings.Join(r.portRanges, " ") + "]" } return description } ================================================ FILE: route/rule/rule_item_preferred_by.go ================================================ 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" ) var _ RuleItem = (*PreferredByItem)(nil) type PreferredByItem struct { ctx context.Context outboundTags []string outbounds []adapter.OutboundWithPreferredRoutes } func NewPreferredByItem(ctx context.Context, outboundTags []string) *PreferredByItem { return &PreferredByItem{ ctx: ctx, outboundTags: outboundTags, } } func (r *PreferredByItem) Start() error { outboundManager := service.FromContext[adapter.OutboundManager](r.ctx) for _, outboundTag := range r.outboundTags { rawOutbound, loaded := outboundManager.Outbound(outboundTag) if !loaded { return E.New("outbound not found: ", outboundTag) } outboundWithPreferredRoutes, withRoutes := rawOutbound.(adapter.OutboundWithPreferredRoutes) if !withRoutes { return E.New("outbound type does not support preferred routes: ", rawOutbound.Type()) } r.outbounds = append(r.outbounds, outboundWithPreferredRoutes) } return nil } func (r *PreferredByItem) Match(metadata *adapter.InboundContext) bool { var domainHost string if metadata.Domain != "" { domainHost = metadata.Domain } else { domainHost = metadata.Destination.Fqdn } if domainHost != "" { for _, outbound := range r.outbounds { if outbound.PreferredDomain(domainHost) { return true } } } if metadata.Destination.IsIP() { for _, outbound := range r.outbounds { if outbound.PreferredAddress(metadata.Destination.Addr) { return true } } } if len(metadata.DestinationAddresses) > 0 { for _, address := range metadata.DestinationAddresses { for _, outbound := range r.outbounds { if outbound.PreferredAddress(address) { return true } } } } return false } func (r *PreferredByItem) String() string { description := "preferred_by=" pLen := len(r.outboundTags) if pLen == 1 { description += F.ToString(r.outboundTags[0]) } else { description += "[" + strings.Join(F.MapToString(r.outboundTags), " ") + "]" } return description } ================================================ FILE: route/rule/rule_item_preferred_by_dns.go ================================================ 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 } ================================================ FILE: route/rule/rule_item_process_name.go ================================================ package rule import ( "path/filepath" "strings" "github.com/sagernet/sing-box/adapter" ) var _ RuleItem = (*ProcessItem)(nil) type ProcessItem struct { processes []string processMap map[string]bool } func NewProcessItem(processNameList []string) *ProcessItem { rule := &ProcessItem{ processes: processNameList, processMap: make(map[string]bool), } for _, processName := range processNameList { rule.processMap[processName] = true } return rule } func (r *ProcessItem) Match(metadata *adapter.InboundContext) bool { if metadata.ProcessInfo == nil || metadata.ProcessInfo.ProcessPath == "" { return false } return r.processMap[filepath.Base(metadata.ProcessInfo.ProcessPath)] } func (r *ProcessItem) String() string { var description string pLen := len(r.processes) if pLen == 1 { description = "process_name=" + r.processes[0] } else { description = "process_name=[" + strings.Join(r.processes, " ") + "]" } return description } ================================================ FILE: route/rule/rule_item_process_path.go ================================================ package rule import ( "strings" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" ) var _ RuleItem = (*ProcessPathItem)(nil) type ProcessPathItem struct { processes []string processMap map[string]bool } func NewProcessPathItem(processNameList []string) *ProcessPathItem { rule := &ProcessPathItem{ processes: processNameList, processMap: make(map[string]bool), } for _, processName := range processNameList { rule.processMap[processName] = true } return rule } func (r *ProcessPathItem) Match(metadata *adapter.InboundContext) bool { if metadata.ProcessInfo == nil { return false } if metadata.ProcessInfo.ProcessPath != "" && r.processMap[metadata.ProcessInfo.ProcessPath] { return true } if C.IsAndroid { for _, packageName := range metadata.ProcessInfo.AndroidPackageNames { if r.processMap[packageName] { return true } } } return false } func (r *ProcessPathItem) String() string { var description string pLen := len(r.processes) if pLen == 1 { description = "process_path=" + r.processes[0] } else { description = "process_path=[" + strings.Join(r.processes, " ") + "]" } return description } ================================================ FILE: route/rule/rule_item_process_path_regex.go ================================================ 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 = (*ProcessPathRegexItem)(nil) type ProcessPathRegexItem struct { matchers []*regexp.Regexp description string } func NewProcessPathRegexItem(expressions []string) (*ProcessPathRegexItem, 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 := "process_path_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 &ProcessPathRegexItem{matchers, description}, nil } func (r *ProcessPathRegexItem) Match(metadata *adapter.InboundContext) bool { if metadata.ProcessInfo == nil || metadata.ProcessInfo.ProcessPath == "" { return false } for _, matcher := range r.matchers { if matcher.MatchString(metadata.ProcessInfo.ProcessPath) { return true } } return false } func (r *ProcessPathRegexItem) String() string { return r.description } ================================================ FILE: route/rule/rule_item_protocol.go ================================================ package rule import ( "strings" "github.com/sagernet/sing-box/adapter" F "github.com/sagernet/sing/common/format" ) var _ RuleItem = (*ProtocolItem)(nil) type ProtocolItem struct { protocols []string protocolMap map[string]bool } func NewProtocolItem(protocols []string) *ProtocolItem { protocolMap := make(map[string]bool) for _, protocol := range protocols { protocolMap[protocol] = true } return &ProtocolItem{ protocols: protocols, protocolMap: protocolMap, } } func (r *ProtocolItem) Match(metadata *adapter.InboundContext) bool { return r.protocolMap[metadata.Protocol] } func (r *ProtocolItem) String() string { if len(r.protocols) == 1 { return F.ToString("protocol=", r.protocols[0]) } return F.ToString("protocol=[", strings.Join(r.protocols, " "), "]") } ================================================ FILE: route/rule/rule_item_query_type.go ================================================ package rule import ( "strings" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" ) var _ RuleItem = (*QueryTypeItem)(nil) type QueryTypeItem struct { typeList []uint16 typeMap map[uint16]bool } func NewQueryTypeItem(typeList []option.DNSQueryType) *QueryTypeItem { rule := &QueryTypeItem{ typeList: common.Map(typeList, func(it option.DNSQueryType) uint16 { return uint16(it) }), typeMap: make(map[uint16]bool), } for _, userId := range rule.typeList { rule.typeMap[userId] = true } return rule } func (r *QueryTypeItem) Match(metadata *adapter.InboundContext) bool { if metadata.QueryType == 0 { return false } return r.typeMap[metadata.QueryType] } func (r *QueryTypeItem) String() string { var description string pLen := len(r.typeList) if pLen == 1 { description = "query_type=" + option.DNSQueryTypeToString(r.typeList[0]) } else { description = "query_type=[" + strings.Join(common.Map(r.typeList, option.DNSQueryTypeToString), " ") + "]" } return description } ================================================ FILE: route/rule/rule_item_response_rcode.go ================================================ 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]) } ================================================ FILE: route/rule/rule_item_response_record.go ================================================ package rule import ( "slices" "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 { if slices.ContainsFunc(records, expected.Match) { 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 } ================================================ FILE: route/rule/rule_item_rule_set.go ================================================ package rule import ( "strings" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" ) var _ RuleItem = (*RuleSetItem)(nil) type RuleSetItem struct { router adapter.Router tagList []string setList []adapter.RuleSet ipCidrMatchSource bool ipCidrAcceptEmpty bool } func NewRuleSetItem(router adapter.Router, tagList []string, ipCIDRMatchSource bool, ipCidrAcceptEmpty bool) *RuleSetItem { return &RuleSetItem{ router: router, tagList: tagList, ipCidrMatchSource: ipCIDRMatchSource, ipCidrAcceptEmpty: ipCidrAcceptEmpty, } } 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() r.setList = append(r.setList, ruleSet) } 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() } func (r *RuleSetItem) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { return r.matchStatesWithBase(metadata, 0) } func (r *RuleSetItem) matchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet { var stateSet ruleMatchStateSet for _, ruleSet := range r.setList { nestedMetadata := *metadata nestedMetadata.ResetRuleMatchCache() nestedMetadata.IPCIDRMatchSource = r.ipCidrMatchSource nestedMetadata.IPCIDRAcceptEmpty = r.ipCidrAcceptEmpty stateSet = stateSet.merge(matchHeadlessRuleStatesWithBase(ruleSet, &nestedMetadata, base)) } return stateSet } func (r *RuleSetItem) ContainsDestinationIPCIDRRule() bool { if r.ipCidrMatchSource { return false } return common.Any(r.setList, func(ruleSet adapter.RuleSet) bool { return ruleSet.Metadata().ContainsIPCIDRRule }) } func (r *RuleSetItem) String() string { if len(r.tagList) == 1 { return F.ToString("rule_set=", r.tagList[0]) } else { return F.ToString("rule_set=[", strings.Join(r.tagList, " "), "]") } } ================================================ FILE: route/rule/rule_item_rule_set_test.go ================================================ 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()) } ================================================ FILE: route/rule/rule_item_source_hostname.go ================================================ 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 } ================================================ FILE: route/rule/rule_item_source_mac_address.go ================================================ 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 } ================================================ FILE: route/rule/rule_item_user.go ================================================ package rule import ( "strings" "github.com/sagernet/sing-box/adapter" F "github.com/sagernet/sing/common/format" ) var _ RuleItem = (*UserItem)(nil) type UserItem struct { users []string userMap map[string]bool } func NewUserItem(users []string) *UserItem { userMap := make(map[string]bool) for _, protocol := range users { userMap[protocol] = true } return &UserItem{ users: users, userMap: userMap, } } func (r *UserItem) Match(metadata *adapter.InboundContext) bool { if metadata.ProcessInfo == nil || metadata.ProcessInfo.UserName == "" { return false } return r.userMap[metadata.ProcessInfo.UserName] } func (r *UserItem) String() string { if len(r.users) == 1 { return F.ToString("user=", r.users[0]) } return F.ToString("user=[", strings.Join(r.users, " "), "]") } ================================================ FILE: route/rule/rule_item_user_id.go ================================================ package rule import ( "strings" "github.com/sagernet/sing-box/adapter" F "github.com/sagernet/sing/common/format" ) var _ RuleItem = (*UserIdItem)(nil) type UserIdItem struct { userIds []int32 userIdMap map[int32]bool } func NewUserIDItem(userIdList []int32) *UserIdItem { rule := &UserIdItem{ userIds: userIdList, userIdMap: make(map[int32]bool), } for _, userId := range userIdList { rule.userIdMap[userId] = true } return rule } func (r *UserIdItem) Match(metadata *adapter.InboundContext) bool { if metadata.ProcessInfo == nil || metadata.ProcessInfo.UserId == -1 { return false } return r.userIdMap[metadata.ProcessInfo.UserId] } func (r *UserIdItem) String() string { var description string pLen := len(r.userIds) if pLen == 1 { description = "user_id=" + F.ToString(r.userIds[0]) } else { description = "user_id=[" + strings.Join(F.MapToString(r.userIds), " ") + "]" } return description } ================================================ FILE: route/rule/rule_item_wifi_bssid.go ================================================ package rule import ( "strings" "github.com/sagernet/sing-box/adapter" F "github.com/sagernet/sing/common/format" ) var _ RuleItem = (*WIFIBSSIDItem)(nil) type WIFIBSSIDItem struct { bssidList []string bssidMap map[string]bool networkManager adapter.NetworkManager } func NewWIFIBSSIDItem(networkManager adapter.NetworkManager, bssidList []string) *WIFIBSSIDItem { bssidMap := make(map[string]bool) for _, bssid := range bssidList { bssidMap[adapter.NormalizeWIFIBSSID(bssid)] = true } return &WIFIBSSIDItem{ bssidList, bssidMap, networkManager, } } func (r *WIFIBSSIDItem) Match(metadata *adapter.InboundContext) bool { return r.bssidMap[r.networkManager.WIFIState().BSSID] } func (r *WIFIBSSIDItem) String() string { if len(r.bssidList) == 1 { return F.ToString("wifi_bssid=", r.bssidList[0]) } return F.ToString("wifi_bssid=[", strings.Join(r.bssidList, " "), "]") } ================================================ FILE: route/rule/rule_item_wifi_ssid.go ================================================ package rule import ( "strings" "github.com/sagernet/sing-box/adapter" F "github.com/sagernet/sing/common/format" ) var _ RuleItem = (*WIFISSIDItem)(nil) type WIFISSIDItem struct { ssidList []string ssidMap map[string]bool networkManager adapter.NetworkManager } func NewWIFISSIDItem(networkManager adapter.NetworkManager, ssidList []string) *WIFISSIDItem { ssidMap := make(map[string]bool) for _, ssid := range ssidList { ssidMap[ssid] = true } return &WIFISSIDItem{ ssidList, ssidMap, networkManager, } } func (r *WIFISSIDItem) Match(metadata *adapter.InboundContext) bool { return r.ssidMap[r.networkManager.WIFIState().SSID] } func (r *WIFISSIDItem) String() string { if len(r.ssidList) == 1 { return F.ToString("wifi_ssid=", r.ssidList[0]) } return F.ToString("wifi_ssid=[", strings.Join(r.ssidList, " "), "]") } ================================================ FILE: route/rule/rule_nested_action.go ================================================ 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 } } ================================================ FILE: route/rule/rule_nested_action_test.go ================================================ 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") } ================================================ FILE: route/rule/rule_network_interface_address.go ================================================ package rule import ( "net/netip" "strings" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/json/badjson" "github.com/sagernet/sing/common/json/badoption" ) var _ RuleItem = (*NetworkInterfaceAddressItem)(nil) type NetworkInterfaceAddressItem struct { networkManager adapter.NetworkManager interfaceAddresses map[C.InterfaceType][]netip.Prefix description string } func NewNetworkInterfaceAddressItem(networkManager adapter.NetworkManager, interfaceAddresses *badjson.TypedMap[option.InterfaceType, badoption.Listable[*badoption.Prefixable]]) *NetworkInterfaceAddressItem { item := &NetworkInterfaceAddressItem{ networkManager: networkManager, interfaceAddresses: make(map[C.InterfaceType][]netip.Prefix, interfaceAddresses.Size()), } var entryDescriptions []string for _, entry := range interfaceAddresses.Entries() { prefixes := make([]netip.Prefix, 0, len(entry.Value)) for _, prefixable := range entry.Value { prefixes = append(prefixes, prefixable.Build(netip.Prefix{})) } item.interfaceAddresses[entry.Key.Build()] = prefixes entryDescriptions = append(entryDescriptions, entry.Key.Build().String()+"="+strings.Join(common.Map(prefixes, netip.Prefix.String), ",")) } item.description = "network_interface_address=[" + strings.Join(entryDescriptions, " ") + "]" return item } func (r *NetworkInterfaceAddressItem) Match(metadata *adapter.InboundContext) bool { interfaces := r.networkManager.NetworkInterfaces() myInterface := r.networkManager.InterfaceMonitor().MyInterface() match: for ifType, addresses := range r.interfaceAddresses { for _, networkInterface := range interfaces { if networkInterface.Name == myInterface { continue } if networkInterface.Type != ifType { continue } if common.Any(networkInterface.Addresses, func(it netip.Prefix) bool { return common.Any(addresses, func(prefix netip.Prefix) bool { return prefix.Overlaps(it) }) }) { continue match } } return false } return true } func (r *NetworkInterfaceAddressItem) String() string { return r.description } ================================================ FILE: route/rule/rule_set.go ================================================ package rule import ( "context" "reflect" "github.com/sagernet/sing-box/adapter" 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" "github.com/sagernet/sing/service" "go4.org/netipx" ) func NewRuleSet(ctx context.Context, logger logger.ContextLogger, options option.RuleSet) (adapter.RuleSet, error) { switch options.Type { case C.RuleSetTypeInline, C.RuleSetTypeLocal, "": return NewLocalRuleSet(ctx, logger, options) case C.RuleSetTypeRemote: return NewRemoteRuleSet(ctx, logger, options) default: return nil, E.New("unknown rule-set type: ", options.Type) } } func extractIPSetFromRule(rawRule adapter.HeadlessRule) []*netipx.IPSet { switch rule := rawRule.(type) { case *DefaultHeadlessRule: return common.FlatMap(rule.destinationIPCIDRItems, func(rawItem RuleItem) []*netipx.IPSet { switch item := rawItem.(type) { case *IPCIDRItem: return []*netipx.IPSet{item.ipSet} default: return nil } }) case *LogicalHeadlessRule: return common.FlatMap(rule.rules, extractIPSetFromRule) default: panic("unexpected rule type") } } func HasHeadlessRule(rules []option.HeadlessRule, cond func(rule option.DefaultHeadlessRule) bool) bool { for _, rule := range rules { switch rule.Type { case C.RuleTypeDefault: if cond(rule.DefaultOptions) { return true } case C.RuleTypeLogical: if HasHeadlessRule(rule.LogicalOptions.Rules, cond) { return true } } } return false } func isProcessHeadlessRule(rule option.DefaultHeadlessRule) bool { 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 { return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0 } 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 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), } } 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) } ================================================ FILE: route/rule/rule_set_local.go ================================================ package rule import ( "context" "os" "path/filepath" "strings" "sync" "sync/atomic" "github.com/sagernet/fswatch" "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/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" "github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/service/filemanager" "go4.org/netipx" ) var _ adapter.RuleSet = (*LocalRuleSet)(nil) type LocalRuleSet struct { ctx context.Context logger logger.Logger tag string access sync.RWMutex rules []adapter.HeadlessRule metadata adapter.RuleSetMetadata fileFormat string watcher *fswatch.Watcher callbacks list.List[adapter.RuleSetUpdateCallback] refs atomic.Int32 } func NewLocalRuleSet(ctx context.Context, logger logger.Logger, options option.RuleSet) (*LocalRuleSet, error) { ruleSet := &LocalRuleSet{ ctx: ctx, logger: logger, tag: options.Tag, fileFormat: options.Format, } if options.Type == C.RuleSetTypeInline { if len(options.InlineOptions.Rules) == 0 { return nil, E.New("empty inline rule-set") } err := ruleSet.reloadRules(options.InlineOptions.Rules) if err != nil { return nil, err } } else { filePath := filemanager.BasePath(ctx, options.LocalOptions.Path) filePath, _ = filepath.Abs(filePath) err := ruleSet.reloadFile(filePath) if err != nil { return nil, err } watcher, err := fswatch.NewWatcher(fswatch.Options{ Path: []string{filePath}, Callback: func(path string) { uErr := ruleSet.reloadFile(path) if uErr != nil { logger.Error(E.Cause(uErr, "reload rule-set ", options.Tag)) } }, }) if err != nil { return nil, err } ruleSet.watcher = watcher } return ruleSet, nil } func (s *LocalRuleSet) Name() string { return s.tag } func (s *LocalRuleSet) String() string { return strings.Join(F.MapToString(s.rules), " ") } func (s *LocalRuleSet) StartContext(ctx context.Context, startContext *adapter.HTTPStartContext) error { if s.watcher != nil { err := s.watcher.Start() if err != nil { s.logger.Error(E.Cause(err, "watch rule-set file")) } } return nil } func (s *LocalRuleSet) reloadFile(path string) error { var ruleSet option.PlainRuleSetCompat switch s.fileFormat { case C.RuleSetFormatSource, "": content, err := os.ReadFile(path) if err != nil { return err } ruleSet, err = json.UnmarshalExtended[option.PlainRuleSetCompat](content) if err != nil { return err } case C.RuleSetFormatBinary: setFile, err := os.Open(path) if err != nil { return err } ruleSet, err = srs.Read(setFile, false) if err != nil { return err } default: return E.New("unknown rule-set format: ", s.fileFormat) } plainRuleSet, err := ruleSet.Upgrade() if err != nil { return err } return s.reloadRules(plainRuleSet.Rules) } func (s *LocalRuleSet) reloadRules(headlessRules []option.HeadlessRule) error { rules := make([]adapter.HeadlessRule, len(headlessRules)) var err error for i, ruleOptions := range headlessRules { rules[i], err = NewHeadlessRule(s.ctx, ruleOptions) if err != nil { return E.Cause(err, "parse rule_set.rules.[", i, "]") } } metadata := buildRuleSetMetadata(headlessRules) err = validateRuleSetMetadataUpdate(s.ctx, s.tag, metadata) if err != nil { return err } s.access.Lock() s.rules = rules s.metadata = metadata callbacks := s.callbacks.Array() s.access.Unlock() for _, callback := range callbacks { callback(s) } return nil } func (s *LocalRuleSet) PostStart() error { return nil } func (s *LocalRuleSet) Metadata() adapter.RuleSetMetadata { s.access.RLock() defer s.access.RUnlock() return s.metadata } func (s *LocalRuleSet) ExtractIPSet() []*netipx.IPSet { s.access.RLock() defer s.access.RUnlock() return common.FlatMap(s.rules, extractIPSetFromRule) } func (s *LocalRuleSet) IncRef() { s.refs.Add(1) } func (s *LocalRuleSet) DecRef() { if s.refs.Add(-1) < 0 { panic("rule-set: negative refs") } } func (s *LocalRuleSet) Cleanup() { if s.refs.Load() == 0 { s.rules = nil } } func (s *LocalRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] { s.access.Lock() defer s.access.Unlock() return s.callbacks.PushBack(callback) } func (s *LocalRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) { s.access.Lock() defer s.access.Unlock() s.callbacks.Remove(element) } func (s *LocalRuleSet) Close() error { s.rules = nil return common.Close(common.PtrOrNil(s.watcher)) } func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool { return !s.matchStates(metadata).isEmpty() } func (s *LocalRuleSet) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { return s.matchStatesWithBase(metadata, 0) } func (s *LocalRuleSet) matchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet { var stateSet ruleMatchStateSet for _, rule := range s.rules { nestedMetadata := *metadata nestedMetadata.ResetRuleMatchCache() stateSet = stateSet.merge(matchHeadlessRuleStatesWithBase(rule, &nestedMetadata, base)) } return stateSet } ================================================ FILE: route/rule/rule_set_remote.go ================================================ package rule import ( "bytes" "context" "io" "net/http" "runtime" "strings" "sync" "sync/atomic" "time" "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" "github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/pause" "go4.org/netipx" ) var _ adapter.RuleSet = (*RemoteRuleSet)(nil) type RemoteRuleSet struct { ctx context.Context cancel context.CancelFunc logger logger.ContextLogger outbound adapter.OutboundManager options option.RuleSet updateInterval time.Duration httpClient *http.Client access sync.RWMutex rules []adapter.HeadlessRule metadata adapter.RuleSetMetadata lastUpdated time.Time lastEtag string updateTicker *time.Ticker cacheFile adapter.CacheFile pauseManager pause.Manager callbacks list.List[adapter.RuleSetUpdateCallback] refs atomic.Int32 } 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 { updateInterval = time.Duration(options.RemoteOptions.UpdateInterval) } else { updateInterval = 24 * time.Hour } return &RemoteRuleSet{ ctx: ctx, cancel: cancel, outbound: service.FromContext[adapter.OutboundManager](ctx), logger: logger, options: options, updateInterval: updateInterval, pauseManager: service.FromContext[pause.Manager](ctx), }, nil } func (s *RemoteRuleSet) Name() string { return s.options.Tag } func (s *RemoteRuleSet) String() string { return strings.Join(F.MapToString(s.rules), " ") } func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext *adapter.HTTPStartContext) error { s.cacheFile = service.FromContext[adapter.CacheFile](s.ctx) transport, err := s.resolveTransport() if err != nil { return E.Cause(err, "create rule-set http client") } 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) if err != nil { return E.Cause(err, "restore cached rule-set") } s.lastUpdated = savedSet.LastUpdated s.lastEtag = savedSet.LastEtag } } if s.lastUpdated.IsZero() { err = s.fetch(ctx, true) if err != nil { return E.Cause(err, "initial rule-set: ", s.options.Tag) } } s.updateTicker = time.NewTicker(s.updateInterval) return nil } func (s *RemoteRuleSet) PostStart() error { go s.loopUpdate() return nil } func (s *RemoteRuleSet) Metadata() adapter.RuleSetMetadata { s.access.RLock() defer s.access.RUnlock() return s.metadata } func (s *RemoteRuleSet) ExtractIPSet() []*netipx.IPSet { s.access.RLock() defer s.access.RUnlock() return common.FlatMap(s.rules, extractIPSetFromRule) } func (s *RemoteRuleSet) IncRef() { s.refs.Add(1) } func (s *RemoteRuleSet) DecRef() { if s.refs.Add(-1) < 0 { panic("rule-set: negative refs") } } func (s *RemoteRuleSet) Cleanup() { if s.refs.Load() == 0 { s.rules = nil } } func (s *RemoteRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] { s.access.Lock() defer s.access.Unlock() return s.callbacks.PushBack(callback) } func (s *RemoteRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) { s.access.Lock() defer s.access.Unlock() s.callbacks.Remove(element) } func (s *RemoteRuleSet) loadBytes(content []byte) error { var ( ruleSet option.PlainRuleSetCompat err error ) switch s.options.Format { case C.RuleSetFormatSource: ruleSet, err = json.UnmarshalExtended[option.PlainRuleSetCompat](content) if err != nil { return err } case C.RuleSetFormatBinary: ruleSet, err = srs.Read(bytes.NewReader(content), false) if err != nil { return err } default: return E.New("unknown rule-set format: ", s.options.Format) } plainRuleSet, err := ruleSet.Upgrade() if err != nil { return err } rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules)) for i, ruleOptions := range plainRuleSet.Rules { rules[i], err = NewHeadlessRule(s.ctx, ruleOptions) if err != nil { 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 = metadata s.rules = rules callbacks := s.callbacks.Array() s.access.Unlock() for _, callback := range callbacks { callback(s) } return nil } func (s *RemoteRuleSet) loopUpdate() { if time.Since(s.lastUpdated) > s.updateInterval { s.updateOnce() } for { runtime.GC() select { case <-s.ctx.Done(): return case <-s.updateTicker.C: s.updateOnce() } } } func (s *RemoteRuleSet) updateOnce() { 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 { s.rules = nil } } 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) request, err := http.NewRequest("GET", s.options.RemoteOptions.URL, nil) if err != nil { return err } if s.lastEtag != "" { request.Header.Set("If-None-Match", s.lastEtag) } 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: s.lastUpdated = time.Now() if s.cacheFile != nil { savedRuleSet := s.cacheFile.LoadRuleSet(s.options.Tag) if savedRuleSet != nil { savedRuleSet.LastUpdated = s.lastUpdated err = s.cacheFile.SaveRuleSet(s.options.Tag, savedRuleSet) if err != nil { s.logger.Error("save rule-set updated time: ", err) return nil } } } s.logger.Info("update rule-set ", s.options.Tag, ": not modified") return nil default: return E.New("unexpected status: ", response.Status) } content, err := io.ReadAll(response.Body) if err != nil { return err } err = s.loadBytes(content) if err != nil { return err } eTagHeader := response.Header.Get("Etag") if eTagHeader != "" { s.lastEtag = eTagHeader } s.lastUpdated = time.Now() if s.cacheFile != nil { err = s.cacheFile.SaveRuleSet(s.options.Tag, &adapter.SavedBinary{ LastUpdated: s.lastUpdated, Content: content, LastEtag: s.lastEtag, }) if err != nil { s.logger.Error("save rule-set cache: ", err) } } s.logger.Info("updated rule-set ", s.options.Tag) 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) 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 { return nil, E.New("default http client transport is not initialized") } return defaultTransport, nil } func (s *RemoteRuleSet) Close() error { s.rules = nil s.cancel() if s.updateTicker != nil { s.updateTicker.Stop() } return nil } func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool { return !s.matchStates(metadata).isEmpty() } func (s *RemoteRuleSet) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { return s.matchStatesWithBase(metadata, 0) } func (s *RemoteRuleSet) matchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet { var stateSet ruleMatchStateSet for _, rule := range s.rules { nestedMetadata := *metadata nestedMetadata.ResetRuleMatchCache() stateSet = stateSet.merge(matchHeadlessRuleStatesWithBase(rule, &nestedMetadata, base)) } return stateSet } ================================================ FILE: route/rule/rule_set_semantics_test.go ================================================ package rule import ( "context" "net" "net/netip" "strings" "testing" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/convertor/adguard" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" slogger "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" mDNS "github.com/miekg/dns" "github.com/stretchr/testify/require" ) func TestRouteRuleSetMergeDestinationAddressGroup(t *testing.T) { t.Parallel() testCases := []struct { name string metadata adapter.InboundContext inner adapter.HeadlessRule }{ { name: "domain", metadata: testMetadata("www.example.com"), inner: headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, []string{"www.example.com"}, nil) }), }, { name: "domain_suffix", metadata: testMetadata("www.example.com"), inner: headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, nil, []string{"example.com"}) }), }, { name: "domain_keyword", metadata: testMetadata("www.example.com"), inner: headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationKeywordItem(rule, []string{"example"}) }), }, { name: "domain_regex", metadata: testMetadata("www.example.com"), inner: headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationRegexItem(t, rule, []string{`^www\.example\.com$`}) }), }, { name: "ip_cidr", metadata: func() adapter.InboundContext { metadata := testMetadata("lookup.example") metadata.DestinationAddresses = []netip.Addr{netip.MustParseAddr("8.8.8.8")} return metadata }(), inner: headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationIPCIDRItem(t, rule, []string{"8.8.8.0/24"}) }), }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { t.Parallel() ruleSet := newLocalRuleSetForTest("merge-destination", testCase.inner) rule := routeRuleForTest(func(rule *abstractDefaultRule) { addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) }) require.True(t, rule.Match(&testCase.metadata)) }) } } func TestRouteRuleSetMergeSourceAndPortGroups(t *testing.T) { t.Parallel() t.Run("source address", func(t *testing.T) { t.Parallel() metadata := testMetadata("www.example.com") ruleSet := newLocalRuleSetForTest("merge-source-address", headlessDefaultRule(t, func(rule *abstractDefaultRule) { addSourceAddressItem(t, rule, []string{"10.0.0.0/8"}) })) rule := routeRuleForTest(func(rule *abstractDefaultRule) { addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) addSourceAddressItem(t, rule, []string{"198.51.100.0/24"}) }) require.True(t, rule.Match(&metadata)) }) t.Run("source address via ruleset ipcidr match source", func(t *testing.T) { t.Parallel() metadata := testMetadata("www.example.com") ruleSet := newLocalRuleSetForTest("merge-source-address-ipcidr", headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationIPCIDRItem(t, rule, []string{"10.0.0.0/8"}) })) rule := routeRuleForTest(func(rule *abstractDefaultRule) { addRuleSetItem(rule, &RuleSetItem{ setList: []adapter.RuleSet{ruleSet}, ipCidrMatchSource: true, }) addSourceAddressItem(t, rule, []string{"198.51.100.0/24"}) }) require.True(t, rule.Match(&metadata)) }) t.Run("destination port", func(t *testing.T) { t.Parallel() metadata := testMetadata("www.example.com") ruleSet := newLocalRuleSetForTest("merge-destination-port", headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationPortItem(rule, []uint16{443}) })) rule := routeRuleForTest(func(rule *abstractDefaultRule) { addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) addDestinationPortItem(rule, []uint16{8443}) }) require.True(t, rule.Match(&metadata)) }) t.Run("destination port range", func(t *testing.T) { t.Parallel() metadata := testMetadata("www.example.com") ruleSet := newLocalRuleSetForTest("merge-destination-port-range", headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationPortRangeItem(t, rule, []string{"400:500"}) })) rule := routeRuleForTest(func(rule *abstractDefaultRule) { addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) addDestinationPortItem(rule, []uint16{8443}) }) require.True(t, rule.Match(&metadata)) }) t.Run("source port", func(t *testing.T) { t.Parallel() metadata := testMetadata("www.example.com") ruleSet := newLocalRuleSetForTest("merge-source-port", headlessDefaultRule(t, func(rule *abstractDefaultRule) { addSourcePortItem(rule, []uint16{1000}) })) rule := routeRuleForTest(func(rule *abstractDefaultRule) { addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) addSourcePortItem(rule, []uint16{2000}) }) require.True(t, rule.Match(&metadata)) }) t.Run("source port range", func(t *testing.T) { t.Parallel() metadata := testMetadata("www.example.com") ruleSet := newLocalRuleSetForTest("merge-source-port-range", headlessDefaultRule(t, func(rule *abstractDefaultRule) { addSourcePortRangeItem(t, rule, []string{"900:1100"}) })) rule := routeRuleForTest(func(rule *abstractDefaultRule) { addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) addSourcePortItem(rule, []uint16{2000}) }) require.True(t, rule.Match(&metadata)) }) } func TestRouteRuleSetOuterGroupedStateMergesIntoSameGroup(t *testing.T) { t.Parallel() testCases := []struct { name string metadata adapter.InboundContext buildOuter func(*testing.T, *abstractDefaultRule) buildInner func(*testing.T, *abstractDefaultRule) }{ { name: "destination address", metadata: testMetadata("www.example.com"), buildOuter: func(t *testing.T, rule *abstractDefaultRule) { t.Helper() addDestinationAddressItem(t, rule, nil, []string{"example.com"}) }, buildInner: func(t *testing.T, rule *abstractDefaultRule) { t.Helper() addDestinationAddressItem(t, rule, nil, []string{"google.com"}) }, }, { name: "source address", metadata: testMetadata("www.example.com"), buildOuter: func(t *testing.T, rule *abstractDefaultRule) { t.Helper() addSourceAddressItem(t, rule, []string{"10.0.0.0/8"}) }, buildInner: func(t *testing.T, rule *abstractDefaultRule) { t.Helper() addSourceAddressItem(t, rule, []string{"198.51.100.0/24"}) }, }, { name: "source port", metadata: testMetadata("www.example.com"), buildOuter: func(t *testing.T, rule *abstractDefaultRule) { t.Helper() addSourcePortItem(rule, []uint16{1000}) }, buildInner: func(t *testing.T, rule *abstractDefaultRule) { t.Helper() addSourcePortItem(rule, []uint16{2000}) }, }, { name: "destination port", metadata: testMetadata("www.example.com"), buildOuter: func(t *testing.T, rule *abstractDefaultRule) { t.Helper() addDestinationPortItem(rule, []uint16{443}) }, buildInner: func(t *testing.T, rule *abstractDefaultRule) { t.Helper() addDestinationPortItem(rule, []uint16{8443}) }, }, { name: "destination ip cidr", metadata: func() adapter.InboundContext { metadata := testMetadata("lookup.example") metadata.DestinationAddresses = []netip.Addr{netip.MustParseAddr("203.0.113.1")} return metadata }(), buildOuter: func(t *testing.T, rule *abstractDefaultRule) { t.Helper() addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) }, buildInner: func(t *testing.T, rule *abstractDefaultRule) { t.Helper() addDestinationIPCIDRItem(t, rule, []string{"198.51.100.0/24"}) }, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { t.Parallel() ruleSet := newLocalRuleSetForTest("outer-merge-"+testCase.name, headlessDefaultRule(t, func(rule *abstractDefaultRule) { testCase.buildInner(t, rule) })) rule := routeRuleForTest(func(rule *abstractDefaultRule) { testCase.buildOuter(t, rule) addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) }) require.True(t, rule.Match(&testCase.metadata)) }) } } func TestRouteRuleSetOtherFieldsStayAnd(t *testing.T) { t.Parallel() metadata := testMetadata("www.example.com") ruleSet := newLocalRuleSetForTest("other-fields-and", headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, nil, []string{"example.com"}) })) rule := routeRuleForTest(func(rule *abstractDefaultRule) { addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) addOtherItem(rule, NewNetworkItem([]string{N.NetworkUDP})) }) require.False(t, rule.Match(&metadata)) } func TestRouteRuleSetMergedBranchKeepsAndConstraints(t *testing.T) { t.Parallel() t.Run("outer group does not bypass inner non grouped condition", func(t *testing.T) { t.Parallel() metadata := testMetadata("www.example.com") ruleSet := newLocalRuleSetForTest("network-and", headlessDefaultRule(t, func(rule *abstractDefaultRule) { addOtherItem(rule, NewNetworkItem([]string{N.NetworkUDP})) })) rule := routeRuleForTest(func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, nil, []string{"example.com"}) addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) }) require.False(t, rule.Match(&metadata)) }) t.Run("outer group does not satisfy different grouped branch", func(t *testing.T) { t.Parallel() metadata := testMetadata("www.example.com") ruleSet := newLocalRuleSetForTest("different-group", headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, nil, []string{"google.com"}) })) rule := routeRuleForTest(func(rule *abstractDefaultRule) { addSourcePortItem(rule, []uint16{1000}) addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) }) require.False(t, rule.Match(&metadata)) }) } func TestRouteRuleSetOrSemantics(t *testing.T) { t.Parallel() t.Run("later ruleset can satisfy outer group", func(t *testing.T) { t.Parallel() metadata := testMetadata("www.example.com") emptyStateSet := newLocalRuleSetForTest("network-only", headlessDefaultRule(t, func(rule *abstractDefaultRule) { addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP})) })) destinationStateSet := newLocalRuleSetForTest("domain-only", headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, nil, []string{"example.com"}) })) rule := routeRuleForTest(func(rule *abstractDefaultRule) { addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{emptyStateSet, destinationStateSet}}) addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) }) require.True(t, rule.Match(&metadata)) }) t.Run("later rule in same set can satisfy outer group", func(t *testing.T) { t.Parallel() metadata := testMetadata("www.example.com") ruleSet := newLocalRuleSetForTest( "rule-set-or", headlessDefaultRule(t, func(rule *abstractDefaultRule) { addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP})) }), headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, nil, []string{"example.com"}) }), ) rule := routeRuleForTest(func(rule *abstractDefaultRule) { addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) }) require.True(t, rule.Match(&metadata)) }) t.Run("cross ruleset union is not allowed", func(t *testing.T) { t.Parallel() metadata := testMetadata("www.example.com") sourceStateSet := newLocalRuleSetForTest("source-only", headlessDefaultRule(t, func(rule *abstractDefaultRule) { addSourcePortItem(rule, []uint16{1000}) })) destinationStateSet := newLocalRuleSetForTest("destination-only", headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, nil, []string{"example.com"}) })) rule := routeRuleForTest(func(rule *abstractDefaultRule) { addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{sourceStateSet, destinationStateSet}}) addSourcePortItem(rule, []uint16{2000}) addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) }) require.False(t, rule.Match(&metadata)) }) } func TestRouteRuleSetLogicalSemantics(t *testing.T) { t.Parallel() t.Run("logical or keeps all successful branch states", func(t *testing.T) { t.Parallel() metadata := testMetadata("www.example.com") ruleSet := newLocalRuleSetForTest("logical-or", headlessLogicalRule( C.LogicalTypeOr, false, headlessDefaultRule(t, func(rule *abstractDefaultRule) { addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP})) }), headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, nil, []string{"example.com"}) }), )) rule := routeRuleForTest(func(rule *abstractDefaultRule) { addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) }) require.True(t, rule.Match(&metadata)) }) t.Run("logical and unions child states", func(t *testing.T) { t.Parallel() metadata := testMetadata("www.example.com") ruleSet := newLocalRuleSetForTest("logical-and", headlessLogicalRule( C.LogicalTypeAnd, false, headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, nil, []string{"example.com"}) }), headlessDefaultRule(t, func(rule *abstractDefaultRule) { addSourcePortItem(rule, []uint16{1000}) }), )) rule := routeRuleForTest(func(rule *abstractDefaultRule) { addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) addSourcePortItem(rule, []uint16{2000}) }) require.True(t, rule.Match(&metadata)) }) t.Run("invert success does not contribute positive state", func(t *testing.T) { t.Parallel() metadata := testMetadata("www.example.com") ruleSet := newLocalRuleSetForTest("invert", headlessDefaultRule(t, func(rule *abstractDefaultRule) { rule.invert = true addDestinationAddressItem(t, rule, nil, []string{"cn"}) })) rule := routeRuleForTest(func(rule *abstractDefaultRule) { addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) }) require.False(t, rule.Match(&metadata)) }) } func TestRouteRuleSetInvertMergedBranchSemantics(t *testing.T) { t.Parallel() t.Run("default invert keeps inherited group outside grouped predicate", func(t *testing.T) { t.Parallel() metadata := testMetadata("www.example.com") ruleSet := newLocalRuleSetForTest("invert-grouped", headlessDefaultRule(t, func(rule *abstractDefaultRule) { rule.invert = true addDestinationAddressItem(t, rule, nil, []string{"google.com"}) })) rule := routeRuleForTest(func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, nil, []string{"example.com"}) addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) }) require.True(t, rule.Match(&metadata)) }) t.Run("default invert keeps inherited group after negation succeeds", func(t *testing.T) { t.Parallel() metadata := testMetadata("www.example.com") ruleSet := newLocalRuleSetForTest("invert-network", headlessDefaultRule(t, func(rule *abstractDefaultRule) { rule.invert = true addOtherItem(rule, NewNetworkItem([]string{N.NetworkUDP})) })) rule := routeRuleForTest(func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, nil, []string{"example.com"}) addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) }) require.True(t, rule.Match(&metadata)) }) t.Run("logical invert keeps inherited group outside grouped predicate", func(t *testing.T) { t.Parallel() metadata := testMetadata("www.example.com") ruleSet := newLocalRuleSetForTest("logical-invert-grouped", headlessLogicalRule( C.LogicalTypeOr, true, headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, nil, []string{"google.com"}) }), )) rule := routeRuleForTest(func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, nil, []string{"example.com"}) addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) }) require.True(t, rule.Match(&metadata)) }) t.Run("logical invert keeps inherited group after negation succeeds", func(t *testing.T) { t.Parallel() metadata := testMetadata("www.example.com") ruleSet := newLocalRuleSetForTest("logical-invert-network", headlessLogicalRule( C.LogicalTypeOr, true, headlessDefaultRule(t, func(rule *abstractDefaultRule) { addOtherItem(rule, NewNetworkItem([]string{N.NetworkUDP})) }), )) rule := routeRuleForTest(func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, nil, []string{"example.com"}) addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) }) require.True(t, rule.Match(&metadata)) }) } func TestRouteRuleSetNoLeakageRegressions(t *testing.T) { t.Parallel() t.Run("same ruleset failed branch does not leak", func(t *testing.T) { t.Parallel() metadata := testMetadata("www.example.com") ruleSet := newLocalRuleSetForTest( "same-set", headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, nil, []string{"example.com"}) addSourcePortItem(rule, []uint16{1}) }), headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) addSourcePortItem(rule, []uint16{1000}) }), ) rule := routeRuleForTest(func(rule *abstractDefaultRule) { addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) }) require.False(t, rule.Match(&metadata)) }) t.Run("adguard exclusion remains isolated across rulesets", func(t *testing.T) { t.Parallel() metadata := testMetadata("im.qq.com") excludeSet := newLocalRuleSetForTest("adguard", mustAdGuardRule(t, "@@||im.qq.com^\n||whatever1.com^\n")) otherSet := newLocalRuleSetForTest("other", headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, nil, []string{"whatever2.com"}) })) rule := routeRuleForTest(func(rule *abstractDefaultRule) { addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{excludeSet, otherSet}}) }) require.False(t, rule.Match(&metadata)) }) } func TestDefaultRuleDoesNotReuseGroupedMatchCacheAcrossEvaluations(t *testing.T) { t.Parallel() metadata := testMetadata("www.example.com") rule := routeRuleForTest(func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, nil, []string{"example.com"}) }) require.True(t, rule.Match(&metadata)) metadata.Destination.Fqdn = "www.example.org" require.False(t, rule.Match(&metadata)) } func TestRouteRuleSetRemoteUsesSameSemantics(t *testing.T) { t.Parallel() metadata := testMetadata("www.example.com") ruleSet := newRemoteRuleSetForTest( "remote", headlessDefaultRule(t, func(rule *abstractDefaultRule) { addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP})) }), headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, nil, []string{"example.com"}) }), ) rule := routeRuleForTest(func(rule *abstractDefaultRule) { addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) }) require.True(t, rule.Match(&metadata)) } func TestDNSRuleSetSemantics(t *testing.T) { t.Parallel() t.Run("outer destination group merges into matching ruleset branch", func(t *testing.T) { t.Parallel() metadata := testMetadata("www.baidu.com") ruleSet := newLocalRuleSetForTest("dns-merged-branch", headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, nil, []string{"google.com"}) })) rule := dnsRuleForTest(func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, nil, []string{"baidu.com"}) addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) }) require.True(t, rule.Match(&metadata)) }) t.Run("outer destination group does not bypass ruleset non grouped condition", func(t *testing.T) { t.Parallel() metadata := testMetadata("www.example.com") ruleSet := newLocalRuleSetForTest("dns-network-and", headlessDefaultRule(t, func(rule *abstractDefaultRule) { addOtherItem(rule, NewNetworkItem([]string{N.NetworkUDP})) })) rule := dnsRuleForTest(func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, nil, []string{"example.com"}) addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) }) require.False(t, rule.Match(&metadata)) }) t.Run("outer destination group stays outside inverted grouped branch", func(t *testing.T) { t.Parallel() metadata := testMetadata("www.baidu.com") ruleSet := newLocalRuleSetForTest("dns-invert-grouped", headlessDefaultRule(t, func(rule *abstractDefaultRule) { rule.invert = true addDestinationAddressItem(t, rule, nil, []string{"google.com"}) })) rule := dnsRuleForTest(func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, nil, []string{"baidu.com"}) addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) }) require.True(t, rule.Match(&metadata)) }) t.Run("outer destination group stays outside inverted logical branch", func(t *testing.T) { t.Parallel() metadata := testMetadata("www.example.com") ruleSet := newLocalRuleSetForTest("dns-logical-invert-network", headlessLogicalRule( C.LogicalTypeOr, true, headlessDefaultRule(t, func(rule *abstractDefaultRule) { addOtherItem(rule, NewNetworkItem([]string{N.NetworkUDP})) }), )) rule := dnsRuleForTest(func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, nil, []string{"example.com"}) addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) }) require.True(t, rule.Match(&metadata)) }) t.Run("match address limit merges destination group", func(t *testing.T) { t.Parallel() metadata := testMetadata("www.example.com") ruleSet := newLocalRuleSetForTest("dns-merge", headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, nil, []string{"example.com"}) })) rule := dnsRuleForTest(func(rule *abstractDefaultRule) { addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) }) 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() metadata := testMetadata("www.example.com") emptyStateSet := newLocalRuleSetForTest("dns-empty", headlessDefaultRule(t, func(rule *abstractDefaultRule) { addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP})) })) destinationStateSet := newLocalRuleSetForTest("dns-destination", headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, nil, []string{"example.com"}) })) rule := dnsRuleForTest(func(rule *abstractDefaultRule) { addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{emptyStateSet, destinationStateSet}}) addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) }) 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() metadata := testMetadata("www.example.com") ruleSet := newLocalRuleSetForTest("dns-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}, ipCidrAcceptEmpty: true, }) }) 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 { 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 { 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 { 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) { 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 { 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.Match(&preLookupMetadata)) matchedMetadata := testMetadata("lookup.example") matchedMetadata.DestinationAddresses = testCase.matchedAddrs require.False(t, rule.MatchAddressLimit(&matchedMetadata, dnsResponseForTest(testCase.matchedAddrs...))) unmatchedMetadata := testMetadata("lookup.example") unmatchedMetadata.DestinationAddresses = testCase.unmatchedAddrs require.True(t, rule.MatchAddressLimit(&unmatchedMetadata, dnsResponseForTest(testCase.unmatchedAddrs...))) }) } 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) { rule.invert = true addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP})) addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) }) require.True(t, rule.Match(&metadata)) }) 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) { addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) })) rule := dnsRuleForTest(func(rule *abstractDefaultRule) { rule.invert = true addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) }) require.True(t, rule.Match(&metadata)) }) } func routeRuleForTest(build func(*abstractDefaultRule)) *DefaultRule { rule := &DefaultRule{} build(&rule.abstractDefaultRule) return rule } func dnsRuleForTest(build func(*abstractDefaultRule)) *DefaultDNSRule { rule := &DefaultDNSRule{} build(&rule.abstractDefaultRule) return rule } func headlessDefaultRule(t *testing.T, build func(*abstractDefaultRule)) *DefaultHeadlessRule { t.Helper() rule := &DefaultHeadlessRule{} build(&rule.abstractDefaultRule) return rule } func headlessLogicalRule(mode string, invert bool, rules ...adapter.HeadlessRule) *LogicalHeadlessRule { return &LogicalHeadlessRule{ abstractLogicalRule: abstractLogicalRule{ rules: rules, mode: mode, invert: invert, }, } } func newLocalRuleSetForTest(tag string, rules ...adapter.HeadlessRule) *LocalRuleSet { return &LocalRuleSet{ tag: tag, rules: rules, } } func newRemoteRuleSetForTest(tag string, rules ...adapter.HeadlessRule) *RemoteRuleSet { return &RemoteRuleSet{ options: option.RuleSet{Tag: tag}, rules: rules, } } func mustAdGuardRule(t *testing.T, content string) adapter.HeadlessRule { t.Helper() rules, err := adguard.ToOptions(strings.NewReader(content), slogger.NOP()) require.NoError(t, err) require.Len(t, rules, 1) rule, err := NewHeadlessRule(context.Background(), rules[0]) require.NoError(t, err) return rule } func testMetadata(domain string) adapter.InboundContext { return adapter.InboundContext{ Network: N.NetworkTCP, Source: M.Socksaddr{ Addr: netip.MustParseAddr("10.0.0.1"), Port: 1000, }, Destination: M.Socksaddr{ Fqdn: domain, Port: 443, }, } } 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) } func addOtherItem(rule *abstractDefaultRule, item RuleItem) { rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } func addSourceAddressItem(t *testing.T, rule *abstractDefaultRule, cidrs []string) { t.Helper() item, err := NewIPCIDRItem(true, cidrs) require.NoError(t, err) rule.sourceAddressItems = append(rule.sourceAddressItems, item) rule.allItems = append(rule.allItems, item) } func addDestinationAddressItem(t *testing.T, rule *abstractDefaultRule, domains []string, suffixes []string) { t.Helper() item, err := NewDomainItem(domains, suffixes) require.NoError(t, err) rule.destinationAddressItems = append(rule.destinationAddressItems, item) rule.allItems = append(rule.allItems, item) } func addDestinationKeywordItem(rule *abstractDefaultRule, keywords []string) { item := NewDomainKeywordItem(keywords) rule.destinationAddressItems = append(rule.destinationAddressItems, item) rule.allItems = append(rule.allItems, item) } func addDestinationRegexItem(t *testing.T, rule *abstractDefaultRule, regexes []string) { t.Helper() item, err := NewDomainRegexItem(regexes) require.NoError(t, err) rule.destinationAddressItems = append(rule.destinationAddressItems, item) rule.allItems = append(rule.allItems, item) } func addDestinationIPCIDRItem(t *testing.T, rule *abstractDefaultRule, cidrs []string) { t.Helper() item, err := NewIPCIDRItem(false, cidrs) require.NoError(t, err) rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) rule.allItems = append(rule.allItems, item) } func addDestinationIPIsPrivateItem(rule *abstractDefaultRule) { item := NewIPIsPrivateItem(false) rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) rule.allItems = append(rule.allItems, item) } func addDestinationIPAcceptAnyItem(rule *abstractDefaultRule) { item := NewIPAcceptAnyItem() rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) rule.allItems = append(rule.allItems, item) } func addSourcePortItem(rule *abstractDefaultRule, ports []uint16) { item := NewPortItem(true, ports) rule.sourcePortItems = append(rule.sourcePortItems, item) rule.allItems = append(rule.allItems, item) } func addSourcePortRangeItem(t *testing.T, rule *abstractDefaultRule, ranges []string) { t.Helper() item, err := NewPortRangeItem(true, ranges) require.NoError(t, err) rule.sourcePortItems = append(rule.sourcePortItems, item) rule.allItems = append(rule.allItems, item) } func addDestinationPortItem(rule *abstractDefaultRule, ports []uint16) { item := NewPortItem(false, ports) rule.destinationPortItems = append(rule.destinationPortItems, item) rule.allItems = append(rule.allItems, item) } func addDestinationPortRangeItem(t *testing.T, rule *abstractDefaultRule, ranges []string) { t.Helper() item, err := NewPortRangeItem(false, ranges) require.NoError(t, err) rule.destinationPortItems = append(rule.destinationPortItems, item) rule.allItems = append(rule.allItems, item) } ================================================ FILE: route/rule/rule_set_update_validation_test.go ================================================ 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"})) } ================================================ FILE: route/rule_conds.go ================================================ package route import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" ) func hasRule(rules []option.Rule, cond func(rule option.DefaultRule) bool) bool { for _, rule := range rules { switch rule.Type { case C.RuleTypeDefault: if cond(rule.DefaultOptions) { return true } case C.RuleTypeLogical: if hasRule(rule.LogicalOptions.Rules, cond) { return true } } } return false } func hasDNSRule(rules []option.DNSRule, cond func(rule option.DefaultDNSRule) bool) bool { for _, rule := range rules { switch rule.Type { case C.RuleTypeDefault: if cond(rule.DefaultOptions) { return true } case C.RuleTypeLogical: if hasDNSRule(rule.LogicalOptions.Rules, cond) { return true } } } return false } 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.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.PackageNameRegex) > 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 } 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 } ================================================ FILE: service/acme/service.go ================================================ //go:build with_acme package acme import ( "bytes" "context" "crypto/tls" "encoding/json" "net/http" "net/url" "reflect" "slices" "strings" "time" "unsafe" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/certificate" 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" "github.com/sagernet/sing/service" "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} } profile := options.Profile if profile == "" && acmeServer == certmagic.LetsEncryptProductionCA && slices.ContainsFunc(options.Domain, certmagic.SubjectIsIP) { profile = "shortlived" } 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), AltTLSALPNPort: int(options.AlternativeTLSPort), Logger: zapLogger, } acmeHTTPClient, err := newACMEHTTPClient(ctx, logger, options) 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, 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 http client") } return &http.Client{ Transport: transport, 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 } ================================================ FILE: service/acme/stub.go ================================================ //go:build !with_acme package acme ================================================ FILE: service/ccm/credential.go ================================================ package ccm import ( "bytes" "encoding/json" "io" "net/http" "os" "os/user" "path/filepath" "time" E "github.com/sagernet/sing/common/exceptions" ) const ( oauth2ClientID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e" oauth2TokenURL = "https://console.anthropic.com/v1/oauth/token" claudeAPIBaseURL = "https://api.anthropic.com" tokenRefreshBufferMs = 60000 anthropicBetaOAuthValue = "oauth-2025-04-20" ) func getRealUser() (*user.User, error) { if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" { sudoUserInfo, err := user.Lookup(sudoUser) if err == nil { return sudoUserInfo, nil } } return user.Current() } func getDefaultCredentialsPath() (string, error) { if configDir := os.Getenv("CLAUDE_CONFIG_DIR"); configDir != "" { return filepath.Join(configDir, ".credentials.json"), nil } userInfo, err := getRealUser() if err != nil { return "", err } return filepath.Join(userInfo.HomeDir, ".claude", ".credentials.json"), nil } func readCredentialsFromFile(path string) (*oauthCredentials, error) { data, err := os.ReadFile(path) if err != nil { return nil, err } var credentialsContainer struct { ClaudeAIAuth *oauthCredentials `json:"claudeAiOauth,omitempty"` } err = json.Unmarshal(data, &credentialsContainer) if err != nil { return nil, err } if credentialsContainer.ClaudeAIAuth == nil { return nil, E.New("claudeAiOauth field not found in credentials") } return credentialsContainer.ClaudeAIAuth, nil } func writeCredentialsToFile(oauthCredentials *oauthCredentials, path string) error { data, err := json.MarshalIndent(map[string]any{ "claudeAiOauth": oauthCredentials, }, "", " ") if err != nil { return err } return os.WriteFile(path, data, 0o600) } type oauthCredentials struct { AccessToken string `json:"accessToken"` RefreshToken string `json:"refreshToken"` ExpiresAt int64 `json:"expiresAt"` Scopes []string `json:"scopes,omitempty"` SubscriptionType string `json:"subscriptionType,omitempty"` IsMax bool `json:"isMax,omitempty"` } func (c *oauthCredentials) needsRefresh() bool { if c.ExpiresAt == 0 { return false } return time.Now().UnixMilli() >= c.ExpiresAt-tokenRefreshBufferMs } func refreshToken(httpClient *http.Client, credentials *oauthCredentials) (*oauthCredentials, error) { if credentials.RefreshToken == "" { return nil, E.New("refresh token is empty") } requestBody, err := json.Marshal(map[string]string{ "grant_type": "refresh_token", "refresh_token": credentials.RefreshToken, "client_id": oauth2ClientID, }) if err != nil { return nil, E.Cause(err, "marshal request") } request, err := http.NewRequest("POST", oauth2TokenURL, bytes.NewReader(requestBody)) if err != nil { return nil, err } request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") response, err := httpClient.Do(request) if err != nil { return nil, err } defer response.Body.Close() if response.StatusCode != http.StatusOK { body, _ := io.ReadAll(response.Body) return nil, E.New("refresh failed: ", response.Status, " ", string(body)) } var tokenResponse struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` ExpiresIn int `json:"expires_in"` } err = json.NewDecoder(response.Body).Decode(&tokenResponse) if err != nil { return nil, E.Cause(err, "decode response") } newCredentials := *credentials newCredentials.AccessToken = tokenResponse.AccessToken if tokenResponse.RefreshToken != "" { newCredentials.RefreshToken = tokenResponse.RefreshToken } newCredentials.ExpiresAt = time.Now().UnixMilli() + int64(tokenResponse.ExpiresIn)*1000 return &newCredentials, nil } ================================================ FILE: service/ccm/credential_darwin.go ================================================ //go:build darwin && cgo package ccm import ( "crypto/sha256" "encoding/hex" "encoding/json" "os" "path/filepath" E "github.com/sagernet/sing/common/exceptions" "github.com/keybase/go-keychain" ) func getKeychainServiceName() string { configDirectory := os.Getenv("CLAUDE_CONFIG_DIR") if configDirectory == "" { return "Claude Code-credentials" } userInfo, err := getRealUser() if err != nil { return "Claude Code-credentials" } defaultConfigDirectory := filepath.Join(userInfo.HomeDir, ".claude") if configDirectory == defaultConfigDirectory { return "Claude Code-credentials" } hash := sha256.Sum256([]byte(configDirectory)) return "Claude Code-credentials-" + hex.EncodeToString(hash[:])[:8] } func platformReadCredentials(customPath string) (*oauthCredentials, error) { if customPath != "" { return readCredentialsFromFile(customPath) } userInfo, err := getRealUser() if err == nil { query := keychain.NewItem() query.SetSecClass(keychain.SecClassGenericPassword) query.SetService(getKeychainServiceName()) query.SetAccount(userInfo.Username) query.SetMatchLimit(keychain.MatchLimitOne) query.SetReturnData(true) results, err := keychain.QueryItem(query) if err == nil && len(results) == 1 { var container struct { ClaudeAIAuth *oauthCredentials `json:"claudeAiOauth,omitempty"` } unmarshalErr := json.Unmarshal(results[0].Data, &container) if unmarshalErr == nil && container.ClaudeAIAuth != nil { return container.ClaudeAIAuth, nil } } if err != nil && err != keychain.ErrorItemNotFound { return nil, E.Cause(err, "query keychain") } } defaultPath, err := getDefaultCredentialsPath() if err != nil { return nil, err } return readCredentialsFromFile(defaultPath) } func platformWriteCredentials(oauthCredentials *oauthCredentials, customPath string) error { if customPath != "" { return writeCredentialsToFile(oauthCredentials, customPath) } userInfo, err := getRealUser() if err == nil { data, err := json.Marshal(map[string]any{"claudeAiOauth": oauthCredentials}) if err == nil { serviceName := getKeychainServiceName() item := keychain.NewItem() item.SetSecClass(keychain.SecClassGenericPassword) item.SetService(serviceName) item.SetAccount(userInfo.Username) item.SetData(data) item.SetAccessible(keychain.AccessibleWhenUnlocked) err = keychain.AddItem(item) if err == nil { return nil } if err == keychain.ErrorDuplicateItem { query := keychain.NewItem() query.SetSecClass(keychain.SecClassGenericPassword) query.SetService(serviceName) query.SetAccount(userInfo.Username) updateItem := keychain.NewItem() updateItem.SetData(data) updateErr := keychain.UpdateItem(query, updateItem) if updateErr == nil { return nil } } } } defaultPath, err := getDefaultCredentialsPath() if err != nil { return err } return writeCredentialsToFile(oauthCredentials, defaultPath) } ================================================ FILE: service/ccm/credential_other.go ================================================ //go:build !darwin || !cgo package ccm func platformReadCredentials(customPath string) (*oauthCredentials, error) { if customPath == "" { var err error customPath, err = getDefaultCredentialsPath() if err != nil { return nil, err } } return readCredentialsFromFile(customPath) } func platformWriteCredentials(oauthCredentials *oauthCredentials, customPath string) error { if customPath == "" { var err error customPath, err = getDefaultCredentialsPath() if err != nil { return err } } return writeCredentialsToFile(oauthCredentials, customPath) } ================================================ FILE: service/ccm/service.go ================================================ package ccm import ( "bytes" "context" stdTLS "crypto/tls" "encoding/json" "errors" "io" "mime" "net" "net/http" "strconv" "strings" "sync" "time" "github.com/sagernet/sing-box/adapter" boxService "github.com/sagernet/sing-box/adapter/service" "github.com/sagernet/sing-box/common/dialer" "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/common" "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/ntp" aTLS "github.com/sagernet/sing/common/tls" "github.com/anthropics/anthropic-sdk-go" "github.com/go-chi/chi/v5" "golang.org/x/net/http2" ) const ( contextWindowStandard = 200000 contextWindowPremium = 1000000 premiumContextThreshold = 200000 ) func RegisterService(registry *boxService.Registry) { boxService.Register[option.CCMServiceOptions](registry, C.TypeCCM, NewService) } type errorResponse struct { Type string `json:"type"` Error errorDetails `json:"error"` RequestID string `json:"request_id,omitempty"` } type errorDetails struct { Type string `json:"type"` Message string `json:"message"` } func writeJSONError(w http.ResponseWriter, r *http.Request, statusCode int, errorType string, message string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(statusCode) json.NewEncoder(w).Encode(errorResponse{ Type: "error", Error: errorDetails{ Type: errorType, Message: message, }, RequestID: r.Header.Get("Request-Id"), }) } func isHopByHopHeader(header string) bool { switch strings.ToLower(header) { case "connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailers", "transfer-encoding", "upgrade", "host": return true default: return false } } const ( weeklyWindowSeconds = 604800 weeklyWindowMinutes = weeklyWindowSeconds / 60 ) func parseInt64Header(headers http.Header, headerName string) (int64, bool) { headerValue := strings.TrimSpace(headers.Get(headerName)) if headerValue == "" { return 0, false } parsedValue, parseError := strconv.ParseInt(headerValue, 10, 64) if parseError != nil { return 0, false } return parsedValue, true } func extractWeeklyCycleHint(headers http.Header) *WeeklyCycleHint { resetAtUnix, hasResetAt := parseInt64Header(headers, "anthropic-ratelimit-unified-7d-reset") if !hasResetAt || resetAtUnix <= 0 { return nil } return &WeeklyCycleHint{ WindowMinutes: weeklyWindowMinutes, ResetAt: time.Unix(resetAtUnix, 0).UTC(), } } type Service struct { boxService.Adapter ctx context.Context logger log.ContextLogger credentialPath string credentials *oauthCredentials users []option.CCMUser httpClient *http.Client httpHeaders http.Header listener *listener.Listener tlsConfig tls.ServerConfig httpServer *http.Server userManager *UserManager accessMutex sync.RWMutex usageTracker *AggregatedUsage } func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.CCMServiceOptions) (adapter.Service, error) { serviceDialer, err := dialer.NewWithOptions(dialer.Options{ Context: ctx, Options: option.DialerOptions{ Detour: options.Detour, }, RemoteIsDomain: true, }) if err != nil { return nil, E.Cause(err, "create dialer") } httpClient := &http.Client{ Transport: &http.Transport{ ForceAttemptHTTP2: true, TLSClientConfig: &stdTLS.Config{ RootCAs: adapter.RootPoolFromContext(ctx), Time: ntp.TimeFuncFromContext(ctx), }, DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { return serviceDialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) }, }, } userManager := &UserManager{ tokenMap: make(map[string]string), } var usageTracker *AggregatedUsage if options.UsagesPath != "" { usageTracker = &AggregatedUsage{ LastUpdated: time.Now(), Combinations: make([]CostCombination, 0), filePath: options.UsagesPath, logger: logger, } } service := &Service{ Adapter: boxService.NewAdapter(C.TypeCCM, tag), ctx: ctx, logger: logger, credentialPath: options.CredentialPath, users: options.Users, httpClient: httpClient, httpHeaders: options.Headers.Build(), listener: listener.New(listener.Options{ Context: ctx, Logger: logger, Network: []string{N.NetworkTCP}, Listen: options.ListenOptions, }), userManager: userManager, usageTracker: usageTracker, } if options.TLS != nil { tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS)) if err != nil { return nil, err } service.tlsConfig = tlsConfig } return service, nil } func (s *Service) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } s.userManager.UpdateUsers(s.users) credentials, err := platformReadCredentials(s.credentialPath) if err != nil { return E.Cause(err, "read credentials") } s.credentials = credentials if s.usageTracker != nil { err = s.usageTracker.Load() if err != nil { s.logger.Warn("load usage statistics: ", err) } } router := chi.NewRouter() router.Mount("/", s) s.httpServer = &http.Server{Handler: router} 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() { serveErr := s.httpServer.Serve(tcpListener) if serveErr != nil && !errors.Is(serveErr, http.ErrServerClosed) { s.logger.Error("serve error: ", serveErr) } }() return nil } func (s *Service) getAccessToken() (string, error) { s.accessMutex.RLock() if !s.credentials.needsRefresh() { token := s.credentials.AccessToken s.accessMutex.RUnlock() return token, nil } s.accessMutex.RUnlock() s.accessMutex.Lock() defer s.accessMutex.Unlock() if !s.credentials.needsRefresh() { return s.credentials.AccessToken, nil } newCredentials, err := refreshToken(s.httpClient, s.credentials) if err != nil { return "", err } s.credentials = newCredentials err = platformWriteCredentials(newCredentials, s.credentialPath) if err != nil { s.logger.Warn("persist refreshed token: ", err) } return newCredentials.AccessToken, nil } func detectContextWindow(betaHeader string, totalInputTokens int64) int { if totalInputTokens > premiumContextThreshold { features := strings.SplitSeq(betaHeader, ",") for feature := range features { if strings.HasPrefix(strings.TrimSpace(feature), "context-1m") { return contextWindowPremium } } } return contextWindowStandard } func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { if !strings.HasPrefix(r.URL.Path, "/v1/") { writeJSONError(w, r, http.StatusNotFound, "not_found_error", "Not found") return } var username string if len(s.users) > 0 { authHeader := r.Header.Get("Authorization") if authHeader == "" { s.logger.Warn("authentication failed for request from ", r.RemoteAddr, ": missing Authorization header") writeJSONError(w, r, http.StatusUnauthorized, "authentication_error", "missing api key") return } clientToken := strings.TrimPrefix(authHeader, "Bearer ") if clientToken == authHeader { s.logger.Warn("authentication failed for request from ", r.RemoteAddr, ": invalid Authorization format") writeJSONError(w, r, http.StatusUnauthorized, "authentication_error", "invalid api key format") return } var ok bool username, ok = s.userManager.Authenticate(clientToken) if !ok { s.logger.Warn("authentication failed for request from ", r.RemoteAddr, ": unknown key: ", clientToken) writeJSONError(w, r, http.StatusUnauthorized, "authentication_error", "invalid api key") return } } var requestModel string var messagesCount int if s.usageTracker != nil && r.Body != nil { bodyBytes, err := io.ReadAll(r.Body) if err == nil { var request struct { Model string `json:"model"` Messages []anthropic.MessageParam `json:"messages"` } err := json.Unmarshal(bodyBytes, &request) if err == nil { requestModel = request.Model messagesCount = len(request.Messages) } r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) } } accessToken, err := s.getAccessToken() if err != nil { s.logger.Error("get access token: ", err) writeJSONError(w, r, http.StatusUnauthorized, "authentication_error", "Authentication failed") return } proxyURL := claudeAPIBaseURL + r.URL.RequestURI() proxyRequest, err := http.NewRequestWithContext(r.Context(), r.Method, proxyURL, r.Body) if err != nil { s.logger.Error("create proxy request: ", err) writeJSONError(w, r, http.StatusInternalServerError, "api_error", "Internal server error") return } for key, values := range r.Header { if !isHopByHopHeader(key) && key != "Authorization" { proxyRequest.Header[key] = values } } serviceOverridesAcceptEncoding := len(s.httpHeaders.Values("Accept-Encoding")) > 0 if s.usageTracker != nil && !serviceOverridesAcceptEncoding { // Strip Accept-Encoding so Go Transport adds it automatically // and transparently decompresses the response for correct usage counting. proxyRequest.Header.Del("Accept-Encoding") } anthropicBetaHeader := proxyRequest.Header.Get("anthropic-beta") if anthropicBetaHeader != "" { proxyRequest.Header.Set("anthropic-beta", anthropicBetaOAuthValue+","+anthropicBetaHeader) } else { proxyRequest.Header.Set("anthropic-beta", anthropicBetaOAuthValue) } for key, values := range s.httpHeaders { proxyRequest.Header.Del(key) proxyRequest.Header[key] = values } proxyRequest.Header.Set("Authorization", "Bearer "+accessToken) response, err := s.httpClient.Do(proxyRequest) if err != nil { writeJSONError(w, r, http.StatusBadGateway, "api_error", err.Error()) return } defer response.Body.Close() for key, values := range response.Header { if !isHopByHopHeader(key) { w.Header()[key] = values } } w.WriteHeader(response.StatusCode) if s.usageTracker != nil && response.StatusCode == http.StatusOK { s.handleResponseWithTracking(w, response, requestModel, anthropicBetaHeader, messagesCount, username) } else { mediaType, _, err := mime.ParseMediaType(response.Header.Get("Content-Type")) if err == nil && mediaType != "text/event-stream" { _, _ = io.Copy(w, response.Body) return } flusher, ok := w.(http.Flusher) if !ok { s.logger.Error("streaming not supported") return } buffer := make([]byte, buf.BufferSize) for { n, err := response.Body.Read(buffer) if n > 0 { _, writeError := w.Write(buffer[:n]) if writeError != nil { s.logger.Error("write streaming response: ", writeError) return } flusher.Flush() } if err != nil { return } } } } func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, response *http.Response, requestModel string, anthropicBetaHeader string, messagesCount int, username string) { weeklyCycleHint := extractWeeklyCycleHint(response.Header) mediaType, _, err := mime.ParseMediaType(response.Header.Get("Content-Type")) isStreaming := err == nil && mediaType == "text/event-stream" if !isStreaming { bodyBytes, err := io.ReadAll(response.Body) if err != nil { s.logger.Error("read response body: ", err) return } var message anthropic.Message var usage anthropic.Usage var responseModel string err = json.Unmarshal(bodyBytes, &message) if err == nil { responseModel = string(message.Model) usage = message.Usage } if responseModel == "" { responseModel = requestModel } if usage.InputTokens > 0 || usage.OutputTokens > 0 { if responseModel != "" { totalInputTokens := usage.InputTokens + usage.CacheCreationInputTokens + usage.CacheReadInputTokens contextWindow := detectContextWindow(anthropicBetaHeader, totalInputTokens) s.usageTracker.AddUsageWithCycleHint( responseModel, contextWindow, messagesCount, usage.InputTokens, usage.OutputTokens, usage.CacheReadInputTokens, usage.CacheCreationInputTokens, usage.CacheCreation.Ephemeral5mInputTokens, usage.CacheCreation.Ephemeral1hInputTokens, username, time.Now(), weeklyCycleHint, ) } } _, _ = writer.Write(bodyBytes) return } flusher, ok := writer.(http.Flusher) if !ok { s.logger.Error("streaming not supported") return } var accumulatedUsage anthropic.Usage var responseModel string buffer := make([]byte, buf.BufferSize) var leftover []byte for { n, err := response.Body.Read(buffer) if n > 0 { data := append(leftover, buffer[:n]...) lines := bytes.Split(data, []byte("\n")) if err == nil { leftover = lines[len(lines)-1] lines = lines[:len(lines)-1] } else { leftover = nil } for _, line := range lines { line = bytes.TrimSpace(line) if len(line) == 0 { continue } if after, ok0 := bytes.CutPrefix(line, []byte("data: ")); ok0 { eventData := after if bytes.Equal(eventData, []byte("[DONE]")) { continue } var event anthropic.MessageStreamEventUnion err := json.Unmarshal(eventData, &event) if err != nil { continue } switch event.Type { case "message_start": messageStart := event.AsMessageStart() if messageStart.Message.Model != "" { responseModel = string(messageStart.Message.Model) } if messageStart.Message.Usage.InputTokens > 0 { accumulatedUsage.InputTokens = messageStart.Message.Usage.InputTokens accumulatedUsage.CacheReadInputTokens = messageStart.Message.Usage.CacheReadInputTokens accumulatedUsage.CacheCreationInputTokens = messageStart.Message.Usage.CacheCreationInputTokens accumulatedUsage.CacheCreation.Ephemeral5mInputTokens = messageStart.Message.Usage.CacheCreation.Ephemeral5mInputTokens accumulatedUsage.CacheCreation.Ephemeral1hInputTokens = messageStart.Message.Usage.CacheCreation.Ephemeral1hInputTokens } case "message_delta": messageDelta := event.AsMessageDelta() if messageDelta.Usage.OutputTokens > 0 { accumulatedUsage.OutputTokens = messageDelta.Usage.OutputTokens } } } } _, writeError := writer.Write(buffer[:n]) if writeError != nil { s.logger.Error("write streaming response: ", writeError) return } flusher.Flush() } if err != nil { if responseModel == "" { responseModel = requestModel } if accumulatedUsage.InputTokens > 0 || accumulatedUsage.OutputTokens > 0 { if responseModel != "" { totalInputTokens := accumulatedUsage.InputTokens + accumulatedUsage.CacheCreationInputTokens + accumulatedUsage.CacheReadInputTokens contextWindow := detectContextWindow(anthropicBetaHeader, totalInputTokens) s.usageTracker.AddUsageWithCycleHint( responseModel, contextWindow, messagesCount, accumulatedUsage.InputTokens, accumulatedUsage.OutputTokens, accumulatedUsage.CacheReadInputTokens, accumulatedUsage.CacheCreationInputTokens, accumulatedUsage.CacheCreation.Ephemeral5mInputTokens, accumulatedUsage.CacheCreation.Ephemeral1hInputTokens, username, time.Now(), weeklyCycleHint, ) } } return } } } func (s *Service) Close() error { err := common.Close( common.PtrOrNil(s.httpServer), common.PtrOrNil(s.listener), s.tlsConfig, ) if s.usageTracker != nil { s.usageTracker.cancelPendingSave() saveErr := s.usageTracker.Save() if saveErr != nil { s.logger.Error("save usage statistics: ", saveErr) } } return err } ================================================ FILE: service/ccm/service_usage.go ================================================ package ccm import ( "encoding/json" "fmt" "math" "os" "regexp" "sync" "time" "github.com/sagernet/sing-box/log" E "github.com/sagernet/sing/common/exceptions" ) type UsageStats struct { RequestCount int `json:"request_count"` MessagesCount int `json:"messages_count"` InputTokens int64 `json:"input_tokens"` OutputTokens int64 `json:"output_tokens"` CacheReadInputTokens int64 `json:"cache_read_input_tokens"` CacheCreationInputTokens int64 `json:"cache_creation_input_tokens"` CacheCreation5MinuteInputTokens int64 `json:"cache_creation_5m_input_tokens,omitempty"` CacheCreation1HourInputTokens int64 `json:"cache_creation_1h_input_tokens,omitempty"` } type CostCombination struct { Model string `json:"model"` ContextWindow int `json:"context_window"` WeekStartUnix int64 `json:"week_start_unix,omitempty"` Total UsageStats `json:"total"` ByUser map[string]UsageStats `json:"by_user"` } type AggregatedUsage struct { LastUpdated time.Time `json:"last_updated"` Combinations []CostCombination `json:"combinations"` mutex sync.Mutex filePath string logger log.ContextLogger lastSaveTime time.Time pendingSave bool saveTimer *time.Timer saveMutex sync.Mutex } type UsageStatsJSON struct { RequestCount int `json:"request_count"` MessagesCount int `json:"messages_count"` InputTokens int64 `json:"input_tokens"` OutputTokens int64 `json:"output_tokens"` CacheReadInputTokens int64 `json:"cache_read_input_tokens"` CacheCreationInputTokens int64 `json:"cache_creation_input_tokens"` CacheCreation5MinuteInputTokens int64 `json:"cache_creation_5m_input_tokens,omitempty"` CacheCreation1HourInputTokens int64 `json:"cache_creation_1h_input_tokens,omitempty"` CostUSD float64 `json:"cost_usd"` } type CostCombinationJSON struct { Model string `json:"model"` ContextWindow int `json:"context_window"` WeekStartUnix int64 `json:"week_start_unix,omitempty"` Total UsageStatsJSON `json:"total"` ByUser map[string]UsageStatsJSON `json:"by_user"` } type CostsSummaryJSON struct { TotalUSD float64 `json:"total_usd"` ByUser map[string]float64 `json:"by_user"` ByWeek map[string]float64 `json:"by_week,omitempty"` ByUserAndWeek map[string]map[string]float64 `json:"by_user_and_week,omitempty"` } type AggregatedUsageJSON struct { LastUpdated time.Time `json:"last_updated"` Costs CostsSummaryJSON `json:"costs"` Combinations []CostCombinationJSON `json:"combinations"` } type WeeklyCycleHint struct { WindowMinutes int64 ResetAt time.Time } type ModelPricing struct { InputPrice float64 OutputPrice float64 CacheReadPrice float64 CacheWritePrice5Minute float64 CacheWritePrice1Hour float64 } type modelFamily struct { pattern *regexp.Regexp standardPricing ModelPricing premiumPricing *ModelPricing } var ( opus46StandardPricing = ModelPricing{ InputPrice: 5.0, OutputPrice: 25.0, CacheReadPrice: 0.5, CacheWritePrice5Minute: 6.25, CacheWritePrice1Hour: 10.0, } opus46PremiumPricing = ModelPricing{ InputPrice: 10.0, OutputPrice: 37.5, CacheReadPrice: 1.0, CacheWritePrice5Minute: 12.5, CacheWritePrice1Hour: 20.0, } opus45Pricing = ModelPricing{ InputPrice: 5.0, OutputPrice: 25.0, CacheReadPrice: 0.5, CacheWritePrice5Minute: 6.25, CacheWritePrice1Hour: 10.0, } opus4Pricing = ModelPricing{ InputPrice: 15.0, OutputPrice: 75.0, CacheReadPrice: 1.5, CacheWritePrice5Minute: 18.75, CacheWritePrice1Hour: 30.0, } sonnet46StandardPricing = ModelPricing{ InputPrice: 3.0, OutputPrice: 15.0, CacheReadPrice: 0.3, CacheWritePrice5Minute: 3.75, CacheWritePrice1Hour: 6.0, } sonnet46PremiumPricing = ModelPricing{ InputPrice: 6.0, OutputPrice: 22.5, CacheReadPrice: 0.6, CacheWritePrice5Minute: 7.5, CacheWritePrice1Hour: 12.0, } sonnet45StandardPricing = ModelPricing{ InputPrice: 3.0, OutputPrice: 15.0, CacheReadPrice: 0.3, CacheWritePrice5Minute: 3.75, CacheWritePrice1Hour: 6.0, } sonnet45PremiumPricing = ModelPricing{ InputPrice: 6.0, OutputPrice: 22.5, CacheReadPrice: 0.6, CacheWritePrice5Minute: 7.5, CacheWritePrice1Hour: 12.0, } sonnet4StandardPricing = ModelPricing{ InputPrice: 3.0, OutputPrice: 15.0, CacheReadPrice: 0.3, CacheWritePrice5Minute: 3.75, CacheWritePrice1Hour: 6.0, } sonnet4PremiumPricing = ModelPricing{ InputPrice: 6.0, OutputPrice: 22.5, CacheReadPrice: 0.6, CacheWritePrice5Minute: 7.5, CacheWritePrice1Hour: 12.0, } sonnet37Pricing = ModelPricing{ InputPrice: 3.0, OutputPrice: 15.0, CacheReadPrice: 0.3, CacheWritePrice5Minute: 3.75, CacheWritePrice1Hour: 6.0, } sonnet35Pricing = ModelPricing{ InputPrice: 3.0, OutputPrice: 15.0, CacheReadPrice: 0.3, CacheWritePrice5Minute: 3.75, CacheWritePrice1Hour: 6.0, } haiku45Pricing = ModelPricing{ InputPrice: 1.0, OutputPrice: 5.0, CacheReadPrice: 0.1, CacheWritePrice5Minute: 1.25, CacheWritePrice1Hour: 2.0, } haiku4Pricing = ModelPricing{ InputPrice: 1.0, OutputPrice: 5.0, CacheReadPrice: 0.1, CacheWritePrice5Minute: 1.25, CacheWritePrice1Hour: 2.0, } haiku35Pricing = ModelPricing{ InputPrice: 0.8, OutputPrice: 4.0, CacheReadPrice: 0.08, CacheWritePrice5Minute: 1.0, CacheWritePrice1Hour: 1.6, } haiku3Pricing = ModelPricing{ InputPrice: 0.25, OutputPrice: 1.25, CacheReadPrice: 0.03, CacheWritePrice5Minute: 0.3, CacheWritePrice1Hour: 0.5, } opus3Pricing = ModelPricing{ InputPrice: 15.0, OutputPrice: 75.0, CacheReadPrice: 1.5, CacheWritePrice5Minute: 18.75, CacheWritePrice1Hour: 30.0, } modelFamilies = []modelFamily{ { pattern: regexp.MustCompile(`^claude-opus-4-6(?:-|$)`), standardPricing: opus46StandardPricing, premiumPricing: &opus46PremiumPricing, }, { pattern: regexp.MustCompile(`^claude-opus-4-5(?:-|$)`), standardPricing: opus45Pricing, premiumPricing: nil, }, { pattern: regexp.MustCompile(`^claude-(?:opus-4(?:-|$)|4-opus-)`), standardPricing: opus4Pricing, premiumPricing: nil, }, { pattern: regexp.MustCompile(`^claude-(?:opus-3(?:-|$)|3-opus-)`), standardPricing: opus3Pricing, premiumPricing: nil, }, { pattern: regexp.MustCompile(`^claude-(?:sonnet-4-6(?:-|$)|4-6-sonnet-)`), standardPricing: sonnet46StandardPricing, premiumPricing: &sonnet46PremiumPricing, }, { pattern: regexp.MustCompile(`^claude-(?:sonnet-4-5(?:-|$)|4-5-sonnet-)`), standardPricing: sonnet45StandardPricing, premiumPricing: &sonnet45PremiumPricing, }, { pattern: regexp.MustCompile(`^claude-(?:sonnet-4(?:-|$)|4-sonnet-)`), standardPricing: sonnet4StandardPricing, premiumPricing: &sonnet4PremiumPricing, }, { pattern: regexp.MustCompile(`^claude-3-7-sonnet(?:-|$)`), standardPricing: sonnet37Pricing, premiumPricing: nil, }, { pattern: regexp.MustCompile(`^claude-3-5-sonnet(?:-|$)`), standardPricing: sonnet35Pricing, premiumPricing: nil, }, { pattern: regexp.MustCompile(`^claude-(?:haiku-4-5(?:-|$)|4-5-haiku-)`), standardPricing: haiku45Pricing, premiumPricing: nil, }, { pattern: regexp.MustCompile(`^claude-haiku-4(?:-|$)`), standardPricing: haiku4Pricing, premiumPricing: nil, }, { pattern: regexp.MustCompile(`^claude-3-5-haiku(?:-|$)`), standardPricing: haiku35Pricing, premiumPricing: nil, }, { pattern: regexp.MustCompile(`^claude-3-haiku(?:-|$)`), standardPricing: haiku3Pricing, premiumPricing: nil, }, } ) func getPricing(model string, contextWindow int) ModelPricing { isPremium := contextWindow >= contextWindowPremium for _, family := range modelFamilies { if family.pattern.MatchString(model) { if isPremium && family.premiumPricing != nil { return *family.premiumPricing } return family.standardPricing } } return sonnet4StandardPricing } func calculateCost(stats UsageStats, model string, contextWindow int) float64 { pricing := getPricing(model, contextWindow) cacheCreationCost := 0.0 if stats.CacheCreation5MinuteInputTokens > 0 || stats.CacheCreation1HourInputTokens > 0 { cacheCreationCost = float64(stats.CacheCreation5MinuteInputTokens)*pricing.CacheWritePrice5Minute + float64(stats.CacheCreation1HourInputTokens)*pricing.CacheWritePrice1Hour } else { // Backward compatibility for usage files generated before TTL split tracking. cacheCreationCost = float64(stats.CacheCreationInputTokens) * pricing.CacheWritePrice5Minute } cost := (float64(stats.InputTokens)*pricing.InputPrice + float64(stats.OutputTokens)*pricing.OutputPrice + float64(stats.CacheReadInputTokens)*pricing.CacheReadPrice + cacheCreationCost) / 1_000_000 return math.Round(cost*100) / 100 } func roundCost(cost float64) float64 { return math.Round(cost*100) / 100 } func normalizeCombinations(combinations []CostCombination) { for index := range combinations { if combinations[index].ByUser == nil { combinations[index].ByUser = make(map[string]UsageStats) } } } func addUsageToCombinations( combinations *[]CostCombination, model string, contextWindow int, weekStartUnix int64, messagesCount int, inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, cacheCreation5MinuteTokens, cacheCreation1HourTokens int64, user string, ) { var matchedCombination *CostCombination for index := range *combinations { combination := &(*combinations)[index] if combination.Model == model && combination.ContextWindow == contextWindow && combination.WeekStartUnix == weekStartUnix { matchedCombination = combination break } } if matchedCombination == nil { newCombination := CostCombination{ Model: model, ContextWindow: contextWindow, WeekStartUnix: weekStartUnix, Total: UsageStats{}, ByUser: make(map[string]UsageStats), } *combinations = append(*combinations, newCombination) matchedCombination = &(*combinations)[len(*combinations)-1] } if cacheCreationTokens == 0 { cacheCreationTokens = cacheCreation5MinuteTokens + cacheCreation1HourTokens } matchedCombination.Total.RequestCount++ matchedCombination.Total.MessagesCount += messagesCount matchedCombination.Total.InputTokens += inputTokens matchedCombination.Total.OutputTokens += outputTokens matchedCombination.Total.CacheReadInputTokens += cacheReadTokens matchedCombination.Total.CacheCreationInputTokens += cacheCreationTokens matchedCombination.Total.CacheCreation5MinuteInputTokens += cacheCreation5MinuteTokens matchedCombination.Total.CacheCreation1HourInputTokens += cacheCreation1HourTokens if user != "" { userStats := matchedCombination.ByUser[user] userStats.RequestCount++ userStats.MessagesCount += messagesCount userStats.InputTokens += inputTokens userStats.OutputTokens += outputTokens userStats.CacheReadInputTokens += cacheReadTokens userStats.CacheCreationInputTokens += cacheCreationTokens userStats.CacheCreation5MinuteInputTokens += cacheCreation5MinuteTokens userStats.CacheCreation1HourInputTokens += cacheCreation1HourTokens matchedCombination.ByUser[user] = userStats } } func buildCombinationJSON(combinations []CostCombination, aggregateUserCosts map[string]float64) ([]CostCombinationJSON, float64) { result := make([]CostCombinationJSON, len(combinations)) var totalCost float64 for index, combination := range combinations { combinationTotalCost := calculateCost(combination.Total, combination.Model, combination.ContextWindow) totalCost += combinationTotalCost combinationJSON := CostCombinationJSON{ Model: combination.Model, ContextWindow: combination.ContextWindow, WeekStartUnix: combination.WeekStartUnix, Total: UsageStatsJSON{ RequestCount: combination.Total.RequestCount, MessagesCount: combination.Total.MessagesCount, InputTokens: combination.Total.InputTokens, OutputTokens: combination.Total.OutputTokens, CacheReadInputTokens: combination.Total.CacheReadInputTokens, CacheCreationInputTokens: combination.Total.CacheCreationInputTokens, CacheCreation5MinuteInputTokens: combination.Total.CacheCreation5MinuteInputTokens, CacheCreation1HourInputTokens: combination.Total.CacheCreation1HourInputTokens, CostUSD: combinationTotalCost, }, ByUser: make(map[string]UsageStatsJSON), } for user, userStats := range combination.ByUser { userCost := calculateCost(userStats, combination.Model, combination.ContextWindow) if aggregateUserCosts != nil { aggregateUserCosts[user] += userCost } combinationJSON.ByUser[user] = UsageStatsJSON{ RequestCount: userStats.RequestCount, MessagesCount: userStats.MessagesCount, InputTokens: userStats.InputTokens, OutputTokens: userStats.OutputTokens, CacheReadInputTokens: userStats.CacheReadInputTokens, CacheCreationInputTokens: userStats.CacheCreationInputTokens, CacheCreation5MinuteInputTokens: userStats.CacheCreation5MinuteInputTokens, CacheCreation1HourInputTokens: userStats.CacheCreation1HourInputTokens, CostUSD: userCost, } } result[index] = combinationJSON } return result, roundCost(totalCost) } func formatUTCOffsetLabel(timestamp time.Time) string { _, offsetSeconds := timestamp.Zone() sign := "+" if offsetSeconds < 0 { sign = "-" offsetSeconds = -offsetSeconds } offsetHours := offsetSeconds / 3600 offsetMinutes := (offsetSeconds % 3600) / 60 if offsetMinutes == 0 { return fmt.Sprintf("UTC%s%d", sign, offsetHours) } return fmt.Sprintf("UTC%s%d:%02d", sign, offsetHours, offsetMinutes) } func formatWeekStartKey(cycleStartAt time.Time) string { localCycleStart := cycleStartAt.In(time.Local) return fmt.Sprintf("%s %s", localCycleStart.Format("2006-01-02 15:04:05"), formatUTCOffsetLabel(localCycleStart)) } func buildByWeekCost(combinations []CostCombination) map[string]float64 { byWeek := make(map[string]float64) for _, combination := range combinations { if combination.WeekStartUnix <= 0 { continue } weekStartAt := time.Unix(combination.WeekStartUnix, 0).UTC() weekKey := formatWeekStartKey(weekStartAt) byWeek[weekKey] += calculateCost(combination.Total, combination.Model, combination.ContextWindow) } for weekKey, weekCost := range byWeek { byWeek[weekKey] = roundCost(weekCost) } return byWeek } func buildByUserAndWeekCost(combinations []CostCombination) map[string]map[string]float64 { byUserAndWeek := make(map[string]map[string]float64) for _, combination := range combinations { if combination.WeekStartUnix <= 0 { continue } weekStartAt := time.Unix(combination.WeekStartUnix, 0).UTC() weekKey := formatWeekStartKey(weekStartAt) for user, userStats := range combination.ByUser { userWeeks, exists := byUserAndWeek[user] if !exists { userWeeks = make(map[string]float64) byUserAndWeek[user] = userWeeks } userWeeks[weekKey] += calculateCost(userStats, combination.Model, combination.ContextWindow) } } for _, weekCosts := range byUserAndWeek { for weekKey, cost := range weekCosts { weekCosts[weekKey] = roundCost(cost) } } return byUserAndWeek } func deriveWeekStartUnix(cycleHint *WeeklyCycleHint) int64 { if cycleHint == nil || cycleHint.WindowMinutes <= 0 || cycleHint.ResetAt.IsZero() { return 0 } windowDuration := time.Duration(cycleHint.WindowMinutes) * time.Minute return cycleHint.ResetAt.UTC().Add(-windowDuration).Unix() } func (u *AggregatedUsage) ToJSON() *AggregatedUsageJSON { u.mutex.Lock() defer u.mutex.Unlock() result := &AggregatedUsageJSON{ LastUpdated: u.LastUpdated, Costs: CostsSummaryJSON{ TotalUSD: 0, ByUser: make(map[string]float64), ByWeek: make(map[string]float64), }, } globalCombinationsJSON, totalCost := buildCombinationJSON(u.Combinations, result.Costs.ByUser) result.Combinations = globalCombinationsJSON result.Costs.TotalUSD = totalCost result.Costs.ByWeek = buildByWeekCost(u.Combinations) if len(result.Costs.ByWeek) == 0 { result.Costs.ByWeek = nil } result.Costs.ByUserAndWeek = buildByUserAndWeekCost(u.Combinations) if len(result.Costs.ByUserAndWeek) == 0 { result.Costs.ByUserAndWeek = nil } for user, cost := range result.Costs.ByUser { result.Costs.ByUser[user] = roundCost(cost) } return result } func (u *AggregatedUsage) Load() error { u.mutex.Lock() defer u.mutex.Unlock() u.LastUpdated = time.Time{} u.Combinations = nil data, err := os.ReadFile(u.filePath) if err != nil { if os.IsNotExist(err) { return nil } return err } var temp struct { LastUpdated time.Time `json:"last_updated"` Combinations []CostCombination `json:"combinations"` } err = json.Unmarshal(data, &temp) if err != nil { return err } u.LastUpdated = temp.LastUpdated u.Combinations = temp.Combinations normalizeCombinations(u.Combinations) return nil } func (u *AggregatedUsage) Save() error { jsonData := u.ToJSON() data, err := json.MarshalIndent(jsonData, "", " ") if err != nil { return err } tmpFile := u.filePath + ".tmp" err = os.WriteFile(tmpFile, data, 0o644) if err != nil { return err } defer os.Remove(tmpFile) err = os.Rename(tmpFile, u.filePath) if err == nil { u.saveMutex.Lock() u.lastSaveTime = time.Now() u.saveMutex.Unlock() } return err } func (u *AggregatedUsage) AddUsage( model string, contextWindow int, messagesCount int, inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, cacheCreation5MinuteTokens, cacheCreation1HourTokens int64, user string, ) error { return u.AddUsageWithCycleHint(model, contextWindow, messagesCount, inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, cacheCreation5MinuteTokens, cacheCreation1HourTokens, user, time.Now(), nil) } func (u *AggregatedUsage) AddUsageWithCycleHint( model string, contextWindow int, messagesCount int, inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, cacheCreation5MinuteTokens, cacheCreation1HourTokens int64, user string, observedAt time.Time, cycleHint *WeeklyCycleHint, ) error { if model == "" { return E.New("model cannot be empty") } if contextWindow <= 0 { return E.New("contextWindow must be positive") } if observedAt.IsZero() { observedAt = time.Now() } u.mutex.Lock() defer u.mutex.Unlock() u.LastUpdated = observedAt weekStartUnix := deriveWeekStartUnix(cycleHint) addUsageToCombinations(&u.Combinations, model, contextWindow, weekStartUnix, messagesCount, inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, cacheCreation5MinuteTokens, cacheCreation1HourTokens, user) go u.scheduleSave() return nil } func (u *AggregatedUsage) scheduleSave() { const saveInterval = time.Minute u.saveMutex.Lock() defer u.saveMutex.Unlock() timeSinceLastSave := time.Since(u.lastSaveTime) if timeSinceLastSave >= saveInterval { go u.saveAsync() return } if u.pendingSave { return } u.pendingSave = true remainingTime := saveInterval - timeSinceLastSave u.saveTimer = time.AfterFunc(remainingTime, func() { u.saveMutex.Lock() u.pendingSave = false u.saveMutex.Unlock() u.saveAsync() }) } func (u *AggregatedUsage) saveAsync() { err := u.Save() if err != nil { if u.logger != nil { u.logger.Error("save usage statistics: ", err) } } } func (u *AggregatedUsage) cancelPendingSave() { u.saveMutex.Lock() defer u.saveMutex.Unlock() if u.saveTimer != nil { u.saveTimer.Stop() u.saveTimer = nil } u.pendingSave = false } ================================================ FILE: service/ccm/service_user.go ================================================ package ccm import ( "sync" "github.com/sagernet/sing-box/option" ) type UserManager struct { accessMutex sync.RWMutex tokenMap map[string]string } func (m *UserManager) UpdateUsers(users []option.CCMUser) { m.accessMutex.Lock() defer m.accessMutex.Unlock() tokenMap := make(map[string]string, len(users)) for _, user := range users { tokenMap[user.Token] = user.Name } m.tokenMap = tokenMap } func (m *UserManager) Authenticate(token string) (string, bool) { m.accessMutex.RLock() username, found := m.tokenMap[token] m.accessMutex.RUnlock() return username, found } ================================================ FILE: service/derp/service.go ================================================ //go:build with_gvisor package derp import ( "bufio" "context" "encoding/json" "fmt" "io" "net" "net/http" "net/netip" "os" "path/filepath" "regexp" "strings" "time" "github.com/sagernet/sing-box/adapter" boxService "github.com/sagernet/sing-box/adapter/service" "github.com/sagernet/sing-box/common/dialer" "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" boxScale "github.com/sagernet/sing-box/protocol/tailscale" "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/badoption" "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" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/filemanager" "github.com/sagernet/tailscale/client/local" "github.com/sagernet/tailscale/derp" "github.com/sagernet/tailscale/derp/derphttp" "github.com/sagernet/tailscale/derp/derpserver" "github.com/sagernet/tailscale/net/netmon" "github.com/sagernet/tailscale/net/stun" "github.com/sagernet/tailscale/net/wsconn" "github.com/sagernet/tailscale/tsweb" "github.com/sagernet/tailscale/types/key" "github.com/coder/websocket" "github.com/go-chi/render" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" ) func Register(registry *boxService.Registry) { boxService.Register[option.DERPServiceOptions](registry, C.TypeDERP, NewService) } type Service struct { boxService.Adapter ctx context.Context logger logger.ContextLogger listener *listener.Listener stunListener *listener.Listener tlsConfig tls.ServerConfig server *derpserver.Server configPath string verifyClientEndpoint []string verifyClientURL []*option.DERPVerifyClientURLOptions home string meshKey string meshKeyPath string meshWith []*option.DERPMeshOptions } func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.DERPServiceOptions) (adapter.Service, error) { if options.TLS == nil || !options.TLS.Enabled { return nil, E.New("TLS is required for DERP server") } tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS)) if err != nil { return nil, err } var configPath string if options.ConfigPath != "" { configPath = filemanager.BasePath(ctx, os.ExpandEnv(options.ConfigPath)) } else { return nil, E.New("missing config_path") } if options.MeshPSK != "" { err = checkMeshKey(options.MeshPSK) if err != nil { return nil, E.Cause(err, "invalid mesh_psk") } } var stunListener *listener.Listener if options.STUN != nil && options.STUN.Enabled { if options.STUN.Listen == nil { options.STUN.Listen = (*badoption.Addr)(common.Ptr(netip.IPv6Unspecified())) } if options.STUN.ListenPort == 0 { options.STUN.ListenPort = 3478 } stunListener = listener.New(listener.Options{ Context: ctx, Logger: logger, Network: []string{N.NetworkUDP}, Listen: options.STUN.ListenOptions, }) } return &Service{ Adapter: boxService.NewAdapter(C.TypeDERP, tag), ctx: ctx, logger: logger, listener: listener.New(listener.Options{ Context: ctx, Logger: logger, Network: []string{N.NetworkTCP}, Listen: options.ListenOptions, }), stunListener: stunListener, tlsConfig: tlsConfig, configPath: configPath, verifyClientEndpoint: options.VerifyClientEndpoint, verifyClientURL: options.VerifyClientURL, home: options.Home, meshKey: options.MeshPSK, meshKeyPath: options.MeshPSKFile, meshWith: options.MeshWith, }, nil } func (d *Service) Start(stage adapter.StartStage) error { switch stage { case adapter.StartStateStart: config, err := readDERPConfig(filemanager.BasePath(d.ctx, d.configPath)) if err != nil { return err } server := derpserver.New(config.PrivateKey, func(format string, args ...any) { d.logger.Debug(fmt.Sprintf(format, args...)) }) if len(d.verifyClientURL) > 0 { var httpClients []*http.Client var urls []string 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: transport}) urls = append(urls, verifyOptions.URL) } server.SetVerifyClientHTTPClient(httpClients) server.SetVerifyClientURL(urls) } if d.meshKey != "" { server.SetMeshKey(d.meshKey) } else if d.meshKeyPath != "" { var meshKeyContent []byte meshKeyContent, err = os.ReadFile(d.meshKeyPath) if err != nil { return err } err = checkMeshKey(string(meshKeyContent)) if err != nil { return E.Cause(err, "invalid mesh_psk_path file") } server.SetMeshKey(string(meshKeyContent)) } d.server = server derpMux := http.NewServeMux() derpHandler := derpserver.Handler(server) derpHandler = addWebSocketSupport(server, derpHandler) derpMux.Handle("/derp", derpHandler) homeHandler, ok := getHomeHandler(d.home) if !ok { return E.New("invalid home value: ", d.home) } derpMux.HandleFunc("/derp/probe", derpserver.ProbeHandler) derpMux.HandleFunc("/derp/latency-check", derpserver.ProbeHandler) derpMux.HandleFunc("/bootstrap-dns", tsweb.BrowserHeaderHandlerFunc(handleBootstrapDNS(d.ctx))) derpMux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tsweb.AddBrowserHeaders(w) homeHandler.ServeHTTP(w, r) })) derpMux.Handle("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tsweb.AddBrowserHeaders(w) io.WriteString(w, "User-agent: *\nDisallow: /\n") })) derpMux.Handle("/generate_204", http.HandlerFunc(derpserver.ServeNoContent)) err = d.tlsConfig.Start() if err != nil { return err } tcpListener, err := d.listener.ListenTCP() if err != nil { return err } if len(d.tlsConfig.NextProtos()) == 0 { d.tlsConfig.SetNextProtos([]string{http2.NextProtoTLS, "http/1.1"}) } else if !common.Contains(d.tlsConfig.NextProtos(), http2.NextProtoTLS) { d.tlsConfig.SetNextProtos(append([]string{http2.NextProtoTLS}, d.tlsConfig.NextProtos()...)) } tcpListener = aTLS.NewListener(tcpListener, d.tlsConfig) httpServer := &http.Server{ Handler: h2c.NewHandler(derpMux, &http2.Server{}), } go httpServer.Serve(tcpListener) if d.stunListener != nil { stunConn, err := d.stunListener.ListenUDP() if err != nil { return err } go d.loopSTUNPacket(stunConn.(*net.UDPConn)) } case adapter.StartStatePostStart: if len(d.verifyClientEndpoint) > 0 { var endpoints []*local.Client endpointManager := service.FromContext[adapter.EndpointManager](d.ctx) for _, endpointTag := range d.verifyClientEndpoint { endpoint, loaded := endpointManager.Get(endpointTag) if !loaded { return E.New("verify_client_endpoint: endpoint not found: ", endpointTag) } tsEndpoint, isTailscale := endpoint.(*boxScale.Endpoint) if !isTailscale { return E.New("verify_client_endpoint: endpoint is not Tailscale: ", endpointTag) } localClient, err := tsEndpoint.Server().LocalClient() if err != nil { return err } endpoints = append(endpoints, localClient) } d.server.SetVerifyClientLocalClient(endpoints) } if len(d.meshWith) > 0 { if !d.server.HasMeshKey() { return E.New("missing mesh psk") } for _, options := range d.meshWith { err := d.startMeshWithHost(d.server, options) if err != nil { return err } } } } return nil } func checkMeshKey(meshKey string) error { checkRegex, err := regexp.Compile(`^[0-9a-f]{64}$`) if err != nil { return err } if !checkRegex.MatchString(meshKey) { return E.New("key must contain exactly 64 hex digits") } return nil } func (d *Service) startMeshWithHost(derpServer *derpserver.Server, server *option.DERPMeshOptions) error { meshDialer, err := dialer.NewWithOptions(dialer.Options{ Context: d.ctx, Options: server.DialerOptions, RemoteIsDomain: server.ServerIsDomain(), NewDialer: true, }) if err != nil { return err } var hostname string if server.Host != "" { hostname = server.Host } else { hostname = server.Server } var stdConfig *tls.STDConfig if server.TLS != nil && server.TLS.Enabled { tlsConfig, err := tls.NewClient(d.ctx, d.logger, hostname, *server.TLS) if err != nil { return err } stdConfig, err = tlsConfig.STDConfig() if err != nil { return err } } logf := func(format string, args ...any) { d.logger.Debug(F.ToString("mesh(", hostname, "): ", fmt.Sprintf(format, args...))) } var meshHost string if server.ServerPort == 0 || server.ServerPort == 443 { meshHost = hostname } else { meshHost = M.ParseSocksaddrHostPort(hostname, server.ServerPort).String() } var serverURL string if stdConfig != nil { serverURL = "https://" + meshHost + "/derp" } else { serverURL = "http://" + meshHost + "/derp" } meshClient, err := derphttp.NewClient(derpServer.PrivateKey(), serverURL, logf, netmon.NewStatic()) if err != nil { return err } meshClient.TLSConfig = stdConfig meshClient.MeshKey = derpServer.MeshKey() meshClient.WatchConnectionChanges = true meshClient.SetURLDialer(func(ctx context.Context, network, addr string) (net.Conn, error) { return meshDialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) }) add := func(m derp.PeerPresentMessage) { derpServer.AddPacketForwarder(m.Key, meshClient) } remove := func(m derp.PeerGoneMessage) { derpServer.RemovePacketForwarder(m.Peer, meshClient) } notifyError := func(err error) { d.logger.Error(err) } go meshClient.RunWatchConnectionLoop(context.Background(), derpServer.PublicKey(), logf, add, remove, notifyError) return nil } func (d *Service) Close() error { err := common.Close( common.PtrOrNil(d.listener), d.tlsConfig, ) return err } var homePage = `

DERP

This is a Tailscale DERP server.

It provides STUN, interactive connectivity establishment, and relaying of end-to-end encrypted traffic for Tailscale clients.

Documentation:

  • About DERP
  • Protocol & Go docs
  • How to run a DERP server
  • ` func getHomeHandler(val string) (_ http.Handler, ok bool) { if val == "" { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(200) w.Write([]byte(homePage)) }), true } if val == "blank" { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(200) }), true } if strings.HasPrefix(val, "http://") || strings.HasPrefix(val, "https://") { return http.RedirectHandler(val, http.StatusFound), true } return nil, false } func addWebSocketSupport(s *derpserver.Server, base http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { up := strings.ToLower(r.Header.Get("Upgrade")) // Very early versions of Tailscale set "Upgrade: WebSocket" but didn't actually // speak WebSockets (they still assumed DERP's binary framing). So to distinguish // clients that actually want WebSockets, look for an explicit "derp" subprotocol. if up != "websocket" || !strings.Contains(r.Header.Get("Sec-Websocket-Protocol"), "derp") { base.ServeHTTP(w, r) return } c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ Subprotocols: []string{"derp"}, OriginPatterns: []string{"*"}, // Disable compression because we transmit WireGuard messages that // are not compressible. // Additionally, Safari has a broken implementation of compression // (see https://github.com/nhooyr/websocket/issues/218) that makes // enabling it actively harmful. CompressionMode: websocket.CompressionDisabled, }) if err != nil { return } defer c.Close(websocket.StatusInternalError, "closing") if c.Subprotocol() != "derp" { c.Close(websocket.StatusPolicyViolation, "client must speak the derp subprotocol") return } wc := wsconn.NetConn(r.Context(), c, websocket.MessageBinary, r.RemoteAddr) brw := bufio.NewReadWriter(bufio.NewReader(wc), bufio.NewWriter(wc)) s.Accept(r.Context(), wc, brw, r.RemoteAddr) }) } func handleBootstrapDNS(ctx context.Context) http.HandlerFunc { dnsRouter := service.FromContext[adapter.DNSRouter](ctx) return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Connection", "close") if queryDomain := r.URL.Query().Get("q"); queryDomain != "" { addresses, err := dnsRouter.Lookup(ctx, queryDomain, adapter.DNSQueryOptions{}) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } render.JSON(w, r, render.M{ queryDomain: addresses, }) return } w.Write([]byte("{}")) } } type derpConfig struct { PrivateKey key.NodePrivate } func readDERPConfig(path string) (*derpConfig, error) { content, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return writeNewDERPConfig(path) } return nil, err } var config derpConfig err = json.Unmarshal(content, &config) if err != nil { return nil, err } return &config, nil } func writeNewDERPConfig(path string) (*derpConfig, error) { newKey := key.NewNode() err := os.MkdirAll(filepath.Dir(path), 0o777) if err != nil { return nil, err } config := derpConfig{ PrivateKey: newKey, } content, err := json.Marshal(config) if err != nil { return nil, err } err = os.WriteFile(path, content, 0o644) if err != nil { return nil, err } return &config, nil } func (d *Service) loopSTUNPacket(packetConn *net.UDPConn) { buffer := make([]byte, 65535) oob := make([]byte, 1024) var ( n int oobN int addrPort netip.AddrPort err error ) for { n, oobN, _, addrPort, err = packetConn.ReadMsgUDPAddrPort(buffer, oob) if err != nil { if E.IsClosedOrCanceled(err) { return } time.Sleep(time.Second) continue } if !stun.Is(buffer[:n]) { continue } txid, err := stun.ParseBindingRequest(buffer[:n]) if err != nil { continue } packetConn.WriteMsgUDPAddrPort(stun.Response(txid, addrPort), oob[:oobN], addrPort) } } ================================================ FILE: service/ocm/credential.go ================================================ package ocm import ( "bytes" "encoding/json" "io" "net/http" "os" "os/user" "path/filepath" "time" E "github.com/sagernet/sing/common/exceptions" ) const ( oauth2ClientID = "app_EMoamEEZ73f0CkXaXp7hrann" oauth2TokenURL = "https://auth.openai.com/oauth/token" openaiAPIBaseURL = "https://api.openai.com" chatGPTBackendURL = "https://chatgpt.com/backend-api/codex" tokenRefreshIntervalDays = 8 ) func getRealUser() (*user.User, error) { if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" { sudoUserInfo, err := user.Lookup(sudoUser) if err == nil { return sudoUserInfo, nil } } return user.Current() } func getDefaultCredentialsPath() (string, error) { if codexHome := os.Getenv("CODEX_HOME"); codexHome != "" { return filepath.Join(codexHome, "auth.json"), nil } userInfo, err := getRealUser() if err != nil { return "", err } return filepath.Join(userInfo.HomeDir, ".codex", "auth.json"), nil } func readCredentialsFromFile(path string) (*oauthCredentials, error) { data, err := os.ReadFile(path) if err != nil { return nil, err } var credentials oauthCredentials err = json.Unmarshal(data, &credentials) if err != nil { return nil, err } return &credentials, nil } func writeCredentialsToFile(credentials *oauthCredentials, path string) error { data, err := json.MarshalIndent(credentials, "", " ") if err != nil { return err } return os.WriteFile(path, data, 0o600) } type oauthCredentials struct { APIKey string `json:"OPENAI_API_KEY,omitempty"` Tokens *tokenData `json:"tokens,omitempty"` LastRefresh *time.Time `json:"last_refresh,omitempty"` } type tokenData struct { IDToken string `json:"id_token,omitempty"` AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` AccountID string `json:"account_id,omitempty"` } func (c *oauthCredentials) isAPIKeyMode() bool { return c.APIKey != "" } func (c *oauthCredentials) getAccessToken() string { if c.APIKey != "" { return c.APIKey } if c.Tokens != nil { return c.Tokens.AccessToken } return "" } func (c *oauthCredentials) getAccountID() string { if c.Tokens != nil { return c.Tokens.AccountID } return "" } func (c *oauthCredentials) needsRefresh() bool { if c.APIKey != "" { return false } if c.Tokens == nil || c.Tokens.RefreshToken == "" { return false } if c.LastRefresh == nil { return true } return time.Since(*c.LastRefresh) >= time.Duration(tokenRefreshIntervalDays)*24*time.Hour } func refreshToken(httpClient *http.Client, credentials *oauthCredentials) (*oauthCredentials, error) { if credentials.Tokens == nil || credentials.Tokens.RefreshToken == "" { return nil, E.New("refresh token is empty") } requestBody, err := json.Marshal(map[string]string{ "grant_type": "refresh_token", "refresh_token": credentials.Tokens.RefreshToken, "client_id": oauth2ClientID, "scope": "openid profile email", }) if err != nil { return nil, E.Cause(err, "marshal request") } request, err := http.NewRequest("POST", oauth2TokenURL, bytes.NewReader(requestBody)) if err != nil { return nil, err } request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") response, err := httpClient.Do(request) if err != nil { return nil, err } defer response.Body.Close() if response.StatusCode != http.StatusOK { body, _ := io.ReadAll(response.Body) return nil, E.New("refresh failed: ", response.Status, " ", string(body)) } var tokenResponse struct { IDToken string `json:"id_token"` AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` } err = json.NewDecoder(response.Body).Decode(&tokenResponse) if err != nil { return nil, E.Cause(err, "decode response") } newCredentials := *credentials if newCredentials.Tokens == nil { newCredentials.Tokens = &tokenData{} } if tokenResponse.IDToken != "" { newCredentials.Tokens.IDToken = tokenResponse.IDToken } if tokenResponse.AccessToken != "" { newCredentials.Tokens.AccessToken = tokenResponse.AccessToken } if tokenResponse.RefreshToken != "" { newCredentials.Tokens.RefreshToken = tokenResponse.RefreshToken } now := time.Now() newCredentials.LastRefresh = &now return &newCredentials, nil } ================================================ FILE: service/ocm/credential_darwin.go ================================================ //go:build darwin package ocm func platformReadCredentials(customPath string) (*oauthCredentials, error) { if customPath == "" { var err error customPath, err = getDefaultCredentialsPath() if err != nil { return nil, err } } return readCredentialsFromFile(customPath) } func platformWriteCredentials(credentials *oauthCredentials, customPath string) error { if customPath == "" { var err error customPath, err = getDefaultCredentialsPath() if err != nil { return err } } return writeCredentialsToFile(credentials, customPath) } ================================================ FILE: service/ocm/credential_other.go ================================================ //go:build !darwin package ocm func platformReadCredentials(customPath string) (*oauthCredentials, error) { if customPath == "" { var err error customPath, err = getDefaultCredentialsPath() if err != nil { return nil, err } } return readCredentialsFromFile(customPath) } func platformWriteCredentials(credentials *oauthCredentials, customPath string) error { if customPath == "" { var err error customPath, err = getDefaultCredentialsPath() if err != nil { return err } } return writeCredentialsToFile(credentials, customPath) } ================================================ FILE: service/ocm/service.go ================================================ package ocm import ( "bytes" "context" stdTLS "crypto/tls" "encoding/json" "errors" "io" "mime" "net" "net/http" "strconv" "strings" "sync" "time" "github.com/sagernet/sing-box/adapter" boxService "github.com/sagernet/sing-box/adapter/service" "github.com/sagernet/sing-box/common/dialer" "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/common" "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/ntp" aTLS "github.com/sagernet/sing/common/tls" "github.com/go-chi/chi/v5" "github.com/openai/openai-go/v3" "github.com/openai/openai-go/v3/responses" "golang.org/x/net/http2" ) func RegisterService(registry *boxService.Registry) { boxService.Register[option.OCMServiceOptions](registry, C.TypeOCM, NewService) } type errorResponse struct { Error errorDetails `json:"error"` } type errorDetails struct { Type string `json:"type"` Code string `json:"code,omitempty"` Message string `json:"message"` } func writeJSONError(w http.ResponseWriter, r *http.Request, statusCode int, errorType string, message string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(statusCode) json.NewEncoder(w).Encode(errorResponse{ Error: errorDetails{ Type: errorType, Message: message, }, }) } func isHopByHopHeader(header string) bool { switch strings.ToLower(header) { case "connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailers", "transfer-encoding", "upgrade", "host": return true default: return false } } func normalizeRateLimitIdentifier(limitIdentifier string) string { trimmedIdentifier := strings.TrimSpace(strings.ToLower(limitIdentifier)) if trimmedIdentifier == "" { return "" } return strings.ReplaceAll(trimmedIdentifier, "_", "-") } func parseInt64Header(headers http.Header, headerName string) (int64, bool) { headerValue := strings.TrimSpace(headers.Get(headerName)) if headerValue == "" { return 0, false } parsedValue, parseError := strconv.ParseInt(headerValue, 10, 64) if parseError != nil { return 0, false } return parsedValue, true } func weeklyCycleHintForLimit(headers http.Header, limitIdentifier string) *WeeklyCycleHint { normalizedLimitIdentifier := normalizeRateLimitIdentifier(limitIdentifier) if normalizedLimitIdentifier == "" { return nil } windowHeader := "x-" + normalizedLimitIdentifier + "-secondary-window-minutes" resetHeader := "x-" + normalizedLimitIdentifier + "-secondary-reset-at" windowMinutes, hasWindowMinutes := parseInt64Header(headers, windowHeader) resetAtUnix, hasResetAt := parseInt64Header(headers, resetHeader) if !hasWindowMinutes || !hasResetAt || windowMinutes <= 0 || resetAtUnix <= 0 { return nil } return &WeeklyCycleHint{ WindowMinutes: windowMinutes, ResetAt: time.Unix(resetAtUnix, 0).UTC(), } } func extractWeeklyCycleHint(headers http.Header) *WeeklyCycleHint { activeLimitIdentifier := normalizeRateLimitIdentifier(headers.Get("x-codex-active-limit")) if activeLimitIdentifier != "" { if activeHint := weeklyCycleHintForLimit(headers, activeLimitIdentifier); activeHint != nil { return activeHint } } return weeklyCycleHintForLimit(headers, "codex") } type Service struct { boxService.Adapter ctx context.Context logger log.ContextLogger credentialPath string credentials *oauthCredentials users []option.OCMUser dialer N.Dialer httpClient *http.Client httpHeaders http.Header listener *listener.Listener tlsConfig tls.ServerConfig httpServer *http.Server userManager *UserManager accessMutex sync.RWMutex usageTracker *AggregatedUsage webSocketMutex sync.Mutex webSocketGroup sync.WaitGroup webSocketConns map[*webSocketSession]struct{} shuttingDown bool } func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.OCMServiceOptions) (adapter.Service, error) { serviceDialer, err := dialer.NewWithOptions(dialer.Options{ Context: ctx, Options: option.DialerOptions{ Detour: options.Detour, }, RemoteIsDomain: true, }) if err != nil { return nil, E.Cause(err, "create dialer") } httpClient := &http.Client{ Transport: &http.Transport{ ForceAttemptHTTP2: true, TLSClientConfig: &stdTLS.Config{ RootCAs: adapter.RootPoolFromContext(ctx), Time: ntp.TimeFuncFromContext(ctx), }, DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { return serviceDialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) }, }, } userManager := &UserManager{ tokenMap: make(map[string]string), } var usageTracker *AggregatedUsage if options.UsagesPath != "" { usageTracker = &AggregatedUsage{ LastUpdated: time.Now(), Combinations: make([]CostCombination, 0), filePath: options.UsagesPath, logger: logger, } } service := &Service{ Adapter: boxService.NewAdapter(C.TypeOCM, tag), ctx: ctx, logger: logger, credentialPath: options.CredentialPath, users: options.Users, dialer: serviceDialer, httpClient: httpClient, httpHeaders: options.Headers.Build(), listener: listener.New(listener.Options{ Context: ctx, Logger: logger, Network: []string{N.NetworkTCP}, Listen: options.ListenOptions, }), userManager: userManager, usageTracker: usageTracker, webSocketConns: make(map[*webSocketSession]struct{}), } if options.TLS != nil { tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS)) if err != nil { return nil, err } service.tlsConfig = tlsConfig } return service, nil } func (s *Service) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } s.userManager.UpdateUsers(s.users) credentials, err := platformReadCredentials(s.credentialPath) if err != nil { return E.Cause(err, "read credentials") } s.credentials = credentials if s.usageTracker != nil { err = s.usageTracker.Load() if err != nil { s.logger.Warn("load usage statistics: ", err) } } router := chi.NewRouter() router.Mount("/", s) s.httpServer = &http.Server{Handler: router} 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() { serveErr := s.httpServer.Serve(tcpListener) if serveErr != nil && !errors.Is(serveErr, http.ErrServerClosed) { s.logger.Error("serve error: ", serveErr) } }() return nil } func (s *Service) getAccessToken() (string, error) { s.accessMutex.RLock() if !s.credentials.needsRefresh() { token := s.credentials.getAccessToken() s.accessMutex.RUnlock() return token, nil } s.accessMutex.RUnlock() s.accessMutex.Lock() defer s.accessMutex.Unlock() if !s.credentials.needsRefresh() { return s.credentials.getAccessToken(), nil } newCredentials, err := refreshToken(s.httpClient, s.credentials) if err != nil { return "", err } s.credentials = newCredentials err = platformWriteCredentials(newCredentials, s.credentialPath) if err != nil { s.logger.Warn("persist refreshed token: ", err) } return newCredentials.getAccessToken(), nil } func (s *Service) getAccountID() string { s.accessMutex.RLock() defer s.accessMutex.RUnlock() return s.credentials.getAccountID() } func (s *Service) isAPIKeyMode() bool { s.accessMutex.RLock() defer s.accessMutex.RUnlock() return s.credentials.isAPIKeyMode() } func (s *Service) getBaseURL() string { if s.isAPIKeyMode() { return openaiAPIBaseURL } return chatGPTBackendURL } func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { path := r.URL.Path if !strings.HasPrefix(path, "/v1/") { writeJSONError(w, r, http.StatusNotFound, "invalid_request_error", "path must start with /v1/") return } var proxyPath string if s.isAPIKeyMode() { proxyPath = path } else { if path == "/v1/chat/completions" { writeJSONError(w, r, http.StatusBadRequest, "invalid_request_error", "chat completions endpoint is only available in API key mode") return } proxyPath = strings.TrimPrefix(path, "/v1") } var username string if len(s.users) > 0 { authHeader := r.Header.Get("Authorization") if authHeader == "" { s.logger.Warn("authentication failed for request from ", r.RemoteAddr, ": missing Authorization header") writeJSONError(w, r, http.StatusUnauthorized, "authentication_error", "missing api key") return } clientToken := strings.TrimPrefix(authHeader, "Bearer ") if clientToken == authHeader { s.logger.Warn("authentication failed for request from ", r.RemoteAddr, ": invalid Authorization format") writeJSONError(w, r, http.StatusUnauthorized, "authentication_error", "invalid api key format") return } var ok bool username, ok = s.userManager.Authenticate(clientToken) if !ok { s.logger.Warn("authentication failed for request from ", r.RemoteAddr, ": unknown key: ", clientToken) writeJSONError(w, r, http.StatusUnauthorized, "authentication_error", "invalid api key") return } } if strings.EqualFold(r.Header.Get("Upgrade"), "websocket") && strings.HasPrefix(path, "/v1/responses") { s.handleWebSocket(w, r, proxyPath, username) return } var requestModel string if s.usageTracker != nil && r.Body != nil { bodyBytes, err := io.ReadAll(r.Body) if err == nil { var request struct { Model string `json:"model"` } err := json.Unmarshal(bodyBytes, &request) if err == nil { requestModel = request.Model } r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) } } accessToken, err := s.getAccessToken() if err != nil { s.logger.Error("get access token: ", err) writeJSONError(w, r, http.StatusUnauthorized, "authentication_error", "Authentication failed") return } proxyURL := s.getBaseURL() + proxyPath if r.URL.RawQuery != "" { proxyURL += "?" + r.URL.RawQuery } proxyRequest, err := http.NewRequestWithContext(r.Context(), r.Method, proxyURL, r.Body) if err != nil { s.logger.Error("create proxy request: ", err) writeJSONError(w, r, http.StatusInternalServerError, "api_error", "Internal server error") return } for key, values := range r.Header { if !isHopByHopHeader(key) && key != "Authorization" { proxyRequest.Header[key] = values } } for key, values := range s.httpHeaders { proxyRequest.Header.Del(key) proxyRequest.Header[key] = values } proxyRequest.Header.Set("Authorization", "Bearer "+accessToken) if accountID := s.getAccountID(); accountID != "" { proxyRequest.Header.Set("ChatGPT-Account-Id", accountID) } response, err := s.httpClient.Do(proxyRequest) if err != nil { writeJSONError(w, r, http.StatusBadGateway, "api_error", err.Error()) return } defer response.Body.Close() for key, values := range response.Header { if !isHopByHopHeader(key) { w.Header()[key] = values } } w.WriteHeader(response.StatusCode) trackUsage := s.usageTracker != nil && response.StatusCode == http.StatusOK && (path == "/v1/chat/completions" || strings.HasPrefix(path, "/v1/responses")) if trackUsage { s.handleResponseWithTracking(w, response, path, requestModel, username) } else { mediaType, _, err := mime.ParseMediaType(response.Header.Get("Content-Type")) if err == nil && mediaType != "text/event-stream" { _, _ = io.Copy(w, response.Body) return } flusher, ok := w.(http.Flusher) if !ok { s.logger.Error("streaming not supported") return } buffer := make([]byte, buf.BufferSize) for { n, err := response.Body.Read(buffer) if n > 0 { _, writeError := w.Write(buffer[:n]) if writeError != nil { s.logger.Error("write streaming response: ", writeError) return } flusher.Flush() } if err != nil { return } } } } func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, response *http.Response, path string, requestModel string, username string) { isChatCompletions := path == "/v1/chat/completions" weeklyCycleHint := extractWeeklyCycleHint(response.Header) mediaType, _, err := mime.ParseMediaType(response.Header.Get("Content-Type")) isStreaming := err == nil && mediaType == "text/event-stream" if !isStreaming && !isChatCompletions && response.Header.Get("Content-Type") == "" { isStreaming = true } if !isStreaming { bodyBytes, err := io.ReadAll(response.Body) if err != nil { s.logger.Error("read response body: ", err) return } var responseModel, serviceTier string var inputTokens, outputTokens, cachedTokens int64 if isChatCompletions { var chatCompletion openai.ChatCompletion if json.Unmarshal(bodyBytes, &chatCompletion) == nil { responseModel = chatCompletion.Model serviceTier = string(chatCompletion.ServiceTier) inputTokens = chatCompletion.Usage.PromptTokens outputTokens = chatCompletion.Usage.CompletionTokens cachedTokens = chatCompletion.Usage.PromptTokensDetails.CachedTokens } } else { var responsesResponse responses.Response if json.Unmarshal(bodyBytes, &responsesResponse) == nil { responseModel = string(responsesResponse.Model) serviceTier = string(responsesResponse.ServiceTier) inputTokens = responsesResponse.Usage.InputTokens outputTokens = responsesResponse.Usage.OutputTokens cachedTokens = responsesResponse.Usage.InputTokensDetails.CachedTokens } } if inputTokens > 0 || outputTokens > 0 { if responseModel == "" { responseModel = requestModel } if responseModel != "" { contextWindow := detectContextWindow(responseModel, serviceTier, inputTokens) s.usageTracker.AddUsageWithCycleHint( responseModel, contextWindow, inputTokens, outputTokens, cachedTokens, serviceTier, username, time.Now(), weeklyCycleHint, ) } } _, _ = writer.Write(bodyBytes) return } flusher, ok := writer.(http.Flusher) if !ok { s.logger.Error("streaming not supported") return } var inputTokens, outputTokens, cachedTokens int64 var responseModel, serviceTier string buffer := make([]byte, buf.BufferSize) var leftover []byte for { n, err := response.Body.Read(buffer) if n > 0 { data := append(leftover, buffer[:n]...) lines := bytes.Split(data, []byte("\n")) if err == nil { leftover = lines[len(lines)-1] lines = lines[:len(lines)-1] } else { leftover = nil } for _, line := range lines { line = bytes.TrimSpace(line) if len(line) == 0 { continue } if after, ok0 := bytes.CutPrefix(line, []byte("data: ")); ok0 { eventData := after if bytes.Equal(eventData, []byte("[DONE]")) { continue } if isChatCompletions { var chatChunk openai.ChatCompletionChunk if json.Unmarshal(eventData, &chatChunk) == nil { if chatChunk.Model != "" { responseModel = chatChunk.Model } if chatChunk.ServiceTier != "" { serviceTier = string(chatChunk.ServiceTier) } if chatChunk.Usage.PromptTokens > 0 { inputTokens = chatChunk.Usage.PromptTokens cachedTokens = chatChunk.Usage.PromptTokensDetails.CachedTokens } if chatChunk.Usage.CompletionTokens > 0 { outputTokens = chatChunk.Usage.CompletionTokens } } } else { var streamEvent responses.ResponseStreamEventUnion if json.Unmarshal(eventData, &streamEvent) == nil { if streamEvent.Type == "response.completed" { completedEvent := streamEvent.AsResponseCompleted() if string(completedEvent.Response.Model) != "" { responseModel = string(completedEvent.Response.Model) } if completedEvent.Response.ServiceTier != "" { serviceTier = string(completedEvent.Response.ServiceTier) } if completedEvent.Response.Usage.InputTokens > 0 { inputTokens = completedEvent.Response.Usage.InputTokens cachedTokens = completedEvent.Response.Usage.InputTokensDetails.CachedTokens } if completedEvent.Response.Usage.OutputTokens > 0 { outputTokens = completedEvent.Response.Usage.OutputTokens } } } } } } _, writeError := writer.Write(buffer[:n]) if writeError != nil { s.logger.Error("write streaming response: ", writeError) return } flusher.Flush() } if err != nil { if responseModel == "" { responseModel = requestModel } if inputTokens > 0 || outputTokens > 0 { if responseModel != "" { contextWindow := detectContextWindow(responseModel, serviceTier, inputTokens) s.usageTracker.AddUsageWithCycleHint( responseModel, contextWindow, inputTokens, outputTokens, cachedTokens, serviceTier, username, time.Now(), weeklyCycleHint, ) } } return } } } func (s *Service) Close() error { webSocketSessions := s.startWebSocketShutdown() err := common.Close( common.PtrOrNil(s.httpServer), common.PtrOrNil(s.listener), s.tlsConfig, ) for _, session := range webSocketSessions { session.Close() } s.webSocketGroup.Wait() if s.usageTracker != nil { s.usageTracker.cancelPendingSave() saveErr := s.usageTracker.Save() if saveErr != nil { s.logger.Error("save usage statistics: ", saveErr) } } return err } func (s *Service) registerWebSocketSession(session *webSocketSession) bool { s.webSocketMutex.Lock() defer s.webSocketMutex.Unlock() if s.shuttingDown { return false } s.webSocketConns[session] = struct{}{} s.webSocketGroup.Add(1) return true } func (s *Service) unregisterWebSocketSession(session *webSocketSession) { s.webSocketMutex.Lock() _, loaded := s.webSocketConns[session] if loaded { delete(s.webSocketConns, session) } s.webSocketMutex.Unlock() if loaded { s.webSocketGroup.Done() } } func (s *Service) isShuttingDown() bool { s.webSocketMutex.Lock() defer s.webSocketMutex.Unlock() return s.shuttingDown } func (s *Service) startWebSocketShutdown() []*webSocketSession { s.webSocketMutex.Lock() defer s.webSocketMutex.Unlock() s.shuttingDown = true webSocketSessions := make([]*webSocketSession, 0, len(s.webSocketConns)) for session := range s.webSocketConns { webSocketSessions = append(webSocketSessions, session) } return webSocketSessions } ================================================ FILE: service/ocm/service_usage.go ================================================ package ocm import ( "encoding/json" "fmt" "math" "os" "regexp" "strings" "sync" "time" "github.com/sagernet/sing-box/log" E "github.com/sagernet/sing/common/exceptions" ) type UsageStats struct { RequestCount int `json:"request_count"` InputTokens int64 `json:"input_tokens"` OutputTokens int64 `json:"output_tokens"` CachedTokens int64 `json:"cached_tokens"` } func (u *UsageStats) UnmarshalJSON(data []byte) error { type Alias UsageStats aux := &struct { *Alias PromptTokens int64 `json:"prompt_tokens"` CompletionTokens int64 `json:"completion_tokens"` }{ Alias: (*Alias)(u), } err := json.Unmarshal(data, aux) if err != nil { return err } if u.InputTokens == 0 && aux.PromptTokens > 0 { u.InputTokens = aux.PromptTokens } if u.OutputTokens == 0 && aux.CompletionTokens > 0 { u.OutputTokens = aux.CompletionTokens } return nil } type CostCombination struct { Model string `json:"model"` ServiceTier string `json:"service_tier,omitempty"` ContextWindow int `json:"context_window"` WeekStartUnix int64 `json:"week_start_unix,omitempty"` Total UsageStats `json:"total"` ByUser map[string]UsageStats `json:"by_user"` } type AggregatedUsage struct { LastUpdated time.Time `json:"last_updated"` Combinations []CostCombination `json:"combinations"` mutex sync.Mutex filePath string logger log.ContextLogger lastSaveTime time.Time pendingSave bool saveTimer *time.Timer saveMutex sync.Mutex } type UsageStatsJSON struct { RequestCount int `json:"request_count"` InputTokens int64 `json:"input_tokens"` OutputTokens int64 `json:"output_tokens"` CachedTokens int64 `json:"cached_tokens"` CostUSD float64 `json:"cost_usd"` } type CostCombinationJSON struct { Model string `json:"model"` ServiceTier string `json:"service_tier,omitempty"` ContextWindow int `json:"context_window"` WeekStartUnix int64 `json:"week_start_unix,omitempty"` Total UsageStatsJSON `json:"total"` ByUser map[string]UsageStatsJSON `json:"by_user"` } type CostsSummaryJSON struct { TotalUSD float64 `json:"total_usd"` ByUser map[string]float64 `json:"by_user"` ByWeek map[string]float64 `json:"by_week,omitempty"` ByUserAndWeek map[string]map[string]float64 `json:"by_user_and_week,omitempty"` } type AggregatedUsageJSON struct { LastUpdated time.Time `json:"last_updated"` Costs CostsSummaryJSON `json:"costs"` Combinations []CostCombinationJSON `json:"combinations"` } type WeeklyCycleHint struct { WindowMinutes int64 ResetAt time.Time } type ModelPricing struct { InputPrice float64 OutputPrice float64 CachedInputPrice float64 } type modelFamily struct { pattern *regexp.Regexp pricing ModelPricing premiumPricing *ModelPricing } const ( serviceTierAuto = "auto" serviceTierDefault = "default" serviceTierFlex = "flex" serviceTierPriority = "priority" serviceTierScale = "scale" ) const ( contextWindowStandard = 272000 contextWindowPremium = 1050000 premiumContextThreshold = 272000 ) var ( gpt52Pricing = ModelPricing{ InputPrice: 1.75, OutputPrice: 14.0, CachedInputPrice: 0.175, } gpt5Pricing = ModelPricing{ InputPrice: 1.25, OutputPrice: 10.0, CachedInputPrice: 0.125, } gpt5MiniPricing = ModelPricing{ InputPrice: 0.25, OutputPrice: 2.0, CachedInputPrice: 0.025, } gpt5NanoPricing = ModelPricing{ InputPrice: 0.05, OutputPrice: 0.4, CachedInputPrice: 0.005, } gpt52CodexPricing = ModelPricing{ InputPrice: 1.75, OutputPrice: 14.0, CachedInputPrice: 0.175, } gpt51CodexPricing = ModelPricing{ InputPrice: 1.25, OutputPrice: 10.0, CachedInputPrice: 0.125, } gpt51CodexMiniPricing = ModelPricing{ InputPrice: 0.25, OutputPrice: 2.0, CachedInputPrice: 0.025, } gpt54StandardPricing = ModelPricing{ InputPrice: 2.5, OutputPrice: 15.0, CachedInputPrice: 0.25, } gpt54PremiumPricing = ModelPricing{ InputPrice: 5.0, OutputPrice: 22.5, CachedInputPrice: 0.5, } gpt54ProPricing = ModelPricing{ InputPrice: 30.0, OutputPrice: 180.0, CachedInputPrice: 30.0, } gpt54ProPremiumPricing = ModelPricing{ InputPrice: 60.0, OutputPrice: 270.0, CachedInputPrice: 60.0, } gpt52ProPricing = ModelPricing{ InputPrice: 21.0, OutputPrice: 168.0, CachedInputPrice: 21.0, } gpt5ProPricing = ModelPricing{ InputPrice: 15.0, OutputPrice: 120.0, CachedInputPrice: 15.0, } gpt54FlexPricing = ModelPricing{ InputPrice: 1.25, OutputPrice: 7.5, CachedInputPrice: 0.125, } gpt54PremiumFlexPricing = ModelPricing{ InputPrice: 2.5, OutputPrice: 11.25, CachedInputPrice: 0.25, } gpt54ProFlexPricing = ModelPricing{ InputPrice: 15.0, OutputPrice: 90.0, CachedInputPrice: 15.0, } gpt54ProPremiumFlexPricing = ModelPricing{ InputPrice: 30.0, OutputPrice: 135.0, CachedInputPrice: 30.0, } gpt52FlexPricing = ModelPricing{ InputPrice: 0.875, OutputPrice: 7.0, CachedInputPrice: 0.0875, } gpt5FlexPricing = ModelPricing{ InputPrice: 0.625, OutputPrice: 5.0, CachedInputPrice: 0.0625, } gpt5MiniFlexPricing = ModelPricing{ InputPrice: 0.125, OutputPrice: 1.0, CachedInputPrice: 0.0125, } gpt5NanoFlexPricing = ModelPricing{ InputPrice: 0.025, OutputPrice: 0.2, CachedInputPrice: 0.0025, } gpt54PriorityPricing = ModelPricing{ InputPrice: 5.0, OutputPrice: 30.0, CachedInputPrice: 0.5, } gpt54PremiumPriorityPricing = ModelPricing{ InputPrice: 10.0, OutputPrice: 45.0, CachedInputPrice: 1.0, } gpt52PriorityPricing = ModelPricing{ InputPrice: 3.5, OutputPrice: 28.0, CachedInputPrice: 0.35, } gpt5PriorityPricing = ModelPricing{ InputPrice: 2.5, OutputPrice: 20.0, CachedInputPrice: 0.25, } gpt5MiniPriorityPricing = ModelPricing{ InputPrice: 0.45, OutputPrice: 3.6, CachedInputPrice: 0.045, } gpt52CodexPriorityPricing = ModelPricing{ InputPrice: 3.5, OutputPrice: 28.0, CachedInputPrice: 0.35, } gpt51CodexPriorityPricing = ModelPricing{ InputPrice: 2.5, OutputPrice: 20.0, CachedInputPrice: 0.25, } gpt4oPricing = ModelPricing{ InputPrice: 2.5, OutputPrice: 10.0, CachedInputPrice: 1.25, } gpt4oMiniPricing = ModelPricing{ InputPrice: 0.15, OutputPrice: 0.6, CachedInputPrice: 0.075, } gpt4oAudioPricing = ModelPricing{ InputPrice: 2.5, OutputPrice: 10.0, CachedInputPrice: 2.5, } gpt4oMiniAudioPricing = ModelPricing{ InputPrice: 0.15, OutputPrice: 0.6, CachedInputPrice: 0.15, } gptAudioMiniPricing = ModelPricing{ InputPrice: 0.6, OutputPrice: 2.4, CachedInputPrice: 0.6, } o1Pricing = ModelPricing{ InputPrice: 15.0, OutputPrice: 60.0, CachedInputPrice: 7.5, } o1ProPricing = ModelPricing{ InputPrice: 150.0, OutputPrice: 600.0, CachedInputPrice: 150.0, } o1MiniPricing = ModelPricing{ InputPrice: 1.1, OutputPrice: 4.4, CachedInputPrice: 0.55, } o3MiniPricing = ModelPricing{ InputPrice: 1.1, OutputPrice: 4.4, CachedInputPrice: 0.55, } o3Pricing = ModelPricing{ InputPrice: 2.0, OutputPrice: 8.0, CachedInputPrice: 0.5, } o3ProPricing = ModelPricing{ InputPrice: 20.0, OutputPrice: 80.0, CachedInputPrice: 20.0, } o3DeepResearchPricing = ModelPricing{ InputPrice: 10.0, OutputPrice: 40.0, CachedInputPrice: 2.5, } o4MiniPricing = ModelPricing{ InputPrice: 1.1, OutputPrice: 4.4, CachedInputPrice: 0.275, } o4MiniDeepResearchPricing = ModelPricing{ InputPrice: 2.0, OutputPrice: 8.0, CachedInputPrice: 0.5, } o3FlexPricing = ModelPricing{ InputPrice: 1.0, OutputPrice: 4.0, CachedInputPrice: 0.25, } o4MiniFlexPricing = ModelPricing{ InputPrice: 0.55, OutputPrice: 2.2, CachedInputPrice: 0.138, } o3PriorityPricing = ModelPricing{ InputPrice: 3.5, OutputPrice: 14.0, CachedInputPrice: 0.875, } o4MiniPriorityPricing = ModelPricing{ InputPrice: 2.0, OutputPrice: 8.0, CachedInputPrice: 0.5, } gpt41Pricing = ModelPricing{ InputPrice: 2.0, OutputPrice: 8.0, CachedInputPrice: 0.5, } gpt41MiniPricing = ModelPricing{ InputPrice: 0.4, OutputPrice: 1.6, CachedInputPrice: 0.1, } gpt41NanoPricing = ModelPricing{ InputPrice: 0.1, OutputPrice: 0.4, CachedInputPrice: 0.025, } gpt41PriorityPricing = ModelPricing{ InputPrice: 3.5, OutputPrice: 14.0, CachedInputPrice: 0.875, } gpt41MiniPriorityPricing = ModelPricing{ InputPrice: 0.7, OutputPrice: 2.8, CachedInputPrice: 0.175, } gpt41NanoPriorityPricing = ModelPricing{ InputPrice: 0.2, OutputPrice: 0.8, CachedInputPrice: 0.05, } gpt4oPriorityPricing = ModelPricing{ InputPrice: 4.25, OutputPrice: 17.0, CachedInputPrice: 2.125, } gpt4oMiniPriorityPricing = ModelPricing{ InputPrice: 0.25, OutputPrice: 1.0, CachedInputPrice: 0.125, } standardModelFamilies = []modelFamily{ { pattern: regexp.MustCompile(`^gpt-5\.4-pro(?:$|-)`), pricing: gpt54ProPricing, premiumPricing: &gpt54ProPremiumPricing, }, { pattern: regexp.MustCompile(`^gpt-5\.4(?:$|-)`), pricing: gpt54StandardPricing, premiumPricing: &gpt54PremiumPricing, }, { pattern: regexp.MustCompile(`^gpt-5\.3-codex(?:$|-)`), pricing: gpt52CodexPricing, }, { pattern: regexp.MustCompile(`^gpt-5\.2-codex(?:$|-)`), pricing: gpt52CodexPricing, }, { pattern: regexp.MustCompile(`^gpt-5\.1-codex-max(?:$|-)`), pricing: gpt51CodexPricing, }, { pattern: regexp.MustCompile(`^gpt-5\.1-codex-mini(?:$|-)`), pricing: gpt51CodexMiniPricing, }, { pattern: regexp.MustCompile(`^gpt-5\.1-codex(?:$|-)`), pricing: gpt51CodexPricing, }, { pattern: regexp.MustCompile(`^gpt-5-codex-mini(?:$|-)`), pricing: gpt51CodexMiniPricing, }, { pattern: regexp.MustCompile(`^gpt-5-codex(?:$|-)`), pricing: gpt51CodexPricing, }, { pattern: regexp.MustCompile(`^gpt-5\.2-chat-latest$`), pricing: gpt52Pricing, }, { pattern: regexp.MustCompile(`^gpt-5\.1-chat-latest$`), pricing: gpt5Pricing, }, { pattern: regexp.MustCompile(`^gpt-5-chat-latest$`), pricing: gpt5Pricing, }, { pattern: regexp.MustCompile(`^gpt-5\.2-pro(?:$|-)`), pricing: gpt52ProPricing, }, { pattern: regexp.MustCompile(`^gpt-5-pro(?:$|-)`), pricing: gpt5ProPricing, }, { pattern: regexp.MustCompile(`^gpt-5-mini(?:$|-)`), pricing: gpt5MiniPricing, }, { pattern: regexp.MustCompile(`^gpt-5-nano(?:$|-)`), pricing: gpt5NanoPricing, }, { pattern: regexp.MustCompile(`^gpt-5\.2(?:$|-)`), pricing: gpt52Pricing, }, { pattern: regexp.MustCompile(`^gpt-5\.1(?:$|-)`), pricing: gpt5Pricing, }, { pattern: regexp.MustCompile(`^gpt-5(?:$|-)`), pricing: gpt5Pricing, }, { pattern: regexp.MustCompile(`^o4-mini-deep-research(?:$|-)`), pricing: o4MiniDeepResearchPricing, }, { pattern: regexp.MustCompile(`^o4-mini(?:$|-)`), pricing: o4MiniPricing, }, { pattern: regexp.MustCompile(`^o3-pro(?:$|-)`), pricing: o3ProPricing, }, { pattern: regexp.MustCompile(`^o3-deep-research(?:$|-)`), pricing: o3DeepResearchPricing, }, { pattern: regexp.MustCompile(`^o3-mini(?:$|-)`), pricing: o3MiniPricing, }, { pattern: regexp.MustCompile(`^o3(?:$|-)`), pricing: o3Pricing, }, { pattern: regexp.MustCompile(`^o1-pro(?:$|-)`), pricing: o1ProPricing, }, { pattern: regexp.MustCompile(`^o1-mini(?:$|-)`), pricing: o1MiniPricing, }, { pattern: regexp.MustCompile(`^o1(?:$|-)`), pricing: o1Pricing, }, { pattern: regexp.MustCompile(`^gpt-4o-mini-audio(?:$|-)`), pricing: gpt4oMiniAudioPricing, }, { pattern: regexp.MustCompile(`^gpt-audio-mini(?:$|-)`), pricing: gptAudioMiniPricing, }, { pattern: regexp.MustCompile(`^(?:gpt-4o-audio|gpt-audio)(?:$|-)`), pricing: gpt4oAudioPricing, }, { pattern: regexp.MustCompile(`^gpt-4\.1-nano(?:$|-)`), pricing: gpt41NanoPricing, }, { pattern: regexp.MustCompile(`^gpt-4\.1-mini(?:$|-)`), pricing: gpt41MiniPricing, }, { pattern: regexp.MustCompile(`^gpt-4\.1(?:$|-)`), pricing: gpt41Pricing, }, { pattern: regexp.MustCompile(`^gpt-4o-mini(?:$|-)`), pricing: gpt4oMiniPricing, }, { pattern: regexp.MustCompile(`^gpt-4o(?:$|-)`), pricing: gpt4oPricing, }, { pattern: regexp.MustCompile(`^chatgpt-4o(?:$|-)`), pricing: gpt4oPricing, }, } flexModelFamilies = []modelFamily{ { pattern: regexp.MustCompile(`^gpt-5\.4-pro(?:$|-)`), pricing: gpt54ProFlexPricing, premiumPricing: &gpt54ProPremiumFlexPricing, }, { pattern: regexp.MustCompile(`^gpt-5\.4(?:$|-)`), pricing: gpt54FlexPricing, premiumPricing: &gpt54PremiumFlexPricing, }, { pattern: regexp.MustCompile(`^gpt-5-mini(?:$|-)`), pricing: gpt5MiniFlexPricing, }, { pattern: regexp.MustCompile(`^gpt-5-nano(?:$|-)`), pricing: gpt5NanoFlexPricing, }, { pattern: regexp.MustCompile(`^gpt-5\.2(?:$|-)`), pricing: gpt52FlexPricing, }, { pattern: regexp.MustCompile(`^gpt-5\.1(?:$|-)`), pricing: gpt5FlexPricing, }, { pattern: regexp.MustCompile(`^gpt-5(?:$|-)`), pricing: gpt5FlexPricing, }, { pattern: regexp.MustCompile(`^o4-mini(?:$|-)`), pricing: o4MiniFlexPricing, }, { pattern: regexp.MustCompile(`^o3(?:$|-)`), pricing: o3FlexPricing, }, } priorityModelFamilies = []modelFamily{ { pattern: regexp.MustCompile(`^gpt-5\.4(?:$|-)`), pricing: gpt54PriorityPricing, premiumPricing: &gpt54PremiumPriorityPricing, }, { pattern: regexp.MustCompile(`^gpt-5\.3-codex(?:$|-)`), pricing: gpt52CodexPriorityPricing, }, { pattern: regexp.MustCompile(`^gpt-5\.2-codex(?:$|-)`), pricing: gpt52CodexPriorityPricing, }, { pattern: regexp.MustCompile(`^gpt-5\.1-codex-max(?:$|-)`), pricing: gpt51CodexPriorityPricing, }, { pattern: regexp.MustCompile(`^gpt-5\.1-codex(?:$|-)`), pricing: gpt51CodexPriorityPricing, }, { pattern: regexp.MustCompile(`^gpt-5-codex-mini(?:$|-)`), pricing: gpt5MiniPriorityPricing, }, { pattern: regexp.MustCompile(`^gpt-5-codex(?:$|-)`), pricing: gpt51CodexPriorityPricing, }, { pattern: regexp.MustCompile(`^gpt-5-mini(?:$|-)`), pricing: gpt5MiniPriorityPricing, }, { pattern: regexp.MustCompile(`^gpt-5\.2(?:$|-)`), pricing: gpt52PriorityPricing, }, { pattern: regexp.MustCompile(`^gpt-5\.1(?:$|-)`), pricing: gpt5PriorityPricing, }, { pattern: regexp.MustCompile(`^gpt-5(?:$|-)`), pricing: gpt5PriorityPricing, }, { pattern: regexp.MustCompile(`^o4-mini(?:$|-)`), pricing: o4MiniPriorityPricing, }, { pattern: regexp.MustCompile(`^o3(?:$|-)`), pricing: o3PriorityPricing, }, { pattern: regexp.MustCompile(`^gpt-4\.1-nano(?:$|-)`), pricing: gpt41NanoPriorityPricing, }, { pattern: regexp.MustCompile(`^gpt-4\.1-mini(?:$|-)`), pricing: gpt41MiniPriorityPricing, }, { pattern: regexp.MustCompile(`^gpt-4\.1(?:$|-)`), pricing: gpt41PriorityPricing, }, { pattern: regexp.MustCompile(`^gpt-4o-mini(?:$|-)`), pricing: gpt4oMiniPriorityPricing, }, { pattern: regexp.MustCompile(`^gpt-4o(?:$|-)`), pricing: gpt4oPriorityPricing, }, } ) func modelFamiliesForTier(serviceTier string) []modelFamily { switch serviceTier { case serviceTierFlex: return flexModelFamilies case serviceTierPriority: return priorityModelFamilies default: return standardModelFamilies } } func findPricingInFamilies(model string, contextWindow int, modelFamilies []modelFamily) (ModelPricing, bool) { isPremium := contextWindow >= contextWindowPremium for _, family := range modelFamilies { if family.pattern.MatchString(model) { if isPremium && family.premiumPricing != nil { return *family.premiumPricing, true } return family.pricing, true } } return ModelPricing{}, false } func hasPremiumPricingInFamilies(model string, modelFamilies []modelFamily) bool { for _, family := range modelFamilies { if family.pattern.MatchString(model) { return family.premiumPricing != nil } } return false } func normalizeServiceTier(serviceTier string) string { switch strings.ToLower(strings.TrimSpace(serviceTier)) { case "", serviceTierAuto, serviceTierDefault: return serviceTierDefault case serviceTierFlex: return serviceTierFlex case serviceTierPriority: return serviceTierPriority case serviceTierScale: // Scale-tier requests are prepaid differently and not listed in this usage file. return serviceTierDefault default: return serviceTierDefault } } func getPricing(model string, serviceTier string, contextWindow int) ModelPricing { normalizedServiceTier := normalizeServiceTier(serviceTier) families := modelFamiliesForTier(normalizedServiceTier) if pricing, found := findPricingInFamilies(model, contextWindow, families); found { return pricing } normalizedModel := normalizeGPT5Model(model) if normalizedModel != model { if pricing, found := findPricingInFamilies(normalizedModel, contextWindow, families); found { return pricing } } if normalizedServiceTier != serviceTierDefault { if pricing, found := findPricingInFamilies(model, contextWindow, standardModelFamilies); found { return pricing } if normalizedModel != model { if pricing, found := findPricingInFamilies(normalizedModel, contextWindow, standardModelFamilies); found { return pricing } } } return gpt4oPricing } func detectContextWindow(model string, serviceTier string, inputTokens int64) int { if inputTokens <= premiumContextThreshold { return contextWindowStandard } normalizedServiceTier := normalizeServiceTier(serviceTier) families := modelFamiliesForTier(normalizedServiceTier) if hasPremiumPricingInFamilies(model, families) { return contextWindowPremium } normalizedModel := normalizeGPT5Model(model) if normalizedModel != model && hasPremiumPricingInFamilies(normalizedModel, families) { return contextWindowPremium } if normalizedServiceTier != serviceTierDefault { if hasPremiumPricingInFamilies(model, standardModelFamilies) { return contextWindowPremium } if normalizedModel != model && hasPremiumPricingInFamilies(normalizedModel, standardModelFamilies) { return contextWindowPremium } } return contextWindowStandard } func normalizeGPT5Model(model string) string { if !strings.HasPrefix(model, "gpt-5.") { return model } switch { case strings.Contains(model, "-codex-mini"): return "gpt-5.1-codex-mini" case strings.Contains(model, "-codex-max"): return "gpt-5.1-codex-max" case strings.Contains(model, "-codex"): return "gpt-5.3-codex" case strings.Contains(model, "-chat-latest"): return "gpt-5.2-chat-latest" case strings.Contains(model, "-pro"): return "gpt-5.4-pro" case strings.Contains(model, "-mini"): return "gpt-5-mini" case strings.Contains(model, "-nano"): return "gpt-5-nano" default: return "gpt-5.4" } } func calculateCost(stats UsageStats, model string, serviceTier string, contextWindow int) float64 { pricing := getPricing(model, serviceTier, contextWindow) regularInputTokens := max(stats.InputTokens-stats.CachedTokens, 0) cost := (float64(regularInputTokens)*pricing.InputPrice + float64(stats.OutputTokens)*pricing.OutputPrice + float64(stats.CachedTokens)*pricing.CachedInputPrice) / 1_000_000 return math.Round(cost*100) / 100 } func roundCost(cost float64) float64 { return math.Round(cost*100) / 100 } func normalizeCombinations(combinations []CostCombination) { for index := range combinations { combinations[index].ServiceTier = normalizeServiceTier(combinations[index].ServiceTier) if combinations[index].ContextWindow <= 0 { combinations[index].ContextWindow = contextWindowStandard } if combinations[index].ByUser == nil { combinations[index].ByUser = make(map[string]UsageStats) } } } func addUsageToCombinations(combinations *[]CostCombination, model string, serviceTier string, contextWindow int, weekStartUnix int64, user string, inputTokens, outputTokens, cachedTokens int64) { var matchedCombination *CostCombination for index := range *combinations { combination := &(*combinations)[index] combinationServiceTier := normalizeServiceTier(combination.ServiceTier) if combination.ServiceTier != combinationServiceTier { combination.ServiceTier = combinationServiceTier } if combination.Model == model && combinationServiceTier == serviceTier && combination.ContextWindow == contextWindow && combination.WeekStartUnix == weekStartUnix { matchedCombination = combination break } } if matchedCombination == nil { newCombination := CostCombination{ Model: model, ServiceTier: serviceTier, ContextWindow: contextWindow, WeekStartUnix: weekStartUnix, Total: UsageStats{}, ByUser: make(map[string]UsageStats), } *combinations = append(*combinations, newCombination) matchedCombination = &(*combinations)[len(*combinations)-1] } matchedCombination.Total.RequestCount++ matchedCombination.Total.InputTokens += inputTokens matchedCombination.Total.OutputTokens += outputTokens matchedCombination.Total.CachedTokens += cachedTokens if user != "" { userStats := matchedCombination.ByUser[user] userStats.RequestCount++ userStats.InputTokens += inputTokens userStats.OutputTokens += outputTokens userStats.CachedTokens += cachedTokens matchedCombination.ByUser[user] = userStats } } func buildCombinationJSON(combinations []CostCombination, aggregateUserCosts map[string]float64) ([]CostCombinationJSON, float64) { result := make([]CostCombinationJSON, len(combinations)) var totalCost float64 for index, combination := range combinations { combinationTotalCost := calculateCost(combination.Total, combination.Model, combination.ServiceTier, combination.ContextWindow) totalCost += combinationTotalCost combinationJSON := CostCombinationJSON{ Model: combination.Model, ServiceTier: combination.ServiceTier, ContextWindow: combination.ContextWindow, WeekStartUnix: combination.WeekStartUnix, Total: UsageStatsJSON{ RequestCount: combination.Total.RequestCount, InputTokens: combination.Total.InputTokens, OutputTokens: combination.Total.OutputTokens, CachedTokens: combination.Total.CachedTokens, CostUSD: combinationTotalCost, }, ByUser: make(map[string]UsageStatsJSON), } for user, userStats := range combination.ByUser { userCost := calculateCost(userStats, combination.Model, combination.ServiceTier, combination.ContextWindow) if aggregateUserCosts != nil { aggregateUserCosts[user] += userCost } combinationJSON.ByUser[user] = UsageStatsJSON{ RequestCount: userStats.RequestCount, InputTokens: userStats.InputTokens, OutputTokens: userStats.OutputTokens, CachedTokens: userStats.CachedTokens, CostUSD: userCost, } } result[index] = combinationJSON } return result, roundCost(totalCost) } func formatUTCOffsetLabel(timestamp time.Time) string { _, offsetSeconds := timestamp.Zone() sign := "+" if offsetSeconds < 0 { sign = "-" offsetSeconds = -offsetSeconds } offsetHours := offsetSeconds / 3600 offsetMinutes := (offsetSeconds % 3600) / 60 if offsetMinutes == 0 { return fmt.Sprintf("UTC%s%d", sign, offsetHours) } return fmt.Sprintf("UTC%s%d:%02d", sign, offsetHours, offsetMinutes) } func formatWeekStartKey(cycleStartAt time.Time) string { localCycleStart := cycleStartAt.In(time.Local) return fmt.Sprintf("%s %s", localCycleStart.Format("2006-01-02 15:04:05"), formatUTCOffsetLabel(localCycleStart)) } func buildByWeekCost(combinations []CostCombination) map[string]float64 { byWeek := make(map[string]float64) for _, combination := range combinations { if combination.WeekStartUnix <= 0 { continue } weekStartAt := time.Unix(combination.WeekStartUnix, 0).UTC() weekKey := formatWeekStartKey(weekStartAt) byWeek[weekKey] += calculateCost(combination.Total, combination.Model, combination.ServiceTier, combination.ContextWindow) } for weekKey, weekCost := range byWeek { byWeek[weekKey] = roundCost(weekCost) } return byWeek } func buildByUserAndWeekCost(combinations []CostCombination) map[string]map[string]float64 { byUserAndWeek := make(map[string]map[string]float64) for _, combination := range combinations { if combination.WeekStartUnix <= 0 { continue } weekStartAt := time.Unix(combination.WeekStartUnix, 0).UTC() weekKey := formatWeekStartKey(weekStartAt) for user, userStats := range combination.ByUser { userWeeks, exists := byUserAndWeek[user] if !exists { userWeeks = make(map[string]float64) byUserAndWeek[user] = userWeeks } userWeeks[weekKey] += calculateCost(userStats, combination.Model, combination.ServiceTier, combination.ContextWindow) } } for _, weekCosts := range byUserAndWeek { for weekKey, cost := range weekCosts { weekCosts[weekKey] = roundCost(cost) } } return byUserAndWeek } func deriveWeekStartUnix(cycleHint *WeeklyCycleHint) int64 { if cycleHint == nil || cycleHint.WindowMinutes <= 0 || cycleHint.ResetAt.IsZero() { return 0 } windowDuration := time.Duration(cycleHint.WindowMinutes) * time.Minute return cycleHint.ResetAt.UTC().Add(-windowDuration).Unix() } func (u *AggregatedUsage) ToJSON() *AggregatedUsageJSON { u.mutex.Lock() defer u.mutex.Unlock() result := &AggregatedUsageJSON{ LastUpdated: u.LastUpdated, Costs: CostsSummaryJSON{ TotalUSD: 0, ByUser: make(map[string]float64), ByWeek: make(map[string]float64), }, } globalCombinationsJSON, totalCost := buildCombinationJSON(u.Combinations, result.Costs.ByUser) result.Combinations = globalCombinationsJSON result.Costs.TotalUSD = totalCost result.Costs.ByWeek = buildByWeekCost(u.Combinations) if len(result.Costs.ByWeek) == 0 { result.Costs.ByWeek = nil } result.Costs.ByUserAndWeek = buildByUserAndWeekCost(u.Combinations) if len(result.Costs.ByUserAndWeek) == 0 { result.Costs.ByUserAndWeek = nil } for user, cost := range result.Costs.ByUser { result.Costs.ByUser[user] = roundCost(cost) } return result } func (u *AggregatedUsage) Load() error { u.mutex.Lock() defer u.mutex.Unlock() u.LastUpdated = time.Time{} u.Combinations = nil data, err := os.ReadFile(u.filePath) if err != nil { if os.IsNotExist(err) { return nil } return err } var temp struct { LastUpdated time.Time `json:"last_updated"` Combinations []CostCombination `json:"combinations"` } err = json.Unmarshal(data, &temp) if err != nil { return err } u.LastUpdated = temp.LastUpdated u.Combinations = temp.Combinations normalizeCombinations(u.Combinations) return nil } func (u *AggregatedUsage) Save() error { jsonData := u.ToJSON() data, err := json.MarshalIndent(jsonData, "", " ") if err != nil { return err } tmpFile := u.filePath + ".tmp" err = os.WriteFile(tmpFile, data, 0o644) if err != nil { return err } defer os.Remove(tmpFile) err = os.Rename(tmpFile, u.filePath) if err == nil { u.saveMutex.Lock() u.lastSaveTime = time.Now() u.saveMutex.Unlock() } return err } func (u *AggregatedUsage) AddUsage(model string, contextWindow int, inputTokens, outputTokens, cachedTokens int64, serviceTier string, user string) error { return u.AddUsageWithCycleHint(model, contextWindow, inputTokens, outputTokens, cachedTokens, serviceTier, user, time.Now(), nil) } func (u *AggregatedUsage) AddUsageWithCycleHint(model string, contextWindow int, inputTokens, outputTokens, cachedTokens int64, serviceTier string, user string, observedAt time.Time, cycleHint *WeeklyCycleHint) error { if model == "" { return E.New("model cannot be empty") } if contextWindow <= 0 { return E.New("contextWindow must be positive") } normalizedServiceTier := normalizeServiceTier(serviceTier) if observedAt.IsZero() { observedAt = time.Now() } u.mutex.Lock() defer u.mutex.Unlock() u.LastUpdated = observedAt weekStartUnix := deriveWeekStartUnix(cycleHint) addUsageToCombinations(&u.Combinations, model, normalizedServiceTier, contextWindow, weekStartUnix, user, inputTokens, outputTokens, cachedTokens) go u.scheduleSave() return nil } func (u *AggregatedUsage) scheduleSave() { const saveInterval = time.Minute u.saveMutex.Lock() defer u.saveMutex.Unlock() timeSinceLastSave := time.Since(u.lastSaveTime) if timeSinceLastSave >= saveInterval { go u.saveAsync() return } if u.pendingSave { return } u.pendingSave = true remainingTime := saveInterval - timeSinceLastSave u.saveTimer = time.AfterFunc(remainingTime, func() { u.saveMutex.Lock() u.pendingSave = false u.saveMutex.Unlock() u.saveAsync() }) } func (u *AggregatedUsage) saveAsync() { err := u.Save() if err != nil { if u.logger != nil { u.logger.Error("save usage statistics: ", err) } } } func (u *AggregatedUsage) cancelPendingSave() { u.saveMutex.Lock() defer u.saveMutex.Unlock() if u.saveTimer != nil { u.saveTimer.Stop() u.saveTimer = nil } u.pendingSave = false } ================================================ FILE: service/ocm/service_user.go ================================================ package ocm import ( "sync" "github.com/sagernet/sing-box/option" ) type UserManager struct { accessMutex sync.RWMutex tokenMap map[string]string } func (m *UserManager) UpdateUsers(users []option.OCMUser) { m.accessMutex.Lock() defer m.accessMutex.Unlock() tokenMap := make(map[string]string, len(users)) for _, user := range users { tokenMap[user.Token] = user.Name } m.tokenMap = tokenMap } func (m *UserManager) Authenticate(token string) (string, bool) { m.accessMutex.RLock() username, found := m.tokenMap[token] m.accessMutex.RUnlock() return username, found } ================================================ FILE: service/ocm/service_websocket.go ================================================ package ocm import ( "context" stdTLS "crypto/tls" "encoding/json" "io" "net" "net/http" "strings" "sync" "time" "github.com/sagernet/sing-box/adapter" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/ntp" "github.com/sagernet/ws" "github.com/sagernet/ws/wsutil" "github.com/openai/openai-go/v3/responses" ) type webSocketSession struct { clientConn net.Conn upstreamConn net.Conn closeOnce sync.Once } func (s *webSocketSession) Close() { s.closeOnce.Do(func() { s.clientConn.Close() s.upstreamConn.Close() }) } func buildUpstreamWebSocketURL(baseURL string, proxyPath string) string { upstreamURL := baseURL if strings.HasPrefix(upstreamURL, "https://") { upstreamURL = "wss://" + upstreamURL[len("https://"):] } else if strings.HasPrefix(upstreamURL, "http://") { upstreamURL = "ws://" + upstreamURL[len("http://"):] } return upstreamURL + proxyPath } func isForwardableResponseHeader(key string) bool { lowerKey := strings.ToLower(key) switch { case strings.HasPrefix(lowerKey, "x-codex-"): return true case strings.HasPrefix(lowerKey, "x-reasoning"): return true case lowerKey == "openai-model": return true case strings.Contains(lowerKey, "-secondary-"): return true default: return false } } func isForwardableWebSocketRequestHeader(key string) bool { if isHopByHopHeader(key) { return false } lowerKey := strings.ToLower(key) switch { case lowerKey == "authorization": return false case strings.HasPrefix(lowerKey, "sec-websocket-"): return false default: return true } } func (s *Service) handleWebSocket(w http.ResponseWriter, r *http.Request, proxyPath string, username string) { accessToken, err := s.getAccessToken() if err != nil { s.logger.Error("get access token for websocket: ", err) writeJSONError(w, r, http.StatusUnauthorized, "authentication_error", "authentication failed") return } upstreamURL := buildUpstreamWebSocketURL(s.getBaseURL(), proxyPath) if r.URL.RawQuery != "" { upstreamURL += "?" + r.URL.RawQuery } upstreamHeaders := make(http.Header) for key, values := range r.Header { if isForwardableWebSocketRequestHeader(key) { upstreamHeaders[key] = values } } for key, values := range s.httpHeaders { upstreamHeaders.Del(key) upstreamHeaders[key] = values } upstreamHeaders.Set("Authorization", "Bearer "+accessToken) if accountID := s.getAccountID(); accountID != "" { upstreamHeaders.Set("ChatGPT-Account-Id", accountID) } upstreamResponseHeaders := make(http.Header) upstreamDialer := ws.Dialer{ NetDial: func(ctx context.Context, network, addr string) (net.Conn, error) { return s.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) }, TLSConfig: &stdTLS.Config{ RootCAs: adapter.RootPoolFromContext(s.ctx), Time: ntp.TimeFuncFromContext(s.ctx), }, Header: ws.HandshakeHeaderHTTP(upstreamHeaders), OnHeader: func(key, value []byte) error { upstreamResponseHeaders.Add(string(key), string(value)) return nil }, } upstreamConn, upstreamBufferedReader, _, err := upstreamDialer.Dial(r.Context(), upstreamURL) if err != nil { s.logger.Error("dial upstream websocket: ", err) writeJSONError(w, r, http.StatusBadGateway, "api_error", "upstream websocket connection failed") return } weeklyCycleHint := extractWeeklyCycleHint(upstreamResponseHeaders) clientResponseHeaders := make(http.Header) for key, values := range upstreamResponseHeaders { if isForwardableResponseHeader(key) { clientResponseHeaders[key] = values } } clientUpgrader := ws.HTTPUpgrader{ Header: clientResponseHeaders, } if s.isShuttingDown() { upstreamConn.Close() writeJSONError(w, r, http.StatusServiceUnavailable, "api_error", "service is shutting down") return } clientConn, _, _, err := clientUpgrader.Upgrade(r, w) if err != nil { s.logger.Error("upgrade client websocket: ", err) upstreamConn.Close() return } session := &webSocketSession{ clientConn: clientConn, upstreamConn: upstreamConn, } if !s.registerWebSocketSession(session) { session.Close() return } defer s.unregisterWebSocketSession(session) var upstreamReadWriter io.ReadWriter if upstreamBufferedReader != nil { upstreamReadWriter = struct { io.Reader io.Writer }{upstreamBufferedReader, upstreamConn} } else { upstreamReadWriter = upstreamConn } modelChannel := make(chan string, 1) var waitGroup sync.WaitGroup waitGroup.Add(2) go func() { defer waitGroup.Done() defer session.Close() s.proxyWebSocketClientToUpstream(clientConn, upstreamConn, modelChannel) }() go func() { defer waitGroup.Done() defer session.Close() s.proxyWebSocketUpstreamToClient(upstreamReadWriter, clientConn, modelChannel, username, weeklyCycleHint) }() waitGroup.Wait() } func (s *Service) proxyWebSocketClientToUpstream(clientConn net.Conn, upstreamConn net.Conn, modelChannel chan<- string) { for { data, opCode, err := wsutil.ReadClientData(clientConn) if err != nil { if !E.IsClosedOrCanceled(err) { s.logger.Debug("read client websocket: ", err) } return } if opCode == ws.OpText && s.usageTracker != nil { var request struct { Type string `json:"type"` Model string `json:"model"` } if json.Unmarshal(data, &request) == nil && request.Type == "response.create" && request.Model != "" { select { case modelChannel <- request.Model: default: } } } err = wsutil.WriteClientMessage(upstreamConn, opCode, data) if err != nil { if !E.IsClosedOrCanceled(err) { s.logger.Debug("write upstream websocket: ", err) } return } } } func (s *Service) proxyWebSocketUpstreamToClient(upstreamReadWriter io.ReadWriter, clientConn net.Conn, modelChannel <-chan string, username string, weeklyCycleHint *WeeklyCycleHint) { var requestModel string for { data, opCode, err := wsutil.ReadServerData(upstreamReadWriter) if err != nil { if !E.IsClosedOrCanceled(err) { s.logger.Debug("read upstream websocket: ", err) } return } if opCode == ws.OpText && s.usageTracker != nil { select { case model := <-modelChannel: requestModel = model default: } var event struct { Type string `json:"type"` } if json.Unmarshal(data, &event) == nil && event.Type == "response.completed" { var streamEvent responses.ResponseStreamEventUnion if json.Unmarshal(data, &streamEvent) == nil { completedEvent := streamEvent.AsResponseCompleted() responseModel := string(completedEvent.Response.Model) serviceTier := string(completedEvent.Response.ServiceTier) inputTokens := completedEvent.Response.Usage.InputTokens outputTokens := completedEvent.Response.Usage.OutputTokens cachedTokens := completedEvent.Response.Usage.InputTokensDetails.CachedTokens if inputTokens > 0 || outputTokens > 0 { if responseModel == "" { responseModel = requestModel } if responseModel != "" { contextWindow := detectContextWindow(responseModel, serviceTier, inputTokens) s.usageTracker.AddUsageWithCycleHint( responseModel, contextWindow, inputTokens, outputTokens, cachedTokens, serviceTier, username, time.Now(), weeklyCycleHint, ) } } } } } err = wsutil.WriteServerMessage(clientConn, opCode, data) if err != nil { if !E.IsClosedOrCanceled(err) { s.logger.Debug("write client websocket: ", err) } return } } } ================================================ FILE: service/oomkiller/badcleanup.go ================================================ //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() } ================================================ FILE: service/oomkiller/badcleanup_stub.go ================================================ //go:build !badlinkname package oomkiller func badCleanup() { } ================================================ FILE: service/oomkiller/policy.go ================================================ 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 } ================================================ FILE: service/oomkiller/service.go ================================================ package oomkiller import ( "context" "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/service" ) type OOMReporter interface { WriteReport(memoryUsage uint64) error WriteDraft(memoryUsage uint64) error DiscardDraft() error } func RegisterService(registry *boxService.Registry) { boxService.Register[option.OOMKillerServiceOptions](registry, boxConstant.TypeOOMKiller, NewService) } type Service struct { boxService.Adapter ctx context.Context logger log.ContextLogger network adapter.NetworkManager timerConfig timerConfig adaptiveTimer *adaptiveTimer lastReportTime atomic.Int64 //nolint:unused // touched only on darwin && cgo via writeOOMDraft/discardOOMDraft. draftCancelled atomic.Bool } func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.OOMKillerServiceOptions) (adapter.Service, error) { memoryLimit, mode := resolvePolicyMode(ctx, options) config, err := buildTimerConfig(options, memoryLimit, mode, options.KillerDisabled) if err != nil { return nil, err } return &Service{ Adapter: boxService.NewAdapter(boxConstant.TypeOOMKiller, tag), ctx: ctx, logger: logger, network: service.FromContext[adapter.NetworkManager](ctx), timerConfig: config, }, nil } func (s *Service) createTimer() { s.adaptiveTimer = newAdaptiveTimer(s.logger, s.network, s.timerConfig, s.writeOOMReport) } func (s *Service) startTimer() { s.createTimer() s.adaptiveTimer.start() } func (s *Service) stopTimer() { if s.adaptiveTimer != nil { s.adaptiveTimer.stop() } } func (s *Service) writeOOMReport(memoryUsage uint64) { now := time.Now().Unix() lastReport := s.lastReportTime.Load() if now-lastReport < 3600 { return } if !s.lastReportTime.CompareAndSwap(lastReport, now) { return } reporter := service.FromContext[OOMReporter](s.ctx) if reporter == nil { return } err := reporter.WriteReport(memoryUsage) if err != nil { s.logger.Warn("failed to write OOM report: ", err) } else { s.logger.Info("OOM report saved") } } ================================================ FILE: service/oomkiller/service_darwin.go ================================================ //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" "github.com/sagernet/sing/service" ) 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() } s.discardOOMDraft() } 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.writeOOMDraft(sample.usage) s.adaptiveTimer.notifyPressure() } } 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") } } ================================================ FILE: service/oomkiller/service_stub.go ================================================ //go:build !darwin || !cgo package oomkiller import ( "github.com/sagernet/sing-box/adapter" E "github.com/sagernet/sing/common/exceptions" ) func (s *Service) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { 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() return nil } ================================================ FILE: service/oomkiller/timer.go ================================================ 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 network adapter.NetworkManager onTriggered func(uint64) limitThresholds pressureThresholds access sync.Mutex cleanupTriggered bool timer *time.Timer state pressureState currentInterval time.Duration forceMinInterval bool pendingPressureBaseline bool pressureBaseline memorySample pressureBaselineTime time.Time } func newAdaptiveTimer(logger log.ContextLogger, network adapter.NetworkManager, config timerConfig, onTriggered func(uint64)) *adaptiveTimer { t := &adaptiveTimer{ timerConfig: config, logger: logger, network: network, 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) 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.timerConfig.policyMode == policyModeNetworkExtension { if t.cleanupTriggered { runtimeDebug.FreeOSMemory() t.cleanupTriggered = true } } 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 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 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 } t.cleanupTriggered = false 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)) } else { t.logger.Error("memory growth rate critical, usage: ", byteformats.FormatMemoryBytes(sample.usage), t.logDetails(sample), ", resetting network") 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.network.ResetNetwork() } } badCleanup() 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 { 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.currentInterval } 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 } ================================================ FILE: service/oomkiller/timer_darwin.go ================================================ //go:build darwin && cgo package oomkiller func (t *adaptiveTimer) notifyPressure() { t.access.Lock() t.startLocked() t.forceMinInterval = true t.pendingPressureBaseline = true t.access.Unlock() t.poll() } ================================================ FILE: service/origin_ca/service.go ================================================ 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" 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" "github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/service" "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) httpClient, err := originCAHTTPClient(ctx, logger, options) if err != nil { cancel() return nil, err } 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: httpClient, storage: storage, storageIssuerKey: storageIssuerKey, storageNamesKey: storageNamesKey, storageLockKey: storageLockKey, apiToken: apiToken, originCAKey: originCAKey, domain: domain, requestType: requestType, requestedValidity: requestedValidity, }, 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 } 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 } 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 = max(refreshAt.Sub(s.timeFunc()), 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) } 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") } 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"` } ================================================ FILE: service/resolved/resolve1.go ================================================ //go:build linux package resolved import ( "context" "errors" "fmt" "net/netip" "os" "os/user" "path/filepath" "slices" "strconv" "strings" "syscall" "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/common" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" M "github.com/sagernet/sing/common/metadata" "github.com/godbus/dbus/v5" mDNS "github.com/miekg/dns" ) type resolve1Manager Service type Address struct { IfIndex int32 Family int32 Address []byte } type Name struct { IfIndex int32 Hostname string } type ResourceRecord struct { IfIndex int32 Type uint16 Class uint16 Data []byte } type SRVRecord struct { Priority uint16 Weight uint16 Port uint16 Hostname string Addresses []Address CNAME string } type TXTRecord []byte type LinkDNS struct { Family int32 Address []byte } type LinkDNSEx struct { Family int32 Address []byte Port uint16 Name string } type LinkDomain struct { Domain string RoutingOnly bool } func (t *resolve1Manager) getLink(ifIndex int32) (*TransportLink, *dbus.Error) { link, loaded := t.links[ifIndex] if !loaded { link = &TransportLink{} t.links[ifIndex] = link iif, err := t.network.InterfaceFinder().ByIndex(int(ifIndex)) if err != nil { return nil, wrapError(err) } link.iif = iif } return link, nil } func (t *resolve1Manager) getSenderProcess(sender dbus.Sender) (int32, error) { var senderPid int32 dbusObject := t.systemBus.Object("org.freedesktop.DBus", "/org/freedesktop/DBus") if dbusObject == nil { return 0, E.New("missing dbus object") } err := dbusObject.Call("org.freedesktop.DBus.GetConnectionUnixProcessID", 0, string(sender)).Store(&senderPid) if err != nil { return 0, E.Cause(err, "GetConnectionUnixProcessID") } return senderPid, nil } func (t *resolve1Manager) createMetadata(sender dbus.Sender) adapter.InboundContext { var metadata adapter.InboundContext metadata.Inbound = t.Tag() metadata.InboundType = C.TypeResolved senderPid, err := t.getSenderProcess(sender) if err != nil { return metadata } var processInfo adapter.ConnectionOwner metadata.ProcessInfo = &processInfo processInfo.ProcessID = uint32(senderPid) processPath, err := os.Readlink(F.ToString("/proc/", senderPid, "/exe")) if err == nil { processInfo.ProcessPath = processPath } else { processPath, err = os.Readlink(F.ToString("/proc/", senderPid, "/comm")) if err == nil { processInfo.ProcessPath = processPath } } var uidFound bool statusContent, err := os.ReadFile(F.ToString("/proc/", senderPid, "/status")) if err == nil { for line := range strings.SplitSeq(string(statusContent), "\n") { line = strings.TrimSpace(line) if strings.HasPrefix(line, "Uid:") { fields := strings.Fields(line) if len(fields) >= 2 { uid, parseErr := strconv.ParseUint(fields[1], 10, 32) if parseErr != nil { break } processInfo.UserId = int32(uid) uidFound = true if osUser, _ := user.LookupId(F.ToString(uid)); osUser != nil { processInfo.UserName = osUser.Username } break } } } } if !uidFound { metadata.ProcessInfo.UserId = -1 } return metadata } func (t *resolve1Manager) log(sender dbus.Sender, message ...any) { metadata := t.createMetadata(sender) if metadata.ProcessInfo != nil { var prefix string if metadata.ProcessInfo.ProcessPath != "" { prefix = filepath.Base(metadata.ProcessInfo.ProcessPath) } else if metadata.ProcessInfo.UserName != "" { prefix = F.ToString("user:", metadata.ProcessInfo.UserName) } else if metadata.ProcessInfo.UserId != 0 { prefix = F.ToString("uid:", metadata.ProcessInfo.UserId) } t.logger.Info("(", prefix, ") ", F.ToString(message...)) } else { t.logger.Info(F.ToString(message...)) } } func (t *resolve1Manager) logRequest(sender dbus.Sender, message ...any) context.Context { ctx := log.ContextWithNewID(t.ctx) metadata := t.createMetadata(sender) if metadata.ProcessInfo != nil { var prefix string if metadata.ProcessInfo.ProcessPath != "" { prefix = filepath.Base(metadata.ProcessInfo.ProcessPath) } else if metadata.ProcessInfo.UserName != "" { prefix = F.ToString("user:", metadata.ProcessInfo.UserName) } else if metadata.ProcessInfo.UserId != 0 { prefix = F.ToString("uid:", metadata.ProcessInfo.UserId) } t.logger.InfoContext(ctx, "(", prefix, ") ", strings.Join(F.MapToString(message), " ")) } else { t.logger.InfoContext(ctx, strings.Join(F.MapToString(message), " ")) } return adapter.WithContext(ctx, &metadata) } func familyToString(family int32) string { switch family { case syscall.AF_UNSPEC: return "AF_UNSPEC" case syscall.AF_INET: return "AF_INET" case syscall.AF_INET6: return "AF_INET6" default: return F.ToString(family) } } func (t *resolve1Manager) ResolveHostname(sender dbus.Sender, ifIndex int32, hostname string, family int32, flags uint64) (addresses []Address, canonical string, outflags uint64, err *dbus.Error) { t.linkAccess.Lock() link, err := t.getLink(ifIndex) if err != nil { return } t.linkAccess.Unlock() var strategy C.DomainStrategy switch family { case syscall.AF_UNSPEC: strategy = C.DomainStrategyAsIS case syscall.AF_INET: strategy = C.DomainStrategyIPv4Only case syscall.AF_INET6: strategy = C.DomainStrategyIPv6Only } ctx := t.logRequest(sender, "ResolveHostname ", link.iif.Name, " ", hostname, " ", familyToString(family), " ", flags) responseAddresses, lookupErr := t.dnsRouter.Lookup(ctx, hostname, adapter.DNSQueryOptions{ LookupStrategy: strategy, }) if lookupErr != nil { err = wrapError(err) return } addresses = common.Map(responseAddresses, func(it netip.Addr) Address { var addrFamily int32 if it.Is4() { addrFamily = syscall.AF_INET } else { addrFamily = syscall.AF_INET6 } return Address{ IfIndex: ifIndex, Family: addrFamily, Address: it.AsSlice(), } }) canonical = mDNS.CanonicalName(hostname) return } func (t *resolve1Manager) ResolveAddress(sender dbus.Sender, ifIndex int32, family int32, address []byte, flags uint64) (names []Name, outflags uint64, err *dbus.Error) { t.linkAccess.Lock() link, err := t.getLink(ifIndex) if err != nil { return } t.linkAccess.Unlock() addr, ok := netip.AddrFromSlice(address) if !ok { err = wrapError(E.New("invalid address")) return } var nibbles []string for _, v := range slices.Backward(address) { b := v nibbles = append(nibbles, fmt.Sprintf("%x", b&0x0F)) nibbles = append(nibbles, fmt.Sprintf("%x", b>>4)) } var ptrDomain string if addr.Is4() { ptrDomain = strings.Join(nibbles, ".") + ".in-addr.arpa." } else { ptrDomain = strings.Join(nibbles, ".") + ".ip6.arpa." } request := &mDNS.Msg{ MsgHdr: mDNS.MsgHdr{ RecursionDesired: true, }, Question: []mDNS.Question{ { Name: mDNS.Fqdn(ptrDomain), Qtype: mDNS.TypePTR, Qclass: mDNS.ClassINET, }, }, } ctx := t.logRequest(sender, "ResolveAddress ", link.iif.Name, familyToString(family), addr, flags) var metadata adapter.InboundContext metadata.InboundType = t.Type() metadata.Inbound = t.Tag() response, lookupErr := t.dnsRouter.Exchange(adapter.WithContext(ctx, &metadata), request, adapter.DNSQueryOptions{}) if lookupErr != nil { err = wrapError(err) return } if response.Rcode != mDNS.RcodeSuccess { err = rcodeError(response.Rcode) return } for _, rawRR := range response.Answer { switch rr := rawRR.(type) { case *mDNS.PTR: names = append(names, Name{ IfIndex: ifIndex, Hostname: rr.Ptr, }) } } return } func (t *resolve1Manager) ResolveRecord(sender dbus.Sender, ifIndex int32, hostname string, qClass uint16, qType uint16, flags uint64) (records []ResourceRecord, outflags uint64, err *dbus.Error) { t.linkAccess.Lock() link, err := t.getLink(ifIndex) if err != nil { return } t.linkAccess.Unlock() request := &mDNS.Msg{ MsgHdr: mDNS.MsgHdr{ RecursionDesired: true, }, Question: []mDNS.Question{ { Name: mDNS.Fqdn(hostname), Qtype: qType, Qclass: qClass, }, }, } ctx := t.logRequest(sender, "ResolveRecord", link.iif.Name, hostname, mDNS.Class(qClass), mDNS.Type(qType), flags) var metadata adapter.InboundContext metadata.InboundType = t.Type() metadata.Inbound = t.Tag() response, exchangeErr := t.dnsRouter.Exchange(adapter.WithContext(ctx, &metadata), request, adapter.DNSQueryOptions{}) if exchangeErr != nil { err = wrapError(exchangeErr) return } if response.Rcode != mDNS.RcodeSuccess { err = rcodeError(response.Rcode) return } for _, rr := range response.Answer { var record ResourceRecord record.IfIndex = ifIndex record.Type = rr.Header().Rrtype record.Class = rr.Header().Class data := make([]byte, mDNS.Len(rr)) _, unpackErr := mDNS.PackRR(rr, data, 0, nil, false) if unpackErr != nil { err = wrapError(unpackErr) } record.Data = data records = append(records, record) } return } func (t *resolve1Manager) ResolveService(sender dbus.Sender, ifIndex int32, hostname string, sType string, domain string, family int32, flags uint64) (srvData []SRVRecord, txtData []TXTRecord, canonicalName string, canonicalType string, canonicalDomain string, outflags uint64, err *dbus.Error) { t.linkAccess.Lock() link, err := t.getLink(ifIndex) if err != nil { return } t.linkAccess.Unlock() serviceName := hostname if hostname != "" && !strings.HasSuffix(hostname, ".") { serviceName += "." } serviceName += sType if !strings.HasSuffix(serviceName, ".") { serviceName += "." } serviceName += domain if !strings.HasSuffix(serviceName, ".") { serviceName += "." } ctx := t.logRequest(sender, "ResolveService ", link.iif.Name, " ", hostname, " ", sType, " ", domain, " ", familyToString(family), " ", flags) srvRequest := &mDNS.Msg{ MsgHdr: mDNS.MsgHdr{ RecursionDesired: true, }, Question: []mDNS.Question{ { Name: serviceName, Qtype: mDNS.TypeSRV, Qclass: mDNS.ClassINET, }, }, } var metadata adapter.InboundContext metadata.InboundType = t.Type() metadata.Inbound = t.Tag() srvResponse, exchangeErr := t.dnsRouter.Exchange(adapter.WithContext(ctx, &metadata), srvRequest, adapter.DNSQueryOptions{}) if exchangeErr != nil { err = wrapError(exchangeErr) return } if srvResponse.Rcode != mDNS.RcodeSuccess { err = rcodeError(srvResponse.Rcode) return } txtRequest := &mDNS.Msg{ MsgHdr: mDNS.MsgHdr{ RecursionDesired: true, }, Question: []mDNS.Question{ { Name: serviceName, Qtype: mDNS.TypeTXT, Qclass: mDNS.ClassINET, }, }, } txtResponse, exchangeErr := t.dnsRouter.Exchange(ctx, txtRequest, adapter.DNSQueryOptions{}) if exchangeErr != nil { err = wrapError(exchangeErr) return } for _, rawRR := range srvResponse.Answer { switch rr := rawRR.(type) { case *mDNS.SRV: var srvRecord SRVRecord srvRecord.Priority = rr.Priority srvRecord.Weight = rr.Weight srvRecord.Port = rr.Port srvRecord.Hostname = rr.Target var strategy C.DomainStrategy switch family { case syscall.AF_UNSPEC: strategy = C.DomainStrategyAsIS case syscall.AF_INET: strategy = C.DomainStrategyIPv4Only case syscall.AF_INET6: strategy = C.DomainStrategyIPv6Only } addrs, lookupErr := t.dnsRouter.Lookup(ctx, rr.Target, adapter.DNSQueryOptions{ LookupStrategy: strategy, }) if lookupErr == nil { srvRecord.Addresses = common.Map(addrs, func(it netip.Addr) Address { var addrFamily int32 if it.Is4() { addrFamily = syscall.AF_INET } else { addrFamily = syscall.AF_INET6 } return Address{ IfIndex: ifIndex, Family: addrFamily, Address: it.AsSlice(), } }) } for _, a := range srvResponse.Answer { if cname, ok := a.(*mDNS.CNAME); ok && cname.Header().Name == rr.Target { srvRecord.CNAME = cname.Target break } } srvData = append(srvData, srvRecord) } } for _, rawRR := range txtResponse.Answer { switch rr := rawRR.(type) { case *mDNS.TXT: data := make([]byte, mDNS.Len(rr)) _, packErr := mDNS.PackRR(rr, data, 0, nil, false) if packErr == nil { txtData = append(txtData, data) } } } canonicalName = mDNS.CanonicalName(hostname) canonicalType = mDNS.CanonicalName(sType) canonicalDomain = mDNS.CanonicalName(domain) return } func (t *resolve1Manager) SetLinkDNS(sender dbus.Sender, ifIndex int32, addresses []LinkDNS) *dbus.Error { t.linkAccess.Lock() defer t.linkAccess.Unlock() link, err := t.getLink(ifIndex) if err != nil { return wrapError(err) } link.address = addresses if len(addresses) > 0 { t.log(sender, "SetLinkDNS ", link.iif.Name, " ", strings.Join(common.Map(addresses, func(it LinkDNS) string { return M.AddrFromIP(it.Address).String() }), ", ")) } else { t.log(sender, "SetLinkDNS ", link.iif.Name, " (empty)") } return t.postUpdate(link) } func (t *resolve1Manager) SetLinkDNSEx(sender dbus.Sender, ifIndex int32, addresses []LinkDNSEx) *dbus.Error { t.linkAccess.Lock() defer t.linkAccess.Unlock() link, err := t.getLink(ifIndex) if err != nil { return wrapError(err) } link.addressEx = addresses if len(addresses) > 0 { t.log(sender, "SetLinkDNSEx ", link.iif.Name, " ", strings.Join(common.Map(addresses, func(it LinkDNSEx) string { return M.SocksaddrFrom(M.AddrFromIP(it.Address), it.Port).String() }), ", ")) } else { t.log(sender, "SetLinkDNSEx ", link.iif.Name, " (empty)") } return t.postUpdate(link) } func (t *resolve1Manager) SetLinkDomains(sender dbus.Sender, ifIndex int32, domains []LinkDomain) *dbus.Error { t.linkAccess.Lock() defer t.linkAccess.Unlock() link, err := t.getLink(ifIndex) if err != nil { return wrapError(err) } link.domain = domains if len(domains) > 0 { t.log(sender, "SetLinkDomains ", link.iif.Name, " ", strings.Join(common.Map(domains, func(domain LinkDomain) string { if !domain.RoutingOnly { return domain.Domain } else { return "~" + domain.Domain } }), ", ")) } else { t.log(sender, "SetLinkDomains ", link.iif.Name, " (empty)") } return t.postUpdate(link) } func (t *resolve1Manager) SetLinkDefaultRoute(sender dbus.Sender, ifIndex int32, defaultRoute bool) *dbus.Error { t.linkAccess.Lock() defer t.linkAccess.Unlock() link, err := t.getLink(ifIndex) if err != nil { return err } link.defaultRoute = defaultRoute if defaultRoute { t.defaultRouteSequence = append(common.Filter(t.defaultRouteSequence, func(it int32) bool { return it != ifIndex }), ifIndex) } else { t.defaultRouteSequence = common.Filter(t.defaultRouteSequence, func(it int32) bool { return it != ifIndex }) } var defaultRouteString string if defaultRoute { defaultRouteString = "yes" } else { defaultRouteString = "no" } t.log(sender, "SetLinkDefaultRoute ", link.iif.Name, " ", defaultRouteString) return t.postUpdate(link) } func (t *resolve1Manager) SetLinkLLMNR(ifIndex int32, llmnrMode string) *dbus.Error { return nil } func (t *resolve1Manager) SetLinkMulticastDNS(ifIndex int32, mdnsMode string) *dbus.Error { return nil } func (t *resolve1Manager) SetLinkDNSOverTLS(sender dbus.Sender, ifIndex int32, dotMode string) *dbus.Error { t.linkAccess.Lock() defer t.linkAccess.Unlock() link, err := t.getLink(ifIndex) if err != nil { return wrapError(err) } switch dotMode { case "yes": link.dnsOverTLS = true case "": dotMode = "no" fallthrough case "opportunistic", "no": link.dnsOverTLS = false } t.log(sender, "SetLinkDNSOverTLS ", link.iif.Name, " ", dotMode) return t.postUpdate(link) } func (t *resolve1Manager) SetLinkDNSSEC(ifIndex int32, dnssecMode string) *dbus.Error { return nil } func (t *resolve1Manager) SetLinkDNSSECNegativeTrustAnchors(ifIndex int32, domains []string) *dbus.Error { return nil } func (t *resolve1Manager) RevertLink(sender dbus.Sender, ifIndex int32) *dbus.Error { t.linkAccess.Lock() defer t.linkAccess.Unlock() link, err := t.getLink(ifIndex) if err != nil { return wrapError(err) } delete(t.links, ifIndex) t.log(sender, "RevertLink ", link.iif.Name) return t.postUpdate(link) } // TODO: implement RegisterService, UnregisterService func (t *resolve1Manager) RegisterService(sender dbus.Sender, identifier string, nameTemplate string, serviceType string, port uint16, priority uint16, weight uint16, txtRecords []TXTRecord) (objectPath dbus.ObjectPath, dbusErr *dbus.Error) { return "", wrapError(E.New("not implemented")) } func (t *resolve1Manager) UnregisterService(sender dbus.Sender, servicePath dbus.ObjectPath) error { return wrapError(E.New("not implemented")) } func (t *resolve1Manager) ResetStatistics() *dbus.Error { return nil } func (t *resolve1Manager) FlushCaches(sender dbus.Sender) *dbus.Error { t.dnsRouter.ClearCache() t.log(sender, "FlushCaches") return nil } func (t *resolve1Manager) ResetServerFeatures() *dbus.Error { return nil } func (t *resolve1Manager) postUpdate(link *TransportLink) *dbus.Error { if t.updateCallback != nil { return wrapError(t.updateCallback(link)) } return nil } func rcodeError(rcode int) *dbus.Error { return dbus.NewError("org.freedesktop.resolve1.DnsError."+mDNS.RcodeToString[rcode], []any{mDNS.RcodeToString[rcode]}) } func wrapError(err error) *dbus.Error { if err == nil { return nil } var rcode dns.RcodeError if errors.As(err, &rcode) { return rcodeError(int(rcode)) } return dbus.MakeFailedError(err) } ================================================ FILE: service/resolved/service.go ================================================ //go:build linux package resolved import ( "context" "net" "strings" "sync" "time" "github.com/sagernet/sing-box/adapter" boxService "github.com/sagernet/sing-box/adapter/service" "github.com/sagernet/sing-box/common/listener" 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" dnsOutbound "github.com/sagernet/sing-box/protocol/dns" tun "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/control" 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/x/list" "github.com/sagernet/sing/service" "github.com/godbus/dbus/v5" mDNS "github.com/miekg/dns" ) func RegisterService(registry *boxService.Registry) { boxService.Register[option.ResolvedServiceOptions](registry, C.TypeResolved, NewService) } type Service struct { boxService.Adapter ctx context.Context logger log.ContextLogger network adapter.NetworkManager dnsRouter adapter.DNSRouter listener *listener.Listener systemBus *dbus.Conn linkAccess sync.RWMutex links map[int32]*TransportLink defaultRouteSequence []int32 networkUpdateCallback *list.Element[tun.NetworkUpdateCallback] updateCallback func(*TransportLink) error deleteCallback func(*TransportLink) } type TransportLink struct { iif *control.Interface address []LinkDNS addressEx []LinkDNSEx domain []LinkDomain defaultRoute bool dnsOverTLS bool // dnsOverTLSFallback bool } func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.ResolvedServiceOptions) (adapter.Service, error) { inbound := &Service{ Adapter: boxService.NewAdapter(C.TypeResolved, tag), ctx: ctx, logger: logger, network: service.FromContext[adapter.NetworkManager](ctx), dnsRouter: service.FromContext[adapter.DNSRouter](ctx), links: make(map[int32]*TransportLink), } inbound.listener = listener.New(listener.Options{ Context: ctx, Logger: logger, Network: []string{N.NetworkTCP, N.NetworkUDP}, Listen: options.ListenOptions, ConnectionHandler: inbound, OOBPacketHandler: inbound, ThreadUnsafePacketWriter: true, }) return inbound, nil } func (i *Service) Start(stage adapter.StartStage) error { switch stage { case adapter.StartStateInitialize: inboundManager := service.FromContext[adapter.ServiceManager](i.ctx) for _, transport := range inboundManager.Services() { if transport.Type() == C.TypeResolved && transport != i { return E.New("multiple resolved service are not supported") } } systemBus, err := dbus.SystemBus() if err != nil { return err } i.systemBus = systemBus err = systemBus.Export((*resolve1Manager)(i), "/org/freedesktop/resolve1", "org.freedesktop.resolve1.Manager") if err != nil { return err } reply, err := systemBus.RequestName("org.freedesktop.resolve1", dbus.NameFlagDoNotQueue) if err != nil { return err } switch reply { case dbus.RequestNameReplyPrimaryOwner: case dbus.RequestNameReplyExists: return E.New("D-Bus object already exists, maybe real resolved is running") default: return E.New("unknown request name reply: ", reply) } i.networkUpdateCallback = i.network.NetworkMonitor().RegisterCallback(i.onNetworkUpdate) case adapter.StartStateStart: err := i.listener.Start() if err != nil { return err } } return nil } func (i *Service) Close() error { if i.networkUpdateCallback != nil { i.network.NetworkMonitor().UnregisterCallback(i.networkUpdateCallback) } if i.systemBus != nil { i.systemBus.ReleaseName("org.freedesktop.resolve1") i.systemBus.Close() } return i.listener.Close() } 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{} for { conn.SetReadDeadline(time.Now().Add(C.DNSTimeout)) err := dnsOutbound.HandleStreamDNSRequest(ctx, i.dnsRouter, conn, metadata) if err != nil { N.CloseOnHandshakeFailure(conn, onClose, err) return } } } func (i *Service) NewPacket(buffer *buf.Buffer, oob []byte, source M.Socksaddr) { go i.exchangePacket(buffer, oob, source) } func (i *Service) exchangePacket(buffer *buf.Buffer, oob []byte, source M.Socksaddr) { ctx := log.ContextWithNewID(i.ctx) err := i.exchangePacket0(ctx, buffer, oob, source) if err != nil { i.logger.ErrorContext(ctx, "process DNS packet: ", err) } } func (i *Service) exchangePacket0(ctx context.Context, buffer *buf.Buffer, oob []byte, source M.Socksaddr) error { var message mDNS.Msg err := message.Unpack(buffer.Bytes()) buffer.Release() if err != nil { return E.Cause(err, "unpack request") } var metadata adapter.InboundContext metadata.Source = source metadata.InboundType = i.Type() metadata.Inbound = i.Tag() response, err := i.dnsRouter.Exchange(adapter.WithContext(ctx, &metadata), &message, adapter.DNSQueryOptions{}) if err != nil { return err } responseBuffer, err := dns.TruncateDNSMessage(&message, response, 0) if err != nil { return err } defer responseBuffer.Release() _, _, err = i.listener.UDPConn().WriteMsgUDPAddrPort(responseBuffer.Bytes(), oob, source.AddrPort()) return err } func (i *Service) onNetworkUpdate() { i.linkAccess.Lock() defer i.linkAccess.Unlock() var deleteIfIndex []int for ifIndex, link := range i.links { iif, err := i.network.InterfaceFinder().ByIndex(int(ifIndex)) if err != nil || iif != link.iif { deleteIfIndex = append(deleteIfIndex, int(ifIndex)) } i.defaultRouteSequence = common.Filter(i.defaultRouteSequence, func(it int32) bool { return it != ifIndex }) if i.deleteCallback != nil { i.deleteCallback(link) } } for _, ifIndex := range deleteIfIndex { delete(i.links, int32(ifIndex)) } } func (conf *TransportLink) nameList(ndots int, name string) []string { search := common.Map(common.Filter(conf.domain, func(it LinkDomain) bool { return !it.RoutingOnly }), func(it LinkDomain) string { return it.Domain }) l := len(name) rooted := l > 0 && name[l-1] == '.' if l > 254 || l == 254 && !rooted { return nil } if rooted { if avoidDNS(name) { return nil } return []string{name} } hasNdots := strings.Count(name, ".") >= ndots name += "." // l++ names := make([]string, 0, 1+len(search)) if hasNdots && !avoidDNS(name) { names = append(names, name) } for _, suffix := range search { fqdn := name + suffix if !avoidDNS(fqdn) && len(fqdn) <= 254 { names = append(names, fqdn) } } if !hasNdots && !avoidDNS(name) { names = append(names, name) } return names } func avoidDNS(name string) bool { if name == "" { return true } if name[len(name)-1] == '.' { name = name[:len(name)-1] } return strings.HasSuffix(name, ".onion") } ================================================ FILE: service/resolved/stub.go ================================================ //go:build !linux package resolved import ( "context" "github.com/sagernet/sing-box/adapter" boxService "github.com/sagernet/sing-box/adapter/service" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" ) func RegisterService(registry *boxService.Registry) { boxService.Register[option.ResolvedServiceOptions](registry, C.TypeResolved, func(ctx context.Context, logger log.ContextLogger, tag string, options option.ResolvedServiceOptions) (adapter.Service, error) { return nil, E.New("resolved service is only supported on Linux") }) } func RegisterTransport(registry *dns.TransportRegistry) { dns.RegisterTransport[option.ResolvedDNSServerOptions](registry, C.TypeResolved, func(ctx context.Context, logger log.ContextLogger, tag string, options option.ResolvedDNSServerOptions) (adapter.DNSTransport, error) { return nil, E.New("resolved DNS server is only supported on Linux") }) } ================================================ FILE: service/resolved/transport.go ================================================ //go:build linux package resolved import ( "context" "net/netip" "os" "strings" "sync" "sync/atomic" "time" "github.com/sagernet/sing-box/adapter" "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/dns" "github.com/sagernet/sing-box/dns/transport" "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" "github.com/sagernet/sing/service" mDNS "github.com/miekg/dns" ) func RegisterTransport(registry *dns.TransportRegistry) { dns.RegisterTransport[option.ResolvedDNSServerOptions](registry, C.TypeResolved, NewTransport) } var ( _ adapter.DNSTransport = (*Transport)(nil) _ adapter.DNSTransportWithPreferredDomain = (*Transport)(nil) ) type Transport struct { dns.TransportAdapter ctx context.Context logger logger.ContextLogger serviceTag string acceptDefaultResolvers bool ndots int timeout time.Duration attempts int rotate bool service *Service linkAccess sync.RWMutex linkServers map[*TransportLink]*LinkServers } type LinkServers struct { Link *TransportLink Servers []adapter.DNSTransport serverOffset uint32 } func (c *LinkServers) ServerOffset(rotate bool) uint32 { if rotate { return atomic.AddUint32(&c.serverOffset, 1) - 1 } return 0 } func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.ResolvedDNSServerOptions) (adapter.DNSTransport, error) { return &Transport{ TransportAdapter: dns.NewTransportAdapter(C.DNSTypeDHCP, tag, nil), ctx: ctx, logger: logger, serviceTag: options.Service, acceptDefaultResolvers: options.AcceptDefaultResolvers, // ndots: options.NDots, // timeout: time.Duration(options.Timeout), // attempts: options.Attempts, // rotate: options.Rotate, ndots: 1, timeout: 5 * time.Second, attempts: 2, linkServers: make(map[*TransportLink]*LinkServers), }, nil } func (t *Transport) Start(stage adapter.StartStage) error { if stage != adapter.StartStateInitialize { return nil } serviceManager := service.FromContext[adapter.ServiceManager](t.ctx) service, loaded := serviceManager.Get(t.serviceTag) if !loaded { return E.New("service not found: ", t.serviceTag) } resolvedInbound, isResolved := service.(*Service) if !isResolved { return E.New("service is not resolved: ", t.serviceTag) } resolvedInbound.updateCallback = t.updateTransports resolvedInbound.deleteCallback = t.deleteTransport t.service = resolvedInbound return nil } func (t *Transport) Close() error { t.linkAccess.RLock() defer t.linkAccess.RUnlock() for _, servers := range t.linkServers { for _, server := range servers.Servers { server.Close() } } return nil } func (t *Transport) Reset() { t.linkAccess.RLock() defer t.linkAccess.RUnlock() for _, servers := range t.linkServers { for _, server := range servers.Servers { server.Reset() } } } func (t *Transport) updateTransports(link *TransportLink) error { t.linkAccess.Lock() defer t.linkAccess.Unlock() if servers, loaded := t.linkServers[link]; loaded { for _, server := range servers.Servers { server.Close() } } serverDialer := common.Must1(dialer.NewDefault(t.ctx, option.DialerOptions{ BindInterface: link.iif.Name, UDPFragmentDefault: true, })) var transports []adapter.DNSTransport for _, address := range link.address { serverAddr, ok := netip.AddrFromSlice(address.Address) if !ok { return os.ErrInvalid } if link.dnsOverTLS { tlsConfig := common.Must1(tls.NewClient(t.ctx, t.logger, serverAddr.String(), option.OutboundTLSOptions{ Enabled: true, ServerName: serverAddr.String(), })) transports = append(transports, transport.NewTLSRaw(t.logger, t.TransportAdapter, serverDialer, M.SocksaddrFrom(serverAddr, 53), tlsConfig)) } else { transports = append(transports, transport.NewUDPRaw(t.logger, t.TransportAdapter, serverDialer, M.SocksaddrFrom(serverAddr, 53))) } } for _, address := range link.addressEx { serverAddr, ok := netip.AddrFromSlice(address.Address) if !ok { return os.ErrInvalid } if link.dnsOverTLS { var serverName string if address.Name != "" { serverName = address.Name } else { serverName = serverAddr.String() } tlsConfig := common.Must1(tls.NewClient(t.ctx, t.logger, serverAddr.String(), option.OutboundTLSOptions{ Enabled: true, ServerName: serverName, })) transports = append(transports, transport.NewTLSRaw(t.logger, t.TransportAdapter, serverDialer, M.SocksaddrFrom(serverAddr, address.Port), tlsConfig)) } else { transports = append(transports, transport.NewUDPRaw(t.logger, t.TransportAdapter, serverDialer, M.SocksaddrFrom(serverAddr, address.Port))) } } t.linkServers[link] = &LinkServers{ Link: link, Servers: transports, } return nil } func (t *Transport) deleteTransport(link *TransportLink) { t.linkAccess.Lock() defer t.linkAccess.Unlock() servers, loaded := t.linkServers[link] if !loaded { return } for _, server := range servers.Servers { server.Close() } 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 t.service.linkAccess.RLock() for _, link := range t.service.links { for _, domain := range link.domain { if domain.Domain == "." && domain.RoutingOnly && !t.acceptDefaultResolvers { continue } if strings.HasSuffix(question.Name, domain.Domain) { selectedLink = link } } } if selectedLink == nil && t.acceptDefaultResolvers { for l := len(t.service.defaultRouteSequence); l > 0; l-- { selectedLink = t.service.links[t.service.defaultRouteSequence[l-1]] if len(selectedLink.address) > 0 || len(selectedLink.addressEx) > 0 { break } } } t.service.linkAccess.RUnlock() if selectedLink == nil { return dns.FixedResponseStatus(message, mDNS.RcodeNameError), nil } t.linkAccess.RLock() servers := t.linkServers[selectedLink] t.linkAccess.RUnlock() if len(servers.Servers) == 0 { return dns.FixedResponseStatus(message, mDNS.RcodeNameError), nil } if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA { return t.exchangeParallel(ctx, servers, message) } else { return t.exchangeSingleRequest(ctx, servers, message) } } func (t *Transport) exchangeSingleRequest(ctx context.Context, servers *LinkServers, message *mDNS.Msg) (*mDNS.Msg, error) { var lastErr error for _, fqdn := range servers.Link.nameList(t.ndots, message.Question[0].Name) { response, err := t.tryOneName(ctx, servers, message, fqdn) if err != nil { lastErr = err continue } return response, nil } return nil, lastErr } func (t *Transport) tryOneName(ctx context.Context, servers *LinkServers, message *mDNS.Msg, fqdn string) (*mDNS.Msg, error) { serverOffset := servers.ServerOffset(t.rotate) sLen := uint32(len(servers.Servers)) var lastErr error for i := 0; i < t.attempts; i++ { for j := range sLen { server := servers.Servers[(serverOffset+j)%sLen] question := message.Question[0] question.Name = fqdn exchangeMessage := *message exchangeMessage.Question = []mDNS.Question{question} exchangeCtx, cancel := context.WithTimeout(ctx, t.timeout) response, err := server.Exchange(exchangeCtx, &exchangeMessage) cancel() if err != nil { lastErr = err continue } return response, nil } } return nil, E.Cause(lastErr, fqdn) } func (t *Transport) exchangeParallel(ctx context.Context, servers *LinkServers, message *mDNS.Msg) (*mDNS.Msg, error) { returned := make(chan struct{}) defer close(returned) type queryResult struct { response *mDNS.Msg err error } results := make(chan queryResult) startRacer := func(ctx context.Context, fqdn string) { response, err := t.tryOneName(ctx, servers, message, fqdn) select { case results <- queryResult{response, err}: case <-returned: } } queryCtx, queryCancel := context.WithCancel(ctx) defer queryCancel() var nameCount int for _, fqdn := range servers.Link.nameList(t.ndots, message.Question[0].Name) { nameCount++ go startRacer(queryCtx, fqdn) } var errors []error for { select { case <-ctx.Done(): return nil, ctx.Err() case result := <-results: if result.err == nil { return result.response, nil } errors = append(errors, result.err) if len(errors) == nameCount { return nil, E.Errors(errors...) } } } } ================================================ FILE: service/ssmapi/api.go ================================================ package ssmapi import ( "net/http" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common/logger" sHTTP "github.com/sagernet/sing/protocol/http" "github.com/go-chi/chi/v5" "github.com/go-chi/render" ) type APIServer struct { logger logger.Logger traffic *TrafficManager user *UserManager } func NewAPIServer(logger logger.Logger, traffic *TrafficManager, user *UserManager) *APIServer { return &APIServer{ logger: logger, traffic: traffic, user: user, } } func (s *APIServer) Route(r chi.Router) { r.Route("/server/v1", func(r chi.Router) { r.Use(func(handler http.Handler) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { s.logger.Debug(request.Method, " ", request.RequestURI, " ", sHTTP.SourceAddress(request)) handler.ServeHTTP(writer, request) }) }) r.Get("/", s.getServerInfo) r.Get("/users", s.listUser) r.Post("/users", s.addUser) r.Get("/users/{username}", s.getUser) r.Put("/users/{username}", s.updateUser) r.Delete("/users/{username}", s.deleteUser) r.Get("/stats", s.getStats) }) } func (s *APIServer) getServerInfo(writer http.ResponseWriter, request *http.Request) { render.JSON(writer, request, render.M{ "server": "sing-box " + C.Version, "apiVersion": "v1", }) } type UserObject struct { UserName string `json:"username"` Password string `json:"uPSK,omitempty"` DownlinkBytes int64 `json:"downlinkBytes"` UplinkBytes int64 `json:"uplinkBytes"` DownlinkPackets int64 `json:"downlinkPackets"` UplinkPackets int64 `json:"uplinkPackets"` TCPSessions int64 `json:"tcpSessions"` UDPSessions int64 `json:"udpSessions"` } func (s *APIServer) listUser(writer http.ResponseWriter, request *http.Request) { render.JSON(writer, request, render.M{ "users": s.user.List(), }) } func (s *APIServer) addUser(writer http.ResponseWriter, request *http.Request) { var addRequest struct { UserName string `json:"username"` Password string `json:"uPSK"` } err := render.DecodeJSON(request.Body, &addRequest) if err != nil { render.Status(request, http.StatusBadRequest) render.PlainText(writer, request, err.Error()) return } err = s.user.Add(addRequest.UserName, addRequest.Password) if err != nil { render.Status(request, http.StatusBadRequest) render.PlainText(writer, request, err.Error()) return } writer.WriteHeader(http.StatusCreated) } func (s *APIServer) getUser(writer http.ResponseWriter, request *http.Request) { userName := chi.URLParam(request, "username") if userName == "" { writer.WriteHeader(http.StatusBadRequest) return } uPSK, loaded := s.user.Get(userName) if !loaded { writer.WriteHeader(http.StatusNotFound) return } user := UserObject{ UserName: userName, Password: uPSK, } s.traffic.ReadUser(&user) render.JSON(writer, request, user) } func (s *APIServer) updateUser(writer http.ResponseWriter, request *http.Request) { userName := chi.URLParam(request, "username") if userName == "" { writer.WriteHeader(http.StatusBadRequest) return } var updateRequest struct { Password string `json:"uPSK"` } err := render.DecodeJSON(request.Body, &updateRequest) if err != nil { render.Status(request, http.StatusBadRequest) render.PlainText(writer, request, err.Error()) return } _, loaded := s.user.Get(userName) if !loaded { writer.WriteHeader(http.StatusNotFound) return } err = s.user.Update(userName, updateRequest.Password) if err != nil { render.Status(request, http.StatusBadRequest) render.PlainText(writer, request, err.Error()) return } writer.WriteHeader(http.StatusNoContent) } func (s *APIServer) deleteUser(writer http.ResponseWriter, request *http.Request) { userName := chi.URLParam(request, "username") if userName == "" { writer.WriteHeader(http.StatusBadRequest) return } _, loaded := s.user.Get(userName) if !loaded { writer.WriteHeader(http.StatusNotFound) return } err := s.user.Delete(userName) if err != nil { render.Status(request, http.StatusBadRequest) render.PlainText(writer, request, err.Error()) return } writer.WriteHeader(http.StatusNoContent) } func (s *APIServer) getStats(writer http.ResponseWriter, request *http.Request) { requireClear := request.URL.Query().Get("clear") == "true" users := s.user.List() s.traffic.ReadUsers(users, requireClear) for i := range users { users[i].Password = "" } uplinkBytes, downlinkBytes, uplinkPackets, downlinkPackets, tcpSessions, udpSessions := s.traffic.ReadGlobal(requireClear) render.JSON(writer, request, render.M{ "uplinkBytes": uplinkBytes, "downlinkBytes": downlinkBytes, "uplinkPackets": uplinkPackets, "downlinkPackets": downlinkPackets, "tcpSessions": tcpSessions, "udpSessions": udpSessions, "users": users, }) } ================================================ FILE: service/ssmapi/cache.go ================================================ package ssmapi import ( "bytes" "os" "path/filepath" "sort" "sync/atomic" "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json/badjson" "github.com/sagernet/sing/service/filemanager" ) type Cache struct { Endpoints *badjson.TypedMap[string, *EndpointCache] `json:"endpoints"` } type EndpointCache struct { GlobalUplink int64 `json:"global_uplink"` GlobalDownlink int64 `json:"global_downlink"` GlobalUplinkPackets int64 `json:"global_uplink_packets"` GlobalDownlinkPackets int64 `json:"global_downlink_packets"` GlobalTCPSessions int64 `json:"global_tcp_sessions"` GlobalUDPSessions int64 `json:"global_udp_sessions"` UserUplink *badjson.TypedMap[string, int64] `json:"user_uplink"` UserDownlink *badjson.TypedMap[string, int64] `json:"user_downlink"` UserUplinkPackets *badjson.TypedMap[string, int64] `json:"user_uplink_packets"` UserDownlinkPackets *badjson.TypedMap[string, int64] `json:"user_downlink_packets"` UserTCPSessions *badjson.TypedMap[string, int64] `json:"user_tcp_sessions"` UserUDPSessions *badjson.TypedMap[string, int64] `json:"user_udp_sessions"` Users *badjson.TypedMap[string, string] `json:"users"` } func (s *Service) loadCache() error { if s.cachePath == "" { return nil } basePath := filemanager.BasePath(s.ctx, s.cachePath) cacheBinary, err := os.ReadFile(basePath) if err != nil { if os.IsNotExist(err) { return nil } return err } err = s.decodeCache(cacheBinary) if err != nil { os.RemoveAll(basePath) return err } s.cacheMutex.Lock() s.lastSavedCache = cacheBinary s.cacheMutex.Unlock() return nil } func (s *Service) saveCache() error { if s.cachePath == "" { return nil } cacheBinary, err := s.encodeCache() if err != nil { return err } s.cacheMutex.Lock() defer s.cacheMutex.Unlock() if bytes.Equal(s.lastSavedCache, cacheBinary) { return nil } return s.writeCache(cacheBinary) } func (s *Service) writeCache(cacheBinary []byte) error { basePath := filemanager.BasePath(s.ctx, s.cachePath) err := os.MkdirAll(filepath.Dir(basePath), 0o777) if err != nil { return err } err = os.WriteFile(basePath, cacheBinary, 0o644) if err != nil { return err } s.lastSavedCache = cacheBinary return nil } func (s *Service) decodeCache(cacheBinary []byte) error { if len(cacheBinary) == 0 { return nil } cache, err := json.UnmarshalExtended[*Cache](cacheBinary) if err != nil { return err } if cache.Endpoints == nil || cache.Endpoints.Size() == 0 { return nil } for _, entry := range cache.Endpoints.Entries() { trafficManager, loaded := s.traffics[entry.Key] if !loaded { continue } trafficManager.globalUplink.Store(entry.Value.GlobalUplink) trafficManager.globalDownlink.Store(entry.Value.GlobalDownlink) trafficManager.globalUplinkPackets.Store(entry.Value.GlobalUplinkPackets) trafficManager.globalDownlinkPackets.Store(entry.Value.GlobalDownlinkPackets) trafficManager.globalTCPSessions.Store(entry.Value.GlobalTCPSessions) trafficManager.globalUDPSessions.Store(entry.Value.GlobalUDPSessions) trafficManager.userUplink = typedAtomicInt64Map(entry.Value.UserUplink) trafficManager.userDownlink = typedAtomicInt64Map(entry.Value.UserDownlink) trafficManager.userUplinkPackets = typedAtomicInt64Map(entry.Value.UserUplinkPackets) trafficManager.userDownlinkPackets = typedAtomicInt64Map(entry.Value.UserDownlinkPackets) trafficManager.userTCPSessions = typedAtomicInt64Map(entry.Value.UserTCPSessions) trafficManager.userUDPSessions = typedAtomicInt64Map(entry.Value.UserUDPSessions) userManager, loaded := s.users[entry.Key] if !loaded { continue } userManager.usersMap = typedMap(entry.Value.Users) _ = userManager.postUpdate(false) } return nil } func (s *Service) encodeCache() ([]byte, error) { endpoints := new(badjson.TypedMap[string, *EndpointCache]) for tag, traffic := range s.traffics { var ( userUplink = new(badjson.TypedMap[string, int64]) userDownlink = new(badjson.TypedMap[string, int64]) userUplinkPackets = new(badjson.TypedMap[string, int64]) userDownlinkPackets = new(badjson.TypedMap[string, int64]) userTCPSessions = new(badjson.TypedMap[string, int64]) userUDPSessions = new(badjson.TypedMap[string, int64]) userMap = new(badjson.TypedMap[string, string]) ) for user, uplink := range traffic.userUplink { if uplink.Load() > 0 { userUplink.Put(user, uplink.Load()) } } for user, downlink := range traffic.userDownlink { if downlink.Load() > 0 { userDownlink.Put(user, downlink.Load()) } } for user, uplinkPackets := range traffic.userUplinkPackets { if uplinkPackets.Load() > 0 { userUplinkPackets.Put(user, uplinkPackets.Load()) } } for user, downlinkPackets := range traffic.userDownlinkPackets { if downlinkPackets.Load() > 0 { userDownlinkPackets.Put(user, downlinkPackets.Load()) } } for user, tcpSessions := range traffic.userTCPSessions { if tcpSessions.Load() > 0 { userTCPSessions.Put(user, tcpSessions.Load()) } } for user, udpSessions := range traffic.userUDPSessions { if udpSessions.Load() > 0 { userUDPSessions.Put(user, udpSessions.Load()) } } userManager := s.users[tag] if userManager != nil && len(userManager.usersMap) > 0 { userMap = new(badjson.TypedMap[string, string]) for username, password := range userManager.usersMap { if username != "" && password != "" { userMap.Put(username, password) } } } endpoints.Put(tag, &EndpointCache{ GlobalUplink: traffic.globalUplink.Load(), GlobalDownlink: traffic.globalDownlink.Load(), GlobalUplinkPackets: traffic.globalUplinkPackets.Load(), GlobalDownlinkPackets: traffic.globalDownlinkPackets.Load(), GlobalTCPSessions: traffic.globalTCPSessions.Load(), GlobalUDPSessions: traffic.globalUDPSessions.Load(), UserUplink: sortTypedMap(userUplink), UserDownlink: sortTypedMap(userDownlink), UserUplinkPackets: sortTypedMap(userUplinkPackets), UserDownlinkPackets: sortTypedMap(userDownlinkPackets), UserTCPSessions: sortTypedMap(userTCPSessions), UserUDPSessions: sortTypedMap(userUDPSessions), Users: sortTypedMap(userMap), }) } var buffer bytes.Buffer encoder := json.NewEncoder(&buffer) encoder.SetIndent("", " ") err := encoder.Encode(&Cache{ Endpoints: sortTypedMap(endpoints), }) if err != nil { return nil, err } return buffer.Bytes(), nil } func sortTypedMap[T comparable](trafficMap *badjson.TypedMap[string, T]) *badjson.TypedMap[string, T] { if trafficMap == nil { return nil } keys := trafficMap.Keys() sort.Strings(keys) sortedMap := new(badjson.TypedMap[string, T]) for _, key := range keys { value, _ := trafficMap.Get(key) sortedMap.Put(key, value) } return sortedMap } func typedAtomicInt64Map(trafficMap *badjson.TypedMap[string, int64]) map[string]*atomic.Int64 { result := make(map[string]*atomic.Int64) if trafficMap != nil { for _, entry := range trafficMap.Entries() { counter := new(atomic.Int64) counter.Store(entry.Value) result[entry.Key] = counter } } return result } func typedMap[T comparable](trafficMap *badjson.TypedMap[string, T]) map[string]T { result := make(map[string]T) if trafficMap != nil { for _, entry := range trafficMap.Entries() { result[entry.Key] = entry.Value } } return result } ================================================ FILE: service/ssmapi/server.go ================================================ package ssmapi import ( "context" "errors" "net/http" "sync" "time" "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/common" E "github.com/sagernet/sing/common/exceptions" N "github.com/sagernet/sing/common/network" aTLS "github.com/sagernet/sing/common/tls" "github.com/sagernet/sing/service" "github.com/go-chi/chi/v5" "golang.org/x/net/http2" ) func RegisterService(registry *boxService.Registry) { boxService.Register[option.SSMAPIServiceOptions](registry, C.TypeSSMAPI, NewService) } type Service struct { boxService.Adapter ctx context.Context cancel context.CancelFunc logger log.ContextLogger listener *listener.Listener tlsConfig tls.ServerConfig httpServer *http.Server traffics map[string]*TrafficManager users map[string]*UserManager cachePath string saveTicker *time.Ticker lastSavedCache []byte cacheMutex sync.Mutex } func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.SSMAPIServiceOptions) (adapter.Service, error) { ctx, cancel := context.WithCancel(ctx) chiRouter := chi.NewRouter() s := &Service{ Adapter: boxService.NewAdapter(C.TypeSSMAPI, 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, }, traffics: make(map[string]*TrafficManager), users: make(map[string]*UserManager), cachePath: options.CachePath, } inboundManager := service.FromContext[adapter.InboundManager](ctx) if options.Servers.Size() == 0 { return nil, E.New("missing servers") } for i, entry := range options.Servers.Entries() { inbound, loaded := inboundManager.Get(entry.Value) if !loaded { return nil, E.New("parse SSM server[", i, "]: inbound ", entry.Value, " not found") } managedServer, isManaged := inbound.(adapter.ManagedSSMServer) if !isManaged { return nil, E.New("parse SSM server[", i, "]: inbound/", inbound.Type(), "[", inbound.Tag(), "] is not a SSM server") } traffic := NewTrafficManager() managedServer.SetTracker(traffic) user := NewUserManager(managedServer, traffic) chiRouter.Route(entry.Key, NewAPIServer(logger, traffic, user).Route) s.traffics[entry.Key] = traffic s.users[entry.Key] = user } 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 *Service) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } err := s.loadCache() if err != nil { s.logger.Error(E.Cause(err, "load cache")) } s.saveTicker = time.NewTicker(1 * time.Minute) go s.loopSaveCache() 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 *Service) loopSaveCache() { for { select { case <-s.ctx.Done(): return case <-s.saveTicker.C: err := s.saveCache() if err != nil { s.logger.Error(E.Cause(err, "save cache")) } } } } func (s *Service) Close() error { if s.cancel != nil { s.cancel() } if s.saveTicker != nil { s.saveTicker.Stop() } err := s.saveCache() if err != nil { s.logger.Error(E.Cause(err, "save cache")) } return common.Close( common.PtrOrNil(s.httpServer), common.PtrOrNil(s.listener), s.tlsConfig, ) } ================================================ FILE: service/ssmapi/traffic.go ================================================ package ssmapi import ( "net" "sync" "sync/atomic" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing/common/bufio" N "github.com/sagernet/sing/common/network" ) var _ adapter.SSMTracker = (*TrafficManager)(nil) type TrafficManager struct { globalUplink atomic.Int64 globalDownlink atomic.Int64 globalUplinkPackets atomic.Int64 globalDownlinkPackets atomic.Int64 globalTCPSessions atomic.Int64 globalUDPSessions atomic.Int64 userAccess sync.Mutex userUplink map[string]*atomic.Int64 userDownlink map[string]*atomic.Int64 userUplinkPackets map[string]*atomic.Int64 userDownlinkPackets map[string]*atomic.Int64 userTCPSessions map[string]*atomic.Int64 userUDPSessions map[string]*atomic.Int64 } func NewTrafficManager() *TrafficManager { manager := &TrafficManager{ userUplink: make(map[string]*atomic.Int64), userDownlink: make(map[string]*atomic.Int64), userUplinkPackets: make(map[string]*atomic.Int64), userDownlinkPackets: make(map[string]*atomic.Int64), userTCPSessions: make(map[string]*atomic.Int64), userUDPSessions: make(map[string]*atomic.Int64), } return manager } func (s *TrafficManager) UpdateUsers(users []string) { s.userAccess.Lock() defer s.userAccess.Unlock() newUserUplink := make(map[string]*atomic.Int64) newUserDownlink := make(map[string]*atomic.Int64) newUserUplinkPackets := make(map[string]*atomic.Int64) newUserDownlinkPackets := make(map[string]*atomic.Int64) newUserTCPSessions := make(map[string]*atomic.Int64) newUserUDPSessions := make(map[string]*atomic.Int64) for _, user := range users { if counter, loaded := s.userUplink[user]; loaded { newUserUplink[user] = counter } if counter, loaded := s.userDownlink[user]; loaded { newUserDownlink[user] = counter } if counter, loaded := s.userUplinkPackets[user]; loaded { newUserUplinkPackets[user] = counter } if counter, loaded := s.userDownlinkPackets[user]; loaded { newUserDownlinkPackets[user] = counter } if counter, loaded := s.userTCPSessions[user]; loaded { newUserTCPSessions[user] = counter } if counter, loaded := s.userUDPSessions[user]; loaded { newUserUDPSessions[user] = counter } } s.userUplink = newUserUplink s.userDownlink = newUserDownlink s.userUplinkPackets = newUserUplinkPackets s.userDownlinkPackets = newUserDownlinkPackets s.userTCPSessions = newUserTCPSessions s.userUDPSessions = newUserUDPSessions } func (s *TrafficManager) userCounter(user string) (*atomic.Int64, *atomic.Int64, *atomic.Int64, *atomic.Int64, *atomic.Int64, *atomic.Int64) { s.userAccess.Lock() defer s.userAccess.Unlock() upCounter, loaded := s.userUplink[user] if !loaded { upCounter = new(atomic.Int64) s.userUplink[user] = upCounter } downCounter, loaded := s.userDownlink[user] if !loaded { downCounter = new(atomic.Int64) s.userDownlink[user] = downCounter } upPacketsCounter, loaded := s.userUplinkPackets[user] if !loaded { upPacketsCounter = new(atomic.Int64) s.userUplinkPackets[user] = upPacketsCounter } downPacketsCounter, loaded := s.userDownlinkPackets[user] if !loaded { downPacketsCounter = new(atomic.Int64) s.userDownlinkPackets[user] = downPacketsCounter } tcpSessionsCounter, loaded := s.userTCPSessions[user] if !loaded { tcpSessionsCounter = new(atomic.Int64) s.userTCPSessions[user] = tcpSessionsCounter } udpSessionsCounter, loaded := s.userUDPSessions[user] if !loaded { udpSessionsCounter = new(atomic.Int64) s.userUDPSessions[user] = udpSessionsCounter } return upCounter, downCounter, upPacketsCounter, downPacketsCounter, tcpSessionsCounter, udpSessionsCounter } func (s *TrafficManager) TrackConnection(conn net.Conn, metadata adapter.InboundContext) net.Conn { s.globalTCPSessions.Add(1) var readCounter []*atomic.Int64 var writeCounter []*atomic.Int64 readCounter = append(readCounter, &s.globalUplink) writeCounter = append(writeCounter, &s.globalDownlink) upCounter, downCounter, _, _, tcpSessionCounter, _ := s.userCounter(metadata.User) readCounter = append(readCounter, upCounter) writeCounter = append(writeCounter, downCounter) tcpSessionCounter.Add(1) return bufio.NewInt64CounterConn(conn, readCounter, writeCounter) } func (s *TrafficManager) TrackPacketConnection(conn N.PacketConn, metadata adapter.InboundContext) N.PacketConn { s.globalUDPSessions.Add(1) var readCounter []*atomic.Int64 var readPacketCounter []*atomic.Int64 var writeCounter []*atomic.Int64 var writePacketCounter []*atomic.Int64 readCounter = append(readCounter, &s.globalUplink) writeCounter = append(writeCounter, &s.globalDownlink) readPacketCounter = append(readPacketCounter, &s.globalUplinkPackets) writePacketCounter = append(writePacketCounter, &s.globalDownlinkPackets) upCounter, downCounter, upPacketsCounter, downPacketsCounter, _, udpSessionCounter := s.userCounter(metadata.User) readCounter = append(readCounter, upCounter) writeCounter = append(writeCounter, downCounter) readPacketCounter = append(readPacketCounter, upPacketsCounter) writePacketCounter = append(writePacketCounter, downPacketsCounter) udpSessionCounter.Add(1) return bufio.NewInt64CounterPacketConn(conn, readCounter, readPacketCounter, writeCounter, writePacketCounter) } func (s *TrafficManager) ReadUser(user *UserObject) { s.userAccess.Lock() defer s.userAccess.Unlock() s.readUser(user, false) } func (s *TrafficManager) readUser(user *UserObject, swap bool) { if counter, loaded := s.userUplink[user.UserName]; loaded { if swap { user.UplinkBytes = counter.Swap(0) } else { user.UplinkBytes = counter.Load() } } if counter, loaded := s.userDownlink[user.UserName]; loaded { if swap { user.DownlinkBytes = counter.Swap(0) } else { user.DownlinkBytes = counter.Load() } } if counter, loaded := s.userUplinkPackets[user.UserName]; loaded { if swap { user.UplinkPackets = counter.Swap(0) } else { user.UplinkPackets = counter.Load() } } if counter, loaded := s.userDownlinkPackets[user.UserName]; loaded { if swap { user.DownlinkPackets = counter.Swap(0) } else { user.DownlinkPackets = counter.Load() } } if counter, loaded := s.userTCPSessions[user.UserName]; loaded { if swap { user.TCPSessions = counter.Swap(0) } else { user.TCPSessions = counter.Load() } } if counter, loaded := s.userUDPSessions[user.UserName]; loaded { if swap { user.UDPSessions = counter.Swap(0) } else { user.UDPSessions = counter.Load() } } } func (s *TrafficManager) ReadUsers(users []*UserObject, swap bool) { s.userAccess.Lock() defer s.userAccess.Unlock() for _, user := range users { s.readUser(user, swap) } } func (s *TrafficManager) ReadGlobal(swap bool) (uplinkBytes int64, downlinkBytes int64, uplinkPackets int64, downlinkPackets int64, tcpSessions int64, udpSessions int64) { if swap { return s.globalUplink.Swap(0), s.globalDownlink.Swap(0), s.globalUplinkPackets.Swap(0), s.globalDownlinkPackets.Swap(0), s.globalTCPSessions.Swap(0), s.globalUDPSessions.Swap(0) } else { return s.globalUplink.Load(), s.globalDownlink.Load(), s.globalUplinkPackets.Load(), s.globalDownlinkPackets.Load(), s.globalTCPSessions.Load(), s.globalUDPSessions.Load() } } ================================================ FILE: service/ssmapi/user.go ================================================ package ssmapi import ( "sync" "github.com/sagernet/sing-box/adapter" E "github.com/sagernet/sing/common/exceptions" ) type UserManager struct { access sync.Mutex usersMap map[string]string server adapter.ManagedSSMServer trafficManager *TrafficManager } func NewUserManager(inbound adapter.ManagedSSMServer, trafficManager *TrafficManager) *UserManager { return &UserManager{ usersMap: make(map[string]string), server: inbound, trafficManager: trafficManager, } } func (m *UserManager) postUpdate(updated bool) error { users := make([]string, 0, len(m.usersMap)) uPSKs := make([]string, 0, len(m.usersMap)) for username, password := range m.usersMap { users = append(users, username) uPSKs = append(uPSKs, password) } err := m.server.UpdateUsers(users, uPSKs) if err != nil { return err } if updated { m.trafficManager.UpdateUsers(users) } return nil } func (m *UserManager) List() []*UserObject { m.access.Lock() defer m.access.Unlock() users := make([]*UserObject, 0, len(m.usersMap)) for username, password := range m.usersMap { users = append(users, &UserObject{ UserName: username, Password: password, }) } return users } func (m *UserManager) Add(username string, password string) error { m.access.Lock() defer m.access.Unlock() if _, found := m.usersMap[username]; found { return E.New("user ", username, " already exists") } m.usersMap[username] = password return m.postUpdate(true) } func (m *UserManager) Get(username string) (string, bool) { m.access.Lock() defer m.access.Unlock() if password, found := m.usersMap[username]; found { return password, true } return "", false } func (m *UserManager) Update(username string, password string) error { m.access.Lock() defer m.access.Unlock() m.usersMap[username] = password return m.postUpdate(true) } func (m *UserManager) Delete(username string) error { m.access.Lock() defer m.access.Unlock() delete(m.usersMap, username) return m.postUpdate(true) } ================================================ FILE: test/box_test.go ================================================ package main import ( "context" "crypto/tls" "io" "net" "net/http" "testing" "time" "github.com/sagernet/quic-go" "github.com/sagernet/quic-go/http3" "github.com/sagernet/sing-box" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/include" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/debug" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/protocol/socks" "github.com/stretchr/testify/require" "go.uber.org/goleak" ) func TestMain(m *testing.M) { goleak.VerifyTestMain(m) } var globalCtx context.Context func init() { globalCtx = include.Context(context.Background()) } func startInstance(t *testing.T, options option.Options) *box.Box { if debug.Enabled { options.Log = &option.LogOptions{ Level: "trace", } } else { options.Log = &option.LogOptions{ Level: "warning", } } ctx, cancel := context.WithCancel(globalCtx) var instance *box.Box var err error for retry := 0; retry < 3; retry++ { instance, err = box.New(box.Options{ Context: ctx, Options: options, }) require.NoError(t, err) err = instance.Start() if err != nil { time.Sleep(time.Second) continue } break } require.NoError(t, err) t.Cleanup(func() { instance.Close() cancel() }) return instance } func testSuit(t *testing.T, clientPort uint16, testPort uint16) { dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "") dialTCP := func() (net.Conn, error) { return dialer.DialContext(context.Background(), "tcp", M.ParseSocksaddrHostPort("127.0.0.1", testPort)) } dialUDP := func() (net.PacketConn, error) { return dialer.ListenPacket(context.Background(), M.ParseSocksaddrHostPort("127.0.0.1", testPort)) } require.NoError(t, testPingPongWithConn(t, testPort, dialTCP)) require.NoError(t, testPingPongWithPacketConn(t, testPort, dialUDP)) require.NoError(t, testLargeDataWithConn(t, testPort, dialTCP)) require.NoError(t, testLargeDataWithPacketConn(t, testPort, dialUDP)) // require.NoError(t, testPacketConnTimeout(t, dialUDP)) } func testQUIC(t *testing.T, clientPort uint16) { dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "") client := &http.Client{ Transport: &http3.Transport{ Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) { destination := M.ParseSocksaddr(addr) udpConn, err := dialer.DialContext(ctx, N.NetworkUDP, destination) if err != nil { return nil, err } return quic.DialEarly(ctx, udpConn.(net.PacketConn), destination, tlsCfg, cfg) }, }, } response, err := client.Get("https://cloudflare.com/cdn-cgi/trace") require.NoError(t, err) require.Equal(t, http.StatusOK, response.StatusCode) content, err := io.ReadAll(response.Body) require.NoError(t, err) println(string(content)) } func testSuitLargeUDP(t *testing.T, clientPort uint16, testPort uint16) { dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "") dialTCP := func() (net.Conn, error) { return dialer.DialContext(context.Background(), "tcp", M.ParseSocksaddrHostPort("127.0.0.1", testPort)) } dialUDP := func() (net.PacketConn, error) { return dialer.ListenPacket(context.Background(), M.ParseSocksaddrHostPort("127.0.0.1", testPort)) } require.NoError(t, testPingPongWithConn(t, testPort, dialTCP)) require.NoError(t, testPingPongWithPacketConn(t, testPort, dialUDP)) require.NoError(t, testLargeDataWithConn(t, testPort, dialTCP)) require.NoError(t, testLargeDataWithPacketConn(t, testPort, dialUDP)) require.NoError(t, testLargeDataWithPacketConnSize(t, testPort, 4096, dialUDP)) } func testTCP(t *testing.T, clientPort uint16, testPort uint16) { dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "") dialTCP := func() (net.Conn, error) { return dialer.DialContext(context.Background(), "tcp", M.ParseSocksaddrHostPort("127.0.0.1", testPort)) } require.NoError(t, testPingPongWithConn(t, testPort, dialTCP)) require.NoError(t, testLargeDataWithConn(t, testPort, dialTCP)) } func testSuitSimple(t *testing.T, clientPort uint16, testPort uint16) { dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "") dialTCP := func() (net.Conn, error) { return dialer.DialContext(context.Background(), "tcp", M.ParseSocksaddrHostPort("127.0.0.1", testPort)) } dialUDP := func() (net.PacketConn, error) { return dialer.ListenPacket(context.Background(), M.ParseSocksaddrHostPort("127.0.0.1", testPort)) } require.NoError(t, testPingPongWithConn(t, testPort, dialTCP)) require.NoError(t, testPingPongWithPacketConn(t, testPort, dialUDP)) require.NoError(t, testPingPongWithConn(t, testPort, dialTCP)) require.NoError(t, testPingPongWithPacketConn(t, testPort, dialUDP)) } func testSuitSimple1(t *testing.T, clientPort uint16, testPort uint16) { dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "") dialTCP := func() (net.Conn, error) { return dialer.DialContext(context.Background(), "tcp", M.ParseSocksaddrHostPort("127.0.0.1", testPort)) } dialUDP := func() (net.PacketConn, error) { return dialer.ListenPacket(context.Background(), M.ParseSocksaddrHostPort("127.0.0.1", testPort)) } require.NoError(t, testPingPongWithConn(t, testPort, dialTCP)) if !C.IsDarwin { require.NoError(t, testPingPongWithPacketConn(t, testPort, dialUDP)) } require.NoError(t, testPingPongWithConn(t, testPort, dialTCP)) if !C.IsDarwin { require.NoError(t, testLargeDataWithPacketConn(t, testPort, dialUDP)) } } func testSuitWg(t *testing.T, clientPort uint16, testPort uint16) { dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "") dialTCP := func() (net.Conn, error) { return dialer.DialContext(context.Background(), "tcp", M.ParseSocksaddrHostPort("10.0.0.1", testPort)) } dialUDP := func() (net.PacketConn, error) { conn, err := dialer.DialContext(context.Background(), "udp", M.ParseSocksaddrHostPort("10.0.0.1", testPort)) if err != nil { return nil, err } return bufio.NewUnbindPacketConn(conn), nil } require.NoError(t, testPingPongWithConn(t, testPort, dialTCP)) require.NoError(t, testPingPongWithPacketConn(t, testPort, dialUDP)) require.NoError(t, testLargeDataWithConn(t, testPort, dialTCP)) require.NoError(t, testLargeDataWithPacketConn(t, testPort, dialUDP)) } ================================================ FILE: test/brutal_test.go ================================================ package main import ( "net/netip" "testing" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-shadowsocks/shadowaead_2022" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/json/badoption" "github.com/gofrs/uuid/v5" ) func TestBrutalShadowsocks(t *testing.T) { method := shadowaead_2022.List[0] password := mkBase64(t, 16) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeShadowsocks, Options: &option.ShadowsocksInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Method: method, Password: password, Multiplex: &option.InboundMultiplexOptions{ Enabled: true, Brutal: &option.BrutalOptions{ Enabled: true, UpMbps: 100, DownMbps: 100, }, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeShadowsocks, Tag: "ss-out", Options: &option.ShadowsocksOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, Method: method, Password: password, Multiplex: &option.OutboundMultiplexOptions{ Enabled: true, Protocol: "smux", Padding: true, Brutal: &option.BrutalOptions{ Enabled: true, UpMbps: 100, DownMbps: 100, }, }, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "ss-out", }, }, }, }, }, }, }) testSuit(t, clientPort, testPort) } func TestBrutalTrojan(t *testing.T) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") password := mkBase64(t, 16) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeTrojan, Options: &option.TrojanInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.TrojanUser{{Password: password}}, Multiplex: &option.InboundMultiplexOptions{ Enabled: true, Brutal: &option.BrutalOptions{ Enabled: true, UpMbps: 100, DownMbps: 100, }, }, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, KeyPath: keyPem, }, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeTrojan, Tag: "ss-out", Options: &option.TrojanOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, Password: password, Multiplex: &option.OutboundMultiplexOptions{ Enabled: true, Protocol: "yamux", Padding: true, Brutal: &option.BrutalOptions{ Enabled: true, UpMbps: 100, DownMbps: 100, }, }, OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, }, }, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "ss-out", }, }, }, }, }, }, }) testSuit(t, clientPort, testPort) } func TestBrutalVMess(t *testing.T) { user, _ := uuid.NewV4() startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeVMess, Options: &option.VMessInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.VMessUser{{UUID: user.String()}}, Multiplex: &option.InboundMultiplexOptions{ Enabled: true, Brutal: &option.BrutalOptions{ Enabled: true, UpMbps: 100, DownMbps: 100, }, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeVMess, Tag: "ss-out", Options: &option.VMessOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, UUID: user.String(), Multiplex: &option.OutboundMultiplexOptions{ Enabled: true, Protocol: "h2mux", Padding: true, Brutal: &option.BrutalOptions{ Enabled: true, UpMbps: 100, DownMbps: 100, }, }, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "ss-out", }, }, }, }, }, }, }) testSuit(t, clientPort, testPort) } func TestBrutalVLESS(t *testing.T) { user, _ := uuid.NewV4() startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeVLESS, Options: &option.VLESSInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.VLESSUser{{UUID: user.String()}}, Multiplex: &option.InboundMultiplexOptions{ Enabled: true, Brutal: &option.BrutalOptions{ Enabled: true, UpMbps: 100, DownMbps: 100, }, }, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, ServerName: "google.com", Reality: &option.InboundRealityOptions{ Enabled: true, Handshake: option.InboundRealityHandshakeOptions{ ServerOptions: option.ServerOptions{ Server: "google.com", ServerPort: 443, }, }, ShortID: []string{"0123456789abcdef"}, PrivateKey: "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc", }, }, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeVLESS, Tag: "ss-out", Options: &option.VLESSOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, UUID: user.String(), OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "google.com", Reality: &option.OutboundRealityOptions{ Enabled: true, ShortID: "0123456789abcdef", PublicKey: "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0", }, UTLS: &option.OutboundUTLSOptions{ Enabled: true, }, }, }, Multiplex: &option.OutboundMultiplexOptions{ Enabled: true, Protocol: "h2mux", Padding: true, Brutal: &option.BrutalOptions{ Enabled: true, UpMbps: 100, DownMbps: 100, }, }, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "ss-out", }, }, }, }, }, }, }) testSuit(t, clientPort, testPort) } ================================================ FILE: test/clash_darwin_test.go ================================================ package main import ( "errors" "fmt" "net" "net/netip" "syscall" "golang.org/x/net/route" ) func defaultRouteIP() (netip.Addr, error) { idx, err := defaultRouteInterfaceIndex() if err != nil { return netip.Addr{}, err } iface, err := net.InterfaceByIndex(idx) if err != nil { return netip.Addr{}, err } addrs, err := iface.Addrs() if err != nil { return netip.Addr{}, err } for _, addr := range addrs { ip := addr.(*net.IPNet).IP if ip.To4() != nil { return netip.AddrFrom4([4]byte(ip)), nil } } return netip.Addr{}, errors.New("no ipv4 addr") } func defaultRouteInterfaceIndex() (int, error) { rib, err := route.FetchRIB(syscall.AF_UNSPEC, syscall.NET_RT_DUMP2, 0) if err != nil { return 0, fmt.Errorf("route.FetchRIB: %w", err) } msgs, err := route.ParseRIB(syscall.NET_RT_IFLIST2, rib) if err != nil { return 0, fmt.Errorf("route.ParseRIB: %w", err) } for _, message := range msgs { routeMessage := message.(*route.RouteMessage) if routeMessage.Flags&(syscall.RTF_UP|syscall.RTF_GATEWAY|syscall.RTF_STATIC) == 0 { continue } addresses := routeMessage.Addrs destination, ok := addresses[0].(*route.Inet4Addr) if !ok { continue } if destination.IP != [4]byte{0, 0, 0, 0} { continue } switch addresses[1].(type) { case *route.Inet4Addr: return routeMessage.Index, nil default: continue } } return 0, fmt.Errorf("ambiguous gateway interfaces found") } ================================================ FILE: test/clash_other_test.go ================================================ //go:build !darwin package main import ( "errors" "net/netip" ) func defaultRouteIP() (netip.Addr, error) { return netip.Addr{}, errors.New("not supported") } ================================================ FILE: test/clash_test.go ================================================ package main import ( "context" "crypto/md5" "crypto/rand" "errors" "io" "net" _ "net/http/pprof" "net/netip" "sync" "testing" "time" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing/common/control" F "github.com/sagernet/sing/common/format" "github.com/docker/docker/api/types/image" "github.com/docker/docker/client" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // kanged from clash const ( ImageShadowsocksRustServer = "ghcr.io/shadowsocks/ssserver-rust:latest" ImageShadowsocksRustClient = "ghcr.io/shadowsocks/sslocal-rust:latest" ImageV2RayCore = "v2fly/v2fly-core:latest" ImageTrojan = "trojangfw/trojan:latest" ImageNaive = "pocat/naiveproxy:client" ImageBoringTun = "ghcr.io/ntkme/boringtun:edge" ImageHysteria = "tobyxdd/hysteria:v1.3.5" ImageHysteria2 = "tobyxdd/hysteria:v2" ImageNginx = "nginx:stable" ImageShadowTLS = "ghcr.io/ihciah/shadow-tls:latest" ImageXRayCore = "teddysun/xray:latest" ImageShadowsocksLegacy = "mritd/shadowsocks:latest" ImageTUICServer = "kilvn/tuic-server:latest" ImageTUICClient = "kilvn/tuic-client:latest" ) var allImages = []string{ ImageShadowsocksRustServer, ImageShadowsocksRustClient, ImageV2RayCore, ImageTrojan, ImageNaive, ImageBoringTun, ImageHysteria, ImageHysteria2, ImageNginx, ImageShadowTLS, ImageXRayCore, ImageShadowsocksLegacy, ImageTUICServer, ImageTUICClient, } var localIP = netip.MustParseAddr("127.0.0.1") func init() { dockerClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { panic(err) } defer dockerClient.Close() list, err := dockerClient.ImageList(context.Background(), image.ListOptions{All: true}) if err != nil { log.Warn(err) return } imageExist := func(image string) bool { for _, item := range list { for _, tag := range item.RepoTags { if image == tag { return true } } } return false } for _, i := range allImages { if imageExist(i) { continue } log.Info("pulling image: ", i) imageStream, err := dockerClient.ImagePull(context.Background(), i, image.PullOptions{}) if err != nil { panic(err) } io.Copy(io.Discard, imageStream) } } func newPingPongPair() (chan []byte, chan []byte, func(t *testing.T) error) { pingCh := make(chan []byte) pongCh := make(chan []byte) test := func(t *testing.T) error { defer close(pingCh) defer close(pongCh) pingOpen := false pongOpen := false var recv []byte for { if pingOpen && pongOpen { break } select { case recv, pingOpen = <-pingCh: assert.True(t, pingOpen) assert.Equal(t, []byte("ping"), recv) case recv, pongOpen = <-pongCh: assert.True(t, pongOpen) assert.Equal(t, []byte("pong"), recv) case <-time.After(10 * time.Second): return errors.New("timeout") } } return nil } return pingCh, pongCh, test } func newLargeDataPair() (chan hashPair, chan hashPair, func(t *testing.T) error) { pingCh := make(chan hashPair) pongCh := make(chan hashPair) test := func(t *testing.T) error { defer close(pingCh) defer close(pongCh) pingOpen := false pongOpen := false var serverPair hashPair var clientPair hashPair for { if pingOpen && pongOpen { break } select { case serverPair, pingOpen = <-pingCh: assert.True(t, pingOpen) case clientPair, pongOpen = <-pongCh: assert.True(t, pongOpen) case <-time.After(10 * time.Second): return errors.New("timeout") } } assert.Equal(t, serverPair.recvHash, clientPair.sendHash) assert.Equal(t, serverPair.sendHash, clientPair.recvHash) return nil } return pingCh, pongCh, test } func testPingPongWithConn(t *testing.T, port uint16, cc func() (net.Conn, error)) error { l, err := listen("tcp", ":"+F.ToString(port)) if err != nil { return err } defer l.Close() c, err := cc() if err != nil { return err } defer c.Close() pingCh, pongCh, test := newPingPongPair() go func() { c, err := l.Accept() if err != nil { return } buf := make([]byte, 4) if _, err := io.ReadFull(c, buf); err != nil { return } pingCh <- buf if _, err := c.Write([]byte("pong")); err != nil { return } }() go func() { if _, err := c.Write([]byte("ping")); err != nil { return } buf := make([]byte, 4) if _, err := io.ReadFull(c, buf); err != nil { return } pongCh <- buf }() return test(t) } func testPingPongWithPacketConn(t *testing.T, port uint16, pcc func() (net.PacketConn, error)) error { l, err := listenPacket("udp", ":"+F.ToString(port)) if err != nil { return err } defer l.Close() rAddr := &net.UDPAddr{IP: localIP.AsSlice(), Port: int(port)} pingCh, pongCh, test := newPingPongPair() go func() { buf := make([]byte, 1024) n, rAddr, err := l.ReadFrom(buf) if err != nil { return } pingCh <- buf[:n] if _, err := l.WriteTo([]byte("pong"), rAddr); err != nil { return } }() pc, err := pcc() if err != nil { return err } defer pc.Close() go func() { if _, err := pc.WriteTo([]byte("ping"), rAddr); err != nil { return } buf := make([]byte, 1024) n, _, err := pc.ReadFrom(buf) if err != nil { return } pongCh <- buf[:n] }() return test(t) } type hashPair struct { sendHash map[int][]byte recvHash map[int][]byte } func testLargeDataWithConn(t *testing.T, port uint16, cc func() (net.Conn, error)) error { l, err := listen("tcp", ":"+F.ToString(port)) require.NoError(t, err) defer l.Close() times := 100 chunkSize := int64(64 * 1024) pingCh, pongCh, test := newLargeDataPair() writeRandData := func(conn net.Conn) (map[int][]byte, error) { buf := make([]byte, chunkSize) hashMap := map[int][]byte{} for i := 0; i < times; i++ { if _, err := rand.Read(buf[1:]); err != nil { return nil, err } buf[0] = byte(i) hash := md5.Sum(buf) hashMap[i] = hash[:] if _, err := conn.Write(buf); err != nil { return nil, err } } return hashMap, nil } c, err := cc() if err != nil { return err } defer c.Close() go func() { c, err := l.Accept() if err != nil { return } defer c.Close() hashMap := map[int][]byte{} buf := make([]byte, chunkSize) for i := 0; i < times; i++ { _, err := io.ReadFull(c, buf) if err != nil { t.Log(err.Error()) return } hash := md5.Sum(buf) hashMap[int(buf[0])] = hash[:] } sendHash, err := writeRandData(c) if err != nil { t.Log(err.Error()) return } pingCh <- hashPair{ sendHash: sendHash, recvHash: hashMap, } }() go func() { sendHash, err := writeRandData(c) if err != nil { t.Log(err.Error()) return } hashMap := map[int][]byte{} buf := make([]byte, chunkSize) for i := 0; i < times; i++ { _, err := io.ReadFull(c, buf) if err != nil { t.Log(err.Error()) return } hash := md5.Sum(buf) hashMap[int(buf[0])] = hash[:] } pongCh <- hashPair{ sendHash: sendHash, recvHash: hashMap, } }() return test(t) } func testLargeDataWithPacketConn(t *testing.T, port uint16, pcc func() (net.PacketConn, error)) error { return testLargeDataWithPacketConnSize(t, port, 1500, pcc) } func testLargeDataWithPacketConnSize(t *testing.T, port uint16, chunkSize int, pcc func() (net.PacketConn, error)) error { l, err := listenPacket("udp", ":"+F.ToString(port)) if err != nil { return err } defer l.Close() rAddr := &net.UDPAddr{IP: localIP.AsSlice(), Port: int(port)} times := 50 pingCh, pongCh, test := newLargeDataPair() writeRandData := func(pc net.PacketConn, addr net.Addr) (map[int][]byte, error) { hashMap := map[int][]byte{} mux := sync.Mutex{} for i := 0; i < times; i++ { buf := make([]byte, chunkSize) if _, err := rand.Read(buf[1:]); err != nil { t.Log(err.Error()) continue } buf[0] = byte(i) hash := md5.Sum(buf) mux.Lock() hashMap[i] = hash[:] mux.Unlock() if _, err := pc.WriteTo(buf, addr); err != nil { t.Log(err.Error()) } time.Sleep(10 * time.Millisecond) } return hashMap, nil } go func() { var rAddr net.Addr hashMap := map[int][]byte{} buf := make([]byte, 64*1024) for i := 0; i < times; i++ { _, rAddr, err = l.ReadFrom(buf) if err != nil { t.Log(err.Error()) return } hash := md5.Sum(buf[:chunkSize]) hashMap[int(buf[0])] = hash[:] } sendHash, err := writeRandData(l, rAddr) if err != nil { t.Log(err.Error()) return } pingCh <- hashPair{ sendHash: sendHash, recvHash: hashMap, } }() pc, err := pcc() if err != nil { return err } defer pc.Close() go func() { sendHash, err := writeRandData(pc, rAddr) if err != nil { t.Log(err.Error()) return } hashMap := map[int][]byte{} buf := make([]byte, 64*1024) for i := 0; i < times; i++ { _, _, err := pc.ReadFrom(buf) if err != nil { t.Log(err.Error()) return } hash := md5.Sum(buf[:chunkSize]) hashMap[int(buf[0])] = hash[:] } pongCh <- hashPair{ sendHash: sendHash, recvHash: hashMap, } }() return test(t) } func testPacketConnTimeout(t *testing.T, pcc func() (net.PacketConn, error)) error { pc, err := pcc() if err != nil { return err } err = pc.SetReadDeadline(time.Now().Add(time.Millisecond * 300)) require.NoError(t, err) errCh := make(chan error, 1) go func() { buf := make([]byte, 1024) _, _, err := pc.ReadFrom(buf) errCh <- err }() select { case <-errCh: return nil case <-time.After(time.Second * 10): return errors.New("timeout") } } func listen(network, address string) (net.Listener, error) { var lc net.ListenConfig lc.Control = control.ReuseAddr() var lastErr error for i := 0; i < 5; i++ { l, err := lc.Listen(context.Background(), network, address) if err == nil { return l, nil } lastErr = err time.Sleep(5 * time.Millisecond) } return nil, lastErr } func listenPacket(network, address string) (net.PacketConn, error) { var lc net.ListenConfig lc.Control = control.ReuseAddr() var lastErr error for i := 0; i < 5; i++ { l, err := lc.ListenPacket(context.Background(), network, address) if err == nil { return l, nil } lastErr = err time.Sleep(5 * time.Millisecond) } return nil, lastErr } ================================================ FILE: test/config/hysteria-client.json ================================================ { "server": "127.0.0.1:10000", "auth_str": "password", "obfs": "fuck me till the daylight", "up_mbps": 100, "down_mbps": 100, "socks5": { "listen": "127.0.0.1:10001" }, "server_name": "example.org", "ca": "/etc/hysteria/ca.pem" } ================================================ FILE: test/config/hysteria-server.json ================================================ { "listen": ":10000", "cert": "/etc/hysteria/cert.pem", "key": "/etc/hysteria/key.pem", "auth_str": "password", "obfs": "fuck me till the daylight", "up_mbps": 100, "down_mbps": 100 } ================================================ FILE: test/config/hysteria2-client.yml ================================================ server: 127.0.0.1:10000 auth: password socks5: listen: 127.0.0.1:10001 tls: sni: example.org ca: /etc/hysteria/ca.pem obfs: type: salamander salamander: password: cry_me_a_r1ver ================================================ FILE: test/config/hysteria2-server.yml ================================================ listen: 127.0.0.1:10000 auth: type: password password: password tls: sni: example.org cert: /etc/hysteria/cert.pem key: /etc/hysteria/key.pem obfs: type: salamander salamander: password: cry_me_a_r1ver ================================================ FILE: test/config/naive-nginx.conf ================================================ stream { server { listen 10000 ssl; listen [::]:10000 ssl; ssl_certificate /etc/nginx/cert.pem; ssl_certificate_key /etc/nginx/key.pem; ssl_session_timeout 1d; ssl_session_cache shared:MozSSL:10m; # about 40000 sessions ssl_session_tickets off; # modern configuration ssl_protocols TLSv1.3; ssl_prefer_server_ciphers off; proxy_pass 127.0.0.1:10003; } } ================================================ FILE: test/config/naive-quic.json ================================================ { "listen": "socks://127.0.0.1:10001", "proxy": "quic://sekai:password@example.org:10000", "host-resolver-rules": "MAP example.org 127.0.0.1", "log": "" } ================================================ FILE: test/config/naive.json ================================================ { "listen": "socks://127.0.0.1:10001", "proxy": "https://sekai:password@example.org:10000", "host-resolver-rules": "MAP example.org 127.0.0.1", "log": "" } ================================================ FILE: test/config/nginx.conf ================================================ user nginx; worker_processes auto; error_log /var/log/nginx/error.log notice; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; #tcp_nopush on; keepalive_timeout 65; #gzip on; } include /etc/nginx/conf.d/naive.conf; ================================================ FILE: test/config/shadowsocksr.json ================================================ { "server": "0.0.0.0", "server_ipv6": "::", "server_port": 10000, "local_address": "127.0.0.1", "local_port": 1080, "password": "password0", "timeout": 120, "method": "aes-256-cfb", "protocol": "origin", "protocol_param": "", "obfs": "plain", "obfs_param": "", "redirect": "", "dns_ipv6": false, "fast_open": true, "workers": 1, "forbidden_ip": "" } ================================================ FILE: test/config/trojan.json ================================================ { "run_type": "server", "local_addr": "0.0.0.0", "local_port": 10000, "password": [ "password" ], "log_level": 1, "ssl": { "cert": "/path/to/certificate.crt", "key": "/path/to/private.key", "key_password": "", "cipher": "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384", "cipher_tls13": "TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_256_GCM_SHA384", "prefer_server_cipher": true, "alpn": [ "http/1.1" ], "alpn_port_override": { "h2": 81 }, "reuse_session": true, "session_ticket": false, "session_timeout": 600, "plain_http_response": "", "curves": "", "dhparam": "" }, "tcp": { "prefer_ipv4": false, "no_delay": true, "keep_alive": true, "reuse_port": false, "fast_open": false, "fast_open_qlen": 20 }, "mysql": { "enabled": false } } ================================================ FILE: test/config/tuic-client.json ================================================ { "relay": { "server": "example.org:10000", "ip": "127.0.0.1", "uuid": "FE35D05B-8803-45C4-BAE6-723AD2CD5D3D", "password": "tuic", "certificates": [ "/etc/tuic/ca.pem" ] }, "local": { "server": "127.0.0.1:10001", "max_packet_size": 65535 }, "log_level": "debug" } ================================================ FILE: test/config/tuic-server.json ================================================ { "server": "[::]:10000", "users": { "FE35D05B-8803-45C4-BAE6-723AD2CD5D3D": "tuic" }, "certificate": "/etc/tuic/cert.pem", "private_key": "/etc/tuic/key.pem", "max_external_packet_size": 65535, "log_level": "debug" } ================================================ FILE: test/config/vless-server.json ================================================ { "log": { "loglevel": "debug" }, "inbounds": [ { "listen": "0.0.0.0", "port": 1234, "protocol": "vless", "settings": { "decryption": "none", "clients": [ { "id": "b831381d-6324-4d53-ad4f-8cda48b30811" } ] } } ], "outbounds": [ { "protocol": "freedom" } ] } ================================================ FILE: test/config/vless-tls-client.json ================================================ { "log": { "loglevel": "debug" }, "inbounds": [ { "listen": "0.0.0.0", "port": "1080", "protocol": "socks", "settings": { "auth": "noauth", "udp": true, "ip": "127.0.0.1" } } ], "outbounds": [ { "protocol": "vless", "settings": { "vnext": [ { "address": "host.docker.internal", "port": 1234, "users": [ { "id": "", "encryption": "none", "flow": "" } ] } ] }, "streamSettings": { "network": "tcp", "security": "tls", "tlsSettings": { "serverName": "example.org", "certificates": [ { "certificateFile": "/path/to/certificate.crt", "keyFile": "/path/to/private.key" } ], "fingerprint": "chrome" } } } ] } ================================================ FILE: test/config/vless-tls-server.json ================================================ { "log": { "loglevel": "debug" }, "inbounds": [ { "listen": "0.0.0.0", "port": 1234, "protocol": "vless", "settings": { "decryption": "none", "clients": [ { "id": "b831381d-6324-4d53-ad4f-8cda48b30811", "flow": "" } ] }, "streamSettings": { "network": "tcp", "security": "tls", "tlsSettings": { "serverName": "example.org", "certificates": [ { "certificateFile": "/path/to/certificate.crt", "keyFile": "/path/to/private.key" } ] } } } ], "outbounds": [ { "protocol": "freedom" } ] } ================================================ FILE: test/config/vmess-client.json ================================================ { "log": { "loglevel": "debug" }, "inbounds": [ { "listen": "127.0.0.1", "port": "1080", "protocol": "socks", "settings": { "auth": "noauth", "udp": true, "ip": "127.0.0.1" } } ], "outbounds": [ { "protocol": "vmess", "settings": { "vnext": [ { "address": "127.0.0.1", "port": 1234, "users": [ { "id": "", "alterId": 0, "security": "none", "experiments": "" } ] } ] } } ] } ================================================ FILE: test/config/vmess-grpc-client.json ================================================ { "log": { "loglevel": "debug" }, "inbounds": [ { "listen": "127.0.0.1", "port": "1080", "protocol": "socks", "settings": { "auth": "noauth", "udp": true, "ip": "127.0.0.1" } } ], "outbounds": [ { "protocol": "vmess", "settings": { "vnext": [ { "address": "127.0.0.1", "port": 1234, "users": [ { "id": "" } ] } ] }, "streamSettings": { "network": "gun", "security": "tls", "tlsSettings": { "serverName": "example.org", "certificates": [ { "certificateFile": "/path/to/certificate.crt", "keyFile": "/path/to/private.key" } ] }, "grpcSettings": { "serviceName": "TunService" } } } ] } ================================================ FILE: test/config/vmess-grpc-server.json ================================================ { "log": { "loglevel": "debug" }, "inbounds": [ { "listen": "0.0.0.0", "port": 1234, "protocol": "vmess", "settings": { "clients": [ { "id": "b831381d-6324-4d53-ad4f-8cda48b30811" } ] }, "streamSettings": { "network": "gun", "security": "tls", "tlsSettings": { "serverName": "example.org", "certificates": [ { "certificateFile": "/path/to/certificate.crt", "keyFile": "/path/to/private.key" } ] }, "grpcSettings": { "serviceName": "TunService" } } } ], "outbounds": [ { "protocol": "freedom" } ] } ================================================ FILE: test/config/vmess-mux-client.json ================================================ { "log": { "loglevel": "debug" }, "inbounds": [ { "listen": "127.0.0.1", "port": "1080", "protocol": "socks", "settings": { "auth": "noauth", "udp": true, "ip": "127.0.0.1" } } ], "outbounds": [ { "protocol": "vmess", "settings": { "vnext": [ { "address": "127.0.0.1", "port": 1234, "users": [ { "id": "", "alterId": 0, "security": "none", "experiments": "" } ] } ] }, "mux": { "enabled": true } } ] } ================================================ FILE: test/config/vmess-server.json ================================================ { "log": { "loglevel": "debug" }, "inbounds": [ { "listen": "0.0.0.0", "port": 1234, "protocol": "vmess", "settings": { "clients": [ { "id": "b831381d-6324-4d53-ad4f-8cda48b30811", "alterId": 0 } ] } } ], "outbounds": [ { "protocol": "freedom" } ] } ================================================ FILE: test/config/vmess-ws-client.json ================================================ { "log": { "loglevel": "debug" }, "inbounds": [ { "listen": "127.0.0.1", "port": "1080", "protocol": "socks", "settings": { "auth": "noauth", "udp": true, "ip": "127.0.0.1" } } ], "outbounds": [ { "protocol": "vmess", "settings": { "vnext": [ { "address": "127.0.0.1", "port": 1234, "users": [ { "id": "" } ] } ] }, "streamSettings": { "network": "ws", "security": "tls", "tlsSettings": { "serverName": "example.org", "certificates": [ { "certificateFile": "/path/to/certificate.crt", "keyFile": "/path/to/private.key" } ] }, "wsSettings": { "maxEarlyData": 2048, "earlyDataHeaderName": "" } } } ] } ================================================ FILE: test/config/vmess-ws-server.json ================================================ { "log": { "loglevel": "debug" }, "inbounds": [ { "listen": "0.0.0.0", "port": 1234, "protocol": "vmess", "settings": { "clients": [ { "id": "b831381d-6324-4d53-ad4f-8cda48b30811" } ] }, "streamSettings": { "network": "ws", "security": "tls", "tlsSettings": { "serverName": "example.org", "certificates": [ { "certificateFile": "/path/to/certificate.crt", "keyFile": "/path/to/private.key" } ] }, "wsSettings": { "maxEarlyData": 2048, "earlyDataHeaderName": "" } } } ], "outbounds": [ { "protocol": "freedom" } ] } ================================================ FILE: test/config/wireguard.conf ================================================ [Interface] PrivateKey = gHWUGzTh5YCEV6k8dneVP537XhVtoQJPIlFNs2zsxlE= Address = 10.0.0.1/32 ListenPort = 10000 [Peer] PublicKey = LV2xr9tzxwbs0ZLUlFN9k/0Or9QWqIInvxc/Cu7/2hA= AllowedIPs = 10.0.0.2/32 ================================================ FILE: test/direct_test.go ================================================ package main import ( "net/netip" "testing" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/json/badoption" ) // Since this is a feature one-off added by outsiders, I won't address these anymore. func _TestProxyProtocol(t *testing.T) { startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeDirect, Options: &option.DirectInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, ProxyProtocol: true, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeDirect, Tag: "proxy-out", Options: &option.DirectOutboundOptions{ OverrideAddress: "127.0.0.1", OverridePort: serverPort, ProxyProtocol: 2, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "proxy-out", }, }, }, }, }, }, }) testSuit(t, clientPort, testPort) } ================================================ FILE: test/docker_test.go ================================================ package main import ( "context" "os" "path/filepath" "testing" "time" "github.com/sagernet/sing/common/debug" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/rw" "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" "github.com/docker/docker/pkg/stdcopy" "github.com/docker/go-connections/nat" "github.com/stretchr/testify/require" ) type DockerOptions struct { Image string EntryPoint string Ports []uint16 Cmd []string Env []string Bind map[string]string Stdin []byte Cap []string } func startDockerContainer(t *testing.T, options DockerOptions) { dockerClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) require.NoError(t, err) defer dockerClient.Close() writeStdin := len(options.Stdin) > 0 var containerOptions container.Config if writeStdin { containerOptions.OpenStdin = true containerOptions.StdinOnce = true } containerOptions.Image = options.Image if options.EntryPoint != "" { containerOptions.Entrypoint = []string{options.EntryPoint} } containerOptions.Cmd = options.Cmd containerOptions.Env = options.Env containerOptions.ExposedPorts = make(nat.PortSet) var hostOptions container.HostConfig hostOptions.NetworkMode = "host" hostOptions.CapAdd = options.Cap hostOptions.PortBindings = make(nat.PortMap) for _, port := range options.Ports { containerOptions.ExposedPorts[nat.Port(F.ToString(port, "/tcp"))] = struct{}{} containerOptions.ExposedPorts[nat.Port(F.ToString(port, "/udp"))] = struct{}{} hostOptions.PortBindings[nat.Port(F.ToString(port, "/tcp"))] = []nat.PortBinding{ {HostPort: F.ToString(port), HostIP: "0.0.0.0"}, } hostOptions.PortBindings[nat.Port(F.ToString(port, "/udp"))] = []nat.PortBinding{ {HostPort: F.ToString(port), HostIP: "0.0.0.0"}, } } if len(options.Bind) > 0 { hostOptions.Binds = []string{} for path, internalPath := range options.Bind { if !rw.FileExists(path) { path = filepath.Join("config", path) } path, _ = filepath.Abs(path) hostOptions.Binds = append(hostOptions.Binds, path+":"+internalPath) } } dockerContainer, err := dockerClient.ContainerCreate(context.Background(), &containerOptions, &hostOptions, nil, nil, "") require.NoError(t, err) t.Cleanup(func() { cleanContainer(dockerContainer.ID) }) require.NoError(t, dockerClient.ContainerStart(context.Background(), dockerContainer.ID, container.StartOptions{})) if writeStdin { stdinAttach, err := dockerClient.ContainerAttach(context.Background(), dockerContainer.ID, container.AttachOptions{ Stdin: writeStdin, Stream: true, }) require.NoError(t, err) _, err = stdinAttach.Conn.Write(options.Stdin) require.NoError(t, err) stdinAttach.Close() } if debug.Enabled { attach, err := dockerClient.ContainerAttach(context.Background(), dockerContainer.ID, container.AttachOptions{ Stdout: true, Stderr: true, Logs: true, Stream: true, }) require.NoError(t, err) go func() { stdcopy.StdCopy(os.Stderr, os.Stderr, attach.Reader) }() } time.Sleep(time.Second) } func cleanContainer(id string) error { dockerClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { return err } defer dockerClient.Close() return dockerClient.ContainerRemove(context.Background(), id, container.RemoveOptions{Force: true}) } ================================================ FILE: test/domain_inbound_test.go ================================================ package main import ( "net/netip" "testing" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/json/badoption" "github.com/gofrs/uuid/v5" ) func TestTUICDomainUDP(t *testing.T) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeTUIC, Options: &option.TUICInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.TUICUser{{ UUID: uuid.Nil.String(), }}, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, KeyPath: keyPem, }, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeTUIC, Tag: "tuic-out", Options: &option.TUICOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, UUID: uuid.Nil.String(), OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, }, }, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "tuic-out", }, }, }, }, }, }, }) testQUIC(t, clientPort) } ================================================ FILE: test/ech_test.go ================================================ package main import ( "net/netip" "testing" "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" "github.com/sagernet/sing/common/json/badoption" "github.com/gofrs/uuid/v5" ) func TestECH(t *testing.T) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") echConfig, echKey := common.Must2(tls.ECHKeygenDefault("not.example.org")) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeTrojan, Options: &option.TrojanInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.TrojanUser{ { Name: "sekai", Password: "password", }, }, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, KeyPath: keyPem, ECH: &option.InboundECHOptions{ Enabled: true, Key: []string{echKey}, }, }, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeTrojan, Tag: "trojan-out", Options: &option.TrojanOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, Password: "password", OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, ECH: &option.OutboundECHOptions{ Enabled: true, Config: []string{echConfig}, }, }, }, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "trojan-out", }, }, }, }, }, }, }) testSuit(t, clientPort, testPort) } func TestECHQUIC(t *testing.T) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") echConfig, echKey := common.Must2(tls.ECHKeygenDefault("not.example.org")) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeTUIC, Options: &option.TUICInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.TUICUser{{ UUID: uuid.Nil.String(), }}, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, KeyPath: keyPem, ECH: &option.InboundECHOptions{ Enabled: true, Key: []string{echKey}, }, }, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeTUIC, Tag: "tuic-out", Options: &option.TUICOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, UUID: uuid.Nil.String(), OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, ECH: &option.OutboundECHOptions{ Enabled: true, Config: []string{echConfig}, }, }, }, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "tuic-out", }, }, }, }, }, }, }) testSuitLargeUDP(t, clientPort, testPort) } func TestECHHysteria2(t *testing.T) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") echConfig, echKey := common.Must2(tls.ECHKeygenDefault("not.example.org")) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeHysteria2, Options: &option.Hysteria2InboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.Hysteria2User{{ Password: "password", }}, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, KeyPath: keyPem, ECH: &option.InboundECHOptions{ Enabled: true, Key: []string{echKey}, }, }, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeHysteria2, Tag: "hy2-out", Options: &option.Hysteria2OutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, Password: "password", OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, ECH: &option.OutboundECHOptions{ Enabled: true, Config: []string{echConfig}, }, }, }, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "hy2-out", }, }, }, }, }, }, }) testSuitLargeUDP(t, clientPort, testPort) } ================================================ FILE: test/go.mod ================================================ module test go 1.24.7 require github.com/sagernet/sing-box v0.0.0 replace github.com/sagernet/sing-box => ../ require ( github.com/docker/docker v27.3.1+incompatible github.com/docker/go-connections v0.5.0 github.com/gofrs/uuid/v5 v5.4.0 github.com/sagernet/quic-go v0.59.0-sing-box-mod.2 github.com/sagernet/sing v0.8.0-beta.16 github.com/sagernet/sing-quic v0.6.0-beta.11 github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/spyzhov/ajson v0.9.4 github.com/stretchr/testify v1.11.1 go.uber.org/goleak v1.3.0 golang.org/x/net v0.48.0 ) require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/ajg/form v1.5.1 // indirect github.com/akutz/memconn v0.1.0 // indirect github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/anthropics/anthropic-sdk-go v1.19.0 // indirect github.com/anytls/sing-anytls v0.0.11 // indirect github.com/caddyserver/certmagic v0.25.0 // indirect github.com/caddyserver/zerossl v0.1.3 // indirect github.com/coder/websocket v1.8.14 // indirect github.com/containerd/log v0.1.0 // indirect github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect github.com/cretz/bine v0.2.0 // indirect github.com/database64128/netx-go v0.1.1 // indirect github.com/database64128/tfo-go/v2 v2.3.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect github.com/distribution/reference v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/ebitengine/purego v0.9.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/florianl/go-nfqueue/v2 v2.0.2 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/gaissmai/bart v0.18.0 // indirect github.com/go-chi/chi/v5 v5.2.3 // indirect github.com/go-chi/render v1.0.3 // indirect github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/godbus/dbus/v5 v5.2.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/btree v1.1.3 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/yamux v0.1.2 // indirect github.com/hdevalence/ed25519consensus v0.2.0 // indirect github.com/insomniacslk/dhcp v0.0.0-20251020182700-175e84fbb167 // indirect github.com/jsimonetti/rtnetlink v1.4.0 // indirect github.com/keybase/go-keychain v0.0.1 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/libdns/acmedns v0.5.0 // indirect github.com/libdns/alidns v1.0.6-beta.3 // indirect github.com/libdns/cloudflare v0.2.2 // indirect github.com/libdns/libdns v1.1.1 // indirect github.com/logrusorgru/aurora v2.0.3+incompatible // indirect github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect github.com/mdlayher/socket v0.5.1 // indirect github.com/metacubex/utls v1.8.4 // indirect github.com/mholt/acmez/v3 v3.1.4 // indirect github.com/miekg/dns v1.1.69 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/term v0.5.0 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/openai/openai-go/v3 v3.15.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pires/go-proxyproto v0.8.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 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/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect github.com/sagernet/cors v1.2.1 // indirect github.com/sagernet/cronet-go v0.0.0-20260117110918-dc1cda1fe287 // indirect github.com/sagernet/cronet-go/all v0.0.0-20260117110918-dc1cda1fe287 // indirect github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260117110516-f21660bef13f // indirect github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260117110516-f21660bef13f // indirect github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260117110516-f21660bef13f // indirect github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260117110516-f21660bef13f // indirect github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260117110516-f21660bef13f // indirect github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260117110516-f21660bef13f // indirect github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260117110516-f21660bef13f // indirect github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260117110516-f21660bef13f // indirect github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260117110516-f21660bef13f // indirect github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260117110516-f21660bef13f // indirect github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260117110516-f21660bef13f // indirect github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260117110516-f21660bef13f // indirect github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260117110516-f21660bef13f // indirect github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260117110516-f21660bef13f // indirect github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260117110516-f21660bef13f // indirect github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260117110516-f21660bef13f // indirect github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260117110516-f21660bef13f // indirect github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260117110516-f21660bef13f // indirect github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260117110516-f21660bef13f // indirect github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260117110516-f21660bef13f // indirect github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260117110516-f21660bef13f // indirect github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260117110516-f21660bef13f // indirect github.com/sagernet/fswatch v0.1.1 // indirect github.com/sagernet/gvisor v0.0.0-20250822052253-5558536cf237 // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect github.com/sagernet/nftables v0.3.0-beta.4 // indirect github.com/sagernet/sing-mux v0.3.4 // indirect github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 // indirect github.com/sagernet/sing-tun v0.8.0-beta.17 // indirect github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 // indirect github.com/sagernet/smux v1.5.50-sing-box-mod.1 // indirect github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6 // indirect github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20250917110311-16510ac47288 // indirect github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 // indirect github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 // indirect github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect github.com/vishvananda/netns v0.0.5 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/blake3 v0.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect go.uber.org/zap/exp v0.3.0 // indirect go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect golang.org/x/crypto v0.46.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/mod v0.31.0 // indirect golang.org/x/oauth2 v0.32.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.39.0 // indirect golang.org/x/term v0.38.0 // indirect golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.11.0 // indirect golang.org/x/tools v0.40.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard/windows v0.5.3 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.5.1 // indirect lukechampine.com/blake3 v1.3.0 // indirect ) ================================================ FILE: test/go.sum ================================================ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A= github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/anthropics/anthropic-sdk-go v1.19.0 h1:mO6E+ffSzLRvR/YUH9KJC0uGw0uV8GjISIuzem//3KE= github.com/anthropics/anthropic-sdk-go v1.19.0/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE= 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.0 h1:VMleO/XA48gEWes5l+Fh6tRWo9bHkhwAEhx63i+F5ic= github.com/caddyserver/certmagic v0.25.0/go.mod h1:m9yB7Mud24OQbPHOiipAoyKPn9pKHhpSJxXR1jydBxA= github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk= github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso= github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 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/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo= github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI= github.com/database64128/netx-go v0.1.1 h1:dT5LG7Gs7zFZBthFBbzWE6K8wAHjSNAaK7wCYZT7NzM= github.com/database64128/netx-go v0.1.1/go.mod h1:LNlYVipaYkQArRFDNNJ02VkNV+My9A5XR/IGS7sIBQc= github.com/database64128/tfo-go/v2 v2.3.1 h1:EGE+ELd5/AQ0X6YBlQ9RgKs8+kciNhgN3d8lRvfEJQw= github.com/database64128/tfo-go/v2 v2.3.1/go.mod h1:k9wcpg/8i5zenspBkc9jUEYehpZZccBnCElzOJB++bU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk= github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ= github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 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.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/gaissmai/bart v0.18.0 h1:jQLBT/RduJu0pv/tLwXE+xKPgtWJejbxuXAR+wLJafo= github.com/gaissmai/bart v0.18.0/go.mod h1:JJzMAhNF5Rjo4SF4jWBrANuJfqY+FvsFhW7t1UZJ+XY= github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I= github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo= github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced h1:Q311OHjMh/u5E2TITc++WlTP5We0xNseRMkHDyvhW7I= github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/godbus/dbus/v5 v5.2.1 h1:I4wwMdWSkmI57ewd+elNGwLRf2/dtSaFz1DujfWYvOk= github.com/godbus/dbus/v5 v5.2.1/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0= github.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI= github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= github.com/insomniacslk/dhcp v0.0.0-20251020182700-175e84fbb167 h1:MEufgJohwIjFi2n3eJv4c/8UdRLQVUwPwSWQPoER+eU= github.com/insomniacslk/dhcp v0.0.0-20251020182700-175e84fbb167/go.mod h1:qfvBmyDNp+/liLEYWRvqny/PEz9hGe2Dz833eXILSmo= github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I= github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/libdns/acmedns v0.5.0 h1:5pRtmUj4Lb/QkNJSl1xgOGBUJTWW7RjpNaIhjpDXjPE= github.com/libdns/acmedns v0.5.0/go.mod h1:X7UAFP1Ep9NpTwWpVlrZzJLR7epynAy0wrIxSPFgKjQ= github.com/libdns/alidns v1.0.6-beta.3 h1:KAmb7FQ1tRzKsaAUGa7ZpGKAMRANwg7+1c7tUbSELq8= github.com/libdns/alidns v1.0.6-beta.3/go.mod h1:RECwyQ88e9VqQVtSrvX76o1ux3gQUKGzMgxICi+u7Ec= github.com/libdns/cloudflare v0.2.2 h1:XWHv+C1dDcApqazlh08Q6pjytYLgR2a+Y3xrXFu0vsI= github.com/libdns/cloudflare v0.2.2/go.mod h1:w9uTmRCDlAoafAsTPnn2nJ0XHK/eaUMh86DUk8BWi60= github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U= github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg= github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o= github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= github.com/metacubex/utls v1.8.4 h1:HmL9nUApDdWSkgUyodfwF6hSjtiwCGGdyhaSpEejKpg= github.com/metacubex/utls v1.8.4/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko= github.com/mholt/acmez/v3 v3.1.4 h1:DyzZe/RnAzT3rpZj/2Ii5xZpiEvvYk3cQEN/RmqxwFQ= github.com/mholt/acmez/v3 v3.1.4/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc= github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/openai/openai-go/v3 v3.15.0 h1:hk99rM7YPz+M99/5B/zOQcVwFRLLMdprVGx1vaZ8XMo= github.com/openai/openai-go/v3 v3.15.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= 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= github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4= github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkkD2QgdTuzQG263YZ+2emfpeyGqW0= 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-20260117110918-dc1cda1fe287 h1:0BYNmr0ptjsII948U0oBFmrbo4qEaCFcrE2JPRg3Zlk= github.com/sagernet/cronet-go v0.0.0-20260117110918-dc1cda1fe287/go.mod h1:hwFHBEjjthyEquDULbr4c4ucMedp8Drb6Jvm2kt/0Bw= github.com/sagernet/cronet-go/all v0.0.0-20260117110918-dc1cda1fe287 h1:ghxhYSBQpzkakqWqJDvXr/Zmxe0WjTjKuALEGbjGiGY= github.com/sagernet/cronet-go/all v0.0.0-20260117110918-dc1cda1fe287/go.mod h1:M+4ZjPhLJXIvoxcQsbDofmc19Wrig59hZ+hLvj6S3To= github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260117110516-f21660bef13f h1:8jZbZ4KBTdcXDFLwUBNQt5Xci6ZuAKh255S8TwuBCaM= github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260117110516-f21660bef13f/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw= github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260117110516-f21660bef13f h1:tG0hCx+0u5zca7qQ7AMkcv4DCrBG/DKW1ggs/P+BRRI= github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260117110516-f21660bef13f/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM= github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260117110516-f21660bef13f h1:ZXp5hKJIA7iJ52ZShJCKMQEPLpp/7dDIVZmPGV9Il40= github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260117110516-f21660bef13f/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc= github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260117110516-f21660bef13f h1:gL7H8HS8s38adz4/HZtRHh79qMwsbLTRRPz4GQ9LcWI= github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260117110516-f21660bef13f/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ= github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260117110516-f21660bef13f h1:Dchgc0pAY5Jwb5lzUlE+1nhHIzqLx+YOurXLHgvWd/0= github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260117110516-f21660bef13f/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs= github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260117110516-f21660bef13f h1:+MOLSQoduuKDxF410i1LcSPaQGaiP0eZb0INvMlmjM4= github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260117110516-f21660bef13f/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0= github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260117110516-f21660bef13f h1:lIZna05Vn6n8k21p8OpSUnhwGm+E57PrMjiI4ZUfMSg= github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260117110516-f21660bef13f/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0= github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260117110516-f21660bef13f h1:B2aFQ5CRHI20t8YsEizvtguS5W2QfK7D5XV/NzTIxPE= github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260117110516-f21660bef13f/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4= github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260117110516-f21660bef13f h1:qpSwJ1rFGYCfJDenNCZoWYjoG7N+xEa6ke+E7/JO1i4= github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260117110516-f21660bef13f/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo= github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260117110516-f21660bef13f h1:cx7Ipg0tSvTDjS4maMEYz4vuzz93BMPAysmZ1YLrz80= github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260117110516-f21660bef13f/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ= github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260117110516-f21660bef13f h1:4jOHuUiBxD8pJEpBBVQfJqyLmxjpd3t4MLRzU7YLFyg= github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260117110516-f21660bef13f/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU= github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260117110516-f21660bef13f h1:OpXBa2WlRU+Mam9oRe9Nn4/zf7gQ+qiBTNK8A5RwbfQ= github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260117110516-f21660bef13f/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI= github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260117110516-f21660bef13f h1:nJpGFi+6hI85tl4zoyNFEnFEQ5+xEV5gyvsUoMvd8g0= github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260117110516-f21660bef13f/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ= github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260117110516-f21660bef13f h1:SEy2rpmgOJgrqcEryJI/RSnqUWIsEsp0cfYoA8y21jc= github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260117110516-f21660bef13f/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0= github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260117110516-f21660bef13f h1:EW2TuFMLm0iBGqRZtuGwIZdeYmDtDsDmRcRRJQOMxUo= github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260117110516-f21660bef13f/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s= github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260117110516-f21660bef13f h1:3U5woxrNCkzfv1+UX+mVoWh1228AE1qAiMG02F9oFbY= github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260117110516-f21660bef13f/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ= github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260117110516-f21660bef13f h1:YwFTfuWG3mmctroeDYtFZ6LHjGsedVO+5wInYbbUuUY= github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260117110516-f21660bef13f/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow= github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260117110516-f21660bef13f h1:r4V0ddPCRLgGu0VdgR3aUsO9NjpmyjAf+h+3oTD9D6E= github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260117110516-f21660bef13f/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4= github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260117110516-f21660bef13f h1:B8yf4gFvEYUnwWmtVK9sdwUsflYZ387MhYmlOP2ohFQ= github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260117110516-f21660bef13f/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc= github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260117110516-f21660bef13f h1:9YyaMg4rO1/jIgrxmNb0LKH+X7frSYWfX2pFgW5JUVM= github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260117110516-f21660bef13f/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc= github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260117110516-f21660bef13f h1:B0fnGu0sh9yT/9JDN5u/GqThGoOzNN/daOAuGWFLXEk= github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260117110516-f21660bef13f/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260117110516-f21660bef13f h1:lxPcIXKSSI5JDhc7rx/6yufISWM4vtBS2FY9PavWQTs= github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260117110516-f21660bef13f/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw= github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs= github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o= github.com/sagernet/gvisor v0.0.0-20250822052253-5558536cf237 h1:SUPFNB+vSP4RBPrSEgNII+HkfqC8hKMpYLodom4o4EU= github.com/sagernet/gvisor v0.0.0-20250822052253-5558536cf237/go.mod h1:QkkPEJLw59/tfxgapHta14UL5qMUah5NXhO0Kw2Kan4= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= github.com/sagernet/quic-go v0.59.0-sing-box-mod.2 h1:hJUL+HtxEOjxsa0CsucbBVqI/AMS4k52NwNU637zmdw= github.com/sagernet/quic-go v0.59.0-sing-box-mod.2/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4= github.com/sagernet/sing v0.8.0-beta.16 h1:Fe+6E9VHYky9Mx4cf0ugbZPWDcXRflpAu7JQ5bWXvaA= github.com/sagernet/sing v0.8.0-beta.16/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.0-beta.11 h1:eUusxITKKRedhWC2ScUYFUvD96h/QfbKLaS3N6/7in4= github.com/sagernet/sing-quic v0.6.0-beta.11/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= 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.0-beta.17 h1:6DdbNXeTFYj8Tb4FCh8Mp2boA3rVY6VNqzTOObj7Xis= github.com/sagernet/sing-tun v0.8.0-beta.17/go.mod h1:+HAK/y9GZljdT0KYKMYDR8MjjqnqDDQZYp5ZZQoRzS8= 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= github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8= github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6 h1:eYz/OpMqWCvO2++iw3dEuzrlfC2xv78GdlGvprIM6O8= github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6/go.mod h1:m87GAn4UcesHQF3leaPFEINZETO5za1LGn1GJdNDgNc= github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20250917110311-16510ac47288 h1:E2tZFeg9mGYGQ7E7BbxMv1cU35HxwgRm6tPKI2Pp7DA= github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20250917110311-16510ac47288/go.mod h1:WUxgxUDZoCF2sxVmW+STSxatP02Qn3FcafTiI2BLtE0= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spyzhov/ajson v0.9.4 h1:MVibcTCgO7DY4IlskdqIlCmDOsUOZ9P7oKj8ifdcf84= github.com/spyzhov/ajson v0.9.4/go.mod h1:a6oSw0MMb7Z5aD2tPoPO+jq11ETKgXUr2XktHdT8Wt8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ= github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4= github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4= github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg= github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio= github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8= github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw= github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8= github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU= github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0= github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA= github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc= github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:UBPHPtv8+nEAy2PD8RyAhOYvau1ek0HDJqLS/Pysi14= github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ= github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 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/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= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 h1:K0XaT3DwHAcV4nKLzcQvwAgSyisUghWoY20I7huthMk= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0/go.mod h1:B5Ki776z/MBnVha1Nzwp5arlzBbE3+1jk+pGmaP5HME= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 h1:lUsI2TYsQw2r1IASwoROaCnjdj2cvC2+Jbxvk6nHnWU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0/go.mod h1:2HpZxxQurfGxJlJDblybejHB6RX6pmExPNe517hREw4= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek= go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w= golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= 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= ================================================ FILE: test/http_test.go ================================================ package main import ( "net/netip" "testing" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/json/badoption" ) func TestHTTPSelf(t *testing.T) { startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeMixed, Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeHTTP, Tag: "http-out", Options: &option.HTTPOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "http-out", }, }, }, }, }, }, }) testTCP(t, clientPort, testPort) } ================================================ FILE: test/hysteria2_test.go ================================================ package main import ( "net/netip" "testing" "time" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-quic/hysteria2" "github.com/sagernet/sing/common" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json/badoption" ) func TestHysteria2Self(t *testing.T) { t.Run("self", func(t *testing.T) { testHysteria2Self(t, "", false) }) t.Run("self-salamander", func(t *testing.T) { testHysteria2Self(t, "password", false) }) t.Run("self-hop", func(t *testing.T) { testHysteria2Self(t, "", true) }) t.Run("self-hop-salamander", func(t *testing.T) { testHysteria2Self(t, "password", true) }) } func TestHysteria2Hop(t *testing.T) { testHysteria2Self(t, "password", true) } func testHysteria2Self(t *testing.T, salamanderPassword string, portHop bool) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") var obfs *option.Hysteria2Obfs if salamanderPassword != "" { obfs = &option.Hysteria2Obfs{ Type: hysteria2.ObfsTypeSalamander, Password: salamanderPassword, } } var ( serverPorts []string hopInterval time.Duration ) if portHop { serverPorts = []string{F.ToString(serverPort, ":", serverPort)} hopInterval = 5 * time.Second } startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeHysteria2, Options: &option.Hysteria2InboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, UpMbps: 100, DownMbps: 100, Obfs: obfs, Users: []option.Hysteria2User{{ Password: "password", }}, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, KeyPath: keyPem, }, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeHysteria2, Tag: "hy2-out", Options: &option.Hysteria2OutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, ServerPorts: serverPorts, HopInterval: badoption.Duration(hopInterval), UpMbps: 100, DownMbps: 100, Obfs: obfs, Password: "password", OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, }, }, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "hy2-out", }, }, }, }, }, }, }) testSuitLargeUDP(t, clientPort, testPort) if portHop { time.Sleep(5 * time.Second) testSuitLargeUDP(t, clientPort, testPort) } } func TestHysteria2Inbound(t *testing.T) { caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeHysteria2, Options: &option.Hysteria2InboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Obfs: &option.Hysteria2Obfs{ Type: hysteria2.ObfsTypeSalamander, Password: "cry_me_a_r1ver", }, Users: []option.Hysteria2User{{ Password: "password", }}, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, KeyPath: keyPem, }, }, }, }, }, }) startDockerContainer(t, DockerOptions{ Image: ImageHysteria2, Ports: []uint16{serverPort, clientPort}, Cmd: []string{"client", "-c", "/etc/hysteria/config.yml", "--disable-update-check", "--log-level", "debug"}, Bind: map[string]string{ "hysteria2-client.yml": "/etc/hysteria/config.yml", caPem: "/etc/hysteria/ca.pem", }, }) testSuit(t, clientPort, testPort) } func TestHysteria2Outbound(t *testing.T) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startDockerContainer(t, DockerOptions{ Image: ImageHysteria2, Ports: []uint16{testPort}, Cmd: []string{"server", "-c", "/etc/hysteria/config.yml", "--disable-update-check", "--log-level", "debug"}, Bind: map[string]string{ "hysteria2-server.yml": "/etc/hysteria/config.yml", certPem: "/etc/hysteria/cert.pem", keyPem: "/etc/hysteria/key.pem", }, }) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeHysteria2, Options: &option.Hysteria2OutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, Obfs: &option.Hysteria2Obfs{ Type: hysteria2.ObfsTypeSalamander, Password: "cry_me_a_r1ver", }, Password: "password", OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, }, }, }, }, }, }) testSuitSimple1(t, clientPort, testPort) } ================================================ FILE: test/hysteria_test.go ================================================ package main import ( "net/netip" "testing" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/json/badoption" ) func TestHysteriaSelf(t *testing.T) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeHysteria, Options: &option.HysteriaInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, UpMbps: 100, DownMbps: 100, Users: []option.HysteriaUser{{ AuthString: "password", }}, Obfs: "fuck me till the daylight", InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, KeyPath: keyPem, }, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeHysteria, Tag: "hy-out", Options: &option.HysteriaOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, UpMbps: 100, DownMbps: 100, AuthString: "password", Obfs: "fuck me till the daylight", OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, }, }, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "hy-out", }, }, }, }, }, }, }) testSuit(t, clientPort, testPort) } func TestHysteriaInbound(t *testing.T) { caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeHysteria, Options: &option.HysteriaInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, UpMbps: 100, DownMbps: 100, Users: []option.HysteriaUser{{ AuthString: "password", }}, Obfs: "fuck me till the daylight", InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, KeyPath: keyPem, }, }, }, }, }, }) startDockerContainer(t, DockerOptions{ Image: ImageHysteria, Ports: []uint16{serverPort, clientPort}, Cmd: []string{"-c", "/etc/hysteria/config.json", "client"}, Bind: map[string]string{ "hysteria-client.json": "/etc/hysteria/config.json", caPem: "/etc/hysteria/ca.pem", }, }) testSuit(t, clientPort, testPort) } func TestHysteriaOutbound(t *testing.T) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startDockerContainer(t, DockerOptions{ Image: ImageHysteria, Ports: []uint16{testPort}, Cmd: []string{"-c", "/etc/hysteria/config.json", "server"}, Bind: map[string]string{ "hysteria-server.json": "/etc/hysteria/config.json", certPem: "/etc/hysteria/cert.pem", keyPem: "/etc/hysteria/key.pem", }, }) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeHysteria, Options: &option.HysteriaOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, UpMbps: 100, DownMbps: 100, AuthString: "password", Obfs: "fuck me till the daylight", OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, }, }, }, }, }, }) testSuitSimple1(t, clientPort, testPort) } ================================================ FILE: test/inbound_detour_test.go ================================================ package main import ( "net/netip" "testing" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-shadowsocks/shadowaead_2022" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/json/badoption" ) func TestChainedInbound(t *testing.T) { method := shadowaead_2022.List[0] password := mkBase64(t, 16) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeShadowsocks, Options: &option.ShadowsocksInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, Detour: "detour", }, Method: method, Password: password, }, }, { Type: C.TypeShadowsocks, Tag: "detour", Options: &option.ShadowsocksInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: otherPort, }, Method: method, Password: password, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeShadowsocks, Tag: "ss-out", Options: &option.ShadowsocksOutboundOptions{ Method: method, Password: password, DialerOptions: option.DialerOptions{ Detour: "detour-out", }, }, }, { Type: C.TypeShadowsocks, Tag: "detour-out", Options: &option.ShadowsocksOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, Method: method, Password: password, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "ss-out", }, }, }, }, }, }, }) testTCP(t, clientPort, testPort) } ================================================ FILE: test/ktls_test.go ================================================ package main import ( "net/netip" "testing" "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" "github.com/sagernet/sing/common/json/badoption" "github.com/gofrs/uuid/v5" ) func TestKTLS(t *testing.T) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeTrojan, Options: &option.TrojanInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.TrojanUser{ { Name: "sekai", Password: "password", }, }, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, KeyPath: keyPem, // KernelTx: true, }, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeTrojan, Tag: "trojan-out", Options: &option.TrojanOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, Password: "password", OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, KernelTx: true, KernelRx: true, }, }, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "trojan-out", }, }, }, }, }, }, }) testSuit(t, clientPort, testPort) } func TestKTLSECH(t *testing.T) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") echConfig, echKey := common.Must2(tls.ECHKeygenDefault("not.example.org")) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeTrojan, Options: &option.TrojanInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.TrojanUser{ { Name: "sekai", Password: "password", }, }, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, KeyPath: keyPem, KernelTx: true, ECH: &option.InboundECHOptions{ Enabled: true, Key: []string{echKey}, }, }, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeTrojan, Tag: "trojan-out", Options: &option.TrojanOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, Password: "password", OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, KernelTx: true, KernelRx: true, ECH: &option.OutboundECHOptions{ Enabled: true, Config: []string{echConfig}, }, }, }, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "trojan-out", }, }, }, }, }, }, }) testSuit(t, clientPort, testPort) } func TestKTLSReality(t *testing.T) { user, _ := uuid.NewV4() startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeVLESS, Options: &option.VLESSInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.VLESSUser{{UUID: user.String()}}, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, ServerName: "google.com", KernelTx: true, Reality: &option.InboundRealityOptions{ Enabled: true, Handshake: option.InboundRealityHandshakeOptions{ ServerOptions: option.ServerOptions{ Server: "google.com", ServerPort: 443, }, }, ShortID: []string{"0123456789abcdef"}, PrivateKey: "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc", }, }, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeVLESS, Tag: "ss-out", Options: &option.VLESSOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, UUID: user.String(), OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "google.com", KernelTx: true, KernelRx: true, Reality: &option.OutboundRealityOptions{ Enabled: true, ShortID: "0123456789abcdef", PublicKey: "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0", }, UTLS: &option.OutboundUTLSOptions{ Enabled: true, }, }, }, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "ss-out", }, }, }, }, }, }, }) testSuit(t, clientPort, testPort) } ================================================ FILE: test/mkcert.go ================================================ package main import ( "crypto/rand" "crypto/rsa" "crypto/sha1" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "encoding/pem" "math/big" "os" "path/filepath" "testing" "time" "github.com/sagernet/sing/common/rw" "github.com/stretchr/testify/require" ) func createSelfSignedCertificate(t *testing.T, domain string) (caPem, certPem, keyPem string) { const userAndHostname = "sekai@nekohasekai.local" tempDir, err := os.MkdirTemp("", "sing-box-test") require.NoError(t, err) t.Cleanup(func() { os.RemoveAll(tempDir) }) caKey, err := rsa.GenerateKey(rand.Reader, 3072) require.NoError(t, err) spkiASN1, err := x509.MarshalPKIXPublicKey(caKey.Public()) var spki struct { Algorithm pkix.AlgorithmIdentifier SubjectPublicKey asn1.BitString } _, err = asn1.Unmarshal(spkiASN1, &spki) require.NoError(t, err) skid := sha1.Sum(spki.SubjectPublicKey.Bytes) caTpl := &x509.Certificate{ SerialNumber: randomSerialNumber(t), Subject: pkix.Name{ Organization: []string{"sing-box test CA"}, OrganizationalUnit: []string{userAndHostname}, CommonName: "sing-box " + userAndHostname, }, SubjectKeyId: skid[:], NotAfter: time.Now().AddDate(10, 0, 0), NotBefore: time.Now(), KeyUsage: x509.KeyUsageCertSign, BasicConstraintsValid: true, IsCA: true, MaxPathLenZero: true, } caCert, err := x509.CreateCertificate(rand.Reader, caTpl, caTpl, caKey.Public(), caKey) require.NoError(t, err) err = rw.WriteFile(filepath.Join(tempDir, "ca.pem"), pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert})) require.NoError(t, err) key, err := rsa.GenerateKey(rand.Reader, 2048) domainTpl := &x509.Certificate{ SerialNumber: randomSerialNumber(t), Subject: pkix.Name{ Organization: []string{"sing-box test certificate"}, OrganizationalUnit: []string{"sing-box " + userAndHostname}, }, NotBefore: time.Now(), NotAfter: time.Now().AddDate(0, 0, 30), KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, } domainTpl.DNSNames = append(domainTpl.DNSNames, domain) cert, err := x509.CreateCertificate(rand.Reader, domainTpl, caTpl, key.Public(), caKey) require.NoError(t, err) certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert}) privDER, err := x509.MarshalPKCS8PrivateKey(key) require.NoError(t, err) privPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privDER}) err = rw.WriteFile(filepath.Join(tempDir, domain+".pem"), certPEM) require.NoError(t, err) err = rw.WriteFile(filepath.Join(tempDir, domain+".key.pem"), privPEM) require.NoError(t, err) return filepath.Join(tempDir, "ca.pem"), filepath.Join(tempDir, domain+".pem"), filepath.Join(tempDir, domain+".key.pem") } func randomSerialNumber(t *testing.T) *big.Int { serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) require.NoError(t, err) return serialNumber } ================================================ FILE: test/mux_cool_test.go ================================================ package main import ( "net/netip" "os" "testing" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/json/badoption" "github.com/spyzhov/ajson" "github.com/stretchr/testify/require" ) func TestMuxCoolServer(t *testing.T) { userId := newUUID() content, err := os.ReadFile("config/vmess-mux-client.json") require.NoError(t, err) config, err := ajson.Unmarshal(content) require.NoError(t, err) config.MustKey("inbounds").MustIndex(0).MustKey("port").SetNumeric(float64(clientPort)) outbound := config.MustKey("outbounds").MustIndex(0).MustKey("settings").MustKey("vnext").MustIndex(0) outbound.MustKey("port").SetNumeric(float64(serverPort)) user := outbound.MustKey("users").MustIndex(0) user.MustKey("id").SetString(userId.String()) content, err = ajson.Marshal(config) require.NoError(t, err) startDockerContainer(t, DockerOptions{ Image: ImageV2RayCore, Ports: []uint16{serverPort, testPort}, EntryPoint: "v2ray", Cmd: []string{"run"}, Stdin: content, }) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeVMess, Options: &option.VMessInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.VMessUser{ { Name: "sekai", UUID: userId.String(), }, }, }, }, }, }) testSuitSimple(t, clientPort, testPort) } func TestMuxCoolClient(t *testing.T) { user := newUUID() content, err := os.ReadFile("config/vmess-server.json") require.NoError(t, err) config, err := ajson.Unmarshal(content) require.NoError(t, err) inbound := config.MustKey("inbounds").MustIndex(0) inbound.MustKey("port").SetNumeric(float64(serverPort)) inbound.MustKey("settings").MustKey("clients").MustIndex(0).MustKey("id").SetString(user.String()) content, err = ajson.Marshal(config) require.NoError(t, err) startDockerContainer(t, DockerOptions{ Image: ImageXRayCore, Ports: []uint16{serverPort, testPort}, EntryPoint: "xray", Stdin: content, }) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeVMess, Options: &option.VMessOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, UUID: user.String(), PacketEncoding: "xudp", }, }, }, }) testSuitSimple(t, clientPort, testPort) } func TestMuxCoolSelf(t *testing.T) { user := newUUID() startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeVMess, Options: &option.VMessInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.VMessUser{ { Name: "sekai", UUID: user.String(), }, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeVMess, Tag: "vmess-out", Options: &option.VMessOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, UUID: user.String(), PacketEncoding: "xudp", }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "vmess-out", }, }, }, }, }, }, }) testSuitSimple(t, clientPort, testPort) } ================================================ FILE: test/mux_test.go ================================================ package main import ( "net/netip" "testing" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-shadowsocks/shadowaead_2022" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/json/badoption" "github.com/gofrs/uuid/v5" ) var muxProtocols = []string{ "h2mux", "smux", "yamux", } func TestVMessSMux(t *testing.T) { testVMessMux(t, option.OutboundMultiplexOptions{ Enabled: true, Protocol: "smux", }) } func TestShadowsocksMux(t *testing.T) { for _, protocol := range muxProtocols { t.Run(protocol, func(t *testing.T) { testShadowsocksMux(t, option.OutboundMultiplexOptions{ Enabled: true, Protocol: protocol, }) }) } } func TestShadowsockH2Mux(t *testing.T) { testShadowsocksMux(t, option.OutboundMultiplexOptions{ Enabled: true, Protocol: "h2mux", Padding: true, }) } func TestShadowsockSMuxPadding(t *testing.T) { testShadowsocksMux(t, option.OutboundMultiplexOptions{ Enabled: true, Protocol: "smux", Padding: true, }) } func testShadowsocksMux(t *testing.T, options option.OutboundMultiplexOptions) { method := shadowaead_2022.List[0] password := mkBase64(t, 16) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeShadowsocks, Options: &option.ShadowsocksInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Method: method, Password: password, Multiplex: &option.InboundMultiplexOptions{ Enabled: true, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeShadowsocks, Tag: "ss-out", Options: &option.ShadowsocksOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, Method: method, Password: password, Multiplex: &options, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "ss-out", }, }, }, }, }, }, }) testSuit(t, clientPort, testPort) } func testVMessMux(t *testing.T, options option.OutboundMultiplexOptions) { user, _ := uuid.NewV4() startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeVMess, Options: &option.VMessInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.VMessUser{ { UUID: user.String(), }, }, Multiplex: &option.InboundMultiplexOptions{ Enabled: true, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeVMess, Tag: "vmess-out", Options: &option.VMessOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, Security: "auto", UUID: user.String(), Multiplex: &options, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "vmess-out", }, }, }, }, }, }, }) testSuit(t, clientPort, testPort) } ================================================ FILE: test/naive_self_test.go ================================================ //go:build with_naive_outbound package main import ( "net/netip" "os" "strings" "testing" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/protocol/naive" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/auth" "github.com/sagernet/sing/common/json/badoption" "github.com/sagernet/sing/common/network" "github.com/stretchr/testify/require" ) func TestNaiveSelf(t *testing.T) { caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") caPemContent, err := os.ReadFile(caPem) require.NoError(t, err) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeNaive, Tag: "naive-in", Options: &option.NaiveInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []auth.User{ { Username: "sekai", Password: "password", }, }, Network: network.NetworkTCP, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, KeyPath: keyPem, }, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeNaive, Tag: "naive-out", Options: &option.NaiveOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, Username: "sekai", Password: "password", OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "example.org", Certificate: []string{string(caPemContent)}, }, }, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "naive-out", }, }, }, }, }, }, }) testTCP(t, clientPort, testPort) } func TestNaiveSelfECH(t *testing.T) { caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") caPemContent, err := os.ReadFile(caPem) require.NoError(t, err) echConfig, echKey := common.Must2(tls.ECHKeygenDefault("not.example.org")) instance := startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeNaive, Tag: "naive-in", Options: &option.NaiveInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []auth.User{ { Username: "sekai", Password: "password", }, }, Network: network.NetworkTCP, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, KeyPath: keyPem, ECH: &option.InboundECHOptions{ Enabled: true, Key: []string{echKey}, }, }, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeNaive, Tag: "naive-out", Options: &option.NaiveOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, Username: "sekai", Password: "password", OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "example.org", Certificate: []string{string(caPemContent)}, ECH: &option.OutboundECHOptions{ Enabled: true, Config: []string{echConfig}, }, }, }, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "naive-out", }, }, }, }, }, }, }) naiveOut, ok := instance.Outbound().Outbound("naive-out") require.True(t, ok) naiveOutbound := naiveOut.(*naive.Outbound) netLogPath := "/tmp/naive_ech_netlog.json" require.True(t, naiveOutbound.Client().Engine().StartNetLogToFile(netLogPath, true)) defer naiveOutbound.Client().Engine().StopNetLog() testTCP(t, clientPort, testPort) naiveOutbound.Client().Engine().StopNetLog() logContent, err := os.ReadFile(netLogPath) require.NoError(t, err) logStr := string(logContent) require.True(t, strings.Contains(logStr, `"encrypted_client_hello":true`), "ECH should be accepted in TLS handshake. NetLog saved to: %s", netLogPath) } func TestNaiveSelfInsecureConcurrency(t *testing.T) { caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") caPemContent, err := os.ReadFile(caPem) require.NoError(t, err) instance := startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeNaive, Tag: "naive-in", Options: &option.NaiveInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []auth.User{ { Username: "sekai", Password: "password", }, }, Network: network.NetworkTCP, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, KeyPath: keyPem, }, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeNaive, Tag: "naive-out", Options: &option.NaiveOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, Username: "sekai", Password: "password", InsecureConcurrency: 3, OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "example.org", Certificate: []string{string(caPemContent)}, }, }, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "naive-out", }, }, }, }, }, }, }) naiveOut, ok := instance.Outbound().Outbound("naive-out") require.True(t, ok) naiveOutbound := naiveOut.(*naive.Outbound) netLogPath := "/tmp/naive_concurrency_netlog.json" require.True(t, naiveOutbound.Client().Engine().StartNetLogToFile(netLogPath, true)) defer naiveOutbound.Client().Engine().StopNetLog() // Send multiple sequential connections to trigger round-robin // With insecure_concurrency=3, connections will be distributed to 3 pools for i := 0; i < 6; i++ { testTCP(t, clientPort, testPort) } naiveOutbound.Client().Engine().StopNetLog() // Verify NetLog contains multiple independent HTTP/2 sessions logContent, err := os.ReadFile(netLogPath) require.NoError(t, err) logStr := string(logContent) // Count HTTP2_SESSION_INITIALIZED events to verify connection pool isolation // NetLog stores event types as numeric IDs, HTTP2_SESSION_INITIALIZED = 249 sessionCount := strings.Count(logStr, `"type":249`) require.GreaterOrEqual(t, sessionCount, 3, "Expected at least 3 HTTP/2 sessions with insecure_concurrency=3. NetLog: %s", netLogPath) } func TestNaiveSelfQUIC(t *testing.T) { caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") caPemContent, err := os.ReadFile(caPem) require.NoError(t, err) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeNaive, Tag: "naive-in", Options: &option.NaiveInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []auth.User{ { Username: "sekai", Password: "password", }, }, Network: network.NetworkUDP, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, KeyPath: keyPem, }, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeNaive, Tag: "naive-out", Options: &option.NaiveOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, Username: "sekai", Password: "password", QUIC: true, OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "example.org", Certificate: []string{string(caPemContent)}, }, }, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "naive-out", }, }, }, }, }, }, }) testTCP(t, clientPort, testPort) } func TestNaiveSelfQUICCongestionControl(t *testing.T) { testCases := []struct { name string congestionControl string }{ {"BBR", "bbr"}, {"BBR2", "bbr2"}, {"Cubic", "cubic"}, {"Reno", "reno"}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") caPemContent, err := os.ReadFile(caPem) require.NoError(t, err) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeNaive, Tag: "naive-in", Options: &option.NaiveInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []auth.User{ { Username: "sekai", Password: "password", }, }, Network: network.NetworkUDP, QUICCongestionControl: tc.congestionControl, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, KeyPath: keyPem, }, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeNaive, Tag: "naive-out", Options: &option.NaiveOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, Username: "sekai", Password: "password", QUIC: true, QUICCongestionControl: tc.congestionControl, OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "example.org", Certificate: []string{string(caPemContent)}, }, }, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "naive-out", }, }, }, }, }, }, }) testTCP(t, clientPort, testPort) }) } } ================================================ FILE: test/naive_test.go ================================================ package main import ( "net/netip" "testing" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/auth" "github.com/sagernet/sing/common/json/badoption" "github.com/sagernet/sing/common/network" ) func TestNaiveInboundWithNginx(t *testing.T) { caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeNaive, Options: &option.NaiveInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: otherPort, }, Users: []auth.User{ { Username: "sekai", Password: "password", }, }, Network: network.NetworkTCP, }, }, }, }) startDockerContainer(t, DockerOptions{ Image: ImageNginx, Ports: []uint16{serverPort, otherPort}, Bind: map[string]string{ "nginx.conf": "/etc/nginx/nginx.conf", "naive-nginx.conf": "/etc/nginx/conf.d/naive.conf", certPem: "/etc/nginx/cert.pem", keyPem: "/etc/nginx/key.pem", }, }) startDockerContainer(t, DockerOptions{ Image: ImageNaive, Ports: []uint16{serverPort, clientPort}, Bind: map[string]string{ "naive.json": "/etc/naiveproxy/config.json", caPem: "/etc/naiveproxy/ca.pem", }, Env: []string{ "SSL_CERT_FILE=/etc/naiveproxy/ca.pem", }, }) testTCP(t, clientPort, testPort) } func TestNaiveInbound(t *testing.T) { caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeNaive, Options: &option.NaiveInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []auth.User{ { Username: "sekai", Password: "password", }, }, Network: network.NetworkTCP, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, KeyPath: keyPem, }, }, }, }, }, }) startDockerContainer(t, DockerOptions{ Image: ImageNaive, Ports: []uint16{serverPort, clientPort}, Bind: map[string]string{ "naive.json": "/etc/naiveproxy/config.json", caPem: "/etc/naiveproxy/ca.pem", }, Env: []string{ "SSL_CERT_FILE=/etc/naiveproxy/ca.pem", }, }) testTCP(t, clientPort, testPort) } func TestNaiveHTTP3Inbound(t *testing.T) { caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeNaive, Options: &option.NaiveInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []auth.User{ { Username: "sekai", Password: "password", }, }, Network: network.NetworkUDP, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, KeyPath: keyPem, }, }, }, }, }, }) startDockerContainer(t, DockerOptions{ Image: ImageNaive, Ports: []uint16{serverPort, clientPort}, Bind: map[string]string{ "naive-quic.json": "/etc/naiveproxy/config.json", caPem: "/etc/naiveproxy/ca.pem", }, Env: []string{ "SSL_CERT_FILE=/etc/naiveproxy/ca.pem", }, }) testTCP(t, clientPort, testPort) } ================================================ FILE: test/reality_test.go ================================================ package main import ( "net/netip" "testing" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/json/badoption" "github.com/gofrs/uuid/v5" ) func TestReality(t *testing.T) { user, _ := uuid.NewV4() startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeVLESS, Options: &option.VLESSInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.VLESSUser{{UUID: user.String()}}, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, ServerName: "google.com", Reality: &option.InboundRealityOptions{ Enabled: true, Handshake: option.InboundRealityHandshakeOptions{ ServerOptions: option.ServerOptions{ Server: "google.com", ServerPort: 443, }, }, ShortID: []string{"0123456789abcdef"}, PrivateKey: "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc", }, }, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeVLESS, Tag: "ss-out", Options: &option.VLESSOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, UUID: user.String(), OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "google.com", Reality: &option.OutboundRealityOptions{ Enabled: true, ShortID: "0123456789abcdef", PublicKey: "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0", }, UTLS: &option.OutboundUTLSOptions{ Enabled: true, }, }, }, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "ss-out", }, }, }, }, }, }, }) testSuit(t, clientPort, testPort) } ================================================ FILE: test/shadowsocks_legacy_test.go ================================================ package main import ( "net/netip" "testing" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-shadowsocks2/shadowstream" "github.com/sagernet/sing/common" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json/badoption" ) func TestShadowsocksLegacy(t *testing.T) { testShadowsocksLegacy(t, shadowstream.MethodList[0]) } func testShadowsocksLegacy(t *testing.T, method string) { startDockerContainer(t, DockerOptions{ Image: ImageShadowsocksLegacy, Ports: []uint16{serverPort}, Env: []string{ "SS_MODULE=ss-server", F.ToString("SS_CONFIG=-s 0.0.0.0 -u -p 10000 -m ", method, " -k FzcLbKs2dY9mhL"), }, }) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeShadowsocks, Options: &option.ShadowsocksOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, Method: method, Password: "FzcLbKs2dY9mhL", }, }, }, }) testSuitSimple(t, clientPort, testPort) } ================================================ FILE: test/shadowsocks_test.go ================================================ package main import ( "crypto/rand" "encoding/base64" "net/netip" "testing" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-shadowsocks/shadowaead_2022" "github.com/sagernet/sing/common" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json/badoption" "github.com/stretchr/testify/require" ) const ( serverPort uint16 = 10000 + iota clientPort testPort otherPort otherClientPort ) func TestShadowsocks(t *testing.T) { for _, method := range []string{ "aes-128-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", } { t.Run(method+"-inbound", func(t *testing.T) { testShadowsocksInboundWithShadowsocksRust(t, method, mkBase64(t, 16)) }) t.Run(method+"-outbound", func(t *testing.T) { testShadowsocksOutboundWithShadowsocksRust(t, method, mkBase64(t, 16)) }) t.Run(method+"-self", func(t *testing.T) { testShadowsocksSelf(t, method, mkBase64(t, 16)) }) } } func TestShadowsocksNone(t *testing.T) { testShadowsocksSelf(t, "none", "") } func TestShadowsocks2022(t *testing.T) { for _, method16 := range []string{ "2022-blake3-aes-128-gcm", } { t.Run(method16+"-inbound", func(t *testing.T) { testShadowsocksInboundWithShadowsocksRust(t, method16, mkBase64(t, 16)) }) t.Run(method16+"-outbound", func(t *testing.T) { testShadowsocksOutboundWithShadowsocksRust(t, method16, mkBase64(t, 16)) }) t.Run(method16+"-self", func(t *testing.T) { testShadowsocksSelf(t, method16, mkBase64(t, 16)) }) } for _, method32 := range []string{ "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305", } { t.Run(method32+"-inbound", func(t *testing.T) { testShadowsocksInboundWithShadowsocksRust(t, method32, mkBase64(t, 32)) }) t.Run(method32+"-outbound", func(t *testing.T) { testShadowsocksOutboundWithShadowsocksRust(t, method32, mkBase64(t, 32)) }) t.Run(method32+"-self", func(t *testing.T) { testShadowsocksSelf(t, method32, mkBase64(t, 32)) }) } } func TestShadowsocks2022EIH(t *testing.T) { for _, method16 := range []string{ "2022-blake3-aes-128-gcm", } { t.Run(method16, func(t *testing.T) { testShadowsocks2022EIH(t, method16, mkBase64(t, 16)) }) } for _, method32 := range []string{ "2022-blake3-aes-256-gcm", } { t.Run(method32, func(t *testing.T) { testShadowsocks2022EIH(t, method32, mkBase64(t, 32)) }) } } func testShadowsocksInboundWithShadowsocksRust(t *testing.T, method string, password string) { startDockerContainer(t, DockerOptions{ Image: ImageShadowsocksRustClient, EntryPoint: "sslocal", Ports: []uint16{serverPort, clientPort}, Cmd: []string{"-s", F.ToString("127.0.0.1:", serverPort), "-b", F.ToString("0.0.0.0:", clientPort), "-m", method, "-k", password, "-U"}, }) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeShadowsocks, Options: &option.ShadowsocksInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Method: method, Password: password, }, }, }, }) testSuit(t, clientPort, testPort) } func testShadowsocksOutboundWithShadowsocksRust(t *testing.T, method string, password string) { startDockerContainer(t, DockerOptions{ Image: ImageShadowsocksRustServer, EntryPoint: "ssserver", Ports: []uint16{serverPort, testPort}, Cmd: []string{"-s", F.ToString("0.0.0.0:", serverPort), "-m", method, "-k", password, "-U"}, }) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeShadowsocks, Options: &option.ShadowsocksOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, Method: method, Password: password, }, }, }, }) testSuit(t, clientPort, testPort) } func testShadowsocksSelf(t *testing.T, method string, password string) { startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeShadowsocks, Options: &option.ShadowsocksInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Method: method, Password: password, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeShadowsocks, Tag: "ss-out", Options: &option.ShadowsocksOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, Method: method, Password: password, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "ss-out", }, }, }, }, }, }, }) testSuit(t, clientPort, testPort) } func TestShadowsocksUoT(t *testing.T) { method := shadowaead_2022.List[0] password := mkBase64(t, 16) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeShadowsocks, Options: &option.ShadowsocksInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Method: method, Password: password, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeShadowsocks, Tag: "ss-out", Options: &option.ShadowsocksOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, Method: method, Password: password, UDPOverTCP: &option.UDPOverTCPOptions{ Enabled: true, }, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "ss-out", }, }, }, }, }, }, }) testSuit(t, clientPort, testPort) } func testShadowsocks2022EIH(t *testing.T, method string, password string) { startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeShadowsocks, Options: &option.ShadowsocksInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Method: method, Password: password, Users: []option.ShadowsocksUser{ { Password: password, }, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeShadowsocks, Tag: "ss-out", Options: &option.ShadowsocksOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, Method: method, Password: password + ":" + password, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "ss-out", }, }, }, }, }, }, }) testSuit(t, clientPort, testPort) } func mkBase64(t *testing.T, length int) string { psk := make([]byte, length) _, err := rand.Read(psk) require.NoError(t, err) return base64.StdEncoding.EncodeToString(psk) } ================================================ FILE: test/shadowtls_test.go ================================================ package main import ( "context" "crypto/tls" "net" "net/http" "net/netip" "testing" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-shadowsocks/shadowaead_2022" "github.com/sagernet/sing/common" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json/badoption" "github.com/stretchr/testify/require" ) func TestShadowTLS(t *testing.T) { t.Run("v1", func(t *testing.T) { testShadowTLS(t, 1, "", false, option.ShadowTLSWildcardSNIOff) }) t.Run("v2", func(t *testing.T) { testShadowTLS(t, 2, "hello", false, option.ShadowTLSWildcardSNIOff) }) t.Run("v3", func(t *testing.T) { testShadowTLS(t, 3, "hello", false, option.ShadowTLSWildcardSNIOff) }) t.Run("v2-utls", func(t *testing.T) { testShadowTLS(t, 2, "hello", true, option.ShadowTLSWildcardSNIOff) }) t.Run("v3-utls", func(t *testing.T) { testShadowTLS(t, 3, "hello", true, option.ShadowTLSWildcardSNIOff) }) t.Run("v3-wildcard-sni-authed", func(t *testing.T) { testShadowTLS(t, 3, "hello", false, option.ShadowTLSWildcardSNIAuthed) }) t.Run("v3-wildcard-sni-all", func(t *testing.T) { testShadowTLS(t, 3, "hello", false, option.ShadowTLSWildcardSNIAll) }) t.Run("v3-wildcard-sni-authed-utls", func(t *testing.T) { testShadowTLS(t, 3, "hello", true, option.ShadowTLSWildcardSNIAll) }) t.Run("v3-wildcard-sni-all-utls", func(t *testing.T) { testShadowTLS(t, 3, "hello", true, option.ShadowTLSWildcardSNIAll) }) } func testShadowTLS(t *testing.T, version int, password string, utlsEanbled bool, wildcardSNI option.WildcardSNI) { method := shadowaead_2022.List[0] ssPassword := mkBase64(t, 16) var clientServerName string if wildcardSNI != option.ShadowTLSWildcardSNIOff { clientServerName = "cloudflare.com" } else { clientServerName = "google.com" } startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeShadowTLS, Tag: "in", Options: &option.ShadowTLSInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, Detour: "detour", }, Handshake: option.ShadowTLSHandshakeOptions{ ServerOptions: option.ServerOptions{ Server: "google.com", ServerPort: 443, }, }, Version: version, Password: password, Users: []option.ShadowTLSUser{{Password: password}}, WildcardSNI: wildcardSNI, }, }, { Type: C.TypeShadowsocks, Tag: "detour", Options: &option.ShadowsocksInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: otherPort, }, Method: method, Password: ssPassword, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeShadowsocks, Options: &option.ShadowsocksOutboundOptions{ Method: method, Password: ssPassword, DialerOptions: option.DialerOptions{ Detour: "detour", }, }, }, { Type: C.TypeShadowTLS, Tag: "detour", Options: &option.ShadowTLSOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: clientServerName, UTLS: &option.OutboundUTLSOptions{ Enabled: utlsEanbled, }, }, }, Version: version, Password: password, }, }, { Type: C.TypeDirect, Tag: "direct", }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"detour"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "direct", }, }, }, }, }, }, }) testTCP(t, clientPort, testPort) } func TestShadowTLSFallback(t *testing.T) { startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeShadowTLS, Options: &option.ShadowTLSInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Handshake: option.ShadowTLSHandshakeOptions{ ServerOptions: option.ServerOptions{ Server: "bing.com", ServerPort: 443, }, }, Version: 3, Users: []option.ShadowTLSUser{ {Password: "hello"}, }, }, }, }, }) client := &http.Client{ Transport: &http.Transport{ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { var d net.Dialer return d.DialContext(ctx, network, "127.0.0.1:"+F.ToString(serverPort)) }, }, } response, err := client.Get("https://bing.com") require.NoError(t, err) require.Equal(t, response.StatusCode, 200) response.Body.Close() client.CloseIdleConnections() } func TestShadowTLSFallbackWildcardAll(t *testing.T) { startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeShadowTLS, Options: &option.ShadowTLSInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Version: 3, Users: []option.ShadowTLSUser{ {Password: "hello"}, }, WildcardSNI: option.ShadowTLSWildcardSNIAll, }, }, }, }) client := &http.Client{ Transport: &http.Transport{ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { var d net.Dialer return d.DialContext(ctx, network, "127.0.0.1:"+F.ToString(serverPort)) }, }, } response, err := client.Get("https://www.bing.com") require.NoError(t, err) require.Equal(t, response.StatusCode, 200) response.Body.Close() client.CloseIdleConnections() } func TestShadowTLSFallbackWildcardAuthedFail(t *testing.T) { startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeShadowTLS, Options: &option.ShadowTLSInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Handshake: option.ShadowTLSHandshakeOptions{ ServerOptions: option.ServerOptions{ Server: "bing.com", ServerPort: 443, }, }, Version: 3, Users: []option.ShadowTLSUser{ {Password: "hello"}, }, WildcardSNI: option.ShadowTLSWildcardSNIAuthed, }, }, }, }) client := &http.Client{ Transport: &http.Transport{ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { var d net.Dialer return d.DialContext(ctx, network, "127.0.0.1:"+F.ToString(serverPort)) }, }, } _, err := client.Get("https://baidu.com") expected := &tls.CertificateVerificationError{} require.ErrorAs(t, err, &expected) client.CloseIdleConnections() } func TestShadowTLSFallbackWildcardOffFail(t *testing.T) { startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeShadowTLS, Options: &option.ShadowTLSInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Handshake: option.ShadowTLSHandshakeOptions{ ServerOptions: option.ServerOptions{ Server: "bing.com", ServerPort: 443, }, }, Version: 3, Users: []option.ShadowTLSUser{ {Password: "hello"}, }, WildcardSNI: option.ShadowTLSWildcardSNIOff, }, }, }, }) client := &http.Client{ Transport: &http.Transport{ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { var d net.Dialer return d.DialContext(ctx, network, "127.0.0.1:"+F.ToString(serverPort)) }, }, } _, err := client.Get("https://baidu.com") expected := &tls.CertificateVerificationError{} require.ErrorAs(t, err, &expected) client.CloseIdleConnections() } func TestShadowTLSInbound(t *testing.T) { method := shadowaead_2022.List[0] password := mkBase64(t, 16) startDockerContainer(t, DockerOptions{ Image: ImageShadowTLS, Ports: []uint16{serverPort, otherPort}, EntryPoint: "shadow-tls", Cmd: []string{"--v3", "--threads", "1", "client", "--listen", "0.0.0.0:" + F.ToString(otherPort), "--server", "127.0.0.1:" + F.ToString(serverPort), "--sni", "google.com", "--password", password}, }) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeShadowTLS, Options: &option.ShadowTLSInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, Detour: "detour", }, Handshake: option.ShadowTLSHandshakeOptions{ ServerOptions: option.ServerOptions{ Server: "google.com", ServerPort: 443, }, }, Version: 3, Users: []option.ShadowTLSUser{ {Password: password}, }, }, }, { Type: C.TypeShadowsocks, Tag: "detour", Options: &option.ShadowsocksInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), }, Method: method, Password: password, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeShadowsocks, Tag: "out", Options: &option.ShadowsocksOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: otherPort, }, Method: method, Password: password, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "out", }, }, }, }, }, }, }) testTCP(t, clientPort, testPort) } func TestShadowTLSOutbound(t *testing.T) { method := shadowaead_2022.List[0] password := mkBase64(t, 16) startDockerContainer(t, DockerOptions{ Image: ImageShadowTLS, Ports: []uint16{serverPort, otherPort}, EntryPoint: "shadow-tls", Cmd: []string{"--v3", "--threads", "1", "server", "--listen", "0.0.0.0:" + F.ToString(serverPort), "--server", "127.0.0.1:" + F.ToString(otherPort), "--tls", "google.com:443", "--password", "hello"}, Env: []string{"RUST_LOG=trace"}, }) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeShadowsocks, Tag: "detour", Options: &option.ShadowsocksInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: otherPort, }, Method: method, Password: password, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeShadowsocks, Options: &option.ShadowsocksOutboundOptions{ Method: method, Password: password, DialerOptions: option.DialerOptions{ Detour: "detour", }, }, }, { Type: C.TypeShadowTLS, Tag: "detour", Options: &option.ShadowTLSOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "google.com", }, }, Version: 3, Password: "hello", }, }, { Type: C.TypeDirect, Tag: "direct", }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"detour"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "direct", }, }, }, }, }, }, }) testTCP(t, clientPort, testPort) } ================================================ FILE: test/socks_test.go ================================================ package main import ( "context" "net" "net/netip" "testing" "time" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json/badoption" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/protocol/socks" "github.com/stretchr/testify/require" ) func TestSOCKSUDPTimeout(t *testing.T) { const testTimeout = 2 * time.Second udpTimeout := option.UDPTimeoutCompat(testTimeout) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeSOCKS, Tag: "socks-in", Options: &option.SocksInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, UDPTimeout: udpTimeout, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, }, }) testUDPSessionIdleTimeout(t, clientPort, testPort, testTimeout) } func TestMixedUDPTimeout(t *testing.T) { const testTimeout = 2 * time.Second udpTimeout := option.UDPTimeoutCompat(testTimeout) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, UDPTimeout: udpTimeout, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, }, }) testUDPSessionIdleTimeout(t, clientPort, testPort, testTimeout) } func testUDPSessionIdleTimeout(t *testing.T, proxyPort uint16, echoPort uint16, expectedTimeout time.Duration) { echoServer, err := listenPacket("udp", ":"+F.ToString(echoPort)) require.NoError(t, err) defer echoServer.Close() go func() { buffer := make([]byte, 1024) for { n, address, err := echoServer.ReadFrom(buffer) if err != nil { return } _, _ = echoServer.WriteTo(buffer[:n], address) } }() dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", proxyPort), socks.Version5, "", "") packetConn, err := dialer.ListenPacket(context.Background(), M.ParseSocksaddrHostPort("127.0.0.1", echoPort)) require.NoError(t, err) defer packetConn.Close() remoteAddress := &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: int(echoPort)} _, err = packetConn.WriteTo([]byte("hello"), remoteAddress) require.NoError(t, err) buffer := make([]byte, 1024) packetConn.SetReadDeadline(time.Now().Add(5 * time.Second)) n, _, err := packetConn.ReadFrom(buffer) require.NoError(t, err, "failed to receive echo response") require.Equal(t, "hello", string(buffer[:n])) t.Log("UDP echo successful, session established") packetConn.SetReadDeadline(time.Time{}) waitTime := expectedTimeout + time.Second t.Logf("Waiting %v for UDP session to timeout...", waitTime) time.Sleep(waitTime) _, err = packetConn.WriteTo([]byte("after-timeout"), remoteAddress) if err != nil { t.Logf("Write after timeout correctly failed: %v", err) return } packetConn.SetReadDeadline(time.Now().Add(3 * time.Second)) n, _, err = packetConn.ReadFrom(buffer) if err != nil { t.Logf("Read after timeout correctly failed: %v", err) return } t.Fatalf("UDP session should have timed out after %v, but received response: %s", expectedTimeout, string(buffer[:n])) } ================================================ FILE: test/ss_plugin_test.go ================================================ package main import ( "net/netip" "testing" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/json/badoption" ) func TestShadowsocksObfs(t *testing.T) { for _, mode := range []string{ "http", "tls", } { t.Run("obfs-local "+mode, func(t *testing.T) { testShadowsocksPlugin(t, "obfs-local", "obfs="+mode, "--plugin obfs-server --plugin-opts obfs="+mode) }) } } // Since I can't test this on m1 mac (rosetta error: bss_size overflow), I don't care about it func _TestShadowsocksV2RayPlugin(t *testing.T) { testShadowsocksPlugin(t, "v2ray-plugin", "", "--plugin v2ray-plugin --plugin-opts=server") } func testShadowsocksPlugin(t *testing.T, name string, opts string, args string) { startDockerContainer(t, DockerOptions{ Image: ImageShadowsocksLegacy, Ports: []uint16{serverPort, testPort}, Env: []string{ "SS_MODULE=ss-server", "SS_CONFIG=-s 0.0.0.0 -u -p 10000 -m chacha20-ietf-poly1305 -k FzcLbKs2dY9mhL " + args, }, }) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeShadowsocks, Options: &option.ShadowsocksOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, Method: "chacha20-ietf-poly1305", Password: "FzcLbKs2dY9mhL", Plugin: name, PluginOptions: opts, }, }, }, }) testSuitSimple(t, clientPort, testPort) } ================================================ FILE: test/tfo_test.go ================================================ package main import ( "net/netip" "testing" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-shadowsocks/shadowaead" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/json/badoption" ) func TestTCPSlowOpen(t *testing.T) { method := shadowaead.List[0] password := mkBase64(t, 16) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeShadowsocks, Options: &option.ShadowsocksInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, TCPFastOpen: true, }, Method: method, Password: password, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeShadowsocks, Tag: "ss-out", Options: &option.ShadowsocksOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, DialerOptions: option.DialerOptions{ TCPFastOpen: true, }, Method: method, Password: password, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "ss-out", }, }, }, }, }, }, }) testSuit(t, clientPort, testPort) } ================================================ FILE: test/tls_test.go ================================================ package main import ( "net/netip" "testing" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/json/badoption" ) func TestUTLS(t *testing.T) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeTrojan, Options: &option.TrojanInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.TrojanUser{ { Name: "sekai", Password: "password", }, }, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, KeyPath: keyPem, }, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeTrojan, Tag: "trojan-out", Options: &option.TrojanOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, Password: "password", OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, UTLS: &option.OutboundUTLSOptions{ Enabled: true, Fingerprint: "chrome", }, }, }, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "trojan-out", }, }, }, }, }, }, }) testSuit(t, clientPort, testPort) } ================================================ FILE: test/trojan_test.go ================================================ package main import ( "net/netip" "testing" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/json/badoption" ) func TestTrojanOutbound(t *testing.T) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startDockerContainer(t, DockerOptions{ Image: ImageTrojan, Ports: []uint16{serverPort, testPort}, Bind: map[string]string{ "trojan.json": "/config/config.json", certPem: "/path/to/certificate.crt", keyPem: "/path/to/private.key", }, }) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeTrojan, Options: &option.TrojanOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, Password: "password", OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, }, }, }, }, }, }) testSuit(t, clientPort, testPort) } func TestTrojanSelf(t *testing.T) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeTrojan, Options: &option.TrojanInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.TrojanUser{ { Name: "sekai", Password: "password", }, }, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, KeyPath: keyPem, }, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeTrojan, Tag: "trojan-out", Options: &option.TrojanOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, Password: "password", OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, }, }, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "trojan-out", }, }, }, }, }, }, }) testSuit(t, clientPort, testPort) } func TestTrojanPlainSelf(t *testing.T) { startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeTrojan, Options: &option.TrojanInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.TrojanUser{ { Name: "sekai", Password: "password", }, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeTrojan, Tag: "trojan-out", Options: &option.TrojanOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, Password: "password", }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "trojan-out", }, }, }, }, }, }, }) testSuit(t, clientPort, testPort) } ================================================ FILE: test/tuic_test.go ================================================ package main import ( "net/netip" "testing" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/json/badoption" "github.com/gofrs/uuid/v5" ) func TestTUICSelf(t *testing.T) { t.Run("self", func(t *testing.T) { testTUICSelf(t, false, false) }) t.Run("self-udp-stream", func(t *testing.T) { testTUICSelf(t, true, false) }) t.Run("self-early", func(t *testing.T) { testTUICSelf(t, false, true) }) } func testTUICSelf(t *testing.T, udpStream bool, zeroRTTHandshake bool) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") var udpRelayMode string if udpStream { udpRelayMode = "quic" } startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeTUIC, Options: &option.TUICInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.TUICUser{{ UUID: uuid.Nil.String(), }}, ZeroRTTHandshake: zeroRTTHandshake, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, KeyPath: keyPem, }, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeTUIC, Tag: "tuic-out", Options: &option.TUICOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, UUID: uuid.Nil.String(), UDPRelayMode: udpRelayMode, ZeroRTTHandshake: zeroRTTHandshake, OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, }, }, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "tuic-out", }, }, }, }, }, }, }) testSuitLargeUDP(t, clientPort, testPort) } func TestTUICInbound(t *testing.T) { caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeTUIC, Options: &option.TUICInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.TUICUser{{ UUID: "FE35D05B-8803-45C4-BAE6-723AD2CD5D3D", Password: "tuic", }}, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, KeyPath: keyPem, }, }, }, }, }, }) startDockerContainer(t, DockerOptions{ Image: ImageTUICClient, Ports: []uint16{serverPort, clientPort}, Bind: map[string]string{ "tuic-client.json": "/etc/tuic/config.json", caPem: "/etc/tuic/ca.pem", }, }) testSuitLargeUDP(t, clientPort, testPort) } func TestTUICOutbound(t *testing.T) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startDockerContainer(t, DockerOptions{ Image: ImageTUICServer, Ports: []uint16{testPort}, Bind: map[string]string{ "tuic-server.json": "/etc/tuic/config.json", certPem: "/etc/tuic/cert.pem", keyPem: "/etc/tuic/key.pem", }, }) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeTUIC, Options: &option.TUICOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, UUID: "FE35D05B-8803-45C4-BAE6-723AD2CD5D3D", Password: "tuic", OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, }, }, }, }, }, }) testSuitLargeUDP(t, clientPort, testPort) } ================================================ FILE: test/v2ray_api_test.go ================================================ package main /* import ( "context" "net/netip" "testing" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental/v2rayapi" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/json/badoption" "github.com/stretchr/testify/require" ) func TestV2RayAPI(t *testing.T) { i := startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, Tag: "out", }, }, Experimental: &option.ExperimentalOptions{ V2RayAPI: &option.V2RayAPIOptions{ Listen: "127.0.0.1:8080", Stats: &option.V2RayStatsServiceOptions{ Enabled: true, Inbounds: []string{"in"}, Outbounds: []string{"out"}, }, }, }, }) testSuit(t, clientPort, testPort) statsService := i.Router().V2RayServer().StatsService() require.NotNil(t, statsService) response, err := statsService.(v2rayapi.StatsServiceServer).QueryStats(context.Background(), &v2rayapi.QueryStatsRequest{Regexp: true, Patterns: []string{".*"}}) require.NoError(t, err) count := response.Stat[0].Value require.Equal(t, len(response.Stat), 4) for _, stat := range response.Stat { require.Equal(t, count, stat.Value) } } */ ================================================ FILE: test/v2ray_grpc_test.go ================================================ package main import ( "net/netip" "os" "testing" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/json/badoption" "github.com/gofrs/uuid/v5" "github.com/spyzhov/ajson" "github.com/stretchr/testify/require" ) func TestV2RayGRPCInbound(t *testing.T) { t.Run("origin", func(t *testing.T) { testV2RayGRPCInbound(t, false) }) t.Run("lite", func(t *testing.T) { testV2RayGRPCInbound(t, true) }) } func testV2RayGRPCInbound(t *testing.T, forceLite bool) { userId, err := uuid.DefaultGenerator.NewV4() require.NoError(t, err) _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeVMess, Options: &option.VMessInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.VMessUser{ { Name: "sekai", UUID: userId.String(), }, }, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, KeyPath: keyPem, }, }, Transport: &option.V2RayTransportOptions{ Type: C.V2RayTransportTypeGRPC, GRPCOptions: option.V2RayGRPCOptions{ ServiceName: "TunService", ForceLite: forceLite, }, }, }, }, }, }) content, err := os.ReadFile("config/vmess-grpc-client.json") require.NoError(t, err) config, err := ajson.Unmarshal(content) require.NoError(t, err) config.MustKey("inbounds").MustIndex(0).MustKey("port").SetNumeric(float64(clientPort)) outbound := config.MustKey("outbounds").MustIndex(0).MustKey("settings").MustKey("vnext").MustIndex(0) outbound.MustKey("port").SetNumeric(float64(serverPort)) user := outbound.MustKey("users").MustIndex(0) user.MustKey("id").SetString(userId.String()) content, err = ajson.Marshal(config) require.NoError(t, err) startDockerContainer(t, DockerOptions{ Image: ImageV2RayCore, Ports: []uint16{serverPort, testPort}, EntryPoint: "v2ray", Cmd: []string{"run"}, Stdin: content, Bind: map[string]string{ certPem: "/path/to/certificate.crt", keyPem: "/path/to/private.key", }, }) testSuitSimple(t, clientPort, testPort) } func TestV2RayGRPCOutbound(t *testing.T) { t.Run("origin", func(t *testing.T) { testV2RayGRPCOutbound(t, false) }) t.Run("lite", func(t *testing.T) { testV2RayGRPCOutbound(t, true) }) } func testV2RayGRPCOutbound(t *testing.T, forceLite bool) { userId, err := uuid.DefaultGenerator.NewV4() require.NoError(t, err) _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") content, err := os.ReadFile("config/vmess-grpc-server.json") require.NoError(t, err) config, err := ajson.Unmarshal(content) require.NoError(t, err) inbound := config.MustKey("inbounds").MustIndex(0) inbound.MustKey("port").SetNumeric(float64(serverPort)) inbound.MustKey("settings").MustKey("clients").MustIndex(0).MustKey("id").SetString(userId.String()) content, err = ajson.Marshal(config) require.NoError(t, err) startDockerContainer(t, DockerOptions{ Image: ImageV2RayCore, Ports: []uint16{serverPort, testPort}, EntryPoint: "v2ray", Cmd: []string{"run"}, Stdin: content, Env: []string{"V2RAY_VMESS_AEAD_FORCED=false"}, Bind: map[string]string{ certPem: "/path/to/certificate.crt", keyPem: "/path/to/private.key", }, }) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeVMess, Tag: "vmess-out", Options: &option.VMessOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, UUID: userId.String(), Security: "zero", OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, }, }, Transport: &option.V2RayTransportOptions{ Type: C.V2RayTransportTypeGRPC, GRPCOptions: option.V2RayGRPCOptions{ ServiceName: "TunService", ForceLite: forceLite, }, }, }, }, }, }) testSuit(t, clientPort, testPort) } func TestV2RayGRPCLite(t *testing.T) { t.Run("server", func(t *testing.T) { testV2RayTransportSelfWith(t, &option.V2RayTransportOptions{ Type: C.V2RayTransportTypeGRPC, GRPCOptions: option.V2RayGRPCOptions{ ServiceName: "TunService", ForceLite: true, }, }, &option.V2RayTransportOptions{ Type: C.V2RayTransportTypeGRPC, GRPCOptions: option.V2RayGRPCOptions{ ServiceName: "TunService", }, }) }) t.Run("client", func(t *testing.T) { testV2RayTransportSelfWith(t, &option.V2RayTransportOptions{ Type: C.V2RayTransportTypeGRPC, GRPCOptions: option.V2RayGRPCOptions{ ServiceName: "TunService", }, }, &option.V2RayTransportOptions{ Type: C.V2RayTransportTypeGRPC, GRPCOptions: option.V2RayGRPCOptions{ ServiceName: "TunService", ForceLite: true, }, }) }) t.Run("self", func(t *testing.T) { testV2RayTransportSelfWith(t, &option.V2RayTransportOptions{ Type: C.V2RayTransportTypeGRPC, GRPCOptions: option.V2RayGRPCOptions{ ServiceName: "TunService", ForceLite: true, }, }, &option.V2RayTransportOptions{ Type: C.V2RayTransportTypeGRPC, GRPCOptions: option.V2RayGRPCOptions{ ServiceName: "TunService", ForceLite: true, }, }) }) } ================================================ FILE: test/v2ray_httpupgrade_test.go ================================================ package main import ( "testing" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" ) func TestV2RayHTTPUpgrade(t *testing.T) { t.Run("self", func(t *testing.T) { testV2RayTransportSelf(t, &option.V2RayTransportOptions{ Type: C.V2RayTransportTypeHTTPUpgrade, }) }) } ================================================ FILE: test/v2ray_transport_test.go ================================================ package main import ( "net/netip" "testing" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/json/badoption" "github.com/gofrs/uuid/v5" "github.com/stretchr/testify/require" ) func TestV2RayHTTPSelf(t *testing.T) { testV2RayTransportSelf(t, &option.V2RayTransportOptions{ Type: C.V2RayTransportTypeHTTP, HTTPOptions: option.V2RayHTTPOptions{ Method: "POST", }, }) } func TestV2RayHTTPPlainSelf(t *testing.T) { testV2RayTransportNOTLSSelf(t, &option.V2RayTransportOptions{ Type: C.V2RayTransportTypeHTTP, }) } func testV2RayTransportSelf(t *testing.T, transport *option.V2RayTransportOptions) { testV2RayTransportSelfWith(t, transport, transport) } func testV2RayTransportSelfWith(t *testing.T, server, client *option.V2RayTransportOptions) { t.Run("vmess", func(t *testing.T) { testVMessTransportSelf(t, server, client) }) t.Run("trojan", func(t *testing.T) { testTrojanTransportSelf(t, server, client) }) } func testVMessTransportSelf(t *testing.T, server *option.V2RayTransportOptions, client *option.V2RayTransportOptions) { user, err := uuid.DefaultGenerator.NewV4() require.NoError(t, err) _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeVMess, Options: &option.VMessInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.VMessUser{ { Name: "sekai", UUID: user.String(), }, }, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, KeyPath: keyPem, }, }, Transport: server, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeVMess, Tag: "vmess-out", Options: &option.VMessOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, UUID: user.String(), Security: "zero", OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, }, }, Transport: client, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "vmess-out", }, }, }, }, }, }, }) testSuit(t, clientPort, testPort) } func testTrojanTransportSelf(t *testing.T, server *option.V2RayTransportOptions, client *option.V2RayTransportOptions) { user, err := uuid.DefaultGenerator.NewV4() require.NoError(t, err) _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeTrojan, Options: &option.TrojanInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.TrojanUser{ { Name: "sekai", Password: user.String(), }, }, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, KeyPath: keyPem, }, }, Transport: server, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeTrojan, Tag: "vmess-out", Options: &option.TrojanOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, Password: user.String(), OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, }, }, Transport: client, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "vmess-out", }, }, }, }, }, }, }) testSuit(t, clientPort, testPort) } func TestVMessQUICSelf(t *testing.T) { transport := &option.V2RayTransportOptions{ Type: C.V2RayTransportTypeQUIC, } user, err := uuid.DefaultGenerator.NewV4() require.NoError(t, err) _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeVMess, Options: &option.VMessInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.VMessUser{ { Name: "sekai", UUID: user.String(), }, }, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, KeyPath: keyPem, }, }, Transport: transport, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeVMess, Tag: "vmess-out", Options: &option.VMessOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, UUID: user.String(), Security: "zero", OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, }, }, Transport: transport, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "vmess-out", }, }, }, }, }, }, }) testSuitSimple1(t, clientPort, testPort) } func testV2RayTransportNOTLSSelf(t *testing.T, transport *option.V2RayTransportOptions) { user, err := uuid.DefaultGenerator.NewV4() require.NoError(t, err) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeVMess, Options: &option.VMessInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.VMessUser{ { Name: "sekai", UUID: user.String(), }, }, Transport: transport, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeVMess, Tag: "vmess-out", Options: &option.VMessOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, UUID: user.String(), Security: "zero", Transport: transport, }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "vmess-out", }, }, }, }, }, }, }) testSuit(t, clientPort, testPort) } ================================================ FILE: test/v2ray_ws_test.go ================================================ package main import ( "net/netip" "os" "testing" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/json/badoption" "github.com/gofrs/uuid/v5" "github.com/spyzhov/ajson" "github.com/stretchr/testify/require" ) func TestV2RayWebsocket(t *testing.T) { t.Run("self", func(t *testing.T) { testV2RayTransportSelf(t, &option.V2RayTransportOptions{ Type: C.V2RayTransportTypeWebsocket, }) }) t.Run("self-early-data", func(t *testing.T) { testV2RayTransportSelf(t, &option.V2RayTransportOptions{ Type: C.V2RayTransportTypeWebsocket, WebsocketOptions: option.V2RayWebsocketOptions{ MaxEarlyData: 2048, }, }) }) t.Run("self-xray-early-data", func(t *testing.T) { testV2RayTransportSelf(t, &option.V2RayTransportOptions{ Type: C.V2RayTransportTypeWebsocket, WebsocketOptions: option.V2RayWebsocketOptions{ MaxEarlyData: 2048, EarlyDataHeaderName: "Sec-WebSocket-Protocol", }, }) }) t.Run("inbound", func(t *testing.T) { testV2RayWebsocketInbound(t, 0, "") }) t.Run("inbound-early-data", func(t *testing.T) { testV2RayWebsocketInbound(t, 2048, "") }) t.Run("inbound-xray-early-data", func(t *testing.T) { testV2RayWebsocketInbound(t, 2048, "Sec-WebSocket-Protocol") }) t.Run("outbound", func(t *testing.T) { testV2RayWebsocketOutbound(t, 0, "") }) t.Run("outbound-early-data", func(t *testing.T) { testV2RayWebsocketOutbound(t, 2048, "") }) t.Run("outbound-xray-early-data", func(t *testing.T) { testV2RayWebsocketOutbound(t, 2048, "Sec-WebSocket-Protocol") }) } func testV2RayWebsocketInbound(t *testing.T, maxEarlyData uint32, earlyDataHeaderName string) { userId, err := uuid.DefaultGenerator.NewV4() require.NoError(t, err) _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeVMess, Options: &option.VMessInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.VMessUser{ { Name: "sekai", UUID: userId.String(), }, }, InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, KeyPath: keyPem, }, }, Transport: &option.V2RayTransportOptions{ Type: C.V2RayTransportTypeWebsocket, WebsocketOptions: option.V2RayWebsocketOptions{ MaxEarlyData: maxEarlyData, EarlyDataHeaderName: earlyDataHeaderName, }, }, }, }, }, }) content, err := os.ReadFile("config/vmess-ws-client.json") require.NoError(t, err) config, err := ajson.Unmarshal(content) require.NoError(t, err) config.MustKey("inbounds").MustIndex(0).MustKey("port").SetNumeric(float64(clientPort)) outbound := config.MustKey("outbounds").MustIndex(0) settings := outbound.MustKey("settings").MustKey("vnext").MustIndex(0) settings.MustKey("port").SetNumeric(float64(serverPort)) user := settings.MustKey("users").MustIndex(0) user.MustKey("id").SetString(userId.String()) wsSettings := outbound.MustKey("streamSettings").MustKey("wsSettings") wsSettings.MustKey("maxEarlyData").SetNumeric(float64(maxEarlyData)) wsSettings.MustKey("earlyDataHeaderName").SetString(earlyDataHeaderName) content, err = ajson.Marshal(config) require.NoError(t, err) startDockerContainer(t, DockerOptions{ Image: ImageV2RayCore, Ports: []uint16{serverPort, testPort}, EntryPoint: "v2ray", Cmd: []string{"run"}, Stdin: content, Bind: map[string]string{ certPem: "/path/to/certificate.crt", keyPem: "/path/to/private.key", }, }) testSuitSimple(t, clientPort, testPort) } func testV2RayWebsocketOutbound(t *testing.T, maxEarlyData uint32, earlyDataHeaderName string) { userId, err := uuid.DefaultGenerator.NewV4() require.NoError(t, err) _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") content, err := os.ReadFile("config/vmess-ws-server.json") require.NoError(t, err) config, err := ajson.Unmarshal(content) require.NoError(t, err) inbound := config.MustKey("inbounds").MustIndex(0) inbound.MustKey("port").SetNumeric(float64(serverPort)) inbound.MustKey("settings").MustKey("clients").MustIndex(0).MustKey("id").SetString(userId.String()) wsSettings := inbound.MustKey("streamSettings").MustKey("wsSettings") wsSettings.MustKey("maxEarlyData").SetNumeric(float64(maxEarlyData)) wsSettings.MustKey("earlyDataHeaderName").SetString(earlyDataHeaderName) content, err = ajson.Marshal(config) require.NoError(t, err) startDockerContainer(t, DockerOptions{ Image: ImageV2RayCore, Ports: []uint16{serverPort, testPort}, EntryPoint: "v2ray", Cmd: []string{"run"}, Stdin: content, Env: []string{"V2RAY_VMESS_AEAD_FORCED=false"}, Bind: map[string]string{ certPem: "/path/to/certificate.crt", keyPem: "/path/to/private.key", }, }) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeVMess, Tag: "vmess-out", Options: &option.VMessOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, UUID: userId.String(), Security: "zero", OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, }, }, Transport: &option.V2RayTransportOptions{ Type: C.V2RayTransportTypeWebsocket, WebsocketOptions: option.V2RayWebsocketOptions{ MaxEarlyData: maxEarlyData, EarlyDataHeaderName: earlyDataHeaderName, }, }, }, }, }, }) testSuit(t, clientPort, testPort) } ================================================ FILE: test/vmess_test.go ================================================ package main import ( "net/netip" "os" "testing" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/json/badoption" "github.com/gofrs/uuid/v5" "github.com/spyzhov/ajson" "github.com/stretchr/testify/require" ) func newUUID() uuid.UUID { user, _ := uuid.DefaultGenerator.NewV4() return user } func TestVMessAuto(t *testing.T) { security := "auto" t.Run("self", func(t *testing.T) { testVMessSelf(t, security, 0, false, false, false) }) t.Run("packetaddr", func(t *testing.T) { testVMessSelf(t, security, 0, false, false, true) }) t.Run("inbound", func(t *testing.T) { testVMessInboundWithV2Ray(t, security, 0, false) }) t.Run("outbound", func(t *testing.T) { testVMessOutboundWithV2Ray(t, security, false, false, 0) }) } func TestVMess(t *testing.T) { for _, security := range []string{ "zero", } { t.Run(security, func(t *testing.T) { testVMess0(t, security) }) } for _, security := range []string{ "none", } { t.Run(security, func(t *testing.T) { testVMess1(t, security) }) } for _, security := range []string{ "aes-128-gcm", "chacha20-poly1305", "aes-128-cfb", } { t.Run(security, func(t *testing.T) { testVMess2(t, security) }) } } func testVMess0(t *testing.T, security string) { t.Run("self", func(t *testing.T) { testVMessSelf(t, security, 0, false, false, false) }) t.Run("self-legacy", func(t *testing.T) { testVMessSelf(t, security, 1, false, false, false) }) t.Run("packetaddr", func(t *testing.T) { testVMessSelf(t, security, 0, false, false, true) }) t.Run("outbound", func(t *testing.T) { testVMessOutboundWithV2Ray(t, security, false, false, 0) }) t.Run("outbound-legacy", func(t *testing.T) { testVMessOutboundWithV2Ray(t, security, false, false, 1) }) } func testVMess1(t *testing.T, security string) { t.Run("self", func(t *testing.T) { testVMessSelf(t, security, 0, false, false, false) }) t.Run("self-legacy", func(t *testing.T) { testVMessSelf(t, security, 1, false, false, false) }) t.Run("packetaddr", func(t *testing.T) { testVMessSelf(t, security, 0, false, false, true) }) t.Run("inbound", func(t *testing.T) { testVMessInboundWithV2Ray(t, security, 0, false) }) t.Run("outbound", func(t *testing.T) { testVMessOutboundWithV2Ray(t, security, false, false, 0) }) t.Run("outbound-legacy", func(t *testing.T) { testVMessOutboundWithV2Ray(t, security, false, false, 1) }) } func testVMess2(t *testing.T, security string) { t.Run("self", func(t *testing.T) { testVMessSelf(t, security, 0, false, false, false) }) t.Run("self-padding", func(t *testing.T) { testVMessSelf(t, security, 0, true, false, false) }) t.Run("self-authid", func(t *testing.T) { testVMessSelf(t, security, 0, false, true, false) }) t.Run("self-padding-authid", func(t *testing.T) { testVMessSelf(t, security, 0, true, true, false) }) t.Run("self-legacy", func(t *testing.T) { testVMessSelf(t, security, 1, false, false, false) }) t.Run("self-legacy-padding", func(t *testing.T) { testVMessSelf(t, security, 1, true, false, false) }) t.Run("packetaddr", func(t *testing.T) { testVMessSelf(t, security, 0, false, false, true) }) t.Run("inbound", func(t *testing.T) { testVMessInboundWithV2Ray(t, security, 0, false) }) t.Run("inbound-authid", func(t *testing.T) { testVMessInboundWithV2Ray(t, security, 0, true) }) t.Run("inbound-legacy", func(t *testing.T) { testVMessInboundWithV2Ray(t, security, 64, false) }) t.Run("outbound", func(t *testing.T) { testVMessOutboundWithV2Ray(t, security, false, false, 0) }) t.Run("outbound-padding", func(t *testing.T) { testVMessOutboundWithV2Ray(t, security, true, false, 0) }) t.Run("outbound-authid", func(t *testing.T) { testVMessOutboundWithV2Ray(t, security, false, true, 0) }) t.Run("outbound-padding-authid", func(t *testing.T) { testVMessOutboundWithV2Ray(t, security, true, true, 0) }) t.Run("outbound-legacy", func(t *testing.T) { testVMessOutboundWithV2Ray(t, security, false, false, 1) }) t.Run("outbound-legacy-padding", func(t *testing.T) { testVMessOutboundWithV2Ray(t, security, true, false, 1) }) } func testVMessInboundWithV2Ray(t *testing.T, security string, alterId int, authenticatedLength bool) { userId := newUUID() content, err := os.ReadFile("config/vmess-client.json") require.NoError(t, err) config, err := ajson.Unmarshal(content) require.NoError(t, err) config.MustKey("inbounds").MustIndex(0).MustKey("port").SetNumeric(float64(clientPort)) outbound := config.MustKey("outbounds").MustIndex(0).MustKey("settings").MustKey("vnext").MustIndex(0) outbound.MustKey("port").SetNumeric(float64(serverPort)) user := outbound.MustKey("users").MustIndex(0) user.MustKey("id").SetString(userId.String()) user.MustKey("alterId").SetNumeric(float64(alterId)) user.MustKey("security").SetString(security) var experiments string if authenticatedLength { experiments += "AuthenticatedLength" } user.MustKey("experiments").SetString(experiments) content, err = ajson.Marshal(config) require.NoError(t, err) startDockerContainer(t, DockerOptions{ Image: ImageV2RayCore, Ports: []uint16{serverPort, testPort}, EntryPoint: "v2ray", Cmd: []string{"run"}, Stdin: content, Env: []string{"V2RAY_VMESS_AEAD_FORCED=false"}, }) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeVMess, Options: &option.VMessInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.VMessUser{ { Name: "sekai", UUID: userId.String(), AlterId: alterId, }, }, }, }, }, }) testSuitSimple(t, clientPort, testPort) } func testVMessOutboundWithV2Ray(t *testing.T, security string, globalPadding bool, authenticatedLength bool, alterId int) { user := newUUID() content, err := os.ReadFile("config/vmess-server.json") require.NoError(t, err) config, err := ajson.Unmarshal(content) require.NoError(t, err) inbound := config.MustKey("inbounds").MustIndex(0) inbound.MustKey("port").SetNumeric(float64(serverPort)) inbound.MustKey("settings").MustKey("clients").MustIndex(0).MustKey("id").SetString(user.String()) inbound.MustKey("settings").MustKey("clients").MustIndex(0).MustKey("alterId").SetNumeric(float64(alterId)) content, err = ajson.Marshal(config) require.NoError(t, err) startDockerContainer(t, DockerOptions{ Image: ImageV2RayCore, Ports: []uint16{serverPort, testPort}, EntryPoint: "v2ray", Cmd: []string{"run"}, Stdin: content, Env: []string{"V2RAY_VMESS_AEAD_FORCED=false"}, }) startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeVMess, Options: &option.VMessOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, Security: security, UUID: user.String(), GlobalPadding: globalPadding, AuthenticatedLength: authenticatedLength, AlterId: alterId, }, }, }, }) testSuit(t, clientPort, testPort) } func testVMessSelf(t *testing.T, security string, alterId int, globalPadding bool, authenticatedLength bool, packetAddr bool) { user := newUUID() startInstance(t, option.Options{ Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeVMess, Options: &option.VMessInboundOptions{ ListenOptions: option.ListenOptions{ Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.VMessUser{ { Name: "sekai", UUID: user.String(), AlterId: alterId, }, }, }, }, }, Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeVMess, Tag: "vmess-out", Options: &option.VMessOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, Security: security, UUID: user.String(), AlterId: alterId, GlobalPadding: globalPadding, AuthenticatedLength: authenticatedLength, PacketEncoding: "packetaddr", }, }, }, Route: &option.RouteOptions{ Rules: []option.Rule{ { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ RawDefaultRule: option.RawDefaultRule{ Inbound: []string{"mixed-in"}, }, RuleAction: option.RuleAction{ Action: C.RuleActionTypeRoute, RouteOptions: option.RouteActionOptions{ Outbound: "vmess-out", }, }, }, }, }, }, }) testSuit(t, clientPort, testPort) } ================================================ FILE: test/wrapper_test.go ================================================ package main import ( "testing" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/stretchr/testify/require" ) func TestOptionsWrapper(t *testing.T) { inbound := option.Inbound{ Type: C.TypeHTTP, Options: &option.HTTPMixedInboundOptions{ InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, }, }, }, } tlsOptionsWrapper, loaded := inbound.Options.(option.InboundTLSOptionsWrapper) require.True(t, loaded, "find inbound tls options") tlsOptions := tlsOptionsWrapper.TakeInboundTLSOptions() require.NotNil(t, tlsOptions, "find inbound tls options") tlsOptions.Enabled = false tlsOptionsWrapper.ReplaceInboundTLSOptions(tlsOptions) require.False(t, inbound.Options.(*option.HTTPMixedInboundOptions).TLS.Enabled, "replace tls enabled") } ================================================ FILE: transport/simple-obfs/README.md ================================================ # simple-obfs mod from https://github.com/Dreamacro/clash/transport/simple-obfs version: 1.11.8 ================================================ FILE: transport/simple-obfs/http.go ================================================ package obfs import ( "bytes" "encoding/base64" "fmt" "io" "math/rand" "net" "net/http" B "github.com/sagernet/sing/common/buf" ) // HTTPObfs is shadowsocks http simple-obfs implementation type HTTPObfs struct { net.Conn host string port string buf []byte offset int firstRequest bool firstResponse bool } func (ho *HTTPObfs) Read(b []byte) (int, error) { if ho.buf != nil { n := copy(b, ho.buf[ho.offset:]) ho.offset += n if ho.offset == len(ho.buf) { B.Put(ho.buf) ho.buf = nil } return n, nil } if ho.firstResponse { buf := B.Get(B.BufferSize) n, err := ho.Conn.Read(buf) if err != nil { B.Put(buf) return 0, err } idx := bytes.Index(buf[:n], []byte("\r\n\r\n")) if idx == -1 { B.Put(buf) return 0, io.EOF } ho.firstResponse = false length := n - (idx + 4) n = copy(b, buf[idx+4:n]) if length > n { ho.buf = buf[:idx+4+length] ho.offset = idx + 4 + n } else { B.Put(buf) } return n, nil } return ho.Conn.Read(b) } func (ho *HTTPObfs) Write(b []byte) (int, error) { if ho.firstRequest { randBytes := make([]byte, 16) rand.Read(randBytes) req, _ := http.NewRequest("GET", fmt.Sprintf("http://%s/", ho.host), bytes.NewBuffer(b[:])) req.Header.Set("User-Agent", fmt.Sprintf("curl/7.%d.%d", rand.Int()%54, rand.Int()%2)) req.Header.Set("Upgrade", "websocket") req.Header.Set("Connection", "Upgrade") req.Host = ho.host if ho.port != "80" { req.Host = fmt.Sprintf("%s:%s", ho.host, ho.port) } req.Header.Set("Sec-WebSocket-Key", base64.URLEncoding.EncodeToString(randBytes)) req.ContentLength = int64(len(b)) err := req.Write(ho.Conn) ho.firstRequest = false return len(b), err } return ho.Conn.Write(b) } func (ho *HTTPObfs) Upstream() any { return ho.Conn } // NewHTTPObfs return a HTTPObfs func NewHTTPObfs(conn net.Conn, host string, port string) net.Conn { return &HTTPObfs{ Conn: conn, firstRequest: true, firstResponse: true, host: host, port: port, } } ================================================ FILE: transport/simple-obfs/tls.go ================================================ package obfs import ( "bytes" "encoding/binary" "io" "math/rand" "net" "time" B "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/random" ) func init() { random.InitializeSeed() } const ( chunkSize = 1 << 14 // 2 ** 14 == 16 * 1024 ) // TLSObfs is shadowsocks tls simple-obfs implementation type TLSObfs struct { net.Conn server string remain int firstRequest bool firstResponse bool } func (to *TLSObfs) read(b []byte, discardN int) (int, error) { buf := B.Get(discardN) _, err := io.ReadFull(to.Conn, buf) B.Put(buf) if err != nil { return 0, err } sizeBuf := make([]byte, 2) _, err = io.ReadFull(to.Conn, sizeBuf) if err != nil { return 0, nil } length := int(binary.BigEndian.Uint16(sizeBuf)) if length > len(b) { n, err := to.Conn.Read(b) if err != nil { return n, err } to.remain = length - n return n, nil } return io.ReadFull(to.Conn, b[:length]) } func (to *TLSObfs) Read(b []byte) (int, error) { if to.remain > 0 { length := to.remain if length > len(b) { length = len(b) } n, err := io.ReadFull(to.Conn, b[:length]) to.remain -= n return n, err } if to.firstResponse { // type + ver + lensize + 91 = 96 // type + ver + lensize + 1 = 6 // type + ver = 3 to.firstResponse = false return to.read(b, 105) } // type + ver = 3 return to.read(b, 3) } func (to *TLSObfs) Write(b []byte) (int, error) { length := len(b) for i := 0; i < length; i += chunkSize { end := i + chunkSize if end > length { end = length } n, err := to.write(b[i:end]) if err != nil { return n, err } } return length, nil } func (to *TLSObfs) write(b []byte) (int, error) { if to.firstRequest { helloMsg := makeClientHelloMsg(b, to.server) _, err := to.Conn.Write(helloMsg) to.firstRequest = false return len(b), err } buf := B.NewSize(5 + len(b)) defer buf.Release() buf.Write([]byte{0x17, 0x03, 0x03}) binary.Write(buf, binary.BigEndian, uint16(len(b))) buf.Write(b) _, err := to.Conn.Write(buf.Bytes()) return len(b), err } func (to *TLSObfs) Upstream() any { return to.Conn } // NewTLSObfs return a SimpleObfs func NewTLSObfs(conn net.Conn, server string) net.Conn { return &TLSObfs{ Conn: conn, server: server, firstRequest: true, firstResponse: true, } } func makeClientHelloMsg(data []byte, server string) []byte { random := make([]byte, 28) sessionID := make([]byte, 32) rand.Read(random) rand.Read(sessionID) buf := &bytes.Buffer{} // handshake, TLS 1.0 version, length buf.WriteByte(22) buf.Write([]byte{0x03, 0x01}) length := uint16(212 + len(data) + len(server)) buf.WriteByte(byte(length >> 8)) buf.WriteByte(byte(length & 0xff)) // clientHello, length, TLS 1.2 version buf.WriteByte(1) buf.WriteByte(0) binary.Write(buf, binary.BigEndian, uint16(208+len(data)+len(server))) buf.Write([]byte{0x03, 0x03}) // random with timestamp, sid len, sid binary.Write(buf, binary.BigEndian, uint32(time.Now().Unix())) buf.Write(random) buf.WriteByte(32) buf.Write(sessionID) // cipher suites buf.Write([]byte{0x00, 0x38}) buf.Write([]byte{ 0xc0, 0x2c, 0xc0, 0x30, 0x00, 0x9f, 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0xaa, 0xc0, 0x2b, 0xc0, 0x2f, 0x00, 0x9e, 0xc0, 0x24, 0xc0, 0x28, 0x00, 0x6b, 0xc0, 0x23, 0xc0, 0x27, 0x00, 0x67, 0xc0, 0x0a, 0xc0, 0x14, 0x00, 0x39, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x33, 0x00, 0x9d, 0x00, 0x9c, 0x00, 0x3d, 0x00, 0x3c, 0x00, 0x35, 0x00, 0x2f, 0x00, 0xff, }) // compression buf.Write([]byte{0x01, 0x00}) // extension length binary.Write(buf, binary.BigEndian, uint16(79+len(data)+len(server))) // session ticket buf.Write([]byte{0x00, 0x23}) binary.Write(buf, binary.BigEndian, uint16(len(data))) buf.Write(data) // server name buf.Write([]byte{0x00, 0x00}) binary.Write(buf, binary.BigEndian, uint16(len(server)+5)) binary.Write(buf, binary.BigEndian, uint16(len(server)+3)) buf.WriteByte(0) binary.Write(buf, binary.BigEndian, uint16(len(server))) buf.Write([]byte(server)) // ec_point buf.Write([]byte{0x00, 0x0b, 0x00, 0x04, 0x03, 0x01, 0x00, 0x02}) // groups buf.Write([]byte{0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x19, 0x00, 0x18}) // signature buf.Write([]byte{ 0x00, 0x0d, 0x00, 0x20, 0x00, 0x1e, 0x06, 0x01, 0x06, 0x02, 0x06, 0x03, 0x05, 0x01, 0x05, 0x02, 0x05, 0x03, 0x04, 0x01, 0x04, 0x02, 0x04, 0x03, 0x03, 0x01, 0x03, 0x02, 0x03, 0x03, 0x02, 0x01, 0x02, 0x02, 0x02, 0x03, }) // encrypt then mac buf.Write([]byte{0x00, 0x16, 0x00, 0x00}) // extended master secret buf.Write([]byte{0x00, 0x17, 0x00, 0x00}) return buf.Bytes() } ================================================ FILE: transport/sip003/args.go ================================================ package sip003 import ( "bytes" "fmt" ) // mod from https://github.com/shadowsocks/v2ray-plugin/blob/master/args.go // Args maps a string key to a list of values. It is similar to url.Values. type Args map[string][]string // Get the first value associated with the given key. If there are any values // associated with the key, the value return has the value and ok is set to // true. If there are no values for the given key, value is "" and ok is false. // If you need access to multiple values, use the map directly. func (args Args) Get(key string) (value string, ok bool) { if args == nil { return "", false } vals, ok := args[key] if !ok || len(vals) == 0 { return "", false } return vals[0], true } // Add Append value to the list of values for key. func (args Args) Add(key, value string) { args[key] = append(args[key], value) } // Return the index of the next unescaped byte in s that is in the term set, or // else the length of the string if no terminators appear. Additionally return // the unescaped string up to the returned index. func indexUnescaped(s string, term []byte) (int, string, error) { var i int unesc := make([]byte, 0) for i = 0; i < len(s); i++ { b := s[i] // A terminator byte? if bytes.IndexByte(term, b) != -1 { break } if b == '\\' { i++ if i >= len(s) { return 0, "", fmt.Errorf("nothing following final escape in %q", s) } b = s[i] } unesc = append(unesc, b) } return i, string(unesc), nil } // ParsePluginOptions Parse a name–value mapping as from SS_PLUGIN_OPTIONS. // // " is a k=v string value with options that are to be passed to the // transport. semicolons, equal signs and backslashes must be escaped // with a backslash." // Example: secret=nou;cache=/tmp/cache;secret=yes func ParsePluginOptions(s string) (opts Args, err error) { opts = make(Args) if len(s) == 0 { return } i := 0 for { var key, value string var offset, begin int if i >= len(s) { break } begin = i // Read the key. offset, key, err = indexUnescaped(s[i:], []byte{'=', ';'}) if err != nil { return } if len(key) == 0 { err = fmt.Errorf("empty key in %q", s[begin:i]) return } i += offset // End of string or no equals sign? if i >= len(s) || s[i] != '=' { opts.Add(key, "1") // Skip the semicolon. i++ continue } // Skip the equals sign. i++ // Read the value. offset, value, err = indexUnescaped(s[i:], []byte{';'}) if err != nil { return } i += offset opts.Add(key, value) // Skip the semicolon. i++ } return opts, nil } ================================================ FILE: transport/sip003/obfs.go ================================================ package sip003 import ( "context" "net" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/transport/simple-obfs" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) var _ Plugin = (*ObfsLocal)(nil) func init() { RegisterPlugin("obfs-local", newObfsLocal) } func newObfsLocal(ctx context.Context, pluginOpts Args, router adapter.Router, dialer N.Dialer, serverAddr M.Socksaddr) (Plugin, error) { plugin := &ObfsLocal{ dialer: dialer, serverAddr: serverAddr, } mode := "http" if obfsMode, loaded := pluginOpts.Get("obfs"); loaded { mode = obfsMode } if obfsHost, loaded := pluginOpts.Get("obfs-host"); loaded { plugin.host = obfsHost } switch mode { case "http": case "tls": plugin.tls = true default: return nil, E.New("unknown obfs mode ", mode) } plugin.port = F.ToString(serverAddr.Port) return plugin, nil } type ObfsLocal struct { dialer N.Dialer serverAddr M.Socksaddr tls bool host string port string } func (o *ObfsLocal) DialContext(ctx context.Context) (net.Conn, error) { conn, err := o.dialer.DialContext(ctx, N.NetworkTCP, o.serverAddr) if err != nil { return nil, err } if !o.tls { return obfs.NewHTTPObfs(conn, o.host, o.port), nil } else { return obfs.NewTLSObfs(conn, o.host), nil } } ================================================ FILE: transport/sip003/plugin.go ================================================ package sip003 import ( "context" "net" "github.com/sagernet/sing-box/adapter" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) type PluginConstructor func(ctx context.Context, pluginArgs Args, router adapter.Router, dialer N.Dialer, serverAddr M.Socksaddr) (Plugin, error) type Plugin interface { DialContext(ctx context.Context) (net.Conn, error) } var plugins map[string]PluginConstructor func RegisterPlugin(name string, constructor PluginConstructor) { if plugins == nil { plugins = make(map[string]PluginConstructor) } plugins[name] = constructor } func CreatePlugin(ctx context.Context, name string, pluginArgs string, router adapter.Router, dialer N.Dialer, serverAddr M.Socksaddr) (Plugin, error) { pluginOptions, err := ParsePluginOptions(pluginArgs) if err != nil { return nil, E.Cause(err, "parse plugin_opts") } constructor, loaded := plugins[name] if !loaded { return nil, E.New("plugin not found: ", name) } return constructor(ctx, pluginOptions, router, dialer, serverAddr) } ================================================ FILE: transport/sip003/v2ray.go ================================================ package sip003 import ( "context" "net" "strconv" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/transport/v2ray" "github.com/sagernet/sing-vmess" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json/badoption" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) func init() { RegisterPlugin("v2ray-plugin", newV2RayPlugin) } func newV2RayPlugin(ctx context.Context, pluginOpts Args, router adapter.Router, dialer N.Dialer, serverAddr M.Socksaddr) (Plugin, error) { var tlsOptions option.OutboundTLSOptions if _, loaded := pluginOpts.Get("tls"); loaded { tlsOptions.Enabled = true } if certPath, certLoaded := pluginOpts.Get("cert"); certLoaded { tlsOptions.CertificatePath = certPath } if certRaw, certLoaded := pluginOpts.Get("certRaw"); certLoaded { certHead := "-----BEGIN CERTIFICATE-----" certTail := "-----END CERTIFICATE-----" fixedCert := certHead + "\n" + certRaw + "\n" + certTail tlsOptions.Certificate = []string{fixedCert} } mode := "websocket" if modeOpt, loaded := pluginOpts.Get("mode"); loaded { mode = modeOpt } host := "cloudfront.com" path := "/" if hostOpt, loaded := pluginOpts.Get("host"); loaded { host = hostOpt tlsOptions.ServerName = hostOpt } if pathOpt, loaded := pluginOpts.Get("path"); loaded { path = pathOpt } var tlsClient tls.Config var err error if tlsOptions.Enabled { tlsClient, err = tls.NewClient(ctx, logger.NOP(), serverAddr.AddrString(), tlsOptions) if err != nil { return nil, err } } var mux int var transportOptions option.V2RayTransportOptions switch mode { case "websocket": transportOptions = option.V2RayTransportOptions{ Type: C.V2RayTransportTypeWebsocket, WebsocketOptions: option.V2RayWebsocketOptions{ Headers: map[string]badoption.Listable[string]{ "Host": []string{host}, }, Path: path, }, } if muxOpt, loaded := pluginOpts.Get("mux"); loaded { muxVal, err := strconv.Atoi(muxOpt) if err != nil { return nil, E.Cause(err, "parse mux value") } mux = muxVal } else { mux = 1 } case "quic": transportOptions = option.V2RayTransportOptions{ Type: C.V2RayTransportTypeQUIC, } default: return nil, E.New("v2ray-plugin: unknown mode: " + mode) } transport, err := v2ray.NewClientTransport(context.Background(), dialer, serverAddr, transportOptions, tlsClient) if err != nil { return nil, err } if mux > 0 { return &v2rayMuxWrapper{transport}, nil } return transport, nil } var _ Plugin = (*v2rayMuxWrapper)(nil) type v2rayMuxWrapper struct { adapter.V2RayClientTransport } func (w *v2rayMuxWrapper) DialContext(ctx context.Context) (net.Conn, error) { conn, err := w.V2RayClientTransport.DialContext(ctx) if err != nil { return nil, err } return vmess.NewMuxConnWrapper(conn, vmess.MuxDestination), nil } ================================================ FILE: transport/trojan/mux.go ================================================ package trojan import ( std_bufio "bufio" "context" "net" "os" "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" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/task" "github.com/sagernet/smux" ) func HandleMuxConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, handler Handler, logger logger.ContextLogger, onClose N.CloseHandlerFunc) error { session, err := smux.Server(conn, smuxConfig()) if err != nil { return err } var group task.Group group.Append0(func(_ context.Context) error { var stream net.Conn for { stream, err = session.AcceptStream() if err != nil { return err } go newMuxConnection(ctx, stream, source, handler, logger) } }) group.Cleanup(func() { session.Close() if onClose != nil { onClose(os.ErrClosed) } }) return group.Run(ctx) } func newMuxConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, handler Handler, logger logger.ContextLogger) { err := newMuxConnection0(ctx, conn, source, handler) if err != nil { logger.ErrorContext(ctx, E.Cause(err, "process trojan-go multiplex connection")) } } func newMuxConnection0(ctx context.Context, conn net.Conn, source M.Socksaddr, handler Handler) error { reader := std_bufio.NewReader(conn) command, err := reader.ReadByte() if err != nil { return E.Cause(err, "read command") } destination, err := M.SocksaddrSerializer.ReadAddrPort(reader) if err != nil { return E.Cause(err, "read destination") } if reader.Buffered() > 0 { buffer := buf.NewSize(reader.Buffered()) _, err = buffer.ReadFullFrom(reader, buffer.Len()) if err != nil { return err } conn = bufio.NewCachedConn(conn, buffer) } switch command { case CommandTCP: handler.NewConnectionEx(ctx, conn, source, destination, nil) case CommandUDP: handler.NewPacketConnectionEx(ctx, &PacketConn{Conn: conn}, source, destination, nil) default: return E.New("unknown command ", command) } return nil } func smuxConfig() *smux.Config { config := smux.DefaultConfig() config.KeepAliveDisabled = true return config } ================================================ FILE: transport/trojan/protocol.go ================================================ package trojan import ( "crypto/sha256" "encoding/binary" "encoding/hex" "net" "os" "sync" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/rw" ) const ( KeyLength = 56 CommandTCP = 1 CommandUDP = 3 CommandMux = 0x7f ) var CRLF = []byte{'\r', '\n'} var _ N.EarlyWriter = (*ClientConn)(nil) type ClientConn struct { N.ExtendedConn key [KeyLength]byte destination M.Socksaddr headerWritten bool } func NewClientConn(conn net.Conn, key [KeyLength]byte, destination M.Socksaddr) *ClientConn { return &ClientConn{ ExtendedConn: bufio.NewExtendedConn(conn), key: key, destination: destination, } } func (c *ClientConn) NeedHandshakeForWrite() bool { return !c.headerWritten } func (c *ClientConn) Write(p []byte) (n int, err error) { if c.headerWritten { return c.ExtendedConn.Write(p) } err = ClientHandshake(c.ExtendedConn, c.key, c.destination, p) if err != nil { return } n = len(p) c.headerWritten = true return } func (c *ClientConn) WriteBuffer(buffer *buf.Buffer) error { if c.headerWritten { return c.ExtendedConn.WriteBuffer(buffer) } err := ClientHandshakeBuffer(c.ExtendedConn, c.key, c.destination, buffer) if err != nil { return err } c.headerWritten = true return nil } func (c *ClientConn) FrontHeadroom() int { if !c.headerWritten { return KeyLength + 5 + M.MaxSocksaddrLength } return 0 } func (c *ClientConn) Upstream() any { return c.ExtendedConn } func (c *ClientConn) ReaderReplaceable() bool { return c.headerWritten } func (c *ClientConn) WriterReplaceable() bool { return c.headerWritten } type ClientPacketConn struct { net.Conn access sync.Mutex key [KeyLength]byte headerWritten bool readWaitOptions N.ReadWaitOptions } func NewClientPacketConn(conn net.Conn, key [KeyLength]byte) *ClientPacketConn { return &ClientPacketConn{ Conn: conn, key: key, } } func (c *ClientPacketConn) NeedHandshake() bool { return !c.headerWritten } func (c *ClientPacketConn) ReadPacket(buffer *buf.Buffer) (M.Socksaddr, error) { return ReadPacket(c.Conn, buffer) } func (c *ClientPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { if !c.headerWritten { c.access.Lock() if c.headerWritten { c.access.Unlock() } else { err := ClientHandshakePacket(c.Conn, c.key, destination, buffer) c.headerWritten = true c.access.Unlock() return err } } return WritePacket(c.Conn, buffer, destination) } func (c *ClientPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { buffer := buf.With(p) destination, err := c.ReadPacket(buffer) if err != nil { return } n = buffer.Len() if destination.IsDomain() { addr = destination } else { addr = destination.UDPAddr() } return } func (c *ClientPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { return bufio.WritePacket(c, p, addr) } func (c *ClientPacketConn) Read(p []byte) (n int, err error) { n, _, err = c.ReadFrom(p) return } func (c *ClientPacketConn) Write(p []byte) (n int, err error) { return 0, os.ErrInvalid } func (c *ClientPacketConn) FrontHeadroom() int { if !c.headerWritten { return KeyLength + 2*M.MaxSocksaddrLength + 9 } return M.MaxSocksaddrLength + 4 } func (c *ClientPacketConn) Upstream() any { return c.Conn } func Key(password string) [KeyLength]byte { var key [KeyLength]byte hash := sha256.New224() common.Must1(hash.Write([]byte(password))) hex.Encode(key[:], hash.Sum(nil)) return key } func ClientHandshakeRaw(conn net.Conn, key [KeyLength]byte, command byte, destination M.Socksaddr, payload []byte) error { _, err := conn.Write(key[:]) if err != nil { return err } _, err = conn.Write(CRLF) if err != nil { return err } _, err = conn.Write([]byte{command}) if err != nil { return err } err = M.SocksaddrSerializer.WriteAddrPort(conn, destination) if err != nil { return err } _, err = conn.Write(CRLF) if err != nil { return err } if len(payload) > 0 { _, err = conn.Write(payload) if err != nil { return err } } return nil } func ClientHandshake(conn net.Conn, key [KeyLength]byte, destination M.Socksaddr, payload []byte) error { headerLen := KeyLength + M.SocksaddrSerializer.AddrPortLen(destination) + 5 header := buf.NewSize(headerLen + len(payload)) defer header.Release() common.Must1(header.Write(key[:])) common.Must1(header.Write(CRLF)) common.Must(header.WriteByte(CommandTCP)) err := M.SocksaddrSerializer.WriteAddrPort(header, destination) if err != nil { return err } common.Must1(header.Write(CRLF)) common.Must1(header.Write(payload)) _, err = conn.Write(header.Bytes()) if err != nil { return E.Cause(err, "write request") } return nil } func ClientHandshakeBuffer(conn net.Conn, key [KeyLength]byte, destination M.Socksaddr, payload *buf.Buffer) error { header := buf.With(payload.ExtendHeader(KeyLength + M.SocksaddrSerializer.AddrPortLen(destination) + 5)) common.Must1(header.Write(key[:])) common.Must1(header.Write(CRLF)) common.Must(header.WriteByte(CommandTCP)) err := M.SocksaddrSerializer.WriteAddrPort(header, destination) if err != nil { return err } common.Must1(header.Write(CRLF)) _, err = conn.Write(payload.Bytes()) if err != nil { return E.Cause(err, "write request") } return nil } func ClientHandshakePacket(conn net.Conn, key [KeyLength]byte, destination M.Socksaddr, payload *buf.Buffer) error { headerLen := KeyLength + 2*M.SocksaddrSerializer.AddrPortLen(destination) + 9 payloadLen := payload.Len() var header *buf.Buffer var writeHeader bool if payload.Start() >= headerLen { header = buf.With(payload.ExtendHeader(headerLen)) } else { header = buf.NewSize(headerLen) defer header.Release() writeHeader = true } common.Must1(header.Write(key[:])) common.Must1(header.Write(CRLF)) common.Must(header.WriteByte(CommandUDP)) err := M.SocksaddrSerializer.WriteAddrPort(header, destination) if err != nil { return err } common.Must1(header.Write(CRLF)) common.Must(M.SocksaddrSerializer.WriteAddrPort(header, destination)) common.Must(binary.Write(header, binary.BigEndian, uint16(payloadLen))) common.Must1(header.Write(CRLF)) if writeHeader { _, err := conn.Write(header.Bytes()) if err != nil { return E.Cause(err, "write request") } } _, err = conn.Write(payload.Bytes()) if err != nil { return E.Cause(err, "write payload") } return nil } func ReadPacket(conn net.Conn, buffer *buf.Buffer) (M.Socksaddr, error) { destination, err := M.SocksaddrSerializer.ReadAddrPort(conn) if err != nil { return M.Socksaddr{}, E.Cause(err, "read destination") } var length uint16 err = binary.Read(conn, binary.BigEndian, &length) if err != nil { return M.Socksaddr{}, E.Cause(err, "read chunk length") } err = rw.SkipN(conn, 2) if err != nil { return M.Socksaddr{}, E.Cause(err, "skip crlf") } _, err = buffer.ReadFullFrom(conn, int(length)) return destination, err } func WritePacket(conn net.Conn, buffer *buf.Buffer, destination M.Socksaddr) error { defer buffer.Release() bufferLen := buffer.Len() header := buf.With(buffer.ExtendHeader(M.SocksaddrSerializer.AddrPortLen(destination) + 4)) err := M.SocksaddrSerializer.WriteAddrPort(header, destination) if err != nil { return err } common.Must(binary.Write(header, binary.BigEndian, uint16(bufferLen))) common.Must1(header.Write(CRLF)) _, err = conn.Write(buffer.Bytes()) if err != nil { return E.Cause(err, "write packet") } return nil } ================================================ FILE: transport/trojan/protocol_wait.go ================================================ package trojan import ( "encoding/binary" "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/rw" ) var _ N.PacketReadWaiter = (*ClientPacketConn)(nil) func (c *ClientPacketConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { c.readWaitOptions = options return false } func (c *ClientPacketConn) WaitReadPacket() (buffer *buf.Buffer, destination M.Socksaddr, err error) { destination, err = M.SocksaddrSerializer.ReadAddrPort(c.Conn) if err != nil { return nil, M.Socksaddr{}, E.Cause(err, "read destination") } var length uint16 err = binary.Read(c.Conn, binary.BigEndian, &length) if err != nil { return nil, M.Socksaddr{}, E.Cause(err, "read chunk length") } err = rw.SkipN(c.Conn, 2) if err != nil { return nil, M.Socksaddr{}, E.Cause(err, "skip crlf") } buffer = c.readWaitOptions.NewPacketBuffer() _, err = buffer.ReadFullFrom(c.Conn, int(length)) if err != nil { buffer.Release() return } c.readWaitOptions.PostReturn(buffer) return } ================================================ FILE: transport/trojan/service.go ================================================ package trojan import ( "context" "encoding/binary" "net" "github.com/sagernet/sing/common/auth" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" "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/rw" ) type Handler interface { N.TCPConnectionHandlerEx N.UDPConnectionHandlerEx } type Service[K comparable] struct { users map[K][56]byte keys map[[56]byte]K handler Handler fallbackHandler N.TCPConnectionHandlerEx logger logger.ContextLogger } func NewService[K comparable](handler Handler, fallbackHandler N.TCPConnectionHandlerEx, logger logger.ContextLogger) *Service[K] { return &Service[K]{ users: make(map[K][56]byte), keys: make(map[[56]byte]K), handler: handler, fallbackHandler: fallbackHandler, logger: logger, } } var ErrUserExists = E.New("user already exists") func (s *Service[K]) UpdateUsers(userList []K, passwordList []string) error { users := make(map[K][56]byte) keys := make(map[[56]byte]K) for i, user := range userList { if _, loaded := users[user]; loaded { return ErrUserExists } key := Key(passwordList[i]) if oldUser, loaded := keys[key]; loaded { return E.Extend(ErrUserExists, "password used by ", oldUser) } users[user] = key keys[key] = user } s.users = users s.keys = keys return nil } func (s *Service[K]) NewConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, onClose N.CloseHandlerFunc) error { var key [KeyLength]byte n, err := conn.Read(key[:]) if err != nil { return err } else if n != KeyLength { return s.fallback(ctx, conn, source, key[:n], E.New("bad request size"), onClose) } if user, loaded := s.keys[key]; loaded { ctx = auth.ContextWithUser(ctx, user) } else { return s.fallback(ctx, conn, source, key[:], E.New("bad request"), onClose) } err = rw.SkipN(conn, 2) if err != nil { return E.Cause(err, "skip crlf") } var command byte err = binary.Read(conn, binary.BigEndian, &command) if err != nil { return E.Cause(err, "read command") } switch command { case CommandTCP, CommandUDP, CommandMux: default: return E.New("unknown command ", command) } // var destination M.Socksaddr destination, err := M.SocksaddrSerializer.ReadAddrPort(conn) if err != nil { return E.Cause(err, "read destination") } err = rw.SkipN(conn, 2) if err != nil { return E.Cause(err, "skip crlf") } switch command { case CommandTCP: s.handler.NewConnectionEx(ctx, conn, source, destination, onClose) case CommandUDP: s.handler.NewPacketConnectionEx(ctx, &PacketConn{Conn: conn}, source, destination, onClose) // case CommandMux: default: return HandleMuxConnection(ctx, conn, source, s.handler, s.logger, onClose) } return nil } func (s *Service[K]) fallback(ctx context.Context, conn net.Conn, source M.Socksaddr, header []byte, err error, onClose N.CloseHandlerFunc) error { if s.fallbackHandler == nil { return E.Extend(err, "fallback disabled") } conn = bufio.NewCachedConn(conn, buf.As(header).ToOwned()) s.fallbackHandler.NewConnectionEx(ctx, conn, source, M.Socksaddr{}, onClose) return nil } type PacketConn struct { net.Conn readWaitOptions N.ReadWaitOptions } func (c *PacketConn) ReadPacket(buffer *buf.Buffer) (M.Socksaddr, error) { return ReadPacket(c.Conn, buffer) } func (c *PacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { return WritePacket(c.Conn, buffer, destination) } func (c *PacketConn) FrontHeadroom() int { return M.MaxSocksaddrLength + 4 } func (c *PacketConn) NeedAdditionalReadDeadline() bool { return true } func (c *PacketConn) Upstream() any { return c.Conn } ================================================ FILE: transport/trojan/service_wait.go ================================================ package trojan import ( "encoding/binary" "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/rw" ) var _ N.PacketReadWaiter = (*PacketConn)(nil) func (c *PacketConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { c.readWaitOptions = options return false } func (c *PacketConn) WaitReadPacket() (buffer *buf.Buffer, destination M.Socksaddr, err error) { destination, err = M.SocksaddrSerializer.ReadAddrPort(c.Conn) if err != nil { return nil, M.Socksaddr{}, E.Cause(err, "read destination") } var length uint16 err = binary.Read(c.Conn, binary.BigEndian, &length) if err != nil { return nil, M.Socksaddr{}, E.Cause(err, "read chunk length") } err = rw.SkipN(c.Conn, 2) if err != nil { return nil, M.Socksaddr{}, E.Cause(err, "skip crlf") } buffer = c.readWaitOptions.NewPacketBuffer() _, err = buffer.ReadFullFrom(c.Conn, int(length)) if err != nil { buffer.Release() return } c.readWaitOptions.PostReturn(buffer) return } ================================================ FILE: transport/v2ray/grpc.go ================================================ //go:build with_grpc package v2ray import ( "context" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/transport/v2raygrpc" "github.com/sagernet/sing-box/transport/v2raygrpclite" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) func NewGRPCServer(ctx context.Context, logger logger.ContextLogger, options option.V2RayGRPCOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (adapter.V2RayServerTransport, error) { if options.ForceLite { return v2raygrpclite.NewServer(ctx, logger, options, tlsConfig, handler) } return v2raygrpc.NewServer(ctx, logger, options, tlsConfig, handler) } func NewGRPCClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayGRPCOptions, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) { if options.ForceLite { return v2raygrpclite.NewClient(ctx, dialer, serverAddr, options, tlsConfig), nil } return v2raygrpc.NewClient(ctx, dialer, serverAddr, options, tlsConfig) } ================================================ FILE: transport/v2ray/grpc_lite.go ================================================ //go:build !with_grpc package v2ray import ( "context" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/transport/v2raygrpclite" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) func NewGRPCServer(ctx context.Context, logger logger.ContextLogger, options option.V2RayGRPCOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (adapter.V2RayServerTransport, error) { return v2raygrpclite.NewServer(ctx, logger, options, tlsConfig, handler) } func NewGRPCClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayGRPCOptions, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) { return v2raygrpclite.NewClient(ctx, dialer, serverAddr, options, tlsConfig), nil } ================================================ FILE: transport/v2ray/quic.go ================================================ package v2ray import ( "context" "os" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) var ( quicServerConstructor ServerConstructor[option.V2RayQUICOptions] quicClientConstructor ClientConstructor[option.V2RayQUICOptions] ) func RegisterQUICConstructor(server ServerConstructor[option.V2RayQUICOptions], client ClientConstructor[option.V2RayQUICOptions]) { quicServerConstructor = server quicClientConstructor = client } func NewQUICServer(ctx context.Context, logger logger.ContextLogger, options option.V2RayQUICOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (adapter.V2RayServerTransport, error) { if quicServerConstructor == nil { return nil, os.ErrInvalid } return quicServerConstructor(ctx, logger, options, tlsConfig, handler) } func NewQUICClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayQUICOptions, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) { if quicClientConstructor == nil { return nil, os.ErrInvalid } return quicClientConstructor(ctx, dialer, serverAddr, options, tlsConfig) } ================================================ FILE: transport/v2ray/transport.go ================================================ package v2ray import ( "context" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/transport/v2rayhttp" "github.com/sagernet/sing-box/transport/v2rayhttpupgrade" "github.com/sagernet/sing-box/transport/v2raywebsocket" 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" ) type ( ServerConstructor[O any] func(ctx context.Context, logger logger.ContextLogger, options O, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (adapter.V2RayServerTransport, error) ClientConstructor[O any] func(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options O, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) ) func NewServerTransport(ctx context.Context, logger logger.ContextLogger, options option.V2RayTransportOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (adapter.V2RayServerTransport, error) { if options.Type == "" { return nil, nil } switch options.Type { case C.V2RayTransportTypeHTTP: return v2rayhttp.NewServer(ctx, logger, options.HTTPOptions, tlsConfig, handler) case C.V2RayTransportTypeWebsocket: return v2raywebsocket.NewServer(ctx, logger, options.WebsocketOptions, tlsConfig, handler) case C.V2RayTransportTypeQUIC: if tlsConfig == nil { return nil, C.ErrTLSRequired } return NewQUICServer(ctx, logger, options.QUICOptions, tlsConfig, handler) case C.V2RayTransportTypeGRPC: return NewGRPCServer(ctx, logger, options.GRPCOptions, tlsConfig, handler) case C.V2RayTransportTypeHTTPUpgrade: return v2rayhttpupgrade.NewServer(ctx, logger, options.HTTPUpgradeOptions, tlsConfig, handler) default: return nil, E.New("unknown transport type: " + options.Type) } } func NewClientTransport(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayTransportOptions, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) { if options.Type == "" { return nil, nil } switch options.Type { case C.V2RayTransportTypeHTTP: return v2rayhttp.NewClient(ctx, dialer, serverAddr, options.HTTPOptions, tlsConfig) case C.V2RayTransportTypeGRPC: return NewGRPCClient(ctx, dialer, serverAddr, options.GRPCOptions, tlsConfig) case C.V2RayTransportTypeWebsocket: return v2raywebsocket.NewClient(ctx, dialer, serverAddr, options.WebsocketOptions, tlsConfig) case C.V2RayTransportTypeQUIC: if tlsConfig == nil { return nil, C.ErrTLSRequired } return NewQUICClient(ctx, dialer, serverAddr, options.QUICOptions, tlsConfig) case C.V2RayTransportTypeHTTPUpgrade: return v2rayhttpupgrade.NewClient(ctx, dialer, serverAddr, options.HTTPUpgradeOptions, tlsConfig) default: return nil, E.New("unknown transport type: " + options.Type) } } ================================================ FILE: transport/v2raygrpc/client.go ================================================ package v2raygrpc import ( "context" "net" "sync" "sync/atomic" "time" "github.com/sagernet/sing-box/adapter" "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" "google.golang.org/grpc" "google.golang.org/grpc/backoff" "google.golang.org/grpc/connectivity" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/keepalive" ) var _ adapter.V2RayClientTransport = (*Client)(nil) type Client struct { ctx context.Context dialer N.Dialer serverAddr string serviceName string dialOptions []grpc.DialOption conn atomic.Pointer[grpc.ClientConn] connAccess sync.Mutex } func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayGRPCOptions, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) { var dialOptions []grpc.DialOption if tlsConfig != nil { if len(tlsConfig.NextProtos()) == 0 { tlsConfig.SetNextProtos([]string{http2.NextProtoTLS}) } dialOptions = append(dialOptions, grpc.WithTransportCredentials(NewTLSTransportCredentials(tlsConfig))) } else { dialOptions = append(dialOptions, grpc.WithTransportCredentials(insecure.NewCredentials())) } if options.IdleTimeout > 0 { dialOptions = append(dialOptions, grpc.WithKeepaliveParams(keepalive.ClientParameters{ Time: time.Duration(options.IdleTimeout), Timeout: time.Duration(options.PingTimeout), PermitWithoutStream: options.PermitWithoutStream, })) } dialOptions = append(dialOptions, grpc.WithConnectParams(grpc.ConnectParams{ Backoff: backoff.Config{ BaseDelay: 500 * time.Millisecond, Multiplier: 1.5, Jitter: 0.2, MaxDelay: 19 * time.Second, }, MinConnectTimeout: 5 * time.Second, })) dialOptions = append(dialOptions, grpc.WithContextDialer(func(ctx context.Context, server string) (net.Conn, error) { return dialer.DialContext(ctx, N.NetworkTCP, M.ParseSocksaddr(server)) })) //nolint:staticcheck dialOptions = append(dialOptions, grpc.WithReturnConnectionError()) return &Client{ ctx: ctx, dialer: dialer, serverAddr: serverAddr.String(), serviceName: options.ServiceName, dialOptions: dialOptions, }, nil } func (c *Client) connect() (*grpc.ClientConn, error) { conn := c.conn.Load() if conn != nil && conn.GetState() != connectivity.Shutdown { return conn, nil } c.connAccess.Lock() defer c.connAccess.Unlock() conn = c.conn.Load() if conn != nil && conn.GetState() != connectivity.Shutdown { return conn, nil } //nolint:staticcheck conn, err := grpc.DialContext(c.ctx, c.serverAddr, c.dialOptions...) if err != nil { return nil, err } c.conn.Store(conn) return conn, nil } func (c *Client) DialContext(ctx context.Context) (net.Conn, error) { clientConn, err := c.connect() if err != nil { return nil, err } client := NewGunServiceClient(clientConn).(GunServiceCustomNameClient) ctx, cancel := context.WithCancelCause(ctx) stream, err := client.TunCustomName(ctx, c.serviceName) if err != nil { cancel(err) return nil, err } return NewGRPCConn(stream, cancel), nil } func (c *Client) Close() error { conn := c.conn.Swap(nil) if conn != nil { conn.Close() } return nil } ================================================ FILE: transport/v2raygrpc/conn.go ================================================ package v2raygrpc import ( "context" "net" "os" "sync" "time" "github.com/sagernet/sing/common/baderror" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) var _ net.Conn = (*GRPCConn)(nil) type GRPCConn struct { GunService cache []byte cancel context.CancelCauseFunc closeOnce sync.Once } func NewGRPCConn(service GunService, cancel context.CancelCauseFunc) *GRPCConn { //nolint:staticcheck if client, isClient := service.(GunService_TunClient); isClient { service = &clientConnWrapper{client} } return &GRPCConn{ GunService: service, cancel: cancel, } } func (c *GRPCConn) Read(b []byte) (n int, err error) { if len(c.cache) > 0 { n = copy(b, c.cache) c.cache = c.cache[n:] return } hunk, err := c.Recv() err = baderror.WrapGRPC(err) if err != nil { return } n = copy(b, hunk.Data) if n < len(hunk.Data) { c.cache = hunk.Data[n:] } return } func (c *GRPCConn) Write(b []byte) (n int, err error) { err = baderror.WrapGRPC(c.Send(&Hunk{Data: b})) if err != nil { return } return len(b), nil } func (c *GRPCConn) Close() error { c.closeOnce.Do(func() { if c.cancel != nil { c.cancel(nil) } }) return nil } func (c *GRPCConn) LocalAddr() net.Addr { return M.Socksaddr{} } func (c *GRPCConn) RemoteAddr() net.Addr { return M.Socksaddr{} } func (c *GRPCConn) SetDeadline(t time.Time) error { return os.ErrInvalid } func (c *GRPCConn) SetReadDeadline(t time.Time) error { return os.ErrInvalid } func (c *GRPCConn) SetWriteDeadline(t time.Time) error { return os.ErrInvalid } func (c *GRPCConn) NeedAdditionalReadDeadline() bool { return true } func (c *GRPCConn) Upstream() any { return c.GunService } var _ N.WriteCloser = (*clientConnWrapper)(nil) type clientConnWrapper struct { GunService_TunClient } func (c *clientConnWrapper) CloseWrite() error { return c.CloseSend() } ================================================ FILE: transport/v2raygrpc/credentials/credentials.go ================================================ /* * Copyright 2021 gRPC authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package credentials import ( "context" ) // requestInfoKey is a struct to be used as the key to store RequestInfo in a // context. type requestInfoKey struct{} // NewRequestInfoContext creates a context with ri. func NewRequestInfoContext(ctx context.Context, ri any) context.Context { return context.WithValue(ctx, requestInfoKey{}, ri) } // RequestInfoFromContext extracts the RequestInfo from ctx. func RequestInfoFromContext(ctx context.Context) any { return ctx.Value(requestInfoKey{}) } // clientHandshakeInfoKey is a struct used as the key to store // ClientHandshakeInfo in a context. type clientHandshakeInfoKey struct{} // ClientHandshakeInfoFromContext extracts the ClientHandshakeInfo from ctx. func ClientHandshakeInfoFromContext(ctx context.Context) any { return ctx.Value(clientHandshakeInfoKey{}) } // NewClientHandshakeInfoContext creates a context with chi. func NewClientHandshakeInfoContext(ctx context.Context, chi any) context.Context { return context.WithValue(ctx, clientHandshakeInfoKey{}, chi) } ================================================ FILE: transport/v2raygrpc/credentials/spiffe.go ================================================ /* * * Copyright 2020 gRPC authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // Package credentials defines APIs for parsing SPIFFE ID. // // All APIs in this package are experimental. package credentials import ( "crypto/tls" "crypto/x509" "net/url" "google.golang.org/grpc/grpclog" ) var logger = grpclog.Component("credentials") // SPIFFEIDFromState parses the SPIFFE ID from State. If the SPIFFE ID format // is invalid, return nil with warning. func SPIFFEIDFromState(state tls.ConnectionState) *url.URL { if len(state.PeerCertificates) == 0 || len(state.PeerCertificates[0].URIs) == 0 { return nil } return SPIFFEIDFromCert(state.PeerCertificates[0]) } // SPIFFEIDFromCert parses the SPIFFE ID from x509.Certificate. If the SPIFFE // ID format is invalid, return nil with warning. func SPIFFEIDFromCert(cert *x509.Certificate) *url.URL { if cert == nil || cert.URIs == nil { return nil } var spiffeID *url.URL for _, uri := range cert.URIs { if uri == nil || uri.Scheme != "spiffe" || uri.Opaque != "" || (uri.User != nil && uri.User.Username() != "") { continue } // From this point, we assume the uri is intended for a SPIFFE ID. if len(uri.String()) > 2048 { logger.Warning("invalid SPIFFE ID: total ID length larger than 2048 bytes") return nil } if len(uri.Host) == 0 || len(uri.Path) == 0 { logger.Warning("invalid SPIFFE ID: domain or workload ID is empty") return nil } if len(uri.Host) > 255 { logger.Warning("invalid SPIFFE ID: domain length larger than 255 characters") return nil } // A valid SPIFFE certificate can only have exactly one URI SAN field. if len(cert.URIs) > 1 { logger.Warning("invalid SPIFFE ID: multiple URI SANs") return nil } spiffeID = uri } return spiffeID } ================================================ FILE: transport/v2raygrpc/credentials/syscallconn.go ================================================ /* * * Copyright 2018 gRPC authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package credentials import ( "net" "syscall" ) type sysConn = syscall.Conn // syscallConn keeps reference of rawConn to support syscall.Conn for channelz. // SyscallConn() (the method in interface syscall.Conn) is explicitly // implemented on this type, // // Interface syscall.Conn is implemented by most net.Conn implementations (e.g. // TCPConn, UnixConn), but is not part of net.Conn interface. So wrapper conns // that embed net.Conn don't implement syscall.Conn. (Side note: tls.Conn // doesn't embed net.Conn, so even if syscall.Conn is part of net.Conn, it won't // help here). type syscallConn struct { net.Conn // sysConn is a type alias of syscall.Conn. It's necessary because the name // `Conn` collides with `net.Conn`. sysConn } // WrapSyscallConn tries to wrap rawConn and newConn into a net.Conn that // implements syscall.Conn. rawConn will be used to support syscall, and newConn // will be used for read/write. // // This function returns newConn if rawConn doesn't implement syscall.Conn. func WrapSyscallConn(rawConn, newConn net.Conn) net.Conn { sysConn, ok := rawConn.(syscall.Conn) if !ok { return newConn } return &syscallConn{ Conn: newConn, sysConn: sysConn, } } ================================================ FILE: transport/v2raygrpc/credentials/util.go ================================================ /* * * Copyright 2020 gRPC authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package credentials import ( "crypto/tls" "slices" ) const alpnProtoStrH2 = "h2" // AppendH2ToNextProtos appends h2 to next protos. func AppendH2ToNextProtos(ps []string) []string { if slices.Contains(ps, alpnProtoStrH2) { return ps } ret := make([]string, 0, len(ps)+1) ret = append(ret, ps...) return append(ret, alpnProtoStrH2) } // CloneTLSConfig returns a shallow clone of the exported // fields of cfg, ignoring the unexported sync.Once, which // contains a mutex and must not be copied. // // If cfg is nil, a new zero tls.Config is returned. // // TODO: inline this function if possible. func CloneTLSConfig(cfg *tls.Config) *tls.Config { if cfg == nil { return &tls.Config{} } return cfg.Clone() } ================================================ FILE: transport/v2raygrpc/custom_name.go ================================================ package v2raygrpc import ( "context" "google.golang.org/grpc" ) type GunService interface { Context() context.Context Send(*Hunk) error Recv() (*Hunk, error) } func ServerDesc(name string) grpc.ServiceDesc { return grpc.ServiceDesc{ ServiceName: name, HandlerType: (*GunServiceServer)(nil), Methods: []grpc.MethodDesc{}, Streams: []grpc.StreamDesc{ { StreamName: "Tun", Handler: _GunService_Tun_Handler, ServerStreams: true, ClientStreams: true, }, }, Metadata: "gun.proto", } } func (c *gunServiceClient) TunCustomName(ctx context.Context, name string, opts ...grpc.CallOption) (GunService_TunClient, error) { stream, err := c.cc.NewStream(ctx, &ServerDesc(name).Streams[0], "/"+name+"/Tun", opts...) if err != nil { return nil, err } x := &grpc.GenericClientStream[Hunk, Hunk]{ClientStream: stream} return x, nil } var _ GunServiceCustomNameClient = (*gunServiceClient)(nil) type GunServiceCustomNameClient interface { TunCustomName(ctx context.Context, name string, opts ...grpc.CallOption) (GunService_TunClient, error) Tun(ctx context.Context, opts ...grpc.CallOption) (GunService_TunClient, error) } func RegisterGunServiceCustomNameServer(s *grpc.Server, srv GunServiceServer, name string) { desc := ServerDesc(name) s.RegisterService(&desc, srv) } ================================================ FILE: transport/v2raygrpc/server.go ================================================ package v2raygrpc import ( "context" "net" "os" "strings" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/tls" "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" "golang.org/x/net/http2" "google.golang.org/grpc" "google.golang.org/grpc/keepalive" gM "google.golang.org/grpc/metadata" "google.golang.org/grpc/peer" ) var _ adapter.V2RayServerTransport = (*Server)(nil) type Server struct { ctx context.Context logger logger.ContextLogger handler adapter.V2RayServerTransportHandler server *grpc.Server } func NewServer(ctx context.Context, logger logger.ContextLogger, options option.V2RayGRPCOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) { var serverOptions []grpc.ServerOption if tlsConfig != nil { if !common.Contains(tlsConfig.NextProtos(), http2.NextProtoTLS) { tlsConfig.SetNextProtos(append([]string{"h2"}, tlsConfig.NextProtos()...)) } serverOptions = append(serverOptions, grpc.Creds(NewTLSTransportCredentials(tlsConfig))) } if options.IdleTimeout > 0 { serverOptions = append(serverOptions, grpc.KeepaliveParams(keepalive.ServerParameters{ Time: time.Duration(options.IdleTimeout), Timeout: time.Duration(options.PingTimeout), })) } server := &Server{ctx, logger, handler, grpc.NewServer(serverOptions...)} RegisterGunServiceCustomNameServer(server.server, server, options.ServiceName) return server, nil } func (s *Server) Tun(server GunService_TunServer) error { conn := NewGRPCConn(server, nil) var source M.Socksaddr if remotePeer, loaded := peer.FromContext(server.Context()); loaded { source = M.SocksaddrFromNet(remotePeer.Addr) } if grpcMetadata, loaded := gM.FromIncomingContext(server.Context()); loaded { forwardFrom := strings.Join(grpcMetadata.Get("X-Forwarded-For"), ",") if forwardFrom != "" { for from := range strings.SplitSeq(forwardFrom, ",") { originAddr := M.ParseSocksaddr(from) if originAddr.IsValid() { source = originAddr.Unwrap() } } } } done := make(chan struct{}) go s.handler.NewConnectionEx(log.ContextWithNewID(s.ctx), conn, source, M.Socksaddr{}, N.OnceClose(func(it error) { close(done) })) <-done return nil } func (s *Server) mustEmbedUnimplementedGunServiceServer() { } func (s *Server) Network() []string { return []string{N.NetworkTCP} } func (s *Server) Serve(listener net.Listener) error { return s.server.Serve(listener) } func (s *Server) ServePacket(listener net.PacketConn) error { return os.ErrInvalid } func (s *Server) Close() error { s.server.Stop() return nil } ================================================ FILE: transport/v2raygrpc/stream.pb.go ================================================ package v2raygrpc import ( reflect "reflect" sync "sync" unsafe "unsafe" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) type Hunk struct { state protoimpl.MessageState `protogen:"open.v1"` Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Hunk) Reset() { *x = Hunk{} mi := &file_transport_v2raygrpc_stream_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } func (x *Hunk) String() string { return protoimpl.X.MessageStringOf(x) } func (*Hunk) ProtoMessage() {} func (x *Hunk) ProtoReflect() protoreflect.Message { mi := &file_transport_v2raygrpc_stream_proto_msgTypes[0] 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 Hunk.ProtoReflect.Descriptor instead. func (*Hunk) Descriptor() ([]byte, []int) { return file_transport_v2raygrpc_stream_proto_rawDescGZIP(), []int{0} } func (x *Hunk) GetData() []byte { if x != nil { return x.Data } return nil } var File_transport_v2raygrpc_stream_proto protoreflect.FileDescriptor const file_transport_v2raygrpc_stream_proto_rawDesc = "" + "\n" + " transport/v2raygrpc/stream.proto\x12\x13transport.v2raygrpc\"\x1a\n" + "\x04Hunk\x12\x12\n" + "\x04data\x18\x01 \x01(\fR\x04data2M\n" + "\n" + "GunService\x12?\n" + "\x03Tun\x12\x19.transport.v2raygrpc.Hunk\x1a\x19.transport.v2raygrpc.Hunk(\x010\x01B2Z0github.com/sagernet/sing-box/transport/v2raygrpcb\x06proto3" var ( file_transport_v2raygrpc_stream_proto_rawDescOnce sync.Once file_transport_v2raygrpc_stream_proto_rawDescData []byte ) func file_transport_v2raygrpc_stream_proto_rawDescGZIP() []byte { file_transport_v2raygrpc_stream_proto_rawDescOnce.Do(func() { file_transport_v2raygrpc_stream_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_v2raygrpc_stream_proto_rawDesc), len(file_transport_v2raygrpc_stream_proto_rawDesc))) }) return file_transport_v2raygrpc_stream_proto_rawDescData } var ( file_transport_v2raygrpc_stream_proto_msgTypes = make([]protoimpl.MessageInfo, 1) file_transport_v2raygrpc_stream_proto_goTypes = []any{ (*Hunk)(nil), // 0: transport.v2raygrpc.Hunk } ) var file_transport_v2raygrpc_stream_proto_depIdxs = []int32{ 0, // 0: transport.v2raygrpc.GunService.Tun:input_type -> transport.v2raygrpc.Hunk 0, // 1: transport.v2raygrpc.GunService.Tun:output_type -> transport.v2raygrpc.Hunk 1, // [1:2] is the sub-list for method output_type 0, // [0:1] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name } func init() { file_transport_v2raygrpc_stream_proto_init() } func file_transport_v2raygrpc_stream_proto_init() { if File_transport_v2raygrpc_stream_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_v2raygrpc_stream_proto_rawDesc), len(file_transport_v2raygrpc_stream_proto_rawDesc)), NumEnums: 0, NumMessages: 1, NumExtensions: 0, NumServices: 1, }, GoTypes: file_transport_v2raygrpc_stream_proto_goTypes, DependencyIndexes: file_transport_v2raygrpc_stream_proto_depIdxs, MessageInfos: file_transport_v2raygrpc_stream_proto_msgTypes, }.Build() File_transport_v2raygrpc_stream_proto = out.File file_transport_v2raygrpc_stream_proto_goTypes = nil file_transport_v2raygrpc_stream_proto_depIdxs = nil } ================================================ FILE: transport/v2raygrpc/stream.proto ================================================ syntax = "proto3"; package transport.v2raygrpc; option go_package = "github.com/sagernet/sing-box/transport/v2raygrpc"; message Hunk { bytes data = 1; } service GunService { rpc Tun (stream Hunk) returns (stream Hunk); } ================================================ FILE: transport/v2raygrpc/stream_grpc.pb.go ================================================ package v2raygrpc import ( context "context" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" ) // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. // Requires gRPC-Go v1.64.0 or later. const _ = grpc.SupportPackageIsVersion9 const ( GunService_Tun_FullMethodName = "/transport.v2raygrpc.GunService/Tun" ) // GunServiceClient is the client API for GunService service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type GunServiceClient interface { Tun(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[Hunk, Hunk], error) } type gunServiceClient struct { cc grpc.ClientConnInterface } func NewGunServiceClient(cc grpc.ClientConnInterface) GunServiceClient { return &gunServiceClient{cc} } func (c *gunServiceClient) Tun(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[Hunk, Hunk], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) stream, err := c.cc.NewStream(ctx, &GunService_ServiceDesc.Streams[0], GunService_Tun_FullMethodName, cOpts...) if err != nil { return nil, err } x := &grpc.GenericClientStream[Hunk, Hunk]{ClientStream: stream} 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 GunService_TunClient = grpc.BidiStreamingClient[Hunk, Hunk] // GunServiceServer is the server API for GunService service. // All implementations must embed UnimplementedGunServiceServer // for forward compatibility. type GunServiceServer interface { Tun(grpc.BidiStreamingServer[Hunk, Hunk]) error mustEmbedUnimplementedGunServiceServer() } // UnimplementedGunServiceServer must be embedded to have // forward compatible implementations. // // NOTE: this should be embedded by value instead of pointer to avoid a nil // pointer dereference when methods are called. type UnimplementedGunServiceServer struct{} func (UnimplementedGunServiceServer) Tun(grpc.BidiStreamingServer[Hunk, Hunk]) error { return status.Error(codes.Unimplemented, "method Tun not implemented") } func (UnimplementedGunServiceServer) mustEmbedUnimplementedGunServiceServer() {} func (UnimplementedGunServiceServer) testEmbeddedByValue() {} // UnsafeGunServiceServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to GunServiceServer will // result in compilation errors. type UnsafeGunServiceServer interface { mustEmbedUnimplementedGunServiceServer() } func RegisterGunServiceServer(s grpc.ServiceRegistrar, srv GunServiceServer) { // If the following call panics, it indicates UnimplementedGunServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { t.testEmbeddedByValue() } s.RegisterService(&GunService_ServiceDesc, srv) } func _GunService_Tun_Handler(srv interface{}, stream grpc.ServerStream) error { return srv.(GunServiceServer).Tun(&grpc.GenericServerStream[Hunk, Hunk]{ServerStream: stream}) } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type GunService_TunServer = grpc.BidiStreamingServer[Hunk, Hunk] // GunService_ServiceDesc is the grpc.ServiceDesc for GunService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) var GunService_ServiceDesc = grpc.ServiceDesc{ ServiceName: "transport.v2raygrpc.GunService", HandlerType: (*GunServiceServer)(nil), Methods: []grpc.MethodDesc{}, Streams: []grpc.StreamDesc{ { StreamName: "Tun", Handler: _GunService_Tun_Handler, ServerStreams: true, ClientStreams: true, }, }, Metadata: "transport/v2raygrpc/stream.proto", } ================================================ FILE: transport/v2raygrpc/tls_credentials.go ================================================ package v2raygrpc import ( "context" "net" "os" "github.com/sagernet/sing-box/common/tls" internal_credentials "github.com/sagernet/sing-box/transport/v2raygrpc/credentials" "google.golang.org/grpc/credentials" ) type TLSTransportCredentials struct { config tls.Config } func NewTLSTransportCredentials(config tls.Config) credentials.TransportCredentials { return &TLSTransportCredentials{config} } func (c *TLSTransportCredentials) Info() credentials.ProtocolInfo { return credentials.ProtocolInfo{ SecurityProtocol: "tls", SecurityVersion: "1.2", ServerName: c.config.ServerName(), } } func (c *TLSTransportCredentials) ClientHandshake(ctx context.Context, authority string, rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) { cfg := c.config.Clone() if cfg.ServerName() == "" { serverName, _, err := net.SplitHostPort(authority) if err != nil { serverName = authority } cfg.SetServerName(serverName) } conn, err := tls.ClientHandshake(ctx, rawConn, cfg) if err != nil { return nil, nil, err } tlsInfo := credentials.TLSInfo{ State: conn.ConnectionState(), CommonAuthInfo: credentials.CommonAuthInfo{ SecurityLevel: credentials.PrivacyAndIntegrity, }, } id := internal_credentials.SPIFFEIDFromState(conn.ConnectionState()) if id != nil { tlsInfo.SPIFFEID = id } return internal_credentials.WrapSyscallConn(rawConn, conn), tlsInfo, nil } func (c *TLSTransportCredentials) ServerHandshake(rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) { serverConfig, isServer := c.config.(tls.ServerConfig) if !isServer { return nil, nil, os.ErrInvalid } conn, err := tls.ServerHandshake(context.Background(), rawConn, serverConfig) if err != nil { rawConn.Close() return nil, nil, err } tlsInfo := credentials.TLSInfo{ State: conn.ConnectionState(), CommonAuthInfo: credentials.CommonAuthInfo{ SecurityLevel: credentials.PrivacyAndIntegrity, }, } id := internal_credentials.SPIFFEIDFromState(conn.ConnectionState()) if id != nil { tlsInfo.SPIFFEID = id } return internal_credentials.WrapSyscallConn(rawConn, conn), tlsInfo, nil } func (c *TLSTransportCredentials) Clone() credentials.TransportCredentials { return NewTLSTransportCredentials(c.config) } func (c *TLSTransportCredentials) OverrideServerName(serverNameOverride string) error { c.config.SetServerName(serverNameOverride) return nil } ================================================ FILE: transport/v2raygrpclite/client.go ================================================ package v2raygrpclite import ( "context" "io" "net" "net/http" "net/url" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/transport/v2rayhttp" 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 _ adapter.V2RayClientTransport = (*Client)(nil) var defaultClientHeader = http.Header{ "Content-Type": []string{"application/grpc"}, "User-Agent": []string{"grpc-go/1.48.0"}, "TE": []string{"trailers"}, } type Client struct { ctx context.Context serverAddr M.Socksaddr transport *http2.Transport options option.V2RayGRPCOptions url *url.URL host string } func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayGRPCOptions, tlsConfig tls.Config) adapter.V2RayClientTransport { var host string if tlsConfig != nil && tlsConfig.ServerName() != "" { host = M.ParseSocksaddrHostPort(tlsConfig.ServerName(), serverAddr.Port).String() } else { host = serverAddr.String() } client := &Client{ ctx: ctx, serverAddr: serverAddr, options: options, transport: &http2.Transport{ ReadIdleTimeout: time.Duration(options.IdleTimeout), PingTimeout: time.Duration(options.PingTimeout), DisableCompression: true, }, url: &url.URL{ Scheme: "https", Host: serverAddr.String(), Path: "/" + options.ServiceName + "/Tun", RawPath: "/" + url.PathEscape(options.ServiceName) + "/Tun", }, host: host, } if tlsConfig == nil { client.transport.DialTLSContext = func(ctx context.Context, network, addr string, cfg *tls.STDConfig) (net.Conn, error) { return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) } } else { if len(tlsConfig.NextProtos()) == 0 { tlsConfig.SetNextProtos([]string{http2.NextProtoTLS}) } tlsDialer := tls.NewDialer(dialer, tlsConfig) client.transport.DialTLSContext = func(ctx context.Context, network, addr string, cfg *tls.STDConfig) (net.Conn, error) { return tlsDialer.DialTLSContext(ctx, M.ParseSocksaddr(addr)) } } return client } func (c *Client) DialContext(ctx context.Context) (net.Conn, error) { pipeInReader, pipeInWriter := io.Pipe() request := &http.Request{ Method: http.MethodPost, Body: pipeInReader, URL: c.url, Header: defaultClientHeader, Host: c.host, } request = request.WithContext(ctx) conn := newLateGunConn(pipeInWriter) go func() { response, err := c.transport.RoundTrip(request) if err != nil { conn.setup(nil, err) } else if response.StatusCode != 200 { response.Body.Close() conn.setup(nil, E.New("v2ray-grpc: unexpected status: ", response.Status)) } else { conn.setup(response.Body, nil) } }() return conn, nil } func (c *Client) Close() error { v2rayhttp.ResetTransport(c.transport) return nil } ================================================ FILE: transport/v2raygrpclite/conn.go ================================================ package v2raygrpclite import ( std_bufio "bufio" "encoding/binary" "io" "net" "net/http" "os" "time" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/baderror" "github.com/sagernet/sing/common/buf" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/varbin" ) // kanged from: https://github.com/Qv2ray/gun-lite var _ net.Conn = (*GunConn)(nil) type GunConn struct { rawReader io.Reader reader *std_bufio.Reader writer io.Writer flusher http.Flusher create chan struct{} err error readRemaining int } func newGunConn(reader io.Reader, writer io.Writer, flusher http.Flusher) *GunConn { return &GunConn{ rawReader: reader, reader: std_bufio.NewReader(reader), writer: writer, flusher: flusher, } } func newLateGunConn(writer io.Writer) *GunConn { return &GunConn{ create: make(chan struct{}), writer: writer, } } func (c *GunConn) setup(reader io.Reader, err error) { if reader != nil { c.rawReader = reader c.reader = std_bufio.NewReader(reader) } c.err = err close(c.create) } func (c *GunConn) Read(b []byte) (n int, err error) { n, err = c.read(b) return n, baderror.WrapH2(err) } func (c *GunConn) read(b []byte) (n int, err error) { if c.reader == nil { <-c.create if c.err != nil { return 0, c.err } } if c.readRemaining > 0 { if len(b) > c.readRemaining { b = b[:c.readRemaining] } n, err = c.reader.Read(b) c.readRemaining -= n return } _, err = c.reader.Discard(6) if err != nil { return } dataLen, err := binary.ReadUvarint(c.reader) if err != nil { return } readLen := int(dataLen) c.readRemaining = readLen if len(b) > readLen { b = b[:readLen] } n, err = c.reader.Read(b) c.readRemaining -= n return } func (c *GunConn) Write(b []byte) (n int, err error) { varLen := varbin.UvarintLen(uint64(len(b))) buffer := buf.NewSize(6 + varLen + len(b)) header := buffer.Extend(6 + varLen) header[0] = 0x00 binary.BigEndian.PutUint32(header[1:5], uint32(1+varLen+len(b))) header[5] = 0x0A binary.PutUvarint(header[6:], uint64(len(b))) common.Must1(buffer.Write(b)) _, err = c.writer.Write(buffer.Bytes()) if err != nil { return 0, baderror.WrapH2(err) } if c.flusher != nil { c.flusher.Flush() } return len(b), nil } func (c *GunConn) WriteBuffer(buffer *buf.Buffer) error { defer buffer.Release() dataLen := buffer.Len() varLen := varbin.UvarintLen(uint64(dataLen)) header := buffer.ExtendHeader(6 + varLen) header[0] = 0x00 binary.BigEndian.PutUint32(header[1:5], uint32(1+varLen+dataLen)) header[5] = 0x0A binary.PutUvarint(header[6:], uint64(dataLen)) err := common.Error(c.writer.Write(buffer.Bytes())) if err != nil { return baderror.WrapH2(err) } if c.flusher != nil { c.flusher.Flush() } return nil } func (c *GunConn) FrontHeadroom() int { return 6 + binary.MaxVarintLen64 } func (c *GunConn) Close() error { return common.Close(c.rawReader, c.writer) } func (c *GunConn) LocalAddr() net.Addr { return M.Socksaddr{} } func (c *GunConn) RemoteAddr() net.Addr { return M.Socksaddr{} } func (c *GunConn) SetDeadline(t time.Time) error { return os.ErrInvalid } func (c *GunConn) SetReadDeadline(t time.Time) error { return os.ErrInvalid } func (c *GunConn) SetWriteDeadline(t time.Time) error { return os.ErrInvalid } func (c *GunConn) NeedAdditionalReadDeadline() bool { return true } ================================================ FILE: transport/v2raygrpclite/server.go ================================================ package v2raygrpclite import ( "context" "net" "net/http" "os" "strings" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/transport/v2rayhttp" "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" aTLS "github.com/sagernet/sing/common/tls" sHttp "github.com/sagernet/sing/protocol/http" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" ) var _ adapter.V2RayServerTransport = (*Server)(nil) type Server struct { tlsConfig tls.ServerConfig logger logger.ContextLogger handler adapter.V2RayServerTransportHandler httpServer *http.Server h2Server *http2.Server h2cHandler http.Handler path string } func NewServer(ctx context.Context, logger logger.ContextLogger, options option.V2RayGRPCOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) { server := &Server{ tlsConfig: tlsConfig, logger: logger, handler: handler, path: "/" + options.ServiceName + "/Tun", h2Server: &http2.Server{ IdleTimeout: time.Duration(options.IdleTimeout), }, } server.httpServer = &http.Server{ Handler: server, BaseContext: func(net.Listener) context.Context { return ctx }, ConnContext: func(ctx context.Context, c net.Conn) context.Context { return log.ContextWithNewID(ctx) }, } server.h2cHandler = h2c.NewHandler(server, server.h2Server) return server, nil } func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { if request.Method == "PRI" && len(request.Header) == 0 && request.URL.Path == "*" && request.Proto == "HTTP/2.0" { s.h2cHandler.ServeHTTP(writer, request) return } if request.URL.Path != s.path { s.invalidRequest(writer, request, http.StatusNotFound, E.New("bad path: ", request.URL.Path)) return } if request.Method != http.MethodPost { s.invalidRequest(writer, request, http.StatusNotFound, E.New("bad method: ", request.Method)) return } if ct := request.Header.Get("Content-Type"); !strings.HasPrefix(ct, "application/grpc") { s.invalidRequest(writer, request, http.StatusNotFound, E.New("bad content type: ", ct)) return } writer.Header().Set("Content-Type", "application/grpc") writer.Header().Set("TE", "trailers") writer.WriteHeader(http.StatusOK) done := make(chan struct{}) conn := v2rayhttp.NewHTTP2Wrapper(newGunConn(request.Body, writer, writer.(http.Flusher))) s.handler.NewConnectionEx(request.Context(), conn, sHttp.SourceAddress(request), M.Socksaddr{}, N.OnceClose(func(it error) { close(done) })) <-done conn.CloseWrapper() } func (s *Server) invalidRequest(writer http.ResponseWriter, request *http.Request, statusCode int, err error) { if statusCode > 0 { writer.WriteHeader(statusCode) } s.logger.ErrorContext(request.Context(), E.Cause(err, "process connection from ", request.RemoteAddr)) } func (s *Server) Network() []string { return []string{N.NetworkTCP} } func (s *Server) Serve(listener net.Listener) error { if s.tlsConfig != nil { if !common.Contains(s.tlsConfig.NextProtos(), http2.NextProtoTLS) { s.tlsConfig.SetNextProtos(append([]string{"h2"}, s.tlsConfig.NextProtos()...)) } listener = aTLS.NewListener(listener, s.tlsConfig) } return s.httpServer.Serve(listener) } func (s *Server) ServePacket(listener net.PacketConn) error { return os.ErrInvalid } func (s *Server) Close() error { return common.Close(common.PtrOrNil(s.httpServer)) } ================================================ FILE: transport/v2rayhttp/client.go ================================================ package v2rayhttp import ( "context" "io" "math/rand" "net" "net/http" "net/url" "strings" "time" "github.com/sagernet/sing-box/adapter" "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" sHTTP "github.com/sagernet/sing/protocol/http" "golang.org/x/net/http2" ) var _ adapter.V2RayClientTransport = (*Client)(nil) type Client struct { ctx context.Context dialer N.Dialer serverAddr M.Socksaddr transport http.RoundTripper http2 bool requestURL url.URL host []string method string headers http.Header } func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayHTTPOptions, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) { var transport http.RoundTripper if tlsConfig == nil { transport = &http.Transport{ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) }, } } else { if len(tlsConfig.NextProtos()) == 0 { tlsConfig.SetNextProtos([]string{http2.NextProtoTLS}) } tlsDialer := tls.NewDialer(dialer, tlsConfig) transport = &http2.Transport{ ReadIdleTimeout: time.Duration(options.IdleTimeout), PingTimeout: time.Duration(options.PingTimeout), DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.STDConfig) (net.Conn, error) { return tlsDialer.DialTLSContext(ctx, M.ParseSocksaddr(addr)) }, } } if options.Method == "" { options.Method = http.MethodPut } var requestURL url.URL if tlsConfig == nil { requestURL.Scheme = "http" } else { requestURL.Scheme = "https" } requestURL.Host = serverAddr.String() requestURL.Path = options.Path err := sHTTP.URLSetPath(&requestURL, options.Path) if err != nil { return nil, E.Cause(err, "parse path") } if !strings.HasPrefix(requestURL.Path, "/") { requestURL.Path = "/" + requestURL.Path } return &Client{ ctx: ctx, dialer: dialer, serverAddr: serverAddr, requestURL: requestURL, host: options.Host, method: options.Method, headers: options.Headers.Build(), transport: transport, http2: tlsConfig != nil, }, nil } func (c *Client) DialContext(ctx context.Context) (net.Conn, error) { if !c.http2 { return c.dialHTTP(ctx) } else { return c.dialHTTP2(ctx) } } func (c *Client) dialHTTP(ctx context.Context) (net.Conn, error) { conn, err := c.dialer.DialContext(ctx, N.NetworkTCP, c.serverAddr) if err != nil { return nil, err } request := &http.Request{ Method: c.method, URL: &c.requestURL, Header: c.headers.Clone(), } switch hostLen := len(c.host); hostLen { case 0: request.Host = c.serverAddr.AddrString() case 1: request.Host = c.host[0] default: request.Host = c.host[rand.Intn(hostLen)] } return NewHTTP1Conn(conn, request), nil } func (c *Client) dialHTTP2(ctx context.Context) (net.Conn, error) { pipeInReader, pipeInWriter := io.Pipe() request := &http.Request{ Method: c.method, Body: pipeInReader, URL: &c.requestURL, Header: c.headers.Clone(), } request = request.WithContext(ctx) switch hostLen := len(c.host); hostLen { case 0: // https://github.com/v2fly/v2ray-core/blob/master/transport/internet/http/config.go#L13 request.Host = "www.example.com" case 1: request.Host = c.host[0] default: request.Host = c.host[rand.Intn(hostLen)] } conn := NewLateHTTPConn(pipeInWriter) go func() { response, err := c.transport.RoundTrip(request) if err != nil { conn.Setup(nil, err) } else if response.StatusCode != 200 { response.Body.Close() conn.Setup(nil, E.New("v2ray-http: unexpected status: ", response.Status)) } else { conn.Setup(response.Body, nil) } }() return conn, nil } func (c *Client) Close() error { c.transport = ResetTransport(c.transport) return nil } ================================================ FILE: transport/v2rayhttp/conn.go ================================================ package v2rayhttp import ( std_bufio "bufio" "context" "io" "net" "net/http" "os" "strings" "sync" "time" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/baderror" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) type HTTPConn struct { net.Conn request *http.Request requestWritten bool responseRead bool responseCache *buf.Buffer } func NewHTTP1Conn(conn net.Conn, request *http.Request) *HTTPConn { if request.Header.Get("Host") == "" { request.Header.Set("Host", request.Host) } return &HTTPConn{ Conn: conn, request: request, } } func (c *HTTPConn) Read(b []byte) (n int, err error) { if !c.responseRead { reader := std_bufio.NewReader(c.Conn) response, err := http.ReadResponse(reader, c.request) if err != nil { return 0, E.Cause(err, "read response") } if response.StatusCode != 200 { return 0, E.New("v2ray-http: unexpected status: ", response.Status) } if cacheLen := reader.Buffered(); cacheLen > 0 { c.responseCache = buf.NewSize(cacheLen) _, err = c.responseCache.ReadFullFrom(reader, cacheLen) if err != nil { c.responseCache.Release() return 0, E.Cause(err, "read cache") } } c.responseRead = true } if c.responseCache != nil { n, err = c.responseCache.Read(b) if err == io.EOF { c.responseCache.Release() c.responseCache = nil } if n > 0 { return n, nil } } return c.Conn.Read(b) } func (c *HTTPConn) Write(b []byte) (int, error) { if !c.requestWritten { err := c.writeRequest(b) if err != nil { return 0, E.Cause(err, "write request") } c.requestWritten = true return len(b), nil } return c.Conn.Write(b) } func (c *HTTPConn) writeRequest(payload []byte) error { writer := bufio.NewBufferedWriter(c.Conn, buf.New()) const CRLF = "\r\n" _, err := writer.Write([]byte(F.ToString(c.request.Method, " ", c.request.URL.RequestURI(), " HTTP/1.1", CRLF))) if err != nil { return err } for key, value := range c.request.Header { _, err = writer.Write([]byte(F.ToString(key, ": ", strings.Join(value, ", "), CRLF))) if err != nil { return err } } _, err = writer.Write([]byte(CRLF)) if err != nil { return err } _, err = writer.Write(payload) if err != nil { return err } err = writer.Fallthrough() if err != nil { return err } return nil } func (c *HTTPConn) ReaderReplaceable() bool { return c.responseRead } func (c *HTTPConn) WriterReplaceable() bool { return c.requestWritten } func (c *HTTPConn) NeedHandshake() bool { return !c.requestWritten } func (c *HTTPConn) Upstream() any { return c.Conn } type HTTP2Conn struct { reader io.Reader writer io.Writer create chan struct{} err error } func NewHTTPConn(reader io.Reader, writer io.Writer) HTTP2Conn { return HTTP2Conn{ reader: reader, writer: writer, } } func NewLateHTTPConn(writer io.Writer) *HTTP2Conn { return &HTTP2Conn{ create: make(chan struct{}), writer: writer, } } func (c *HTTP2Conn) Setup(reader io.Reader, err error) { c.reader = reader c.err = err close(c.create) } func (c *HTTP2Conn) Read(b []byte) (n int, err error) { if c.reader == nil { <-c.create if c.err != nil { return 0, c.err } } n, err = c.reader.Read(b) return n, baderror.WrapH2(err) } func (c *HTTP2Conn) Write(b []byte) (n int, err error) { n, err = c.writer.Write(b) return n, baderror.WrapH2(err) } func (c *HTTP2Conn) Close() error { return common.Close(c.reader, c.writer) } func (c *HTTP2Conn) LocalAddr() net.Addr { return M.Socksaddr{} } func (c *HTTP2Conn) RemoteAddr() net.Addr { return M.Socksaddr{} } func (c *HTTP2Conn) SetDeadline(t time.Time) error { return os.ErrInvalid } func (c *HTTP2Conn) SetReadDeadline(t time.Time) error { return os.ErrInvalid } func (c *HTTP2Conn) SetWriteDeadline(t time.Time) error { return os.ErrInvalid } func (c *HTTP2Conn) NeedAdditionalReadDeadline() bool { return true } type ServerHTTPConn struct { HTTP2Conn Flusher http.Flusher } func (c *ServerHTTPConn) Write(b []byte) (n int, err error) { n, err = c.writer.Write(b) if err == nil { c.Flusher.Flush() } return } type HTTP2ConnWrapper struct { N.ExtendedConn access sync.Mutex closed bool } func NewHTTP2Wrapper(conn net.Conn) *HTTP2ConnWrapper { return &HTTP2ConnWrapper{ ExtendedConn: bufio.NewExtendedConn(conn), } } func (w *HTTP2ConnWrapper) Write(p []byte) (n int, err error) { w.access.Lock() defer w.access.Unlock() if w.closed { return 0, net.ErrClosed } return w.ExtendedConn.Write(p) } func (w *HTTP2ConnWrapper) WriteBuffer(buffer *buf.Buffer) error { w.access.Lock() defer w.access.Unlock() if w.closed { return net.ErrClosed } return w.ExtendedConn.WriteBuffer(buffer) } func (w *HTTP2ConnWrapper) CloseWrapper() { w.access.Lock() defer w.access.Unlock() w.closed = true } func (w *HTTP2ConnWrapper) Close() error { w.CloseWrapper() return w.ExtendedConn.Close() } func (w *HTTP2ConnWrapper) Upstream() any { return w.ExtendedConn } func DupContext(ctx context.Context) context.Context { id, loaded := log.IDFromContext(ctx) if !loaded { return context.Background() } return log.ContextWithID(context.Background(), id) } ================================================ FILE: transport/v2rayhttp/force_close.go ================================================ package v2rayhttp import ( "net/http" "reflect" "sync" "unsafe" E "github.com/sagernet/sing/common/exceptions" "golang.org/x/net/http2" ) type clientConnPool struct { t *http2.Transport mu sync.Mutex conns map[string][]*http2.ClientConn // key is host:port } type efaceWords struct { typ unsafe.Pointer data unsafe.Pointer } func ResetTransport(rawTransport http.RoundTripper) http.RoundTripper { switch transport := rawTransport.(type) { case *http.Transport: transport.CloseIdleConnections() return transport.Clone() case *http2.Transport: connPool := transportConnPool(transport) p := (*clientConnPool)((*efaceWords)(unsafe.Pointer(&connPool)).data) p.mu.Lock() defer p.mu.Unlock() for _, vv := range p.conns { for _, cc := range vv { cc.Close() } } return transport default: panic(E.New("unknown transport type: ", reflect.TypeOf(transport))) } } //go:linkname transportConnPool golang.org/x/net/http2.(*Transport).connPool func transportConnPool(t *http2.Transport) http2.ClientConnPool ================================================ FILE: transport/v2rayhttp/pool.go ================================================ package v2rayhttp import "net/http" type ConnectionPool interface { CloseIdleConnections() } func CloseIdleConnections(transport http.RoundTripper) { if connectionPool, ok := transport.(ConnectionPool); ok { connectionPool.CloseIdleConnections() } } ================================================ FILE: transport/v2rayhttp/server.go ================================================ package v2rayhttp import ( "context" "net" "net/http" "os" "strings" "time" "github.com/sagernet/sing-box/adapter" "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" "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" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" aTLS "github.com/sagernet/sing/common/tls" sHttp "github.com/sagernet/sing/protocol/http" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" ) var _ adapter.V2RayServerTransport = (*Server)(nil) type Server struct { ctx context.Context logger logger.ContextLogger tlsConfig tls.ServerConfig handler adapter.V2RayServerTransportHandler httpServer *http.Server h2Server *http2.Server h2cHandler http.Handler host []string path string method string headers http.Header } func NewServer(ctx context.Context, logger logger.ContextLogger, options option.V2RayHTTPOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) { server := &Server{ ctx: ctx, tlsConfig: tlsConfig, logger: logger, handler: handler, h2Server: &http2.Server{ IdleTimeout: time.Duration(options.IdleTimeout), }, host: options.Host, path: options.Path, method: options.Method, headers: options.Headers.Build(), } if !strings.HasPrefix(server.path, "/") { server.path = "/" + server.path } server.httpServer = &http.Server{ Handler: server, ReadHeaderTimeout: C.TCPTimeout, MaxHeaderBytes: http.DefaultMaxHeaderBytes, BaseContext: func(net.Listener) context.Context { return ctx }, ConnContext: func(ctx context.Context, c net.Conn) context.Context { return log.ContextWithNewID(ctx) }, } server.h2cHandler = h2c.NewHandler(server, server.h2Server) return server, nil } func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { if request.Method == "PRI" && len(request.Header) == 0 && request.URL.Path == "*" && request.Proto == "HTTP/2.0" { s.h2cHandler.ServeHTTP(writer, request) return } host := request.Host if len(s.host) > 0 && !common.Contains(s.host, host) { s.invalidRequest(writer, request, http.StatusBadRequest, E.New("bad host: ", host)) return } if !strings.HasPrefix(request.URL.Path, s.path) { s.invalidRequest(writer, request, http.StatusNotFound, E.New("bad path: ", request.URL.Path)) return } if s.method != "" && request.Method != s.method { s.invalidRequest(writer, request, http.StatusNotFound, E.New("bad method: ", request.Method)) return } writer.Header().Set("Cache-Control", "no-store") for key, values := range s.headers { for _, value := range values { writer.Header().Set(key, value) } } source := sHttp.SourceAddress(request) if h, ok := writer.(http.Hijacker); ok { var requestBody *buf.Buffer if contentLength := int(request.ContentLength); contentLength > 0 { requestBody = buf.NewSize(contentLength) _, err := requestBody.ReadFullFrom(request.Body, contentLength) if err != nil { s.invalidRequest(writer, request, 0, E.Cause(err, "read request")) return } } writer.WriteHeader(http.StatusOK) writer.(http.Flusher).Flush() conn, reader, err := h.Hijack() if err != nil { s.invalidRequest(writer, request, 0, E.Cause(err, "hijack conn")) return } if cacheLen := reader.Reader.Buffered(); cacheLen > 0 { cache := buf.NewSize(cacheLen) _, err = cache.ReadFullFrom(reader.Reader, cacheLen) if err != nil { conn.Close() s.invalidRequest(writer, request, 0, E.Cause(err, "read cache")) return } conn = bufio.NewCachedConn(conn, cache) } if requestBody != nil { conn = bufio.NewCachedConn(conn, requestBody) } s.handler.NewConnectionEx(DupContext(request.Context()), conn, source, M.Socksaddr{}, nil) } else { writer.WriteHeader(http.StatusOK) flusher := writer.(http.Flusher) flusher.Flush() done := make(chan struct{}) conn := NewHTTP2Wrapper(&ServerHTTPConn{ NewHTTPConn(request.Body, writer), flusher, }) s.handler.NewConnectionEx(request.Context(), conn, source, M.Socksaddr{}, N.OnceClose(func(it error) { close(done) })) <-done conn.CloseWrapper() } } func (s *Server) invalidRequest(writer http.ResponseWriter, request *http.Request, statusCode int, err error) { if statusCode > 0 { writer.WriteHeader(statusCode) } s.logger.ErrorContext(request.Context(), E.Cause(err, "process connection from ", request.RemoteAddr)) } func (s *Server) Network() []string { return []string{N.NetworkTCP} } func (s *Server) Serve(listener net.Listener) error { if s.tlsConfig != nil { if len(s.tlsConfig.NextProtos()) == 0 { s.tlsConfig.SetNextProtos([]string{http2.NextProtoTLS, "http/1.1"}) } else if !common.Contains(s.tlsConfig.NextProtos(), http2.NextProtoTLS) { s.tlsConfig.SetNextProtos(append([]string{http2.NextProtoTLS}, s.tlsConfig.NextProtos()...)) } listener = aTLS.NewListener(listener, s.tlsConfig) } return s.httpServer.Serve(listener) } func (s *Server) ServePacket(listener net.PacketConn) error { return os.ErrInvalid } func (s *Server) Close() error { return common.Close(common.PtrOrNil(s.httpServer)) } ================================================ FILE: transport/v2rayhttpupgrade/client.go ================================================ package v2rayhttpupgrade import ( std_bufio "bufio" "context" "net" "net/http" "net/url" "strings" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/tls" "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" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" sHTTP "github.com/sagernet/sing/protocol/http" ) var _ adapter.V2RayClientTransport = (*Client)(nil) type Client struct { dialer N.Dialer serverAddr M.Socksaddr requestURL url.URL headers http.Header host string } func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayHTTPUpgradeOptions, tlsConfig tls.Config) (*Client, error) { if tlsConfig != nil { if len(tlsConfig.NextProtos()) == 0 { tlsConfig.SetNextProtos([]string{"http/1.1"}) } dialer = tls.NewDialer(dialer, tlsConfig) } var host string if options.Host != "" { host = options.Host } else if tlsConfig != nil && tlsConfig.ServerName() != "" { host = tlsConfig.ServerName() } else { host = serverAddr.String() } var requestURL url.URL if tlsConfig == nil { requestURL.Scheme = "http" } else { requestURL.Scheme = "https" } requestURL.Host = serverAddr.String() requestURL.Path = options.Path err := sHTTP.URLSetPath(&requestURL, options.Path) if err != nil { return nil, E.Cause(err, "parse path") } if !strings.HasPrefix(requestURL.Path, "/") { requestURL.Path = "/" + requestURL.Path } headers := make(http.Header) for key, value := range options.Headers { headers[key] = value } return &Client{ dialer: dialer, serverAddr: serverAddr, requestURL: requestURL, headers: headers, host: host, }, nil } func (c *Client) DialContext(ctx context.Context) (net.Conn, error) { conn, err := c.dialer.DialContext(ctx, N.NetworkTCP, c.serverAddr) if err != nil { return nil, err } request := &http.Request{ Method: http.MethodGet, URL: &c.requestURL, Header: c.headers.Clone(), Host: c.host, } request.Header.Set("Connection", "Upgrade") request.Header.Set("Upgrade", "websocket") err = request.Write(conn) if err != nil { return nil, err } bufReader := std_bufio.NewReader(conn) response, err := http.ReadResponse(bufReader, request) if err != nil { return nil, err } if response.StatusCode != 101 || !strings.EqualFold(response.Header.Get("Connection"), "upgrade") || !strings.EqualFold(response.Header.Get("Upgrade"), "websocket") { return nil, E.New("v2ray-http-upgrade: unexpected status: ", response.Status) } if bufReader.Buffered() > 0 { buffer := buf.NewSize(bufReader.Buffered()) _, err = buffer.ReadFullFrom(bufReader, buffer.Len()) if err != nil { return nil, err } conn = bufio.NewCachedConn(conn, buffer) } return conn, nil } func (c *Client) Close() error { return nil } ================================================ FILE: transport/v2rayhttpupgrade/server.go ================================================ package v2rayhttpupgrade import ( "context" "net" "net/http" "os" "strings" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/transport/v2rayhttp" "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" aTLS "github.com/sagernet/sing/common/tls" sHttp "github.com/sagernet/sing/protocol/http" ) var _ adapter.V2RayServerTransport = (*Server)(nil) type Server struct { ctx context.Context logger logger.ContextLogger tlsConfig tls.ServerConfig handler adapter.V2RayServerTransportHandler httpServer *http.Server host string path string headers http.Header } func NewServer(ctx context.Context, logger logger.ContextLogger, options option.V2RayHTTPUpgradeOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) { server := &Server{ ctx: ctx, logger: logger, tlsConfig: tlsConfig, handler: handler, host: options.Host, path: options.Path, headers: options.Headers.Build(), } if !strings.HasPrefix(server.path, "/") { server.path = "/" + server.path } server.httpServer = &http.Server{ Handler: server, ReadHeaderTimeout: C.TCPTimeout, MaxHeaderBytes: http.DefaultMaxHeaderBytes, BaseContext: func(net.Listener) context.Context { return ctx }, ConnContext: func(ctx context.Context, c net.Conn) context.Context { return log.ContextWithNewID(ctx) }, TLSNextProto: make(map[string]func(*http.Server, *tls.STDConn, http.Handler)), } return server, nil } type httpFlusher interface { FlushError() error } func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { host := request.Host if len(s.host) > 0 && host != s.host { s.invalidRequest(writer, request, http.StatusBadRequest, E.New("bad host: ", host)) return } if request.URL.Path != s.path { s.invalidRequest(writer, request, http.StatusNotFound, E.New("bad path: ", request.URL.Path)) return } if request.Method != http.MethodGet { s.invalidRequest(writer, request, http.StatusNotFound, E.New("bad method: ", request.Method)) return } if !strings.EqualFold(request.Header.Get("Connection"), "upgrade") { s.invalidRequest(writer, request, http.StatusNotFound, E.New("not a upgrade request")) return } if !strings.EqualFold(request.Header.Get("Upgrade"), "websocket") { s.invalidRequest(writer, request, http.StatusNotFound, E.New("not a websocket request")) return } if request.Header.Get("Sec-WebSocket-Key") != "" { s.invalidRequest(writer, request, http.StatusNotFound, E.New("real websocket request received")) return } writer.Header().Set("Connection", "upgrade") writer.Header().Set("Upgrade", "websocket") writer.WriteHeader(http.StatusSwitchingProtocols) if flusher, isFlusher := writer.(httpFlusher); isFlusher { err := flusher.FlushError() if err != nil { s.invalidRequest(writer, request, http.StatusInternalServerError, E.New("flush response")) } } hijacker, canHijack := writer.(http.Hijacker) if !canHijack { s.invalidRequest(writer, request, http.StatusInternalServerError, E.New("invalid connection, maybe HTTP/2")) return } conn, _, err := hijacker.Hijack() if err != nil { s.invalidRequest(writer, request, http.StatusInternalServerError, E.Cause(err, "hijack failed")) return } s.handler.NewConnectionEx(v2rayhttp.DupContext(request.Context()), conn, sHttp.SourceAddress(request), M.Socksaddr{}, nil) } func (s *Server) invalidRequest(writer http.ResponseWriter, request *http.Request, statusCode int, err error) { if statusCode > 0 { writer.WriteHeader(statusCode) } s.logger.ErrorContext(request.Context(), E.Cause(err, "process connection from ", request.RemoteAddr)) } func (s *Server) Network() []string { return []string{N.NetworkTCP} } func (s *Server) Serve(listener net.Listener) error { if s.tlsConfig != nil { if len(s.tlsConfig.NextProtos()) == 0 { s.tlsConfig.SetNextProtos([]string{"http/1.1"}) } listener = aTLS.NewListener(listener, s.tlsConfig) } return s.httpServer.Serve(listener) } func (s *Server) ServePacket(listener net.PacketConn) error { return os.ErrInvalid } func (s *Server) Close() error { return common.Close(common.PtrOrNil(s.httpServer)) } ================================================ FILE: transport/v2rayquic/client.go ================================================ //go:build with_quic package v2rayquic import ( "context" "net" "sync" "github.com/sagernet/quic-go" "github.com/sagernet/quic-go/http3" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-quic" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/bufio" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) var _ adapter.V2RayClientTransport = (*Client)(nil) type Client struct { ctx context.Context dialer N.Dialer serverAddr M.Socksaddr tlsConfig tls.Config quicConfig *quic.Config connAccess sync.Mutex conn common.TypedValue[*quic.Conn] rawConn net.Conn } func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayQUICOptions, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) { quicConfig := &quic.Config{ DisablePathMTUDiscovery: !C.IsLinux && !C.IsWindows, } if len(tlsConfig.NextProtos()) == 0 { tlsConfig.SetNextProtos([]string{http3.NextProtoH3}) } return &Client{ ctx: ctx, dialer: dialer, serverAddr: serverAddr, tlsConfig: tlsConfig, quicConfig: quicConfig, }, nil } func (c *Client) offer() (*quic.Conn, error) { conn := c.conn.Load() if conn != nil && !common.Done(conn.Context()) { return conn, nil } c.connAccess.Lock() defer c.connAccess.Unlock() conn = c.conn.Load() if conn != nil && !common.Done(conn.Context()) { return conn, nil } conn, err := c.offerNew() if err != nil { return nil, err } return conn, nil } func (c *Client) offerNew() (*quic.Conn, error) { udpConn, err := c.dialer.DialContext(c.ctx, "udp", c.serverAddr) if err != nil { return nil, err } packetConn := bufio.NewUnbindPacketConn(udpConn) quicConn, err := qtls.Dial(c.ctx, packetConn, udpConn.RemoteAddr(), c.tlsConfig, c.quicConfig) if err != nil { packetConn.Close() return nil, err } c.conn.Store(quicConn) c.rawConn = udpConn return quicConn, nil } func (c *Client) DialContext(ctx context.Context) (net.Conn, error) { conn, err := c.offer() if err != nil { return nil, err } stream, err := conn.OpenStream() if err != nil { return nil, err } return &StreamWrapper{Conn: conn, Stream: stream}, nil } func (c *Client) Close() error { c.connAccess.Lock() defer c.connAccess.Unlock() conn := c.conn.Swap(nil) if conn != nil { conn.CloseWithError(0, "") } if c.rawConn != nil { c.rawConn.Close() } c.rawConn = nil return nil } ================================================ FILE: transport/v2rayquic/init.go ================================================ //go:build with_quic package v2rayquic import "github.com/sagernet/sing-box/transport/v2ray" func init() { v2ray.RegisterQUICConstructor(NewServer, NewClient) } ================================================ FILE: transport/v2rayquic/server.go ================================================ //go:build with_quic package v2rayquic import ( "context" "net" "os" "github.com/sagernet/quic-go" "github.com/sagernet/quic-go/http3" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-quic" "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" ) var _ adapter.V2RayServerTransport = (*Server)(nil) type Server struct { ctx context.Context logger logger.ContextLogger tlsConfig tls.ServerConfig quicConfig *quic.Config handler adapter.V2RayServerTransportHandler udpListener net.PacketConn quicListener qtls.Listener } func NewServer(ctx context.Context, logger logger.ContextLogger, options option.V2RayQUICOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (adapter.V2RayServerTransport, error) { quicConfig := &quic.Config{ DisablePathMTUDiscovery: !C.IsLinux && !C.IsWindows, } if len(tlsConfig.NextProtos()) == 0 { tlsConfig.SetNextProtos([]string{http3.NextProtoH3}) } server := &Server{ ctx: ctx, logger: logger, tlsConfig: tlsConfig, quicConfig: quicConfig, handler: handler, } return server, nil } func (s *Server) Network() []string { return []string{N.NetworkUDP} } func (s *Server) Serve(listener net.Listener) error { return os.ErrInvalid } func (s *Server) ServePacket(listener net.PacketConn) error { quicListener, err := qtls.Listen(listener, s.tlsConfig, s.quicConfig) if err != nil { return err } s.udpListener = listener s.quicListener = quicListener go s.acceptLoop() return nil } func (s *Server) acceptLoop() { for { conn, err := s.quicListener.Accept(s.ctx) if err != nil { return } go func() { hErr := s.streamAcceptLoop(conn) if hErr != nil && !E.IsClosedOrCanceled(hErr) { s.logger.ErrorContext(conn.Context(), hErr) } }() } } func (s *Server) streamAcceptLoop(conn *quic.Conn) error { for { stream, err := conn.AcceptStream(s.ctx) if err != nil { return qtls.WrapError(err) } go s.handler.NewConnectionEx(conn.Context(), &StreamWrapper{Conn: conn, Stream: stream}, M.SocksaddrFromNet(conn.RemoteAddr()), M.Socksaddr{}, nil) } } func (s *Server) Close() error { return common.Close(s.udpListener, s.quicListener) } ================================================ FILE: transport/v2rayquic/stream.go ================================================ package v2rayquic import ( "net" "github.com/sagernet/quic-go" qtls "github.com/sagernet/sing-quic" ) type StreamWrapper struct { Conn *quic.Conn *quic.Stream } func (s *StreamWrapper) Read(p []byte) (n int, err error) { n, err = s.Stream.Read(p) return n, qtls.WrapError(err) } func (s *StreamWrapper) Write(p []byte) (n int, err error) { n, err = s.Stream.Write(p) return n, qtls.WrapError(err) } func (s *StreamWrapper) LocalAddr() net.Addr { return s.Conn.LocalAddr() } func (s *StreamWrapper) RemoteAddr() net.Addr { return s.Conn.RemoteAddr() } func (s *StreamWrapper) Upstream() any { return s.Stream } func (s *StreamWrapper) Close() error { s.CancelRead(0) s.Stream.Close() return nil } ================================================ FILE: transport/v2raywebsocket/client.go ================================================ package v2raywebsocket import ( "context" "net" "net/http" "net/url" "strings" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/bufio/deadline" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" sHTTP "github.com/sagernet/sing/protocol/http" "github.com/sagernet/ws" ) var _ adapter.V2RayClientTransport = (*Client)(nil) type Client struct { dialer N.Dialer serverAddr M.Socksaddr requestURL url.URL headers http.Header maxEarlyData uint32 earlyDataHeaderName string } func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayWebsocketOptions, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) { if tlsConfig != nil { if len(tlsConfig.NextProtos()) == 0 { tlsConfig.SetNextProtos([]string{"http/1.1"}) } dialer = tls.NewDialer(dialer, tlsConfig) } var requestURL url.URL if tlsConfig == nil { requestURL.Scheme = "ws" } else { requestURL.Scheme = "wss" } requestURL.Host = serverAddr.String() requestURL.Path = options.Path err := sHTTP.URLSetPath(&requestURL, options.Path) if err != nil { return nil, E.Cause(err, "parse path") } if !strings.HasPrefix(requestURL.Path, "/") { requestURL.Path = "/" + requestURL.Path } headers := options.Headers.Build() if host := headers.Get("Host"); host != "" { headers.Del("Host") requestURL.Host = host } if headers.Get("User-Agent") == "" { headers.Set("User-Agent", "Go-http-client/1.1") } return &Client{ dialer, serverAddr, requestURL, headers, options.MaxEarlyData, options.EarlyDataHeaderName, }, nil } func (c *Client) dialContext(ctx context.Context, requestURL *url.URL, headers http.Header) (*WebsocketConn, error) { conn, err := c.dialer.DialContext(ctx, N.NetworkTCP, c.serverAddr) if err != nil { return nil, err } var deadlineConn net.Conn if deadline.NeedAdditionalReadDeadline(conn) { deadlineConn = deadline.NewConn(conn) } else { deadlineConn = conn } deadlineConn.SetDeadline(time.Now().Add(C.TCPTimeout)) var protocols []string if protocolHeader := headers.Get("Sec-WebSocket-Protocol"); protocolHeader != "" { protocols = []string{protocolHeader} headers.Del("Sec-WebSocket-Protocol") } reader, _, err := ws.Dialer{Header: ws.HandshakeHeaderHTTP(headers), Protocols: protocols}.Upgrade(deadlineConn, requestURL) deadlineConn.SetDeadline(time.Time{}) if err != nil { return nil, err } if reader != nil { buffer := buf.NewSize(reader.Buffered()) _, err = buffer.ReadFullFrom(reader, buffer.Len()) if err != nil { return nil, err } conn = bufio.NewCachedConn(conn, buffer) } return NewConn(conn, nil, ws.StateClientSide), nil } func (c *Client) DialContext(ctx context.Context) (net.Conn, error) { if c.maxEarlyData <= 0 { conn, err := c.dialContext(ctx, &c.requestURL, c.headers) if err != nil { return nil, err } return conn, nil } else { return &EarlyWebsocketConn{Client: c, ctx: ctx, create: make(chan struct{})}, nil } } func (c *Client) Close() error { return nil } ================================================ FILE: transport/v2raywebsocket/conn.go ================================================ package v2raywebsocket import ( "context" "encoding/base64" "errors" "io" "net" "os" "sync" "sync/atomic" "time" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/debug" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/ws" "github.com/sagernet/ws/wsutil" ) type WebsocketConn struct { net.Conn *Writer state ws.State reader *wsutil.Reader controlHandler wsutil.FrameHandlerFunc remoteAddr net.Addr } func NewConn(conn net.Conn, remoteAddr net.Addr, state ws.State) *WebsocketConn { controlHandler := wsutil.ControlFrameHandler(conn, state) return &WebsocketConn{ Conn: conn, state: state, reader: &wsutil.Reader{ Source: conn, State: state, SkipHeaderCheck: !debug.Enabled, OnIntermediate: controlHandler, }, controlHandler: controlHandler, remoteAddr: remoteAddr, Writer: NewWriter(conn, state), } } func (c *WebsocketConn) Close() error { c.Conn.SetWriteDeadline(time.Now().Add(C.TCPTimeout)) frame := ws.NewCloseFrame(ws.NewCloseFrameBody( ws.StatusNormalClosure, "", )) if c.state == ws.StateClientSide { frame = ws.MaskFrameInPlace(frame) } ws.WriteFrame(c.Conn, frame) c.Conn.Close() return nil } func (c *WebsocketConn) Read(b []byte) (n int, err error) { var header ws.Header for { n, err = c.reader.Read(b) if n > 0 { err = nil return } if !E.IsMulti(err, io.EOF, wsutil.ErrNoFrameAdvance) { err = wrapWsError(err) return } header, err = wrapWsError0(c.reader.NextFrame()) if err != nil { return } if header.OpCode.IsControl() { if header.Length > 128 { err = wsutil.ErrFrameTooLarge return } err = wrapWsError(c.controlHandler(header, c.reader)) if err != nil { return } continue } if header.OpCode&ws.OpBinary == 0 { err = wrapWsError(c.reader.Discard()) if err != nil { return } continue } } } func (c *WebsocketConn) Write(p []byte) (n int, err error) { err = wrapWsError(wsutil.WriteMessage(c.Conn, c.state, ws.OpBinary, p)) if err != nil { return } n = len(p) return } func (c *WebsocketConn) RemoteAddr() net.Addr { if c.remoteAddr != nil { return c.remoteAddr } return c.Conn.RemoteAddr() } func (c *WebsocketConn) SetDeadline(t time.Time) error { return os.ErrInvalid } func (c *WebsocketConn) SetReadDeadline(t time.Time) error { return os.ErrInvalid } func (c *WebsocketConn) SetWriteDeadline(t time.Time) error { return os.ErrInvalid } func (c *WebsocketConn) NeedAdditionalReadDeadline() bool { return true } func (c *WebsocketConn) Upstream() any { return c.Conn } type EarlyWebsocketConn struct { *Client ctx context.Context conn atomic.Pointer[WebsocketConn] access sync.Mutex create chan struct{} err error } func (c *EarlyWebsocketConn) Read(b []byte) (n int, err error) { conn := c.conn.Load() if conn == nil { <-c.create if c.err != nil { return 0, c.err } conn = c.conn.Load() } return wrapWsError0(conn.Read(b)) } func (c *EarlyWebsocketConn) writeRequest(content []byte) error { var ( earlyData []byte lateData []byte conn *WebsocketConn err error ) if len(content) > int(c.maxEarlyData) { earlyData = content[:c.maxEarlyData] lateData = content[c.maxEarlyData:] } else { earlyData = content } if len(earlyData) > 0 { earlyDataString := base64.RawURLEncoding.EncodeToString(earlyData) if c.earlyDataHeaderName == "" { requestURL := c.requestURL requestURL.Path += earlyDataString conn, err = c.dialContext(c.ctx, &requestURL, c.headers) } else { headers := c.headers.Clone() headers.Set(c.earlyDataHeaderName, earlyDataString) conn, err = c.dialContext(c.ctx, &c.requestURL, headers) } } else { conn, err = c.dialContext(c.ctx, &c.requestURL, c.headers) } if err != nil { return err } if len(lateData) > 0 { _, err = conn.Write(lateData) if err != nil { return err } } c.conn.Store(conn) return nil } func (c *EarlyWebsocketConn) Write(b []byte) (n int, err error) { conn := c.conn.Load() if conn != nil { return wrapWsError0(conn.Write(b)) } c.access.Lock() defer c.access.Unlock() conn = c.conn.Load() if c.err != nil { return 0, c.err } if conn != nil { return wrapWsError0(conn.Write(b)) } err = c.writeRequest(b) c.err = err close(c.create) if err != nil { return } return len(b), nil } func (c *EarlyWebsocketConn) WriteBuffer(buffer *buf.Buffer) error { conn := c.conn.Load() if conn != nil { return wrapWsError(conn.WriteBuffer(buffer)) } c.access.Lock() defer c.access.Unlock() if c.err != nil { return c.err } conn = c.conn.Load() if conn != nil { return wrapWsError(conn.WriteBuffer(buffer)) } err := c.writeRequest(buffer.Bytes()) c.err = err close(c.create) return err } func (c *EarlyWebsocketConn) Close() error { conn := c.conn.Load() if conn == nil { return nil } return conn.Close() } func (c *EarlyWebsocketConn) LocalAddr() net.Addr { conn := c.conn.Load() if conn == nil { return M.Socksaddr{} } return conn.LocalAddr() } func (c *EarlyWebsocketConn) RemoteAddr() net.Addr { conn := c.conn.Load() if conn == nil { return M.Socksaddr{} } return conn.RemoteAddr() } func (c *EarlyWebsocketConn) SetDeadline(t time.Time) error { return os.ErrInvalid } func (c *EarlyWebsocketConn) SetReadDeadline(t time.Time) error { return os.ErrInvalid } func (c *EarlyWebsocketConn) SetWriteDeadline(t time.Time) error { return os.ErrInvalid } func (c *EarlyWebsocketConn) NeedAdditionalReadDeadline() bool { return true } func (c *EarlyWebsocketConn) Upstream() any { return common.PtrOrNil(c.conn.Load()) } func (c *EarlyWebsocketConn) LazyHeadroom() bool { return c.conn.Load() == nil } func wrapWsError(err error) error { if err == nil { return nil } var closedErr wsutil.ClosedError if errors.As(err, &closedErr) { if closedErr.Code == ws.StatusNormalClosure || closedErr.Code == ws.StatusNoStatusRcvd { err = io.EOF } } return err } func wrapWsError0[T any](value T, err error) (T, error) { if err == nil { return value, nil } return value, wrapWsError(err) } ================================================ FILE: transport/v2raywebsocket/server.go ================================================ package v2raywebsocket import ( "context" "encoding/base64" "net" "net/http" "os" "strings" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/transport/v2rayhttp" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" aTLS "github.com/sagernet/sing/common/tls" sHttp "github.com/sagernet/sing/protocol/http" "github.com/sagernet/ws" ) var _ adapter.V2RayServerTransport = (*Server)(nil) type Server struct { ctx context.Context logger logger.ContextLogger tlsConfig tls.ServerConfig handler adapter.V2RayServerTransportHandler httpServer *http.Server path string maxEarlyData uint32 earlyDataHeaderName string upgrader ws.HTTPUpgrader } func NewServer(ctx context.Context, logger logger.ContextLogger, options option.V2RayWebsocketOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) { server := &Server{ ctx: ctx, logger: logger, tlsConfig: tlsConfig, handler: handler, path: options.Path, maxEarlyData: options.MaxEarlyData, earlyDataHeaderName: options.EarlyDataHeaderName, upgrader: ws.HTTPUpgrader{ Timeout: C.TCPTimeout, Header: options.Headers.Build(), }, } if !strings.HasPrefix(server.path, "/") { server.path = "/" + server.path } server.httpServer = &http.Server{ Handler: server, ReadHeaderTimeout: C.TCPTimeout, MaxHeaderBytes: http.DefaultMaxHeaderBytes, BaseContext: func(net.Listener) context.Context { return ctx }, ConnContext: func(ctx context.Context, c net.Conn) context.Context { return log.ContextWithNewID(ctx) }, } return server, nil } func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { if s.maxEarlyData == 0 || s.earlyDataHeaderName != "" { if request.URL.Path != s.path { s.invalidRequest(writer, request, http.StatusNotFound, E.New("bad path: ", request.URL.Path)) return } } var ( earlyData []byte err error conn net.Conn ) if s.earlyDataHeaderName == "" { if strings.HasPrefix(request.URL.RequestURI(), s.path) { earlyDataStr := request.URL.RequestURI()[len(s.path):] earlyData, err = base64.RawURLEncoding.DecodeString(earlyDataStr) } else { s.invalidRequest(writer, request, http.StatusNotFound, E.New("bad path: ", request.URL.Path)) return } } else { if request.URL.Path != s.path { s.invalidRequest(writer, request, http.StatusNotFound, E.New("bad path: ", request.URL.Path)) return } earlyDataStr := request.Header.Get(s.earlyDataHeaderName) if earlyDataStr != "" { earlyData, err = base64.RawURLEncoding.DecodeString(earlyDataStr) } } if err != nil { s.invalidRequest(writer, request, http.StatusBadRequest, E.Cause(err, "decode early data")) return } wsConn, _, _, err := ws.UpgradeHTTP(request, writer) if err != nil { s.invalidRequest(writer, request, 0, E.Cause(err, "upgrade websocket connection")) return } source := sHttp.SourceAddress(request) conn = NewConn(wsConn, source, ws.StateServerSide) if len(earlyData) > 0 { conn = bufio.NewCachedConn(conn, buf.As(earlyData)) } s.handler.NewConnectionEx(v2rayhttp.DupContext(request.Context()), conn, source, M.Socksaddr{}, nil) } func (s *Server) invalidRequest(writer http.ResponseWriter, request *http.Request, statusCode int, err error) { if statusCode > 0 { writer.WriteHeader(statusCode) } s.logger.ErrorContext(request.Context(), E.Cause(err, "process connection from ", request.RemoteAddr)) } func (s *Server) Network() []string { return []string{N.NetworkTCP} } func (s *Server) Serve(listener net.Listener) error { if s.tlsConfig != nil { listener = aTLS.NewListener(listener, s.tlsConfig) } return s.httpServer.Serve(listener) } func (s *Server) ServePacket(listener net.PacketConn) error { return os.ErrInvalid } func (s *Server) Close() error { return common.Close(common.PtrOrNil(s.httpServer)) } ================================================ FILE: transport/v2raywebsocket/writer.go ================================================ package v2raywebsocket import ( "encoding/binary" "io" "math/rand" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" N "github.com/sagernet/sing/common/network" "github.com/sagernet/ws" ) type Writer struct { writer N.ExtendedWriter isServer bool } func NewWriter(writer io.Writer, state ws.State) *Writer { return &Writer{ bufio.NewExtendedWriter(writer), state == ws.StateServerSide, } } func (w *Writer) WriteBuffer(buffer *buf.Buffer) error { var payloadBitLength int dataLen := buffer.Len() data := buffer.Bytes() if dataLen < 126 { payloadBitLength = 1 } else if dataLen < 65536 { payloadBitLength = 3 } else { payloadBitLength = 9 } var headerLen int headerLen += 1 // FIN / RSV / OPCODE headerLen += payloadBitLength if !w.isServer { headerLen += 4 // MASK KEY } header := buffer.ExtendHeader(headerLen) header[0] = byte(ws.OpBinary) | 0x80 if w.isServer { header[1] = 0 } else { header[1] = 1 << 7 } if dataLen < 126 { header[1] |= byte(dataLen) } else if dataLen < 65536 { header[1] |= 126 binary.BigEndian.PutUint16(header[2:], uint16(dataLen)) } else { header[1] |= 127 binary.BigEndian.PutUint64(header[2:], uint64(dataLen)) } if !w.isServer { maskKey := rand.Uint32() binary.BigEndian.PutUint32(header[1+payloadBitLength:], maskKey) ws.Cipher(data, [4]byte(header[1+payloadBitLength:]), 0) } return wrapWsError(w.writer.WriteBuffer(buffer)) } func (w *Writer) FrontHeadroom() int { return 14 } ================================================ FILE: transport/wireguard/client_bind.go ================================================ package wireguard import ( "context" "net" "net/netip" "sync" "time" "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" "github.com/sagernet/sing/service/pause" "github.com/sagernet/wireguard-go/conn" ) var _ conn.Bind = (*ClientBind)(nil) type ClientBind struct { ctx context.Context logger logger.Logger pauseManager pause.Manager bindCtx context.Context bindDone context.CancelFunc dialer N.Dialer reservedForEndpoint map[netip.AddrPort][3]uint8 connAccess sync.Mutex conn *wireConn done chan struct{} isConnect bool connectAddr netip.AddrPort reserved [3]uint8 } func NewClientBind(ctx context.Context, logger logger.Logger, dialer N.Dialer, isConnect bool, connectAddr netip.AddrPort, reserved [3]uint8) *ClientBind { return &ClientBind{ ctx: ctx, logger: logger, pauseManager: service.FromContext[pause.Manager](ctx), dialer: dialer, reservedForEndpoint: make(map[netip.AddrPort][3]uint8), done: make(chan struct{}), isConnect: isConnect, connectAddr: connectAddr, reserved: reserved, } } func (c *ClientBind) connect() (*wireConn, error) { serverConn := c.conn if serverConn != nil { select { case <-serverConn.done: serverConn = nil default: return serverConn, nil } } c.connAccess.Lock() defer c.connAccess.Unlock() select { case <-c.done: return nil, net.ErrClosed default: } serverConn = c.conn if serverConn != nil { select { case <-serverConn.done: serverConn = nil default: return serverConn, nil } } if c.isConnect { udpConn, err := c.dialer.DialContext(c.bindCtx, N.NetworkUDP, M.SocksaddrFromNetIP(c.connectAddr)) if err != nil { return nil, err } c.conn = &wireConn{ PacketConn: bufio.NewUnbindPacketConn(udpConn), done: make(chan struct{}), } } else { udpConn, err := c.dialer.ListenPacket(c.bindCtx, M.Socksaddr{Addr: netip.IPv4Unspecified()}) if err != nil { return nil, err } c.conn = &wireConn{ PacketConn: bufio.NewPacketConn(udpConn), done: make(chan struct{}), } } return c.conn, nil } func (c *ClientBind) Open(port uint16) (fns []conn.ReceiveFunc, actualPort uint16, err error) { select { case <-c.done: c.done = make(chan struct{}) default: } c.bindCtx, c.bindDone = context.WithCancel(c.ctx) return []conn.ReceiveFunc{c.receive}, 0, nil } func (c *ClientBind) receive(packets [][]byte, sizes []int, eps []conn.Endpoint) (count int, err error) { udpConn, err := c.connect() if err != nil { select { case <-c.done: return default: } c.logger.Error(E.Cause(err, "connect to server")) err = nil c.pauseManager.WaitActive() time.Sleep(time.Second) return } n, addr, err := udpConn.ReadFrom(packets[0]) if err != nil { udpConn.Close() select { case <-c.done: default: c.logger.Error(E.Cause(err, "read packet")) err = nil } return } sizes[0] = n if n > 3 { b := packets[0] clear(b[1:4]) } eps[0] = remoteEndpoint(M.SocksaddrFromNet(addr).Unwrap().AddrPort()) count = 1 return } func (c *ClientBind) Close() error { select { case <-c.done: default: close(c.done) } if c.bindDone != nil { c.bindDone() } c.connAccess.Lock() defer c.connAccess.Unlock() common.Close(common.PtrOrNil(c.conn)) return nil } func (c *ClientBind) SetMark(mark uint32) error { return nil } func (c *ClientBind) Send(bufs [][]byte, ep conn.Endpoint, offset int) error { udpConn, err := c.connect() if err != nil { c.pauseManager.WaitActive() time.Sleep(time.Second) return err } destination := netip.AddrPort(ep.(remoteEndpoint)) for _, buf := range bufs { if offset > 0 { buf = buf[offset:] } if len(buf) > 3 { reserved, loaded := c.reservedForEndpoint[destination] if !loaded { reserved = c.reserved } copy(buf[1:4], reserved[:]) } _, err = udpConn.WriteToUDPAddrPort(buf, destination) if err != nil { udpConn.Close() return err } } return nil } func (c *ClientBind) ParseEndpoint(s string) (conn.Endpoint, error) { ap, err := netip.ParseAddrPort(s) if err != nil { return nil, err } return remoteEndpoint(ap), nil } func (c *ClientBind) BatchSize() int { return 1 } func (c *ClientBind) SetReservedForEndpoint(destination netip.AddrPort, reserved [3]byte) { c.reservedForEndpoint[destination] = reserved } type wireConn struct { net.PacketConn conn net.Conn access sync.Mutex done chan struct{} } func (w *wireConn) WriteToUDPAddrPort(b []byte, addr netip.AddrPort) (int, error) { if w.conn != nil { return w.conn.Write(b) } return w.PacketConn.WriteTo(b, M.SocksaddrFromNetIP(addr).UDPAddr()) } func (w *wireConn) Close() error { w.access.Lock() defer w.access.Unlock() select { case <-w.done: return net.ErrClosed default: } w.PacketConn.Close() close(w.done) return nil } var _ conn.Endpoint = (*remoteEndpoint)(nil) type remoteEndpoint netip.AddrPort func (e remoteEndpoint) ClearSrc() { } func (e remoteEndpoint) SrcToString() string { return "" } func (e remoteEndpoint) DstToString() string { return (netip.AddrPort)(e).String() } func (e remoteEndpoint) DstToBytes() []byte { b, _ := (netip.AddrPort)(e).MarshalBinary() return b } func (e remoteEndpoint) DstIP() netip.Addr { return (netip.AddrPort)(e).Addr() } func (e remoteEndpoint) SrcIP() netip.Addr { return netip.Addr{} } ================================================ FILE: transport/wireguard/device.go ================================================ package wireguard import ( "context" "net/netip" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/logger" N "github.com/sagernet/sing/common/network" "github.com/sagernet/wireguard-go/device" wgTun "github.com/sagernet/wireguard-go/tun" ) type Device interface { wgTun.Device N.Dialer Start() error SetDevice(device *device.Device) Inet4Address() netip.Addr Inet6Address() netip.Addr } type DeviceOptions struct { Context context.Context Logger logger.ContextLogger System bool Handler tun.Handler UDPTimeout time.Duration CreateDialer func(interfaceName string) N.Dialer Name string MTU uint32 Address []netip.Prefix AllowedAddress []netip.Prefix } func NewDevice(options DeviceOptions) (Device, error) { if !options.System { return newStackDevice(options) } else if !tun.WithGVisor { return newSystemDevice(options) } else { return newSystemStackDevice(options) } } type NatDevice interface { Device CreateDestination(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) } ================================================ FILE: transport/wireguard/device_nat.go ================================================ package wireguard import ( "context" "sync/atomic" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun/ping" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/logger" ) var _ Device = (*natDeviceWrapper)(nil) type natDeviceWrapper struct { Device ctx context.Context logger logger.ContextLogger packetOutbound chan *buf.Buffer rewriter *ping.SourceRewriter buffer [][]byte } func NewNATDevice(ctx context.Context, logger logger.ContextLogger, upstream Device) NatDevice { wrapper := &natDeviceWrapper{ Device: upstream, ctx: ctx, logger: logger, packetOutbound: make(chan *buf.Buffer, 256), rewriter: ping.NewSourceRewriter(ctx, logger, upstream.Inet4Address(), upstream.Inet6Address()), } return wrapper } func (d *natDeviceWrapper) Read(bufs [][]byte, sizes []int, offset int) (n int, err error) { select { case packet := <-d.packetOutbound: defer packet.Release() sizes[0] = copy(bufs[0][offset:], packet.Bytes()) return 1, nil default: } return d.Device.Read(bufs, sizes, offset) } func (d *natDeviceWrapper) Write(bufs [][]byte, offset int) (int, error) { for _, buffer := range bufs { handled, err := d.rewriter.WriteBack(buffer[offset:]) if handled { if err != nil { return 0, err } } else { d.buffer = append(d.buffer, buffer) } } if len(d.buffer) > 0 { _, err := d.Device.Write(d.buffer, offset) if err != nil { return 0, err } d.buffer = d.buffer[:0] } return 0, nil } func (d *natDeviceWrapper) CreateDestination(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { ctx := log.ContextWithNewID(d.ctx) session := tun.DirectRouteSession{ Source: metadata.Source.Addr, Destination: metadata.Destination.Addr, } d.rewriter.CreateSession(session, routeContext) d.logger.InfoContext(ctx, "linked ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString()) return &natDestination{device: d, session: session}, nil } var _ tun.DirectRouteDestination = (*natDestination)(nil) type natDestination struct { device *natDeviceWrapper session tun.DirectRouteSession closed atomic.Bool } func (d *natDestination) WritePacket(buffer *buf.Buffer) error { d.device.rewriter.RewritePacket(buffer.Bytes()) d.device.packetOutbound <- buffer return nil } func (d *natDestination) Close() error { d.closed.Store(true) d.device.rewriter.DeleteSession(d.session) return nil } func (d *natDestination) IsClosed() bool { return d.closed.Load() } ================================================ FILE: transport/wireguard/device_stack.go ================================================ //go:build with_gvisor package wireguard import ( "context" "net" "net/netip" "os" "sync" "time" "github.com/sagernet/gvisor/pkg/buffer" "github.com/sagernet/gvisor/pkg/tcpip" "github.com/sagernet/gvisor/pkg/tcpip/adapters/gonet" "github.com/sagernet/gvisor/pkg/tcpip/header" "github.com/sagernet/gvisor/pkg/tcpip/network/ipv4" "github.com/sagernet/gvisor/pkg/tcpip/network/ipv6" "github.com/sagernet/gvisor/pkg/tcpip/stack" "github.com/sagernet/gvisor/pkg/tcpip/transport/icmp" "github.com/sagernet/gvisor/pkg/tcpip/transport/tcp" "github.com/sagernet/gvisor/pkg/tcpip/transport/udp" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun/ping" "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/wireguard-go/device" wgTun "github.com/sagernet/wireguard-go/tun" ) var _ NatDevice = (*stackDevice)(nil) type stackDevice struct { ctx context.Context logger log.ContextLogger stack *stack.Stack mtu uint32 events chan wgTun.Event outbound chan *stack.PacketBuffer packetOutbound chan *buf.Buffer done chan struct{} closeOnce sync.Once dispatcher stack.NetworkDispatcher inet4Address netip.Addr inet6Address netip.Addr } func newStackDevice(options DeviceOptions) (*stackDevice, error) { tunDevice := &stackDevice{ ctx: options.Context, logger: options.Logger, mtu: options.MTU, events: make(chan wgTun.Event, 1), outbound: make(chan *stack.PacketBuffer, 256), packetOutbound: make(chan *buf.Buffer, 256), done: make(chan struct{}), } ipStack, err := tun.NewGVisorStackWithOptions((*wireEndpoint)(tunDevice), stack.NICOptions{}, true) if err != nil { return nil, err } var ( inet4Address netip.Addr inet6Address netip.Addr ) for _, prefix := range options.Address { addr := tun.AddressFromAddr(prefix.Addr()) protoAddr := tcpip.ProtocolAddress{ AddressWithPrefix: tcpip.AddressWithPrefix{ Address: addr, PrefixLen: prefix.Bits(), }, } if prefix.Addr().Is4() { inet4Address = prefix.Addr() tunDevice.inet4Address = inet4Address protoAddr.Protocol = ipv4.ProtocolNumber } else { inet6Address = prefix.Addr() tunDevice.inet6Address = inet6Address protoAddr.Protocol = ipv6.ProtocolNumber } gErr := ipStack.AddProtocolAddress(tun.DefaultNIC, protoAddr, stack.AddressProperties{}) if gErr != nil { return nil, E.New("parse local address ", protoAddr.AddressWithPrefix, ": ", gErr.String()) } } tunDevice.stack = ipStack if options.Handler != nil { ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tun.NewTCPForwarder(options.Context, ipStack, options.Handler).HandlePacket) ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, tun.NewUDPForwarder(options.Context, ipStack, options.Handler, options.UDPTimeout).HandlePacket) icmpForwarder := tun.NewICMPForwarder(options.Context, ipStack, options.Handler, options.UDPTimeout) icmpForwarder.SetLocalAddresses(inet4Address, inet6Address) ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket) ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket) } return tunDevice, nil } func (w *stackDevice) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { addr := tcpip.FullAddress{ NIC: tun.DefaultNIC, Port: destination.Port, Addr: tun.AddressFromAddr(destination.Addr), } bind := tcpip.FullAddress{ NIC: tun.DefaultNIC, } var networkProtocol tcpip.NetworkProtocolNumber if destination.IsIPv4() { if !w.inet4Address.IsValid() { return nil, E.New("missing IPv4 local address") } networkProtocol = header.IPv4ProtocolNumber bind.Addr = tun.AddressFromAddr(w.inet4Address) } else { if !w.inet6Address.IsValid() { return nil, E.New("missing IPv6 local address") } networkProtocol = header.IPv6ProtocolNumber bind.Addr = tun.AddressFromAddr(w.inet6Address) } switch N.NetworkName(network) { case N.NetworkTCP: tcpConn, err := DialTCPWithBind(ctx, w.stack, bind, addr, networkProtocol) if err != nil { return nil, err } return tcpConn, nil case N.NetworkUDP: udpConn, err := gonet.DialUDP(w.stack, &bind, &addr, networkProtocol) if err != nil { return nil, err } return udpConn, nil default: return nil, E.Extend(N.ErrUnknownNetwork, network) } } func (w *stackDevice) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { bind := tcpip.FullAddress{ NIC: tun.DefaultNIC, } var networkProtocol tcpip.NetworkProtocolNumber if destination.IsIPv4() { if !w.inet4Address.IsValid() { return nil, E.New("missing IPv4 local address") } networkProtocol = header.IPv4ProtocolNumber bind.Addr = tun.AddressFromAddr(w.inet4Address) } else { if !w.inet6Address.IsValid() { return nil, E.New("missing IPv6 local address") } networkProtocol = header.IPv6ProtocolNumber bind.Addr = tun.AddressFromAddr(w.inet6Address) } udpConn, err := gonet.DialUDP(w.stack, &bind, nil, networkProtocol) if err != nil { return nil, err } return udpConn, nil } func (w *stackDevice) Inet4Address() netip.Addr { return w.inet4Address } func (w *stackDevice) Inet6Address() netip.Addr { return w.inet6Address } func (w *stackDevice) SetDevice(device *device.Device) { } func (w *stackDevice) Start() error { w.events <- wgTun.EventUp return nil } func (w *stackDevice) File() *os.File { return nil } func (w *stackDevice) Read(bufs [][]byte, sizes []int, offset int) (count int, err error) { select { case packet, ok := <-w.outbound: if !ok { return 0, os.ErrClosed } defer packet.DecRef() var copyN int /*rangeIterate(packet.Data().AsRange(), func(view *buffer.View) { copyN += copy(bufs[0][offset+copyN:], view.AsSlice()) })*/ for _, view := range packet.AsSlices() { copyN += copy(bufs[0][offset+copyN:], view) } sizes[0] = copyN return 1, nil case packet := <-w.packetOutbound: defer packet.Release() sizes[0] = copy(bufs[0][offset:], packet.Bytes()) return 1, nil case <-w.done: return 0, os.ErrClosed } } func (w *stackDevice) Write(bufs [][]byte, offset int) (count int, err error) { for _, b := range bufs { b = b[offset:] if len(b) == 0 { continue } var networkProtocol tcpip.NetworkProtocolNumber switch header.IPVersion(b) { case header.IPv4Version: networkProtocol = header.IPv4ProtocolNumber case header.IPv6Version: networkProtocol = header.IPv6ProtocolNumber } packetBuffer := stack.NewPacketBuffer(stack.PacketBufferOptions{ Payload: buffer.MakeWithData(b), }) w.dispatcher.DeliverNetworkPacket(networkProtocol, packetBuffer) packetBuffer.DecRef() count++ } return } func (w *stackDevice) Flush() error { return nil } func (w *stackDevice) MTU() (int, error) { return int(w.mtu), nil } func (w *stackDevice) Name() (string, error) { return "sing-box", nil } func (w *stackDevice) Events() <-chan wgTun.Event { return w.events } func (w *stackDevice) Close() error { w.closeOnce.Do(func() { close(w.done) close(w.events) w.stack.Close() for _, endpoint := range w.stack.CleanupEndpoints() { endpoint.Abort() } w.stack.Wait() }) return nil } func (w *stackDevice) BatchSize() int { return 1 } func (w *stackDevice) CreateDestination(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { ctx := log.ContextWithNewID(w.ctx) destination, err := ping.ConnectGVisor( ctx, w.logger, metadata.Source.Addr, metadata.Destination.Addr, routeContext, w.stack, w.inet4Address, w.inet6Address, timeout, ) if err != nil { return nil, err } w.logger.InfoContext(ctx, "linked ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString()) return destination, nil } var _ stack.LinkEndpoint = (*wireEndpoint)(nil) type wireEndpoint stackDevice func (ep *wireEndpoint) MTU() uint32 { return ep.mtu } func (ep *wireEndpoint) SetMTU(mtu uint32) { } func (ep *wireEndpoint) MaxHeaderLength() uint16 { return 0 } func (ep *wireEndpoint) LinkAddress() tcpip.LinkAddress { return "" } func (ep *wireEndpoint) SetLinkAddress(addr tcpip.LinkAddress) { } func (ep *wireEndpoint) Capabilities() stack.LinkEndpointCapabilities { return stack.CapabilityRXChecksumOffload } func (ep *wireEndpoint) Attach(dispatcher stack.NetworkDispatcher) { ep.dispatcher = dispatcher } func (ep *wireEndpoint) IsAttached() bool { return ep.dispatcher != nil } func (ep *wireEndpoint) Wait() { } func (ep *wireEndpoint) ARPHardwareType() header.ARPHardwareType { return header.ARPHardwareNone } func (ep *wireEndpoint) AddHeader(buffer *stack.PacketBuffer) { } func (ep *wireEndpoint) ParseHeader(ptr *stack.PacketBuffer) bool { return true } func (ep *wireEndpoint) WritePackets(list stack.PacketBufferList) (int, tcpip.Error) { for _, packetBuffer := range list.AsSlice() { packetBuffer.IncRef() select { case <-ep.done: return 0, &tcpip.ErrClosedForSend{} case ep.outbound <- packetBuffer: } } return list.Len(), nil } func (ep *wireEndpoint) Close() { } func (ep *wireEndpoint) SetOnCloseAction(f func()) { } ================================================ FILE: transport/wireguard/device_stack_gonet.go ================================================ //go:build with_gvisor package wireguard import ( "context" "errors" "fmt" "net" "net/netip" "time" "github.com/sagernet/gvisor/pkg/tcpip" "github.com/sagernet/gvisor/pkg/tcpip/adapters/gonet" "github.com/sagernet/gvisor/pkg/tcpip/stack" "github.com/sagernet/gvisor/pkg/tcpip/transport/tcp" "github.com/sagernet/gvisor/pkg/waiter" "github.com/sagernet/sing-tun" M "github.com/sagernet/sing/common/metadata" ) func DialTCPWithBind(ctx context.Context, s *stack.Stack, localAddr, remoteAddr tcpip.FullAddress, network tcpip.NetworkProtocolNumber) (*gonet.TCPConn, error) { // Create TCP endpoint, then connect. var wq waiter.Queue ep, err := s.NewEndpoint(tcp.ProtocolNumber, network, &wq) if err != nil { return nil, errors.New(err.String()) } // Create wait queue entry that notifies a channel. // // We do this unconditionally as Connect will always return an error. waitEntry, notifyCh := waiter.NewChannelEntry(waiter.WritableEvents) wq.EventRegister(&waitEntry) defer wq.EventUnregister(&waitEntry) select { case <-ctx.Done(): return nil, ctx.Err() default: } // Bind before connect if requested. if localAddr != (tcpip.FullAddress{}) { if err = ep.Bind(localAddr); err != nil { return nil, fmt.Errorf("ep.Bind(%+v) = %s", localAddr, err) } } err = ep.Connect(remoteAddr) if _, ok := err.(*tcpip.ErrConnectStarted); ok { select { case <-ctx.Done(): ep.Close() return nil, ctx.Err() case <-notifyCh: } err = ep.LastError() } if err != nil { ep.Close() return nil, &net.OpError{ Op: "connect", Net: "tcp", Addr: M.SocksaddrFromNetIP(netip.AddrPortFrom(tun.AddrFromAddress(remoteAddr.Addr), remoteAddr.Port)).TCPAddr(), Err: errors.New(err.String()), } } // sing-box added: set keepalive ep.SocketOptions().SetKeepAlive(true) keepAliveIdle := tcpip.KeepaliveIdleOption(15 * time.Second) ep.SetSockOpt(&keepAliveIdle) keepAliveInterval := tcpip.KeepaliveIntervalOption(15 * time.Second) ep.SetSockOpt(&keepAliveInterval) return gonet.NewTCPConn(&wq, ep), nil } ================================================ FILE: transport/wireguard/device_stack_stub.go ================================================ //go:build !with_gvisor package wireguard import "github.com/sagernet/sing-tun" func newStackDevice(options DeviceOptions) (Device, error) { return nil, tun.ErrGVisorNotIncluded } func newSystemStackDevice(options DeviceOptions) (Device, error) { return nil, tun.ErrGVisorNotIncluded } ================================================ FILE: transport/wireguard/device_system.go ================================================ package wireguard import ( "context" "errors" "net" "net/netip" "os" "runtime" "sync" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/service" "github.com/sagernet/wireguard-go/device" wgTun "github.com/sagernet/wireguard-go/tun" ) var _ Device = (*systemDevice)(nil) type systemDevice struct { options DeviceOptions dialer N.Dialer device tun.Tun batchDevice tun.LinuxTUN events chan wgTun.Event closeOnce sync.Once inet4Address netip.Addr inet6Address netip.Addr } func newSystemDevice(options DeviceOptions) (*systemDevice, error) { if options.Name == "" { options.Name = tun.CalculateInterfaceName("wg") } var inet4Address netip.Addr var inet6Address netip.Addr if len(options.Address) > 0 { if prefix := common.Find(options.Address, func(it netip.Prefix) bool { return it.Addr().Is4() }); prefix.IsValid() { inet4Address = prefix.Addr() } } if len(options.Address) > 0 { if prefix := common.Find(options.Address, func(it netip.Prefix) bool { return it.Addr().Is6() }); prefix.IsValid() { inet6Address = prefix.Addr() } } return &systemDevice{ options: options, dialer: options.CreateDialer(options.Name), events: make(chan wgTun.Event, 1), inet4Address: inet4Address, inet6Address: inet6Address, }, nil } func (w *systemDevice) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { return w.dialer.DialContext(ctx, network, destination) } func (w *systemDevice) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { return w.dialer.ListenPacket(ctx, destination) } func (w *systemDevice) Inet4Address() netip.Addr { return w.inet4Address } func (w *systemDevice) Inet6Address() netip.Addr { return w.inet6Address } func (w *systemDevice) SetDevice(device *device.Device) { } func (w *systemDevice) Start() error { networkManager := service.FromContext[adapter.NetworkManager](w.options.Context) tunOptions := tun.Options{ Name: w.options.Name, Inet4Address: common.Filter(w.options.Address, func(it netip.Prefix) bool { return it.Addr().Is4() }), Inet6Address: common.Filter(w.options.Address, func(it netip.Prefix) bool { return it.Addr().Is6() }), MTU: w.options.MTU, GSO: true, InterfaceScope: true, Inet4RouteAddress: common.Filter(w.options.AllowedAddress, func(it netip.Prefix) bool { return it.Addr().Is4() }), Inet6RouteAddress: common.Filter(w.options.AllowedAddress, func(it netip.Prefix) bool { return it.Addr().Is6() }), InterfaceMonitor: networkManager.InterfaceMonitor(), InterfaceFinder: networkManager.InterfaceFinder(), Logger: w.options.Logger, } // works with Linux, macOS with IFSCOPE routes, not tested on Windows if runtime.GOOS == "darwin" { tunOptions.AutoRoute = true } tunInterface, err := tun.New(tunOptions) if err != nil { return err } err = tunInterface.Start() if err != nil { tunInterface.Close() return err } w.options.Logger.Info("started at ", w.options.Name) w.device = tunInterface batchTUN, isBatchTUN := tunInterface.(tun.LinuxTUN) if isBatchTUN && batchTUN.BatchSize() > 1 { w.batchDevice = batchTUN } w.events <- wgTun.EventUp return nil } func (w *systemDevice) File() *os.File { return nil } func (w *systemDevice) Read(bufs [][]byte, sizes []int, offset int) (count int, err error) { if w.batchDevice != nil { count, err = w.batchDevice.BatchRead(bufs, offset-tun.PacketOffset, sizes) } else { sizes[0], err = w.device.Read(bufs[0][offset-tun.PacketOffset:]) if err == nil { count = 1 } else if errors.Is(err, tun.ErrTooManySegments) { err = wgTun.ErrTooManySegments } } return } func (w *systemDevice) Write(bufs [][]byte, offset int) (count int, err error) { if w.batchDevice != nil { return w.batchDevice.BatchWrite(bufs, offset) } else { for _, packet := range bufs { if tun.PacketOffset > 0 { clear(packet[offset-tun.PacketOffset : offset]) tun.PacketFillHeader(packet[offset-tun.PacketOffset:], tun.PacketIPVersion(packet[offset:])) } _, err = w.device.Write(packet[offset-tun.PacketOffset:]) if err != nil { return } } } // WireGuard will not read count return } func (w *systemDevice) Flush() error { return nil } func (w *systemDevice) MTU() (int, error) { return int(w.options.MTU), nil } func (w *systemDevice) Name() (string, error) { return w.options.Name, nil } func (w *systemDevice) Events() <-chan wgTun.Event { return w.events } func (w *systemDevice) Close() error { var err error w.closeOnce.Do(func() { close(w.events) if w.device != nil { err = w.device.Close() } }) return err } func (w *systemDevice) BatchSize() int { if w.batchDevice != nil { return w.batchDevice.BatchSize() } return 1 } ================================================ FILE: transport/wireguard/device_system_stack.go ================================================ //go:build with_gvisor package wireguard import ( "context" "net/netip" "sync" "time" "github.com/sagernet/gvisor/pkg/buffer" "github.com/sagernet/gvisor/pkg/tcpip" "github.com/sagernet/gvisor/pkg/tcpip/header" "github.com/sagernet/gvisor/pkg/tcpip/network/ipv4" "github.com/sagernet/gvisor/pkg/tcpip/network/ipv6" "github.com/sagernet/gvisor/pkg/tcpip/stack" "github.com/sagernet/gvisor/pkg/tcpip/transport/icmp" "github.com/sagernet/gvisor/pkg/tcpip/transport/tcp" "github.com/sagernet/gvisor/pkg/tcpip/transport/udp" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun/ping" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" "github.com/sagernet/wireguard-go/device" ) var _ Device = (*systemStackDevice)(nil) type systemStackDevice struct { *systemDevice ctx context.Context logger logger.ContextLogger stack *stack.Stack endpoint *deviceEndpoint writeBufs [][]byte closeOnce sync.Once } func newSystemStackDevice(options DeviceOptions) (*systemStackDevice, error) { system, err := newSystemDevice(options) if err != nil { return nil, err } endpoint := &deviceEndpoint{ mtu: options.MTU, done: make(chan struct{}), } ipStack, err := tun.NewGVisorStackWithOptions(endpoint, stack.NICOptions{}, true) if err != nil { return nil, err } var ( inet4Address netip.Addr inet6Address netip.Addr ) for _, prefix := range options.Address { addr := tun.AddressFromAddr(prefix.Addr()) protoAddr := tcpip.ProtocolAddress{ AddressWithPrefix: tcpip.AddressWithPrefix{ Address: addr, PrefixLen: prefix.Bits(), }, } if prefix.Addr().Is4() { inet4Address = prefix.Addr() protoAddr.Protocol = ipv4.ProtocolNumber } else { inet6Address = prefix.Addr() protoAddr.Protocol = ipv6.ProtocolNumber } gErr := ipStack.AddProtocolAddress(tun.DefaultNIC, protoAddr, stack.AddressProperties{}) if gErr != nil { return nil, E.New("parse local address ", protoAddr.AddressWithPrefix, ": ", gErr.String()) } } if options.Handler != nil { ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tun.NewTCPForwarder(options.Context, ipStack, options.Handler).HandlePacket) ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, tun.NewUDPForwarder(options.Context, ipStack, options.Handler, options.UDPTimeout).HandlePacket) icmpForwarder := tun.NewICMPForwarder(options.Context, ipStack, options.Handler, options.UDPTimeout) icmpForwarder.SetLocalAddresses(inet4Address, inet6Address) ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket) ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket) } return &systemStackDevice{ ctx: options.Context, logger: options.Logger, systemDevice: system, stack: ipStack, endpoint: endpoint, }, nil } func (w *systemStackDevice) SetDevice(device *device.Device) { w.endpoint.device = device } func (w *systemStackDevice) Write(bufs [][]byte, offset int) (count int, err error) { if w.batchDevice != nil { w.writeBufs = w.writeBufs[:0] for _, packet := range bufs { if !w.writeStack(packet[offset:]) { w.writeBufs = append(w.writeBufs, packet) } } if len(w.writeBufs) > 0 { return w.batchDevice.BatchWrite(w.writeBufs, offset) } } else { for _, packet := range bufs { if !w.writeStack(packet[offset:]) { if tun.PacketOffset > 0 { clear(packet[offset-tun.PacketOffset : offset]) tun.PacketFillHeader(packet[offset-tun.PacketOffset:], tun.PacketIPVersion(packet[offset:])) } _, err = w.device.Write(packet[offset-tun.PacketOffset:]) } if err != nil { return } } } // WireGuard will not read count return } func (w *systemStackDevice) Close() error { var err error w.closeOnce.Do(func() { close(w.endpoint.done) w.stack.Close() for _, endpoint := range w.stack.CleanupEndpoints() { endpoint.Abort() } w.stack.Wait() err = w.systemDevice.Close() }) return err } func (w *systemStackDevice) writeStack(packet []byte) bool { var ( networkProtocol tcpip.NetworkProtocolNumber destination netip.Addr ) switch header.IPVersion(packet) { case header.IPv4Version: networkProtocol = header.IPv4ProtocolNumber destination = netip.AddrFrom4(header.IPv4(packet).DestinationAddress().As4()) case header.IPv6Version: networkProtocol = header.IPv6ProtocolNumber destination = netip.AddrFrom16(header.IPv6(packet).DestinationAddress().As16()) } for _, prefix := range w.options.Address { if prefix.Contains(destination) { return false } } packetBuffer := stack.NewPacketBuffer(stack.PacketBufferOptions{ Payload: buffer.MakeWithData(packet), }) w.endpoint.dispatcher.DeliverNetworkPacket(networkProtocol, packetBuffer) packetBuffer.DecRef() return true } func (w *systemStackDevice) CreateDestination(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { ctx := log.ContextWithNewID(w.ctx) destination, err := ping.ConnectGVisor( ctx, w.logger, metadata.Source.Addr, metadata.Destination.Addr, routeContext, w.stack, w.inet4Address, w.inet6Address, timeout, ) if err != nil { return nil, err } w.logger.InfoContext(ctx, "linked ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString()) return destination, nil } type deviceEndpoint struct { mtu uint32 done chan struct{} device *device.Device dispatcher stack.NetworkDispatcher } func (ep *deviceEndpoint) MTU() uint32 { return ep.mtu } func (ep *deviceEndpoint) SetMTU(mtu uint32) { } func (ep *deviceEndpoint) MaxHeaderLength() uint16 { return 0 } func (ep *deviceEndpoint) LinkAddress() tcpip.LinkAddress { return "" } func (ep *deviceEndpoint) SetLinkAddress(addr tcpip.LinkAddress) { } func (ep *deviceEndpoint) Capabilities() stack.LinkEndpointCapabilities { return stack.CapabilityRXChecksumOffload } func (ep *deviceEndpoint) Attach(dispatcher stack.NetworkDispatcher) { ep.dispatcher = dispatcher } func (ep *deviceEndpoint) IsAttached() bool { return ep.dispatcher != nil } func (ep *deviceEndpoint) Wait() { } func (ep *deviceEndpoint) ARPHardwareType() header.ARPHardwareType { return header.ARPHardwareNone } func (ep *deviceEndpoint) AddHeader(buffer *stack.PacketBuffer) { } func (ep *deviceEndpoint) ParseHeader(ptr *stack.PacketBuffer) bool { return true } func (ep *deviceEndpoint) WritePackets(list stack.PacketBufferList) (int, tcpip.Error) { for _, packetBuffer := range list.AsSlice() { destination := packetBuffer.Network().DestinationAddress() ep.device.InputPacket(destination.AsSlice(), packetBuffer.AsSlices()) } return list.Len(), nil } func (ep *deviceEndpoint) Close() { } func (ep *deviceEndpoint) SetOnCloseAction(f func()) { } ================================================ FILE: transport/wireguard/endpoint.go ================================================ package wireguard import ( "context" "encoding/base64" "encoding/hex" "fmt" "net" "net/netip" "os" "reflect" "strings" "time" "unsafe" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/pause" "github.com/sagernet/wireguard-go/conn" "github.com/sagernet/wireguard-go/device" "go4.org/netipx" ) type Endpoint struct { options EndpointOptions peers []peerConfig ipcConf string allowedAddress []netip.Prefix tunDevice Device natDevice NatDevice device *device.Device allowedIPs *device.AllowedIPs pause pause.Manager pauseCallback *list.Element[pause.Callback] } func NewEndpoint(options EndpointOptions) (*Endpoint, error) { if options.PrivateKey == "" { return nil, E.New("missing private key") } privateKeyBytes, err := base64.StdEncoding.DecodeString(options.PrivateKey) if err != nil { return nil, E.Cause(err, "decode private key") } privateKey := hex.EncodeToString(privateKeyBytes) ipcConf := "private_key=" + privateKey if options.ListenPort != 0 { ipcConf += "\nlisten_port=" + F.ToString(options.ListenPort) } var peers []peerConfig for peerIndex, rawPeer := range options.Peers { peer := peerConfig{ allowedIPs: rawPeer.AllowedIPs, keepalive: rawPeer.PersistentKeepaliveInterval, } if rawPeer.Endpoint.Addr.IsValid() { peer.endpoint = rawPeer.Endpoint.AddrPort() } else if rawPeer.Endpoint.IsDomain() { peer.destination = rawPeer.Endpoint } publicKeyBytes, err := base64.StdEncoding.DecodeString(rawPeer.PublicKey) if err != nil { return nil, E.Cause(err, "decode public key for peer ", peerIndex) } peer.publicKeyHex = hex.EncodeToString(publicKeyBytes) if rawPeer.PreSharedKey != "" { preSharedKeyBytes, err := base64.StdEncoding.DecodeString(rawPeer.PreSharedKey) if err != nil { return nil, E.Cause(err, "decode pre shared key for peer ", peerIndex) } peer.preSharedKeyHex = hex.EncodeToString(preSharedKeyBytes) } if len(rawPeer.AllowedIPs) == 0 { return nil, E.New("missing allowed ips for peer ", peerIndex) } if len(rawPeer.Reserved) > 0 { if len(rawPeer.Reserved) != 3 { return nil, E.New("invalid reserved value for peer ", peerIndex, ", required 3 bytes, got ", len(peer.reserved)) } copy(peer.reserved[:], rawPeer.Reserved[:]) } peers = append(peers, peer) } var allowedPrefixBuilder netipx.IPSetBuilder for _, peer := range options.Peers { for _, prefix := range peer.AllowedIPs { allowedPrefixBuilder.AddPrefix(prefix) } } allowedIPSet, err := allowedPrefixBuilder.IPSet() if err != nil { return nil, err } allowedAddresses := allowedIPSet.Prefixes() if options.MTU == 0 { options.MTU = 1408 } deviceOptions := DeviceOptions{ Context: options.Context, Logger: options.Logger, System: options.System, Handler: options.Handler, UDPTimeout: options.UDPTimeout, CreateDialer: options.CreateDialer, Name: options.Name, MTU: options.MTU, Address: options.Address, AllowedAddress: allowedAddresses, } tunDevice, err := NewDevice(deviceOptions) if err != nil { return nil, E.Cause(err, "create WireGuard device") } natDevice, isNatDevice := tunDevice.(NatDevice) if !isNatDevice { natDevice = NewNATDevice(options.Context, options.Logger, tunDevice) } return &Endpoint{ options: options, peers: peers, ipcConf: ipcConf, allowedAddress: allowedAddresses, tunDevice: tunDevice, natDevice: natDevice, }, nil } func (e *Endpoint) Start(resolve bool) error { if common.Any(e.peers, func(peer peerConfig) bool { return !peer.endpoint.IsValid() && peer.destination.IsDomain() }) { if !resolve { return nil } for peerIndex, peer := range e.peers { if peer.endpoint.IsValid() || !peer.destination.IsDomain() { continue } destinationAddress, err := e.options.ResolvePeer(peer.destination.Fqdn) if err != nil { return E.Cause(err, "resolve endpoint domain for peer[", peerIndex, "]: ", peer.destination) } e.peers[peerIndex].endpoint = netip.AddrPortFrom(destinationAddress, peer.destination.Port) } } else if resolve { return nil } var bind conn.Bind wgListener, isWgListener := common.Cast[dialer.WireGuardListener](e.options.Dialer) if isWgListener { bind = conn.NewStdNetBind(wgListener.WireGuardControl()) } else { var ( isConnect bool connectAddr netip.AddrPort reserved [3]uint8 ) if len(e.peers) == 1 && e.peers[0].endpoint.IsValid() { isConnect = true connectAddr = e.peers[0].endpoint reserved = e.peers[0].reserved } bind = NewClientBind(e.options.Context, e.options.Logger, e.options.Dialer, isConnect, connectAddr, reserved) } if isWgListener || len(e.peers) > 1 { for _, peer := range e.peers { if peer.reserved != [3]uint8{} { bind.SetReservedForEndpoint(peer.endpoint, peer.reserved) } } } err := e.tunDevice.Start() if err != nil { return err } logger := &device.Logger{ Verbosef: func(format string, args ...any) { e.options.Logger.Debug(fmt.Sprintf(strings.ToLower(format), args...)) }, Errorf: func(format string, args ...any) { e.options.Logger.Error(fmt.Sprintf(strings.ToLower(format), args...)) }, } var deviceInput Device if e.natDevice != nil { deviceInput = e.natDevice } else { deviceInput = e.tunDevice } wgDevice := device.NewDevice(e.options.Context, deviceInput, bind, logger, e.options.Workers) e.tunDevice.SetDevice(wgDevice) var ipcConf strings.Builder ipcConf.WriteString(e.ipcConf) for _, peer := range e.peers { ipcConf.WriteString(peer.GenerateIpcLines()) } err = wgDevice.IpcSet(ipcConf.String()) if err != nil { wgDevice.Close() return E.Cause(err, "setup wireguard: \n", ipcConf.String()) } e.device = wgDevice e.pause = service.FromContext[pause.Manager](e.options.Context) if e.pause != nil { e.pauseCallback = e.pause.RegisterCallback(e.onPauseUpdated) } e.allowedIPs = (*device.AllowedIPs)(unsafe.Pointer(reflect.Indirect(reflect.ValueOf(wgDevice)).FieldByName("allowedips").UnsafeAddr())) return nil } func (e *Endpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { if !destination.Addr.IsValid() { return nil, E.Cause(os.ErrInvalid, "invalid non-IP destination") } return e.tunDevice.DialContext(ctx, network, destination) } func (e *Endpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { if !destination.Addr.IsValid() { return nil, E.Cause(os.ErrInvalid, "invalid non-IP destination") } return e.tunDevice.ListenPacket(ctx, destination) } func (e *Endpoint) Close() error { if e.pauseCallback != nil { e.pause.UnregisterCallback(e.pauseCallback) e.pauseCallback = nil } if e.device != nil { e.device.Down() e.device.Close() e.device = nil } return nil } func (e *Endpoint) Lookup(address netip.Addr) *device.Peer { if e.allowedIPs == nil { return nil } return e.allowedIPs.Lookup(address.AsSlice()) } func (e *Endpoint) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { if e.natDevice == nil { return nil, os.ErrInvalid } return e.natDevice.CreateDestination(metadata, routeContext, timeout) } func (e *Endpoint) onPauseUpdated(event int) { switch event { case pause.EventDevicePaused, pause.EventNetworkPause: e.device.Down() case pause.EventDeviceWake, pause.EventNetworkWake: e.device.Up() } } type peerConfig struct { destination M.Socksaddr endpoint netip.AddrPort publicKeyHex string preSharedKeyHex string allowedIPs []netip.Prefix keepalive uint16 reserved [3]uint8 } func (c peerConfig) GenerateIpcLines() string { var ipcLines strings.Builder ipcLines.WriteString("\npublic_key=" + c.publicKeyHex) if c.endpoint.IsValid() { ipcLines.WriteString("\nendpoint=" + c.endpoint.String()) } if c.preSharedKeyHex != "" { ipcLines.WriteString("\npreshared_key=" + c.preSharedKeyHex) } for _, allowedIP := range c.allowedIPs { ipcLines.WriteString("\nallowed_ip=" + allowedIP.String()) } if c.keepalive > 0 { ipcLines.WriteString("\npersistent_keepalive_interval=" + F.ToString(c.keepalive)) } return ipcLines.String() } ================================================ FILE: transport/wireguard/endpoint_options.go ================================================ package wireguard import ( "context" "net/netip" "time" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) type EndpointOptions struct { Context context.Context Logger logger.ContextLogger System bool Handler tun.Handler UDPTimeout time.Duration Dialer N.Dialer CreateDialer func(interfaceName string) N.Dialer Name string MTU uint32 Address []netip.Prefix PrivateKey string ListenPort uint16 ResolvePeer func(domain string) (netip.Addr, error) Peers []PeerOptions Workers int } type PeerOptions struct { Endpoint M.Socksaddr PublicKey string PreSharedKey string AllowedIPs []netip.Prefix PersistentKeepaliveInterval uint16 Reserved []uint8 }